001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
003 * agreements. See the NOTICE file distributed with this work for additional information regarding
004 * copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the
005 * "License"); you may not use this file except in compliance with the License. You may obtain a
006 * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable
007 * law or agreed to in writing, software distributed under the License is distributed on an "AS IS"
008 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
009 * for the specific language governing permissions and limitations under the License.
010 */
011
012package org.apache.hadoop.hbase.coprocessor;
013
014import java.io.IOException;
015import java.nio.charset.StandardCharsets;
016import java.util.List;
017import java.util.Map;
018import java.util.Optional;
019import java.util.Set;
020import java.util.concurrent.ConcurrentHashMap;
021import org.apache.hadoop.hbase.Cell;
022import org.apache.hadoop.hbase.CoprocessorEnvironment;
023import org.apache.hadoop.hbase.TableName;
024import org.apache.hadoop.hbase.client.Delete;
025import org.apache.hadoop.hbase.client.Durability;
026import org.apache.hadoop.hbase.client.Get;
027import org.apache.hadoop.hbase.client.Put;
028import org.apache.hadoop.hbase.client.Row;
029import org.apache.hadoop.hbase.ipc.RpcServer;
030import org.apache.hadoop.hbase.metrics.Meter;
031import org.apache.hadoop.hbase.metrics.Metric;
032import org.apache.hadoop.hbase.metrics.MetricRegistry;
033import org.apache.hadoop.hbase.util.LossyCounting;
034import org.apache.hadoop.hbase.wal.WALEdit;
035import org.apache.yetus.audience.InterfaceAudience;
036import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableMap;
037
038
039/**
040 * A coprocessor that collects metrics from meta table.
041 * <p>
042 * These metrics will be available through the regular Hadoop metrics2 sinks (ganglia, opentsdb,
043 * etc) as well as JMX output.
044 * </p>
045 * @see MetaTableMetrics
046 */
047
048@InterfaceAudience.Private
049public class MetaTableMetrics implements RegionCoprocessor {
050
051  private ExampleRegionObserverMeta observer;
052  private Map<String, Optional<Metric>> requestsMap;
053  private RegionCoprocessorEnvironment regionCoprocessorEnv;
054  private LossyCounting clientMetricsLossyCounting;
055  private boolean active = false;
056
057  enum MetaTableOps {
058    GET, PUT, DELETE;
059  }
060
061  private ImmutableMap<Class, MetaTableOps> opsNameMap =
062      ImmutableMap.<Class, MetaTableOps>builder()
063              .put(Put.class, MetaTableOps.PUT)
064              .put(Get.class, MetaTableOps.GET)
065              .put(Delete.class, MetaTableOps.DELETE)
066              .build();
067
068  class ExampleRegionObserverMeta implements RegionCoprocessor, RegionObserver {
069
070    @Override
071    public Optional<RegionObserver> getRegionObserver() {
072      return Optional.of(this);
073    }
074
075    @Override
076    public void preGetOp(ObserverContext<RegionCoprocessorEnvironment> e, Get get,
077        List<Cell> results) throws IOException {
078      if (!active || !isMetaTableOp(e)) {
079        return;
080      }
081      tableMetricRegisterAndMark(e, get);
082      clientMetricRegisterAndMark(e);
083      regionMetricRegisterAndMark(e, get);
084      opMetricRegisterAndMark(e, get);
085      opWithClientMetricRegisterAndMark(e, get);
086    }
087
088    @Override
089    public void prePut(ObserverContext<RegionCoprocessorEnvironment> e, Put put, WALEdit edit,
090        Durability durability) throws IOException {
091      if (!active || !isMetaTableOp(e)) {
092        return;
093      }
094      tableMetricRegisterAndMark(e, put);
095      clientMetricRegisterAndMark(e);
096      regionMetricRegisterAndMark(e, put);
097      opMetricRegisterAndMark(e, put);
098      opWithClientMetricRegisterAndMark(e, put);
099    }
100
101    @Override
102    public void preDelete(ObserverContext<RegionCoprocessorEnvironment> e, Delete delete,
103        WALEdit edit, Durability durability) throws IOException {
104      if (!active || !isMetaTableOp(e)) {
105        return;
106      }
107      tableMetricRegisterAndMark(e, delete);
108      clientMetricRegisterAndMark(e);
109      regionMetricRegisterAndMark(e, delete);
110      opMetricRegisterAndMark(e, delete);
111      opWithClientMetricRegisterAndMark(e, delete);
112    }
113
114    private void markMeterIfPresent(String requestMeter) {
115      if (requestMeter.isEmpty()) {
116        return;
117      }
118      Metric metric =
119          requestsMap.get(requestMeter).isPresent() ? requestsMap.get(requestMeter).get() : null;
120      if (metric != null) {
121        ((Meter) metric).mark();
122      }
123    }
124
125    private void registerMeterIfNotPresent(ObserverContext<RegionCoprocessorEnvironment> e,
126        String requestMeter) {
127      if (requestMeter.isEmpty()) {
128        return;
129      }
130      if (!requestsMap.containsKey(requestMeter)) {
131        MetricRegistry registry = regionCoprocessorEnv.getMetricRegistryForRegionServer();
132        registry.meter(requestMeter);
133        requestsMap.put(requestMeter, registry.get(requestMeter));
134      }
135    }
136
137    /**
138     * Registers and counts lossyCount for Meters that kept by lossy counting.
139     * By using lossy count to maintain meters, at most 7 / e meters will be kept  (e is error rate)
140     * e.g. when e is 0.02 by default, at most 50 Clients request metrics will be kept
141     *      also, all kept elements have frequency higher than e * N. (N is total count)
142     * @param e Region coprocessor environment
143     * @param requestMeter meter to be registered
144     * @param lossyCounting lossyCounting object for one type of meters.
145     */
146    private void registerLossyCountingMeterIfNotPresent(
147        ObserverContext<RegionCoprocessorEnvironment> e,
148        String requestMeter, LossyCounting lossyCounting) {
149      if (requestMeter.isEmpty()) {
150        return;
151      }
152      Set<String> metersToBeRemoved = lossyCounting.addByOne(requestMeter);
153      if(!requestsMap.containsKey(requestMeter) && metersToBeRemoved.contains(requestMeter)){
154        for(String meter: metersToBeRemoved) {
155          //cleanup requestsMap according swept data from lossy count;
156          requestsMap.remove(meter);
157          MetricRegistry registry = regionCoprocessorEnv.getMetricRegistryForRegionServer();
158          registry.remove(meter);
159        }
160        // newly added meter is swept by lossy counting cleanup. No need to put it into requestsMap.
161        return;
162      }
163
164      if (!requestsMap.containsKey(requestMeter)) {
165        MetricRegistry registry = regionCoprocessorEnv.getMetricRegistryForRegionServer();
166        registry.meter(requestMeter);
167        requestsMap.put(requestMeter, registry.get(requestMeter));
168      }
169    }
170
171    /**
172     * Get table name from Ops such as: get, put, delete.
173     * @param op such as get, put or delete.
174     */
175    private String getTableNameFromOp(Row op) {
176      String tableName = null;
177      String tableRowKey = new String(((Row) op).getRow(), StandardCharsets.UTF_8);
178      if (tableRowKey.isEmpty()) {
179        return null;
180      }
181      tableName = tableRowKey.split(",").length > 0 ? tableRowKey.split(",")[0] : null;
182      return tableName;
183    }
184
185    /**
186     * Get regionId from Ops such as: get, put, delete.
187     * @param op  such as get, put or delete.
188     */
189    private String getRegionIdFromOp(Row op) {
190      String regionId = null;
191      String tableRowKey = new String(((Row) op).getRow(), StandardCharsets.UTF_8);
192      if (tableRowKey.isEmpty()) {
193        return null;
194      }
195      regionId = tableRowKey.split(",").length > 2 ? tableRowKey.split(",")[2] : null;
196      return regionId;
197    }
198
199    private boolean isMetaTableOp(ObserverContext<RegionCoprocessorEnvironment> e) {
200      return TableName.META_TABLE_NAME.toString()
201          .equals(new String(e.getEnvironment().getRegionInfo().getTable().getName(),
202              StandardCharsets.UTF_8));
203    }
204
205    private void clientMetricRegisterAndMark(ObserverContext<RegionCoprocessorEnvironment> e) {
206      String clientIP = RpcServer.getRemoteIp() != null ? RpcServer.getRemoteIp().toString() : "";
207
208      String clientRequestMeter = clientRequestMeterName(clientIP);
209      registerLossyCountingMeterIfNotPresent(e, clientRequestMeter, clientMetricsLossyCounting);
210      markMeterIfPresent(clientRequestMeter);
211    }
212
213    private void tableMetricRegisterAndMark(ObserverContext<RegionCoprocessorEnvironment> e,
214        Row op) {
215      // Mark the meta table meter whenever the coprocessor is called
216      String tableName = getTableNameFromOp(op);
217      String tableRequestMeter = tableMeterName(tableName);
218      registerMeterIfNotPresent(e, tableRequestMeter);
219      markMeterIfPresent(tableRequestMeter);
220    }
221
222    private void regionMetricRegisterAndMark(ObserverContext<RegionCoprocessorEnvironment> e,
223        Row op) {
224      // Mark the meta table meter whenever the coprocessor is called
225      String regionId = getRegionIdFromOp(op);
226      String regionRequestMeter = regionMeterName(regionId);
227      registerMeterIfNotPresent(e, regionRequestMeter);
228      markMeterIfPresent(regionRequestMeter);
229    }
230
231    private void opMetricRegisterAndMark(ObserverContext<RegionCoprocessorEnvironment> e,
232        Row op) {
233      String opMeterName = opMeterName(op);
234      registerMeterIfNotPresent(e, opMeterName);
235      markMeterIfPresent(opMeterName);
236    }
237
238    private void opWithClientMetricRegisterAndMark(ObserverContext<RegionCoprocessorEnvironment> e,
239        Object op) {
240      String opWithClientMeterName = opWithClientMeterName(op);
241      registerMeterIfNotPresent(e, opWithClientMeterName);
242      markMeterIfPresent(opWithClientMeterName);
243    }
244
245    private String opWithClientMeterName(Object op) {
246      String clientIP = RpcServer.getRemoteIp() != null ? RpcServer.getRemoteIp().toString() : "";
247      if (clientIP.isEmpty()) {
248        return "";
249      }
250      MetaTableOps ops = opsNameMap.get(op.getClass());
251      String opWithClientMeterName = "";
252      switch (ops) {
253        case GET:
254          opWithClientMeterName = String.format("MetaTable_client_%s_get_request", clientIP);
255          break;
256        case PUT:
257          opWithClientMeterName = String.format("MetaTable_client_%s_put_request", clientIP);
258          break;
259        case DELETE:
260          opWithClientMeterName = String.format("MetaTable_client_%s_delete_request", clientIP);
261          break;
262        default:
263          break;
264      }
265      return opWithClientMeterName;
266    }
267
268    private String opMeterName(Object op) {
269      MetaTableOps ops = opsNameMap.get(op.getClass());
270      String opMeterName = "";
271      switch (ops) {
272        case GET:
273          opMeterName = "MetaTable_get_request";
274          break;
275        case PUT:
276          opMeterName = "MetaTable_put_request";
277          break;
278        case DELETE:
279          opMeterName = "MetaTable_delete_request";
280          break;
281        default:
282          break;
283      }
284      return opMeterName;
285    }
286
287    private String tableMeterName(String tableName) {
288      return String.format("MetaTable_table_%s_request", tableName);
289    }
290
291    private String clientRequestMeterName(String clientIP) {
292      if (clientIP.isEmpty()) {
293        return "";
294      }
295      return String.format("MetaTable_client_%s_request", clientIP);
296    }
297
298    private String regionMeterName(String regionId) {
299      return String.format("MetaTable_region_%s_request", regionId);
300    }
301  }
302
303  @Override
304  public Optional<RegionObserver> getRegionObserver() {
305    return Optional.of(observer);
306  }
307
308  @Override
309  public void start(CoprocessorEnvironment env) throws IOException {
310    if (env instanceof RegionCoprocessorEnvironment
311        && ((RegionCoprocessorEnvironment) env).getRegionInfo().getTable() != null
312        && ((RegionCoprocessorEnvironment) env).getRegionInfo().getTable().getName() != null
313        && new String(((RegionCoprocessorEnvironment) env).getRegionInfo().getTable().getName(),
314          StandardCharsets.UTF_8).equals(TableName.META_TABLE_NAME.toString())) {
315      regionCoprocessorEnv = (RegionCoprocessorEnvironment) env;
316      observer = new ExampleRegionObserverMeta();
317      requestsMap = new ConcurrentHashMap<>();
318      clientMetricsLossyCounting = new LossyCounting();
319      // only be active mode when this region holds meta table.
320      active = true;
321    } else {
322      observer = new ExampleRegionObserverMeta();
323    }
324  }
325
326  @Override
327  public void stop(CoprocessorEnvironment e) throws IOException {
328    // since meta region can move around, clear stale metrics when stop.
329    for (String meterName : requestsMap.keySet()) {
330      MetricRegistry registry = regionCoprocessorEnv.getMetricRegistryForRegionServer();
331      registry.remove(meterName);
332    }
333  }
334
335}