All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.hadoop.hbase.client.MetricsConnection Maven / Gradle / Ivy

The newest version!
/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.hadoop.hbase.client;

import com.google.common.annotations.VisibleForTesting;
import com.google.protobuf.Descriptors.MethodDescriptor;
import com.google.protobuf.Message;
import com.yammer.metrics.core.Counter;
import com.yammer.metrics.core.Histogram;
import com.yammer.metrics.core.MetricName;
import com.yammer.metrics.core.MetricsRegistry;
import com.yammer.metrics.core.Timer;
import com.yammer.metrics.reporting.JmxReporter;
import com.yammer.metrics.util.RatioGauge;
import org.apache.hadoop.hbase.ServerName;
import org.apache.hadoop.hbase.classification.InterfaceAudience;
import org.apache.hadoop.hbase.protobuf.generated.ClientProtos;
import org.apache.hadoop.hbase.protobuf.generated.ClientProtos.ClientService;
import org.apache.hadoop.hbase.protobuf.generated.ClientProtos.MutateRequest;
import org.apache.hadoop.hbase.protobuf.generated.ClientProtos.MutationProto.MutationType;
import org.apache.hadoop.hbase.util.Bytes;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * This class is for maintaining the various connection statistics and publishing them through
 * the metrics interfaces.
 *
 * This class manages its own {@link MetricsRegistry} and {@link JmxReporter} so as to not
 * conflict with other uses of Yammer Metrics within the client application. Instantiating
 * this class implicitly creates and "starts" instances of these classes; be sure to call
 * {@link #shutdown()} to terminate the thread pools they allocate.
 */
@InterfaceAudience.Private
public class MetricsConnection implements StatisticTrackable {

  /** Set this key to {@code true} to enable metrics collection of client requests. */
  public static final String CLIENT_SIDE_METRICS_ENABLED_KEY = "hbase.client.metrics.enable";

  private static final String DRTN_BASE = "rpcCallDurationMs_";
  private static final String REQ_BASE = "rpcCallRequestSizeBytes_";
  private static final String RESP_BASE = "rpcCallResponseSizeBytes_";
  private static final String MEMLOAD_BASE = "memstoreLoad_";
  private static final String HEAP_BASE = "heapOccupancy_";
  private static final String CACHE_BASE = "cacheDroppingExceptions_";
  private static final String UNKNOWN_EXCEPTION = "UnknownException";
  private static final String CLIENT_SVC = ClientService.getDescriptor().getName();

  /** A container class for collecting details about the RPC call as it percolates. */
  public static class CallStats {
    private long requestSizeBytes = 0;
    private long responseSizeBytes = 0;
    private long startTime = 0;
    private long callTimeMs = 0;
    private int concurrentCallsPerServer = 0;

    public long getRequestSizeBytes() {
      return requestSizeBytes;
    }

    public void setRequestSizeBytes(long requestSizeBytes) {
      this.requestSizeBytes = requestSizeBytes;
    }

    public long getResponseSizeBytes() {
      return responseSizeBytes;
    }

    public void setResponseSizeBytes(long responseSizeBytes) {
      this.responseSizeBytes = responseSizeBytes;
    }

    public long getStartTime() {
      return startTime;
    }

    public void setStartTime(long startTime) {
      this.startTime = startTime;
    }

    public long getCallTimeMs() {
      return callTimeMs;
    }

    public void setCallTimeMs(long callTimeMs) {
      this.callTimeMs = callTimeMs;
    }

    public int getConcurrentCallsPerServer() {
      return concurrentCallsPerServer;
    }

    public void setConcurrentCallsPerServer(int callsPerServer) {
      this.concurrentCallsPerServer = callsPerServer;
    }
  }

  @VisibleForTesting
  protected static final class CallTracker {
    private final String name;
    @VisibleForTesting final Timer callTimer;
    @VisibleForTesting final Histogram reqHist;
    @VisibleForTesting final Histogram respHist;

    private CallTracker(MetricsRegistry registry, String name, String subName, String scope) {
      StringBuilder sb = new StringBuilder(CLIENT_SVC).append("_").append(name);
      if (subName != null) {
        sb.append("(").append(subName).append(")");
      }
      this.name = sb.toString();
      this.callTimer = registry.newTimer(MetricsConnection.class, DRTN_BASE + this.name, scope);
      this.reqHist = registry.newHistogram(MetricsConnection.class, REQ_BASE + this.name, scope);
      this.respHist = registry.newHistogram(MetricsConnection.class, RESP_BASE + this.name, scope);
    }

    private CallTracker(MetricsRegistry registry, String name, String scope) {
      this(registry, name, null, scope);
    }

    public void updateRpc(CallStats stats) {
      this.callTimer.update(stats.getCallTimeMs(), TimeUnit.MILLISECONDS);
      this.reqHist.update(stats.getRequestSizeBytes());
      this.respHist.update(stats.getResponseSizeBytes());
    }

    @Override
    public String toString() {
      return "CallTracker:" + name;
    }
  }

  protected static class RegionStats {
    final String name;
    final Histogram memstoreLoadHist;
    final Histogram heapOccupancyHist;

    public RegionStats(MetricsRegistry registry, String name) {
      this.name = name;
      this.memstoreLoadHist = registry.newHistogram(MetricsConnection.class,
          MEMLOAD_BASE + this.name);
      this.heapOccupancyHist = registry.newHistogram(MetricsConnection.class,
          HEAP_BASE + this.name);
    }

    public void update(ClientProtos.RegionLoadStats regionStatistics) {
      this.memstoreLoadHist.update(regionStatistics.getMemstoreLoad());
      this.heapOccupancyHist.update(regionStatistics.getHeapOccupancy());
    }
  }

  @VisibleForTesting
  protected static class RunnerStats {
    final Counter normalRunners;
    final Counter delayRunners;
    final Histogram delayIntevalHist;

    public RunnerStats(MetricsRegistry registry) {
      this.normalRunners = registry.newCounter(MetricsConnection.class, "normalRunnersCount");
      this.delayRunners = registry.newCounter(MetricsConnection.class, "delayRunnersCount");
      this.delayIntevalHist = registry.newHistogram(MetricsConnection.class, "delayIntervalHist");
    }

    public void incrNormalRunners() {
      this.normalRunners.inc();
    }

    public void incrDelayRunners() {
      this.delayRunners.inc();
    }

    public void updateDelayInterval(long interval) {
      this.delayIntevalHist.update(interval);
    }
  }

  @VisibleForTesting
  protected ConcurrentHashMap> serverStats
          = new ConcurrentHashMap>();

  public void updateServerStats(ServerName serverName, byte[] regionName,
                                Object r) {
    if (!(r instanceof Result)) {
      return;
    }
    Result result = (Result) r;
    ClientProtos.RegionLoadStats stats = result.getStats();
    if (stats == null) {
      return;
    }
    updateRegionStats(serverName, regionName, stats);
  }

  @Override
  public void updateRegionStats(ServerName serverName, byte[] regionName,
    ClientProtos.RegionLoadStats stats) {
    String name = serverName.getServerName() + "," + Bytes.toStringBinary(regionName);
    ConcurrentMap rsStats = null;
    if (serverStats.containsKey(serverName)) {
      rsStats = serverStats.get(serverName);
    } else {
      rsStats = serverStats.putIfAbsent(serverName,
          new ConcurrentSkipListMap(Bytes.BYTES_COMPARATOR));
      if (rsStats == null) {
        rsStats = serverStats.get(serverName);
      }
    }
    RegionStats regionStats = null;
    if (rsStats.containsKey(regionName)) {
      regionStats = rsStats.get(regionName);
    } else {
      regionStats = rsStats.putIfAbsent(regionName, new RegionStats(this.registry, name));
      if (regionStats == null) {
        regionStats = rsStats.get(regionName);
      }
    }
    regionStats.update(stats);
  }


  /** A lambda for dispatching to the appropriate metric factory method */
  private static interface NewMetric {
    T newMetric(Class clazz, String name, String scope);
  }

  /** Anticipated number of metric entries */
  private static final int CAPACITY = 50;
  /** Default load factor from {@link java.util.HashMap#DEFAULT_LOAD_FACTOR} */
  private static final float LOAD_FACTOR = 0.75f;
  /**
   * Anticipated number of concurrent accessor threads, from
   * {@link ConnectionManager.HConnectionImplementation#getBatchPool()}
   */
  private static final int CONCURRENCY_LEVEL = 256;

  private final MetricsRegistry registry;
  private final JmxReporter reporter;
  private final String scope;

  private final NewMetric timerFactory = new NewMetric() {
    @Override public Timer newMetric(Class clazz, String name, String scope) {
      return registry.newTimer(clazz, name, scope);
    }
  };

  private final NewMetric histogramFactory = new NewMetric() {
    @Override public Histogram newMetric(Class clazz, String name, String scope) {
      return registry.newHistogram(clazz, name, scope);
    }
  };

  private final NewMetric counterFactory = new NewMetric() {
    @Override public Counter newMetric(Class clazz, String name, String scope) {
      return registry.newCounter(clazz, name, scope);
    }
  };

  // static metrics

  @VisibleForTesting protected final Counter metaCacheHits;
  @VisibleForTesting protected final Counter metaCacheMisses;
  @VisibleForTesting protected final CallTracker getTracker;
  @VisibleForTesting protected final CallTracker scanTracker;
  @VisibleForTesting protected final CallTracker appendTracker;
  @VisibleForTesting protected final CallTracker deleteTracker;
  @VisibleForTesting protected final CallTracker incrementTracker;
  @VisibleForTesting protected final CallTracker putTracker;
  @VisibleForTesting protected final CallTracker multiTracker;
  @VisibleForTesting protected final RunnerStats runnerStats;
  @VisibleForTesting protected final Counter metaCacheNumClearServer;
  @VisibleForTesting protected final Counter metaCacheNumClearRegion;
  @VisibleForTesting protected final Counter hedgedReadOps;
  @VisibleForTesting protected final Counter hedgedReadWin;
  @VisibleForTesting protected final Histogram concurrentCallsPerServerHist;

  // dynamic metrics

  // These maps are used to cache references to the metric instances that are managed by the
  // registry. I don't think their use perfectly removes redundant allocations, but it's
  // a big improvement over calling registry.newMetric each time.
  @VisibleForTesting protected final ConcurrentMap rpcTimers =
      new ConcurrentHashMap<>(CAPACITY, LOAD_FACTOR, CONCURRENCY_LEVEL);
  @VisibleForTesting protected final ConcurrentMap rpcHistograms =
      new ConcurrentHashMap<>(CAPACITY * 2 /* tracking both request and response sizes */,
          LOAD_FACTOR, CONCURRENCY_LEVEL);
  private final ConcurrentMap cacheDroppingExceptions =
    new ConcurrentHashMap<>(CAPACITY, LOAD_FACTOR, CONCURRENCY_LEVEL);

  public MetricsConnection(final ConnectionManager.HConnectionImplementation conn) {
    this.scope = conn.toString();
    this.registry = new MetricsRegistry();

    this.registry.newGauge(getExecutorPoolName(),
        new RatioGauge() {
          @Override protected double getNumerator() {
            ThreadPoolExecutor batchPool = (ThreadPoolExecutor) conn.getCurrentBatchPool();
            if (batchPool == null) {
              return 0;
            }
            return batchPool.getActiveCount();
          }
          @Override protected double getDenominator() {
            ThreadPoolExecutor batchPool = (ThreadPoolExecutor) conn.getCurrentBatchPool();
            if (batchPool == null) {
              return 0;
            }
            return batchPool.getMaximumPoolSize();
          }
        });
    this.registry.newGauge(getMetaPoolName(),
        new RatioGauge() {
          @Override protected double getNumerator() {
            ThreadPoolExecutor metaPool = (ThreadPoolExecutor) conn.getCurrentMetaLookupPool();
            if (metaPool == null) {
              return 0;
            }
            return metaPool.getActiveCount();
          }
          @Override protected double getDenominator() {
            ThreadPoolExecutor metaPool = (ThreadPoolExecutor) conn.getCurrentMetaLookupPool();
            if (metaPool == null) {
              return 0;
            }
            return metaPool.getMaximumPoolSize();
          }
        });
    this.metaCacheHits = registry.newCounter(this.getClass(), "metaCacheHits", scope);
    this.metaCacheMisses = registry.newCounter(this.getClass(), "metaCacheMisses", scope);
    this.metaCacheNumClearServer = registry.newCounter(this.getClass(),
      "metaCacheNumClearServer", scope);
    this.metaCacheNumClearRegion = registry.newCounter(this.getClass(),
      "metaCacheNumClearRegion", scope);
    this.hedgedReadOps = registry.newCounter(this.getClass(), "hedgedReadOps", scope);
    this.hedgedReadWin = registry.newCounter(this.getClass(), "hedgedReadWin", scope);
    this.getTracker = new CallTracker(this.registry, "Get", scope);
    this.scanTracker = new CallTracker(this.registry, "Scan", scope);
    this.appendTracker = new CallTracker(this.registry, "Mutate", "Append", scope);
    this.deleteTracker = new CallTracker(this.registry, "Mutate", "Delete", scope);
    this.incrementTracker = new CallTracker(this.registry, "Mutate", "Increment", scope);
    this.putTracker = new CallTracker(this.registry, "Mutate", "Put", scope);
    this.multiTracker = new CallTracker(this.registry, "Multi", scope);
    this.runnerStats = new RunnerStats(this.registry);
    this.concurrentCallsPerServerHist = registry.newHistogram(this.getClass(), 
      "concurrentCallsPerServer", scope);

    this.reporter = new JmxReporter(this.registry);
    this.reporter.start();
  }

  @VisibleForTesting
  final MetricName getExecutorPoolName() {
    return new MetricName(getClass(), "executorPoolActiveThreads", scope);
  }

  @VisibleForTesting
  final MetricName getMetaPoolName() {
    return new MetricName(getClass(), "metaPoolActiveThreads", scope);
  }

  @VisibleForTesting
  MetricsRegistry getMetricsRegistry() {
    return registry;
  }

  public void shutdown() {
    this.reporter.shutdown();
    this.registry.shutdown();
  }

  /** Produce an instance of {@link CallStats} for clients to attach to RPCs. */
  public static CallStats newCallStats() {
    // TODO: instance pool to reduce GC?
    return new CallStats();
  }

  /** Increment the number of meta cache hits. */
  public void incrMetaCacheHit() {
    metaCacheHits.inc();
  }

  /** Increment the number of meta cache misses. */
  public void incrMetaCacheMiss() {
    metaCacheMisses.inc();
  }

  /** Increment the number of meta cache drops requested for entire RegionServer. */
  public void incrMetaCacheNumClearServer() {
    metaCacheNumClearServer.inc();
  }

  /** Increment the number of meta cache drops requested for individual region. */
  public void incrMetaCacheNumClearRegion() {
    metaCacheNumClearRegion.inc();
  }

  /** Increment the number of hedged read that have occurred. */
  public void incrHedgedReadOps() {
    hedgedReadOps.inc();
  }

  /** Increment the number of hedged read returned faster than the original read. */
  public void incrHedgedReadWin() {
    hedgedReadWin.inc();
  }

  /** Increment the number of normal runner counts. */
  public void incrNormalRunners() {
    this.runnerStats.incrNormalRunners();
  }

  /** Increment the number of delay runner counts. */
  public void incrDelayRunners() {
    this.runnerStats.incrDelayRunners();
  }

  /** Update delay interval of delay runner. */
  public void updateDelayInterval(long interval) {
    this.runnerStats.updateDelayInterval(interval);
  }

  /**
   * Get a metric for {@code key} from {@code map}, or create it with {@code factory}.
   */
  private  T getMetric(String key, ConcurrentMap map, NewMetric factory) {
    T t = map.get(key);
    if (t == null) {
      t = factory.newMetric(this.getClass(), key, scope);
      T tmp = map.putIfAbsent(key, t);
      t = (tmp == null) ? t : tmp;
    }
    return t;
  }

  /** Update call stats for non-critical-path methods */
  private void updateRpcGeneric(MethodDescriptor method, CallStats stats) {
    final String methodName = method.getService().getName() + "_" + method.getName();
    getMetric(DRTN_BASE + methodName, rpcTimers, timerFactory)
        .update(stats.getCallTimeMs(), TimeUnit.MILLISECONDS);
    getMetric(REQ_BASE + methodName, rpcHistograms, histogramFactory)
        .update(stats.getRequestSizeBytes());
    getMetric(RESP_BASE + methodName, rpcHistograms, histogramFactory)
        .update(stats.getResponseSizeBytes());
  }

  /** Report RPC context to metrics system. */
  public void updateRpc(MethodDescriptor method, Message param, CallStats stats) {
    int callsPerServer = stats.getConcurrentCallsPerServer();
    if (callsPerServer > 0) {
      concurrentCallsPerServerHist.update(callsPerServer);
    }
    // this implementation is tied directly to protobuf implementation details. would be better
    // if we could dispatch based on something static, ie, request Message type.
    if (method.getService() == ClientService.getDescriptor()) {
      switch(method.getIndex()) {
      case 0:
        assert "Get".equals(method.getName());
        getTracker.updateRpc(stats);
        return;
      case 1:
        assert "Mutate".equals(method.getName());
        final MutationType mutationType = ((MutateRequest) param).getMutation().getMutateType();
        switch(mutationType) {
        case APPEND:
          appendTracker.updateRpc(stats);
          return;
        case DELETE:
          deleteTracker.updateRpc(stats);
          return;
        case INCREMENT:
          incrementTracker.updateRpc(stats);
          return;
        case PUT:
          putTracker.updateRpc(stats);
          return;
        default:
          throw new RuntimeException("Unrecognized mutation type " + mutationType);
        }
      case 2:
        assert "Scan".equals(method.getName());
        scanTracker.updateRpc(stats);
        return;
      case 3:
        assert "BulkLoadHFile".equals(method.getName());
        // use generic implementation
        break;
      case 4:
        assert "ExecService".equals(method.getName());
        // use generic implementation
        break;
      case 5:
        assert "ExecRegionServerService".equals(method.getName());
        // use generic implementation
        break;
      case 6:
        assert "Multi".equals(method.getName());
        multiTracker.updateRpc(stats);
        return;
      default:
        throw new RuntimeException("Unrecognized ClientService RPC type " + method.getFullName());
      }
    }
    // Fallback to dynamic registry lookup for DDL methods.
    updateRpcGeneric(method, stats);
  }

  public void incrCacheDroppingExceptions(Object exception) {
    getMetric(CACHE_BASE +
      (exception == null? UNKNOWN_EXCEPTION : exception.getClass().getSimpleName()),
      cacheDroppingExceptions, counterFactory).inc();
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy