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