/* * Copyright The Apache Software Foundation * * 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.regionserver.metrics; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLongArray; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.mutable.MutableDouble; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.io.hfile.BlockType.BlockCategory; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.Pair; /** * A collection of metric names in a given column family or a (table, column * family) combination. The following "dimensions" are supported: * <ul> * <li>Table name (optional; enabled based on configuration)</li> * <li>Per-column family vs. aggregated. The aggregated mode is only supported * when table name is not included.</li> * <li>Block category (data, index, bloom filter, etc.)</li> * <li>Whether the request is part of a compaction</li> * <li>Metric type (read time, block read count, cache hits/misses, etc.)</li> * </ul> * <p> * An instance of this class does not store any metric values. It just allows * to determine the correct metric name for each combination of the above * dimensions. * <p> * <table> * <tr> * <th rowspan="2">Metric key</th> * <th colspan="2">Per-table metrics conf setting</th> * <th rowspan="2">Description</th> * </tr> * <tr> * <th>On</th> * <th>Off</th> * </th> * <tr> * <td> tbl.T.cf.CF.M </td> <td> Include </td> <td> Skip </td> * <td> A specific column family of a specific table </td> * </tr> * <tr> * <td> tbl.T.M </td> <td> Skip </td> <td> Skip </td> * <td> All column families in the given table </td> * </tr> * <tr> * <td> cf.CF.M </td> <td> Skip </td> <td> Include </td> * <td> A specific column family in all tables </td> * </tr> * <tr> * <td> M </td> <td> Include </td> <td> Include </td> * <td> All column families in all tables </td> * </tr> * </table> */ public class SchemaMetrics { public interface SchemaAware { public String getTableName(); public String getColumnFamilyName(); public SchemaMetrics getSchemaMetrics(); } private static final Log LOG = LogFactory.getLog(SchemaMetrics.class); public static enum BlockMetricType { // Metric configuration: compactionAware, timeVarying READ_TIME("Read", true, true), READ_COUNT("BlockReadCnt", true, false), CACHE_HIT("BlockReadCacheHitCnt", true, false), CACHE_MISS("BlockReadCacheMissCnt", true, false), CACHE_SIZE("blockCacheSize", false, false), CACHED("blockCacheNumCached", false, false), EVICTED("blockCacheNumEvicted", false, false); private final String metricStr; private final boolean compactionAware; private final boolean timeVarying; BlockMetricType(String metricStr, boolean compactionAware, boolean timeVarying) { this.metricStr = metricStr; this.compactionAware = compactionAware; this.timeVarying = timeVarying; } @Override public String toString() { return metricStr; } private static final String BLOCK_METRIC_TYPE_RE; static { StringBuilder sb = new StringBuilder(); for (BlockMetricType bmt : values()) { if (sb.length() > 0) sb.append("|"); sb.append(bmt); } BLOCK_METRIC_TYPE_RE = sb.toString(); } }; public static enum StoreMetricType { STORE_FILE_COUNT("storeFileCount"), STORE_FILE_INDEX_SIZE("storeFileIndexSizeMB"), STORE_FILE_SIZE_MB("storeFileSizeMB"), STATIC_BLOOM_SIZE_KB("staticBloomSizeKB"), MEMSTORE_SIZE_MB("memstoreSizeMB"), STATIC_INDEX_SIZE_KB("staticIndexSizeKB"), FLUSH_SIZE("flushSize"); private final String metricStr; StoreMetricType(String metricStr) { this.metricStr = metricStr; } @Override public String toString() { return metricStr; } }; // Constants /** * A string used when column family or table name is unknown, and in some * unit tests. This should not normally show up in metric names but if it * does it is better than creating a silent discrepancy in total vs. * per-CF/table metrics. */ public static final String UNKNOWN = "__unknown"; public static final String TABLE_PREFIX = "tbl."; public static final String CF_PREFIX = "cf."; public static final String BLOCK_TYPE_PREFIX = "bt."; public static final String REGION_PREFIX = "region."; public static final String CF_UNKNOWN_PREFIX = CF_PREFIX + UNKNOWN + "."; public static final String CF_BAD_FAMILY_PREFIX = CF_PREFIX + "__badfamily."; /** Use for readability when obtaining non-compaction counters */ public static final boolean NO_COMPACTION = false; public static final String METRIC_GETSIZE = "getsize"; public static final String METRIC_NEXTSIZE = "nextsize"; /** * A special schema metric value that means "all tables aggregated" or * "all column families aggregated" when used as a table name or a column * family name. */ public static final String TOTAL_KEY = ""; /** * Special handling for meta-block-specific metrics for * backwards-compatibility. */ private static final String META_BLOCK_CATEGORY_STR = "Meta"; private static final int NUM_BLOCK_CATEGORIES = BlockCategory.values().length; private static final int NUM_METRIC_TYPES = BlockMetricType.values().length; static final boolean[] BOOL_VALUES = new boolean[] { false, true }; private static final int NUM_BLOCK_METRICS = NUM_BLOCK_CATEGORIES * // blockCategory BOOL_VALUES.length * // isCompaction NUM_METRIC_TYPES; // metricType private static final int NUM_STORE_METRIC_TYPES = StoreMetricType.values().length; /** Conf key controlling whether we include table name in metric names */ private static final String SHOW_TABLE_NAME_CONF_KEY = "hbase.metrics.showTableName"; /** We use this when too many column families are involved in a request. */ private static final String MORE_CFS_OMITTED_STR = "__more"; /** * Maximum length of a metric name prefix. Used when constructing metric * names from a set of column families participating in a request. */ private static final int MAX_METRIC_PREFIX_LENGTH = 256 - MORE_CFS_OMITTED_STR.length(); // Global variables /** * Maps a string key consisting of table name and column family name, with * table name optionally replaced with {@link #TOTAL_KEY} if per-table * metrics are disabled, to an instance of this class. */ private static final ConcurrentHashMap<String, SchemaMetrics> tableAndFamilyToMetrics = new ConcurrentHashMap<String, SchemaMetrics>(); /** Metrics for all tables and column families. */ // This has to be initialized after cfToMetrics. public static final SchemaMetrics ALL_SCHEMA_METRICS = getInstance(TOTAL_KEY, TOTAL_KEY); /** Threshold for flush the metrics, currently used only for "on cache hit" */ private static final long THRESHOLD_METRICS_FLUSH = 100l; /** * Whether to include table name in metric names. If this is null, it has not * been initialized. This is a global instance, but we also have a copy of it * per a {@link SchemaMetrics} object to avoid synchronization overhead. */ private static volatile Boolean useTableNameGlobally; /** Whether we logged a message about configuration inconsistency */ private static volatile boolean loggedConfInconsistency; // Instance variables private final String[] blockMetricNames = new String[NUM_BLOCK_METRICS]; private final boolean[] blockMetricTimeVarying = new boolean[NUM_BLOCK_METRICS]; private final String[] bloomMetricNames = new String[2]; private final String[] storeMetricNames = new String[NUM_STORE_METRIC_TYPES]; private final String[] storeMetricNamesMax = new String[NUM_STORE_METRIC_TYPES]; private final AtomicLongArray onHitCacheMetrics= new AtomicLongArray(NUM_BLOCK_CATEGORIES * BOOL_VALUES.length); private SchemaMetrics(final String tableName, final String cfName) { String metricPrefix = SchemaMetrics.generateSchemaMetricsPrefix( tableName, cfName); for (BlockCategory blockCategory : BlockCategory.values()) { for (boolean isCompaction : BOOL_VALUES) { // initialize the cache metrics onHitCacheMetrics.set(getCacheHitMetricIndex(blockCategory, isCompaction), 0); for (BlockMetricType metricType : BlockMetricType.values()) { if (!metricType.compactionAware && isCompaction) { continue; } StringBuilder sb = new StringBuilder(metricPrefix); if (blockCategory != BlockCategory.ALL_CATEGORIES && blockCategory != BlockCategory.META) { String categoryStr = blockCategory.toString(); categoryStr = categoryStr.charAt(0) + categoryStr.substring(1).toLowerCase(); sb.append(BLOCK_TYPE_PREFIX + categoryStr + "."); } if (metricType.compactionAware) { sb.append(isCompaction ? "compaction" : "fs"); } // A special-case for meta blocks for backwards-compatibility. if (blockCategory == BlockCategory.META) { sb.append(META_BLOCK_CATEGORY_STR); } sb.append(metricType); int i = getBlockMetricIndex(blockCategory, isCompaction, metricType); blockMetricNames[i] = sb.toString().intern(); blockMetricTimeVarying[i] = metricType.timeVarying; } } } for (boolean isInBloom : BOOL_VALUES) { bloomMetricNames[isInBloom ? 1 : 0] = metricPrefix + (isInBloom ? "keyMaybeInBloomCnt" : "keyNotInBloomCnt"); } for (StoreMetricType storeMetric : StoreMetricType.values()) { String coreName = metricPrefix + storeMetric.toString(); storeMetricNames[storeMetric.ordinal()] = coreName; storeMetricNamesMax[storeMetric.ordinal()] = coreName + ".max"; } } /** * Returns a {@link SchemaMetrics} object for the given table and column * family, instantiating it if necessary. * * @param tableName table name (null is interpreted as "unknown"). This is * ignored * @param cfName column family name (null is interpreted as "unknown") */ public static SchemaMetrics getInstance(String tableName, String cfName) { if (tableName == null) { tableName = UNKNOWN; } if (cfName == null) { cfName = UNKNOWN; } tableName = getEffectiveTableName(tableName); final String instanceKey = tableName + "\t" + cfName; SchemaMetrics schemaMetrics = tableAndFamilyToMetrics.get(instanceKey); if (schemaMetrics != null) { return schemaMetrics; } schemaMetrics = new SchemaMetrics(tableName, cfName); SchemaMetrics existingMetrics = tableAndFamilyToMetrics.putIfAbsent(instanceKey, schemaMetrics); return existingMetrics != null ? existingMetrics : schemaMetrics; } private static final int getCacheHitMetricIndex (BlockCategory blockCategory, boolean isCompaction) { return blockCategory.ordinal() * BOOL_VALUES.length + (isCompaction ? 1 : 0); } private static final int getBlockMetricIndex(BlockCategory blockCategory, boolean isCompaction, BlockMetricType metricType) { int i = 0; i = i * NUM_BLOCK_CATEGORIES + blockCategory.ordinal(); i = i * BOOL_VALUES.length + (isCompaction ? 1 : 0); i = i * NUM_METRIC_TYPES + metricType.ordinal(); return i; } public String getBlockMetricName(BlockCategory blockCategory, boolean isCompaction, BlockMetricType metricType) { if (isCompaction && !metricType.compactionAware) { throw new IllegalArgumentException("isCompaction cannot be true for " + metricType); } return blockMetricNames[getBlockMetricIndex(blockCategory, isCompaction, metricType)]; } public String getBloomMetricName(boolean isInBloom) { return bloomMetricNames[isInBloom ? 1 : 0]; } /** * Increments the given metric, both per-CF and aggregate, for both the given * category and all categories in aggregate (four counters total). */ private void incrNumericMetric(BlockCategory blockCategory, boolean isCompaction, BlockMetricType metricType) { incrNumericMetric (blockCategory, isCompaction, metricType, 1); } /** * Increments the given metric, both per-CF and aggregate, for both the given * category and all categories in aggregate (four counters total). */ private void incrNumericMetric(BlockCategory blockCategory, boolean isCompaction, BlockMetricType metricType, long amount) { if (blockCategory == null) { blockCategory = BlockCategory.UNKNOWN; // So that we see this in stats. } RegionMetricsStorage.incrNumericMetric(getBlockMetricName(blockCategory, isCompaction, metricType), amount); if (blockCategory != BlockCategory.ALL_CATEGORIES) { incrNumericMetric(BlockCategory.ALL_CATEGORIES, isCompaction, metricType, amount); } } private void addToReadTime(BlockCategory blockCategory, boolean isCompaction, long timeMs) { RegionMetricsStorage.incrTimeVaryingMetric(getBlockMetricName(blockCategory, isCompaction, BlockMetricType.READ_TIME), timeMs); // Also update the read time aggregated across all block categories if (blockCategory != BlockCategory.ALL_CATEGORIES) { addToReadTime(BlockCategory.ALL_CATEGORIES, isCompaction, timeMs); } } /** * Used to accumulate store metrics across multiple regions in a region * server. These metrics are not "persistent", i.e. we keep overriding them * on every update instead of incrementing, so we need to accumulate them in * a temporary map before pushing them to the global metric collection. * @param tmpMap a temporary map for accumulating store metrics * @param storeMetricType the store metric type to increment * @param val the value to add to the metric */ public void accumulateStoreMetric(final Map<String, MutableDouble> tmpMap, StoreMetricType storeMetricType, double val) { final String key = getStoreMetricName(storeMetricType); if (tmpMap.get(key) == null) { tmpMap.put(key, new MutableDouble(val)); } else { tmpMap.get(key).add(val); } if (this == ALL_SCHEMA_METRICS) { // also compute the max value across all Stores on this server final String maxKey = getStoreMetricNameMax(storeMetricType); MutableDouble cur = tmpMap.get(maxKey); if (cur == null) { tmpMap.put(maxKey, new MutableDouble(val)); } else if (cur.doubleValue() < val) { cur.setValue(val); } } else { ALL_SCHEMA_METRICS.accumulateStoreMetric(tmpMap, storeMetricType, val); } } public String getStoreMetricName(StoreMetricType storeMetricType) { return storeMetricNames[storeMetricType.ordinal()]; } public String getStoreMetricNameMax(StoreMetricType storeMetricType) { return storeMetricNamesMax[storeMetricType.ordinal()]; } /** * Update a metric that does not get reset on every poll. * @param storeMetricType the store metric to update * @param value the value to update the metric to */ public void updatePersistentStoreMetric(StoreMetricType storeMetricType, long value) { RegionMetricsStorage.incrNumericPersistentMetric( storeMetricNames[storeMetricType.ordinal()], value); } /** * Updates the number of hits and the total number of block reads on a block * cache hit. */ public void updateOnCacheHit(BlockCategory blockCategory, boolean isCompaction) { updateOnCacheHit(blockCategory, isCompaction, 1); } /** * Updates the number of hits and the total number of block reads on a block * cache hit. */ public void updateOnCacheHit(BlockCategory blockCategory, boolean isCompaction, long count) { blockCategory.expectSpecific(); int idx = getCacheHitMetricIndex(blockCategory, isCompaction); if (this.onHitCacheMetrics.addAndGet(idx, count) > THRESHOLD_METRICS_FLUSH) { flushCertainOnCacheHitMetrics(blockCategory, isCompaction); } if (this != ALL_SCHEMA_METRICS) { ALL_SCHEMA_METRICS.updateOnCacheHit(blockCategory, isCompaction, count); } } private void flushCertainOnCacheHitMetrics(BlockCategory blockCategory, boolean isCompaction) { int idx = getCacheHitMetricIndex(blockCategory, isCompaction); long tempCount = this.onHitCacheMetrics.getAndSet(idx, 0); if (tempCount > 0) { incrNumericMetric(blockCategory, isCompaction, BlockMetricType.CACHE_HIT, tempCount); incrNumericMetric(blockCategory, isCompaction, BlockMetricType.READ_COUNT, tempCount); } } /** * Flush the on cache hit metrics; */ private void flushOnCacheHitMetrics() { for (BlockCategory blockCategory : BlockCategory.values()) { for (boolean isCompaction : BOOL_VALUES) { flushCertainOnCacheHitMetrics (blockCategory, isCompaction); } } if (this != ALL_SCHEMA_METRICS) { ALL_SCHEMA_METRICS.flushOnCacheHitMetrics(); } } /** * Notify the SchemaMetrics to flush all of the the metrics */ public void flushMetrics() { // currently only for "on cache hit metrics" flushOnCacheHitMetrics(); } /** * Updates read time, the number of misses, and the total number of block * reads on a block cache miss. */ public void updateOnCacheMiss(BlockCategory blockCategory, boolean isCompaction, long timeMs) { blockCategory.expectSpecific(); addToReadTime(blockCategory, isCompaction, timeMs); incrNumericMetric(blockCategory, isCompaction, BlockMetricType.CACHE_MISS); incrNumericMetric(blockCategory, isCompaction, BlockMetricType.READ_COUNT); if (this != ALL_SCHEMA_METRICS) { ALL_SCHEMA_METRICS.updateOnCacheMiss(blockCategory, isCompaction, timeMs); } } /** * Adds the given delta to the cache size for the given block category and * the aggregate metric for all block categories. Updates both the per-CF * counter and the counter for all CFs (four metrics total). The cache size * metric is "persistent", i.e. it does not get reset when metrics are * collected. */ public void addToCacheSize(BlockCategory category, long cacheSizeDelta) { if (category == null) { category = BlockCategory.ALL_CATEGORIES; } RegionMetricsStorage.incrNumericPersistentMetric(getBlockMetricName(category, false, BlockMetricType.CACHE_SIZE), cacheSizeDelta); if (category != BlockCategory.ALL_CATEGORIES) { addToCacheSize(BlockCategory.ALL_CATEGORIES, cacheSizeDelta); } } public void updateOnCachePutOrEvict(BlockCategory blockCategory, long cacheSizeDelta, boolean isEviction) { addToCacheSize(blockCategory, cacheSizeDelta); incrNumericMetric(blockCategory, false, isEviction ? BlockMetricType.EVICTED : BlockMetricType.CACHED); if (this != ALL_SCHEMA_METRICS) { ALL_SCHEMA_METRICS.updateOnCachePutOrEvict(blockCategory, cacheSizeDelta, isEviction); } } /** * Increments both the per-CF and the aggregate counter of bloom * positives/negatives as specified by the argument. */ public void updateBloomMetrics(boolean isInBloom) { RegionMetricsStorage.incrNumericMetric(getBloomMetricName(isInBloom), 1); if (this != ALL_SCHEMA_METRICS) { ALL_SCHEMA_METRICS.updateBloomMetrics(isInBloom); } } /** * Sets the flag whether to use table name in metric names according to the * given configuration. This must be called at least once before * instantiating HFile readers/writers. */ public static void configureGlobally(Configuration conf) { if (conf != null) { final boolean useTableNameNew = conf.getBoolean(SHOW_TABLE_NAME_CONF_KEY, false); setUseTableName(useTableNameNew); } else { setUseTableName(false); } } /** * Determine the table name to be included in metric keys. If the global * configuration says that we should not use table names in metrics, * we always return {@link #TOTAL_KEY} even if nontrivial table name is * provided. * * @param tableName a table name or {@link #TOTAL_KEY} when aggregating * across all tables * @return the table name to use in metric keys */ private static String getEffectiveTableName(String tableName) { if (!tableName.equals(TOTAL_KEY)) { // We are provided with a non-trivial table name (including "unknown"). // We need to know whether table name should be included into metrics. if (useTableNameGlobally == null) { throw new IllegalStateException("The value of the " + SHOW_TABLE_NAME_CONF_KEY + " conf option has not been specified " + "in SchemaMetrics"); } final boolean useTableName = useTableNameGlobally; if (!useTableName) { // Don't include table name in metric keys. tableName = TOTAL_KEY; } } return tableName; } /** * Method to transform a combination of a table name and a column family name * into a metric key prefix. Tables/column family names equal to * {@link #TOTAL_KEY} are omitted from the prefix. * * @param tableName the table name or {@link #TOTAL_KEY} for all tables * @param cfName the column family name or {@link #TOTAL_KEY} for all CFs * @return the metric name prefix, ending with a dot. */ public static String generateSchemaMetricsPrefix(String tableName, final String cfName) { tableName = getEffectiveTableName(tableName); String schemaMetricPrefix = tableName.equals(TOTAL_KEY) ? "" : TABLE_PREFIX + tableName + "."; schemaMetricPrefix += cfName.equals(TOTAL_KEY) ? "" : CF_PREFIX + cfName + "."; return schemaMetricPrefix; } public static String generateSchemaMetricsPrefix(byte[] tableName, byte[] cfName) { return generateSchemaMetricsPrefix(Bytes.toString(tableName), Bytes.toString(cfName)); } /** * Method to transform a set of column families in byte[] format with table * name into a metric key prefix. * * @param tableName the table name or {@link #TOTAL_KEY} for all tables * @param families the ordered set of column families * @return the metric name prefix, ending with a dot, or an empty string in * case of invalid arguments. This is OK since we always expect * some CFs to be included. */ public static String generateSchemaMetricsPrefix(String tableName, Set<byte[]> families) { if (families == null || families.isEmpty() || tableName == null || tableName.isEmpty()) { return ""; } if (families.size() == 1) { return generateSchemaMetricsPrefix(tableName, Bytes.toString(families.iterator().next())); } tableName = getEffectiveTableName(tableName); List<byte[]> sortedFamilies = new ArrayList<byte[]>(families); Collections.sort(sortedFamilies, Bytes.BYTES_COMPARATOR); StringBuilder sb = new StringBuilder(); int numCFsLeft = families.size(); for (byte[] family : sortedFamilies) { if (sb.length() > MAX_METRIC_PREFIX_LENGTH) { sb.append(MORE_CFS_OMITTED_STR); break; } --numCFsLeft; sb.append(Bytes.toString(family)); if (numCFsLeft > 0) { sb.append("~"); } } return SchemaMetrics.generateSchemaMetricsPrefix(tableName, sb.toString()); } /** * Get the prefix for metrics generated about a single region. * * @param tableName * the table name or {@link #TOTAL_KEY} for all tables * @param regionName * regionName * @return the prefix for this table/region combination. */ static String generateRegionMetricsPrefix(String tableName, String regionName) { tableName = getEffectiveTableName(tableName); String schemaMetricPrefix = tableName.equals(TOTAL_KEY) ? "" : TABLE_PREFIX + tableName + "."; schemaMetricPrefix += regionName.equals(TOTAL_KEY) ? "" : REGION_PREFIX + regionName + "."; return schemaMetricPrefix; } /** * Sets the flag of whether to use table name in metric names. This flag * is specified in configuration and is not expected to change at runtime, * so we log an error message when it does change. */ private static void setUseTableName(final boolean useTableNameNew) { if (useTableNameGlobally == null) { // This configuration option has not yet been set. useTableNameGlobally = useTableNameNew; } else if (useTableNameGlobally != useTableNameNew && !loggedConfInconsistency) { // The configuration is inconsistent and we have not reported it // previously. Once we report it, just keep ignoring the new setting. LOG.error("Inconsistent configuration. Previous configuration " + "for using table name in metrics: " + useTableNameGlobally + ", " + "new configuration: " + useTableNameNew); loggedConfInconsistency = true; } } // Methods used in testing private static final String regexEscape(String s) { return s.replace(".", "\\."); } /** * Assume that table names used in tests don't contain dots, except for the * META table. */ private static final String WORD_AND_DOT_RE_STR = "([^.]+|" + regexEscape(Bytes.toString(HConstants.META_TABLE_NAME)) + ")\\."; /** "tbl.<table_name>." */ private static final String TABLE_NAME_RE_STR = "\\b" + regexEscape(TABLE_PREFIX) + WORD_AND_DOT_RE_STR; /** "cf.<cf_name>." */ private static final String CF_NAME_RE_STR = "\\b" + regexEscape(CF_PREFIX) + WORD_AND_DOT_RE_STR; private static final Pattern CF_NAME_RE = Pattern.compile(CF_NAME_RE_STR); /** "tbl.<table_name>.cf.<cf_name>." */ private static final Pattern TABLE_AND_CF_NAME_RE = Pattern.compile( TABLE_NAME_RE_STR + CF_NAME_RE_STR); private static final Pattern BLOCK_CATEGORY_RE = Pattern.compile( "\\b" + regexEscape(BLOCK_TYPE_PREFIX) + "[^.]+\\." + // Also remove the special-case block type marker for meta blocks "|" + META_BLOCK_CATEGORY_STR + "(?=" + BlockMetricType.BLOCK_METRIC_TYPE_RE + ")"); /** * A suffix for the "number of operations" part of "time-varying metrics". We * only use this for metric verification in unit testing. Time-varying * metrics are handled by a different code path in production. */ private static String NUM_OPS_SUFFIX = "numops"; /** * A custom suffix that we use for verifying the second component of * a "time-varying metric". */ private static String TOTAL_SUFFIX = "_total"; private static final Pattern TIME_VARYING_SUFFIX_RE = Pattern.compile( "(" + NUM_OPS_SUFFIX + "|" + TOTAL_SUFFIX + ")$"); void printMetricNames() { for (BlockCategory blockCategory : BlockCategory.values()) { for (boolean isCompaction : BOOL_VALUES) { for (BlockMetricType metricType : BlockMetricType.values()) { int i = getBlockMetricIndex(blockCategory, isCompaction, metricType); LOG.debug("blockCategory=" + blockCategory + ", " + "metricType=" + metricType + ", isCompaction=" + isCompaction + ", metricName=" + blockMetricNames[i]); } } } } private Collection<String> getAllMetricNames() { List<String> allMetricNames = new ArrayList<String>(); for (int i = 0; i < blockMetricNames.length; ++i) { final String blockMetricName = blockMetricNames[i]; final boolean timeVarying = blockMetricTimeVarying[i]; if (blockMetricName != null) { if (timeVarying) { allMetricNames.add(blockMetricName + NUM_OPS_SUFFIX); allMetricNames.add(blockMetricName + TOTAL_SUFFIX); } else { allMetricNames.add(blockMetricName); } } } allMetricNames.addAll(Arrays.asList(bloomMetricNames)); return allMetricNames; } private static final boolean isTimeVaryingKey(String metricKey) { return metricKey.endsWith(NUM_OPS_SUFFIX) || metricKey.endsWith(TOTAL_SUFFIX); } private static final String stripTimeVaryingSuffix(String metricKey) { return TIME_VARYING_SUFFIX_RE.matcher(metricKey).replaceAll(""); } public static Map<String, Long> getMetricsSnapshot() { Map<String, Long> metricsSnapshot = new TreeMap<String, Long>(); for (SchemaMetrics cfm : tableAndFamilyToMetrics.values()) { cfm.flushMetrics(); for (String metricName : cfm.getAllMetricNames()) { long metricValue; if (isTimeVaryingKey(metricName)) { Pair<Long, Integer> totalAndCount = RegionMetricsStorage.getTimeVaryingMetric(stripTimeVaryingSuffix(metricName)); metricValue = metricName.endsWith(TOTAL_SUFFIX) ? totalAndCount.getFirst() : totalAndCount.getSecond(); } else { metricValue = RegionMetricsStorage.getNumericMetric(metricName); } metricsSnapshot.put(metricName, metricValue); } } return metricsSnapshot; } public static long getLong(Map<String, Long> m, String k) { Long l = m.get(k); return l != null ? l : 0; } private static void putLong(Map<String, Long> m, String k, long v) { if (v != 0) { m.put(k, v); } else { m.remove(k); } } /** * @return the difference between two sets of metrics (second minus first). * Only includes keys that have nonzero difference. */ public static Map<String, Long> diffMetrics(Map<String, Long> a, Map<String, Long> b) { Set<String> allKeys = new TreeSet<String>(a.keySet()); allKeys.addAll(b.keySet()); Map<String, Long> diff = new TreeMap<String, Long>(); for (String k : allKeys) { long aVal = getLong(a, k); long bVal = getLong(b, k); if (aVal != bVal) { diff.put(k, bVal - aVal); } } return diff; } public static void validateMetricChanges(Map<String, Long> oldMetrics) { final Map<String, Long> newMetrics = getMetricsSnapshot(); final Map<String, Long> allCfDeltas = new TreeMap<String, Long>(); final Map<String, Long> allBlockCategoryDeltas = new TreeMap<String, Long>(); final Map<String, Long> deltas = diffMetrics(oldMetrics, newMetrics); final Pattern cfTableMetricRE = useTableNameGlobally ? TABLE_AND_CF_NAME_RE : CF_NAME_RE; final Set<String> allKeys = new TreeSet<String>(oldMetrics.keySet()); allKeys.addAll(newMetrics.keySet()); for (SchemaMetrics cfm : tableAndFamilyToMetrics.values()) { for (String metricName : cfm.getAllMetricNames()) { if (metricName.startsWith(CF_PREFIX + CF_PREFIX)) { throw new AssertionError("Column family prefix used twice: " + metricName); } final long oldValue = getLong(oldMetrics, metricName); final long newValue = getLong(newMetrics, metricName); final long delta = newValue - oldValue; // Re-calculate values of metrics with no column family (or CF/table) // specified based on all metrics with CF (or CF/table) specified. if (delta != 0) { if (cfm != ALL_SCHEMA_METRICS) { final String aggregateMetricName = cfTableMetricRE.matcher(metricName).replaceAll(""); if (!aggregateMetricName.equals(metricName)) { LOG.debug("Counting " + delta + " units of " + metricName + " towards " + aggregateMetricName); putLong(allCfDeltas, aggregateMetricName, getLong(allCfDeltas, aggregateMetricName) + delta); } } else { LOG.debug("Metric=" + metricName + ", delta=" + delta); } } Matcher matcher = BLOCK_CATEGORY_RE.matcher(metricName); if (matcher.find()) { // Only process per-block-category metrics String metricNoBlockCategory = matcher.replaceAll(""); putLong(allBlockCategoryDeltas, metricNoBlockCategory, getLong(allBlockCategoryDeltas, metricNoBlockCategory) + delta); } } } StringBuilder errors = new StringBuilder(); for (String key : ALL_SCHEMA_METRICS.getAllMetricNames()) { long actual = getLong(deltas, key); long expected = getLong(allCfDeltas, key); if (actual != expected) { if (errors.length() > 0) errors.append("\n"); errors.append("The all-CF metric " + key + " changed by " + actual + " but the aggregation of per-CF/table metrics " + "yields " + expected); } } // Verify metrics computed for all block types based on the aggregation // of per-block-type metrics. for (String key : allKeys) { if (BLOCK_CATEGORY_RE.matcher(key).find() || key.contains(ALL_SCHEMA_METRICS.getBloomMetricName(false)) || key.contains(ALL_SCHEMA_METRICS.getBloomMetricName(true))){ // Skip per-block-category metrics. Also skip bloom filters, because // they are not aggregated per block type. continue; } long actual = getLong(deltas, key); long expected = getLong(allBlockCategoryDeltas, key); if (actual != expected) { if (errors.length() > 0) errors.append("\n"); errors.append("The all-block-category metric " + key + " changed by " + actual + " but the aggregation of " + "per-block-category metrics yields " + expected); } } if (errors.length() > 0) { throw new AssertionError(errors.toString()); } } /** * Creates an instance pretending both the table and column family are * unknown. Used in unit tests. */ public static SchemaMetrics getUnknownInstanceForTest() { return getInstance(UNKNOWN, UNKNOWN); } /** * Set the flag to use or not use table name in metric names. Used in unit * tests, so the flag can be set arbitrarily. */ public static void setUseTableNameInTest(final boolean useTableNameNew) { useTableNameGlobally = useTableNameNew; } /** Formats the given map of metrics in a human-readable way. */ public static String formatMetrics(Map<String, Long> metrics) { StringBuilder sb = new StringBuilder(); for (Map.Entry<String, Long> entry : metrics.entrySet()) { if (sb.length() > 0) { sb.append('\n'); } sb.append(entry.getKey() + " : " + entry.getValue()); } return sb.toString(); } }