View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.hadoop.hbase.client;
19  
20  import com.google.common.annotations.VisibleForTesting;
21  import com.google.protobuf.Descriptors.MethodDescriptor;
22  import com.google.protobuf.Message;
23  import com.yammer.metrics.core.Counter;
24  import com.yammer.metrics.core.Histogram;
25  import com.yammer.metrics.core.MetricName;
26  import com.yammer.metrics.core.MetricsRegistry;
27  import com.yammer.metrics.core.Timer;
28  import com.yammer.metrics.reporting.JmxReporter;
29  import com.yammer.metrics.util.RatioGauge;
30  import org.apache.hadoop.hbase.ServerName;
31  import org.apache.hadoop.hbase.classification.InterfaceAudience;
32  import org.apache.hadoop.hbase.protobuf.generated.ClientProtos;
33  import org.apache.hadoop.hbase.protobuf.generated.ClientProtos.ClientService;
34  import org.apache.hadoop.hbase.protobuf.generated.ClientProtos.MutateRequest;
35  import org.apache.hadoop.hbase.protobuf.generated.ClientProtos.MutationProto.MutationType;
36  import org.apache.hadoop.hbase.util.Bytes;
37  
38  import java.util.concurrent.ConcurrentHashMap;
39  import java.util.concurrent.ConcurrentSkipListMap;
40  import java.util.concurrent.ConcurrentMap;
41  import java.util.concurrent.ThreadPoolExecutor;
42  import java.util.concurrent.TimeUnit;
43  
44  /**
45   * This class is for maintaining the various connection statistics and publishing them through
46   * the metrics interfaces.
47   *
48   * This class manages its own {@link MetricsRegistry} and {@link JmxReporter} so as to not
49   * conflict with other uses of Yammer Metrics within the client application. Instantiating
50   * this class implicitly creates and "starts" instances of these classes; be sure to call
51   * {@link #shutdown()} to terminate the thread pools they allocate.
52   */
53  @InterfaceAudience.Private
54  public class MetricsConnection implements StatisticTrackable {
55  
56    /** Set this key to {@code true} to enable metrics collection of client requests. */
57    public static final String CLIENT_SIDE_METRICS_ENABLED_KEY = "hbase.client.metrics.enable";
58  
59    private static final String DRTN_BASE = "rpcCallDurationMs_";
60    private static final String REQ_BASE = "rpcCallRequestSizeBytes_";
61    private static final String RESP_BASE = "rpcCallResponseSizeBytes_";
62    private static final String MEMLOAD_BASE = "memstoreLoad_";
63    private static final String HEAP_BASE = "heapOccupancy_";
64    private static final String CACHE_BASE = "cacheDroppingExceptions_";
65    private static final String UNKNOWN_EXCEPTION = "UnknownException";
66    private static final String CLIENT_SVC = ClientService.getDescriptor().getName();
67  
68    /** A container class for collecting details about the RPC call as it percolates. */
69    public static class CallStats {
70      private long requestSizeBytes = 0;
71      private long responseSizeBytes = 0;
72      private long startTime = 0;
73      private long callTimeMs = 0;
74      private int concurrentCallsPerServer = 0;
75  
76      public long getRequestSizeBytes() {
77        return requestSizeBytes;
78      }
79  
80      public void setRequestSizeBytes(long requestSizeBytes) {
81        this.requestSizeBytes = requestSizeBytes;
82      }
83  
84      public long getResponseSizeBytes() {
85        return responseSizeBytes;
86      }
87  
88      public void setResponseSizeBytes(long responseSizeBytes) {
89        this.responseSizeBytes = responseSizeBytes;
90      }
91  
92      public long getStartTime() {
93        return startTime;
94      }
95  
96      public void setStartTime(long startTime) {
97        this.startTime = startTime;
98      }
99  
100     public long getCallTimeMs() {
101       return callTimeMs;
102     }
103 
104     public void setCallTimeMs(long callTimeMs) {
105       this.callTimeMs = callTimeMs;
106     }
107 
108     public int getConcurrentCallsPerServer() {
109       return concurrentCallsPerServer;
110     }
111 
112     public void setConcurrentCallsPerServer(int callsPerServer) {
113       this.concurrentCallsPerServer = callsPerServer;
114     }
115   }
116 
117   @VisibleForTesting
118   protected static final class CallTracker {
119     private final String name;
120     @VisibleForTesting final Timer callTimer;
121     @VisibleForTesting final Histogram reqHist;
122     @VisibleForTesting final Histogram respHist;
123 
124     private CallTracker(MetricsRegistry registry, String name, String subName, String scope) {
125       StringBuilder sb = new StringBuilder(CLIENT_SVC).append("_").append(name);
126       if (subName != null) {
127         sb.append("(").append(subName).append(")");
128       }
129       this.name = sb.toString();
130       this.callTimer = registry.newTimer(MetricsConnection.class, DRTN_BASE + this.name, scope);
131       this.reqHist = registry.newHistogram(MetricsConnection.class, REQ_BASE + this.name, scope);
132       this.respHist = registry.newHistogram(MetricsConnection.class, RESP_BASE + this.name, scope);
133     }
134 
135     private CallTracker(MetricsRegistry registry, String name, String scope) {
136       this(registry, name, null, scope);
137     }
138 
139     public void updateRpc(CallStats stats) {
140       this.callTimer.update(stats.getCallTimeMs(), TimeUnit.MILLISECONDS);
141       this.reqHist.update(stats.getRequestSizeBytes());
142       this.respHist.update(stats.getResponseSizeBytes());
143     }
144 
145     @Override
146     public String toString() {
147       return "CallTracker:" + name;
148     }
149   }
150 
151   protected static class RegionStats {
152     final String name;
153     final Histogram memstoreLoadHist;
154     final Histogram heapOccupancyHist;
155 
156     public RegionStats(MetricsRegistry registry, String name) {
157       this.name = name;
158       this.memstoreLoadHist = registry.newHistogram(MetricsConnection.class,
159           MEMLOAD_BASE + this.name);
160       this.heapOccupancyHist = registry.newHistogram(MetricsConnection.class,
161           HEAP_BASE + this.name);
162     }
163 
164     public void update(ClientProtos.RegionLoadStats regionStatistics) {
165       this.memstoreLoadHist.update(regionStatistics.getMemstoreLoad());
166       this.heapOccupancyHist.update(regionStatistics.getHeapOccupancy());
167     }
168   }
169 
170   @VisibleForTesting
171   protected static class RunnerStats {
172     final Counter normalRunners;
173     final Counter delayRunners;
174     final Histogram delayIntevalHist;
175 
176     public RunnerStats(MetricsRegistry registry) {
177       this.normalRunners = registry.newCounter(MetricsConnection.class, "normalRunnersCount");
178       this.delayRunners = registry.newCounter(MetricsConnection.class, "delayRunnersCount");
179       this.delayIntevalHist = registry.newHistogram(MetricsConnection.class, "delayIntervalHist");
180     }
181 
182     public void incrNormalRunners() {
183       this.normalRunners.inc();
184     }
185 
186     public void incrDelayRunners() {
187       this.delayRunners.inc();
188     }
189 
190     public void updateDelayInterval(long interval) {
191       this.delayIntevalHist.update(interval);
192     }
193   }
194 
195   @VisibleForTesting
196   protected ConcurrentHashMap<ServerName, ConcurrentMap<byte[], RegionStats>> serverStats
197           = new ConcurrentHashMap<ServerName, ConcurrentMap<byte[], RegionStats>>();
198 
199   public void updateServerStats(ServerName serverName, byte[] regionName,
200                                 Object r) {
201     if (!(r instanceof Result)) {
202       return;
203     }
204     Result result = (Result) r;
205     ClientProtos.RegionLoadStats stats = result.getStats();
206     if (stats == null) {
207       return;
208     }
209     updateRegionStats(serverName, regionName, stats);
210   }
211 
212   @Override
213   public void updateRegionStats(ServerName serverName, byte[] regionName,
214     ClientProtos.RegionLoadStats stats) {
215     String name = serverName.getServerName() + "," + Bytes.toStringBinary(regionName);
216     ConcurrentMap<byte[], RegionStats> rsStats = null;
217     if (serverStats.containsKey(serverName)) {
218       rsStats = serverStats.get(serverName);
219     } else {
220       rsStats = serverStats.putIfAbsent(serverName,
221           new ConcurrentSkipListMap<byte[], RegionStats>(Bytes.BYTES_COMPARATOR));
222       if (rsStats == null) {
223         rsStats = serverStats.get(serverName);
224       }
225     }
226     RegionStats regionStats = null;
227     if (rsStats.containsKey(regionName)) {
228       regionStats = rsStats.get(regionName);
229     } else {
230       regionStats = rsStats.putIfAbsent(regionName, new RegionStats(this.registry, name));
231       if (regionStats == null) {
232         regionStats = rsStats.get(regionName);
233       }
234     }
235     regionStats.update(stats);
236   }
237 
238 
239   /** A lambda for dispatching to the appropriate metric factory method */
240   private static interface NewMetric<T> {
241     T newMetric(Class<?> clazz, String name, String scope);
242   }
243 
244   /** Anticipated number of metric entries */
245   private static final int CAPACITY = 50;
246   /** Default load factor from {@link java.util.HashMap#DEFAULT_LOAD_FACTOR} */
247   private static final float LOAD_FACTOR = 0.75f;
248   /**
249    * Anticipated number of concurrent accessor threads, from
250    * {@link ConnectionManager.HConnectionImplementation#getBatchPool()}
251    */
252   private static final int CONCURRENCY_LEVEL = 256;
253 
254   private final MetricsRegistry registry;
255   private final JmxReporter reporter;
256   private final String scope;
257 
258   private final NewMetric<Timer> timerFactory = new NewMetric<Timer>() {
259     @Override public Timer newMetric(Class<?> clazz, String name, String scope) {
260       return registry.newTimer(clazz, name, scope);
261     }
262   };
263 
264   private final NewMetric<Histogram> histogramFactory = new NewMetric<Histogram>() {
265     @Override public Histogram newMetric(Class<?> clazz, String name, String scope) {
266       return registry.newHistogram(clazz, name, scope);
267     }
268   };
269 
270   private final NewMetric<Counter> counterFactory = new NewMetric<Counter>() {
271     @Override public Counter newMetric(Class<?> clazz, String name, String scope) {
272       return registry.newCounter(clazz, name, scope);
273     }
274   };
275 
276   // static metrics
277 
278   @VisibleForTesting protected final Counter metaCacheHits;
279   @VisibleForTesting protected final Counter metaCacheMisses;
280   @VisibleForTesting protected final CallTracker getTracker;
281   @VisibleForTesting protected final CallTracker scanTracker;
282   @VisibleForTesting protected final CallTracker appendTracker;
283   @VisibleForTesting protected final CallTracker deleteTracker;
284   @VisibleForTesting protected final CallTracker incrementTracker;
285   @VisibleForTesting protected final CallTracker putTracker;
286   @VisibleForTesting protected final CallTracker multiTracker;
287   @VisibleForTesting protected final RunnerStats runnerStats;
288   @VisibleForTesting protected final Counter metaCacheNumClearServer;
289   @VisibleForTesting protected final Counter metaCacheNumClearRegion;
290   @VisibleForTesting protected final Counter hedgedReadOps;
291   @VisibleForTesting protected final Counter hedgedReadWin;
292   @VisibleForTesting protected final Histogram concurrentCallsPerServerHist;
293 
294   // dynamic metrics
295 
296   // These maps are used to cache references to the metric instances that are managed by the
297   // registry. I don't think their use perfectly removes redundant allocations, but it's
298   // a big improvement over calling registry.newMetric each time.
299   @VisibleForTesting protected final ConcurrentMap<String, Timer> rpcTimers =
300       new ConcurrentHashMap<>(CAPACITY, LOAD_FACTOR, CONCURRENCY_LEVEL);
301   @VisibleForTesting protected final ConcurrentMap<String, Histogram> rpcHistograms =
302       new ConcurrentHashMap<>(CAPACITY * 2 /* tracking both request and response sizes */,
303           LOAD_FACTOR, CONCURRENCY_LEVEL);
304   private final ConcurrentMap<String, Counter> cacheDroppingExceptions =
305     new ConcurrentHashMap<>(CAPACITY, LOAD_FACTOR, CONCURRENCY_LEVEL);
306 
307   public MetricsConnection(final ConnectionManager.HConnectionImplementation conn) {
308     this.scope = conn.toString();
309     this.registry = new MetricsRegistry();
310 
311     this.registry.newGauge(getExecutorPoolName(),
312         new RatioGauge() {
313           @Override protected double getNumerator() {
314             ThreadPoolExecutor batchPool = (ThreadPoolExecutor) conn.getCurrentBatchPool();
315             if (batchPool == null) {
316               return 0;
317             }
318             return batchPool.getActiveCount();
319           }
320           @Override protected double getDenominator() {
321             ThreadPoolExecutor batchPool = (ThreadPoolExecutor) conn.getCurrentBatchPool();
322             if (batchPool == null) {
323               return 0;
324             }
325             return batchPool.getMaximumPoolSize();
326           }
327         });
328     this.registry.newGauge(getMetaPoolName(),
329         new RatioGauge() {
330           @Override protected double getNumerator() {
331             ThreadPoolExecutor metaPool = (ThreadPoolExecutor) conn.getCurrentMetaLookupPool();
332             if (metaPool == null) {
333               return 0;
334             }
335             return metaPool.getActiveCount();
336           }
337           @Override protected double getDenominator() {
338             ThreadPoolExecutor metaPool = (ThreadPoolExecutor) conn.getCurrentMetaLookupPool();
339             if (metaPool == null) {
340               return 0;
341             }
342             return metaPool.getMaximumPoolSize();
343           }
344         });
345     this.metaCacheHits = registry.newCounter(this.getClass(), "metaCacheHits", scope);
346     this.metaCacheMisses = registry.newCounter(this.getClass(), "metaCacheMisses", scope);
347     this.metaCacheNumClearServer = registry.newCounter(this.getClass(),
348       "metaCacheNumClearServer", scope);
349     this.metaCacheNumClearRegion = registry.newCounter(this.getClass(),
350       "metaCacheNumClearRegion", scope);
351     this.hedgedReadOps = registry.newCounter(this.getClass(), "hedgedReadOps", scope);
352     this.hedgedReadWin = registry.newCounter(this.getClass(), "hedgedReadWin", scope);
353     this.getTracker = new CallTracker(this.registry, "Get", scope);
354     this.scanTracker = new CallTracker(this.registry, "Scan", scope);
355     this.appendTracker = new CallTracker(this.registry, "Mutate", "Append", scope);
356     this.deleteTracker = new CallTracker(this.registry, "Mutate", "Delete", scope);
357     this.incrementTracker = new CallTracker(this.registry, "Mutate", "Increment", scope);
358     this.putTracker = new CallTracker(this.registry, "Mutate", "Put", scope);
359     this.multiTracker = new CallTracker(this.registry, "Multi", scope);
360     this.runnerStats = new RunnerStats(this.registry);
361     this.concurrentCallsPerServerHist = registry.newHistogram(this.getClass(), 
362       "concurrentCallsPerServer", scope);
363 
364     this.reporter = new JmxReporter(this.registry);
365     this.reporter.start();
366   }
367 
368   @VisibleForTesting
369   final MetricName getExecutorPoolName() {
370     return new MetricName(getClass(), "executorPoolActiveThreads", scope);
371   }
372 
373   @VisibleForTesting
374   final MetricName getMetaPoolName() {
375     return new MetricName(getClass(), "metaPoolActiveThreads", scope);
376   }
377 
378   @VisibleForTesting
379   MetricsRegistry getMetricsRegistry() {
380     return registry;
381   }
382 
383   public void shutdown() {
384     this.reporter.shutdown();
385     this.registry.shutdown();
386   }
387 
388   /** Produce an instance of {@link CallStats} for clients to attach to RPCs. */
389   public static CallStats newCallStats() {
390     // TODO: instance pool to reduce GC?
391     return new CallStats();
392   }
393 
394   /** Increment the number of meta cache hits. */
395   public void incrMetaCacheHit() {
396     metaCacheHits.inc();
397   }
398 
399   /** Increment the number of meta cache misses. */
400   public void incrMetaCacheMiss() {
401     metaCacheMisses.inc();
402   }
403 
404   /** Increment the number of meta cache drops requested for entire RegionServer. */
405   public void incrMetaCacheNumClearServer() {
406     metaCacheNumClearServer.inc();
407   }
408 
409   /** Increment the number of meta cache drops requested for individual region. */
410   public void incrMetaCacheNumClearRegion() {
411     metaCacheNumClearRegion.inc();
412   }
413 
414   /** Increment the number of hedged read that have occurred. */
415   public void incrHedgedReadOps() {
416     hedgedReadOps.inc();
417   }
418 
419   /** Increment the number of hedged read returned faster than the original read. */
420   public void incrHedgedReadWin() {
421     hedgedReadWin.inc();
422   }
423 
424   /** Increment the number of normal runner counts. */
425   public void incrNormalRunners() {
426     this.runnerStats.incrNormalRunners();
427   }
428 
429   /** Increment the number of delay runner counts. */
430   public void incrDelayRunners() {
431     this.runnerStats.incrDelayRunners();
432   }
433 
434   /** Update delay interval of delay runner. */
435   public void updateDelayInterval(long interval) {
436     this.runnerStats.updateDelayInterval(interval);
437   }
438 
439   /**
440    * Get a metric for {@code key} from {@code map}, or create it with {@code factory}.
441    */
442   private <T> T getMetric(String key, ConcurrentMap<String, T> map, NewMetric<T> factory) {
443     T t = map.get(key);
444     if (t == null) {
445       t = factory.newMetric(this.getClass(), key, scope);
446       T tmp = map.putIfAbsent(key, t);
447       t = (tmp == null) ? t : tmp;
448     }
449     return t;
450   }
451 
452   /** Update call stats for non-critical-path methods */
453   private void updateRpcGeneric(MethodDescriptor method, CallStats stats) {
454     final String methodName = method.getService().getName() + "_" + method.getName();
455     getMetric(DRTN_BASE + methodName, rpcTimers, timerFactory)
456         .update(stats.getCallTimeMs(), TimeUnit.MILLISECONDS);
457     getMetric(REQ_BASE + methodName, rpcHistograms, histogramFactory)
458         .update(stats.getRequestSizeBytes());
459     getMetric(RESP_BASE + methodName, rpcHistograms, histogramFactory)
460         .update(stats.getResponseSizeBytes());
461   }
462 
463   /** Report RPC context to metrics system. */
464   public void updateRpc(MethodDescriptor method, Message param, CallStats stats) {
465     int callsPerServer = stats.getConcurrentCallsPerServer();
466     if (callsPerServer > 0) {
467       concurrentCallsPerServerHist.update(callsPerServer);
468     }
469     // this implementation is tied directly to protobuf implementation details. would be better
470     // if we could dispatch based on something static, ie, request Message type.
471     if (method.getService() == ClientService.getDescriptor()) {
472       switch(method.getIndex()) {
473       case 0:
474         assert "Get".equals(method.getName());
475         getTracker.updateRpc(stats);
476         return;
477       case 1:
478         assert "Mutate".equals(method.getName());
479         final MutationType mutationType = ((MutateRequest) param).getMutation().getMutateType();
480         switch(mutationType) {
481         case APPEND:
482           appendTracker.updateRpc(stats);
483           return;
484         case DELETE:
485           deleteTracker.updateRpc(stats);
486           return;
487         case INCREMENT:
488           incrementTracker.updateRpc(stats);
489           return;
490         case PUT:
491           putTracker.updateRpc(stats);
492           return;
493         default:
494           throw new RuntimeException("Unrecognized mutation type " + mutationType);
495         }
496       case 2:
497         assert "Scan".equals(method.getName());
498         scanTracker.updateRpc(stats);
499         return;
500       case 3:
501         assert "BulkLoadHFile".equals(method.getName());
502         // use generic implementation
503         break;
504       case 4:
505         assert "ExecService".equals(method.getName());
506         // use generic implementation
507         break;
508       case 5:
509         assert "ExecRegionServerService".equals(method.getName());
510         // use generic implementation
511         break;
512       case 6:
513         assert "Multi".equals(method.getName());
514         multiTracker.updateRpc(stats);
515         return;
516       default:
517         throw new RuntimeException("Unrecognized ClientService RPC type " + method.getFullName());
518       }
519     }
520     // Fallback to dynamic registry lookup for DDL methods.
521     updateRpcGeneric(method, stats);
522   }
523 
524   public void incrCacheDroppingExceptions(Object exception) {
525     getMetric(CACHE_BASE +
526       (exception == null? UNKNOWN_EXCEPTION : exception.getClass().getSimpleName()),
527       cacheDroppingExceptions, counterFactory).inc();
528   }
529 }