Skip to content

Commit 989ca5f

Browse files
author
Ravindra Dingankar
committed
HDFS-16949 Introduce inverse quantiles for metrics where higher numeric value is better
1 parent f8d0949 commit 989ca5f

File tree

5 files changed

+175
-7
lines changed

5 files changed

+175
-7
lines changed

hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/metrics2/lib/MetricsRegistry.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,29 @@ public synchronized MutableQuantiles newQuantiles(String name, String desc,
227227
return ret;
228228
}
229229

230+
/**
231+
* Create a mutable inverse metric that estimates inverse quantiles of a stream of values
232+
* @param name of the metric
233+
* @param desc metric description
234+
* @param sampleName of the metric (e.g., "Ops")
235+
* @param valueName of the metric (e.g., "Rate")
236+
* @param interval rollover interval of estimator in seconds
237+
* @return a new inverse quantile estimator object
238+
* @throws MetricsException if interval is not a positive integer
239+
*/
240+
public synchronized MutableQuantiles newInverseQuantiles(String name, String desc,
241+
String sampleName, String valueName, int interval) {
242+
checkMetricName(name);
243+
if (interval <= 0) {
244+
throw new MetricsException("Interval should be positive. Value passed" +
245+
" is: " + interval);
246+
}
247+
MutableQuantiles ret =
248+
new MutableInverseQuantiles(name, desc, sampleName, valueName, interval);
249+
metricsMap.put(name, ret);
250+
return ret;
251+
}
252+
230253
/**
231254
* Create a mutable metric with stats
232255
* @param name of the metric
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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.metrics2.lib;
19+
20+
import org.apache.commons.lang3.StringUtils;
21+
import org.apache.hadoop.classification.InterfaceAudience;
22+
import org.apache.hadoop.classification.InterfaceStability;
23+
import org.apache.hadoop.classification.VisibleForTesting;
24+
import org.apache.hadoop.metrics2.MetricsInfo;
25+
import org.apache.hadoop.metrics2.util.Quantile;
26+
import org.apache.hadoop.metrics2.util.SampleQuantiles;
27+
import org.apache.hadoop.thirdparty.com.google.common.util.concurrent.ThreadFactoryBuilder;
28+
import java.util.concurrent.Executors;
29+
import java.util.concurrent.ScheduledExecutorService;
30+
import java.util.concurrent.ScheduledFuture;
31+
import java.util.concurrent.TimeUnit;
32+
import static org.apache.hadoop.metrics2.lib.Interns.info;
33+
34+
/**
35+
* Watches a stream of long values, maintaining online estimates of specific
36+
* quantiles with provably low error bounds. Inverse quantiles are meant for
37+
* highly accurate low-percentile (e.g. 1st, 5th) latency metrics.
38+
* InverseQuantiles are used for metrics where higher the value better it is.
39+
* ( eg: data transfer rate ).
40+
* The 1st percentile here corresponds to the 99th inverse percentile metric,
41+
* 5th percentile to 95th and so on.
42+
*/
43+
@InterfaceAudience.Public
44+
@InterfaceStability.Evolving
45+
public class MutableInverseQuantiles extends MutableQuantiles{
46+
47+
@VisibleForTesting
48+
public static final Quantile[] inverseQuantiles = { new Quantile(0.50, 0.050),
49+
new Quantile(0.25, 0.025), new Quantile(0.10, 0.010),
50+
new Quantile(0.05, 0.005), new Quantile(0.01, 0.001) };
51+
52+
private ScheduledFuture<?> scheduledTask = null;
53+
54+
private static final ScheduledExecutorService scheduler = Executors
55+
.newScheduledThreadPool(1, new ThreadFactoryBuilder().setDaemon(true)
56+
.setNameFormat("MutableInverseQuantiles-%d").build());
57+
58+
/**
59+
* Instantiates a new {@link MutableInverseQuantiles} for a metric that rolls itself
60+
* over on the specified time interval.
61+
*
62+
* @param name of the metric
63+
* @param description long-form textual description of the metric
64+
* @param sampleName type of items in the stream (e.g., "Ops")
65+
* @param valueName type of the values
66+
* @param interval
67+
*/
68+
public MutableInverseQuantiles(String name, String description, String sampleName,
69+
String valueName, int interval) {
70+
String ucName = StringUtils.capitalize(name);
71+
String usName = StringUtils.capitalize(sampleName);
72+
String uvName = StringUtils.capitalize(valueName);
73+
String desc = StringUtils.uncapitalize(description);
74+
String lsName = StringUtils.uncapitalize(sampleName);
75+
String lvName = StringUtils.uncapitalize(valueName);
76+
77+
numInfo = info(ucName + "Num" + usName, String.format(
78+
"Number of %s for %s with %ds interval", lsName, desc, interval));
79+
// Construct the MetricsInfos for the inverse quantiles, converting to inverse percentiles
80+
quantileInfos = new MetricsInfo[inverseQuantiles.length];
81+
String nameTemplate = ucName + "%dthInversePercentile" + uvName;
82+
String descTemplate = "%d inverse percentile " + lvName + " with " + interval
83+
+ " second interval for " + desc;
84+
for (int i = 0; i < inverseQuantiles.length; i++) {
85+
int inversePercentile = (int) (100 * (1 - inverseQuantiles[i].quantile));
86+
quantileInfos[i] = info(String.format(nameTemplate, inversePercentile),
87+
String.format(descTemplate, inversePercentile));
88+
}
89+
90+
estimator = new SampleQuantiles(inverseQuantiles);
91+
setInterval(interval);
92+
scheduledTask = scheduler.scheduleWithFixedDelay(new RolloverSample(this),
93+
interval, interval, TimeUnit.SECONDS);
94+
}
95+
96+
public void stop() {
97+
if (scheduledTask != null) {
98+
scheduledTask.cancel(false);
99+
}
100+
scheduledTask = null;
101+
}
102+
}

hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/metrics2/lib/MutableQuantiles.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ public class MutableQuantiles extends MutableMetric {
5252
new Quantile(0.75, 0.025), new Quantile(0.90, 0.010),
5353
new Quantile(0.95, 0.005), new Quantile(0.99, 0.001) };
5454

55-
private final MetricsInfo numInfo;
56-
private final MetricsInfo[] quantileInfos;
57-
private final int interval;
55+
MetricsInfo numInfo;
56+
MetricsInfo[] quantileInfos;
57+
private int interval;
5858

59-
private QuantileEstimator estimator;
59+
QuantileEstimator estimator;
6060
private long previousCount = 0;
6161
private ScheduledFuture<?> scheduledTask = null;
6262

@@ -106,11 +106,13 @@ public MutableQuantiles(String name, String description, String sampleName,
106106

107107
estimator = new SampleQuantiles(quantiles);
108108

109-
this.interval = interval;
109+
setInterval(interval);
110110
scheduledTask = scheduler.scheduleWithFixedDelay(new RolloverSample(this),
111111
interval, interval, TimeUnit.SECONDS);
112112
}
113113

114+
public MutableQuantiles() {}
115+
114116
@Override
115117
public synchronized void snapshot(MetricsRecordBuilder builder, boolean all) {
116118
if (all || changed()) {
@@ -133,6 +135,10 @@ public synchronized void add(long value) {
133135
estimator.insert(value);
134136
}
135137

138+
synchronized void setInterval(int interval) {
139+
this.interval = interval;
140+
}
141+
136142
public int getInterval() {
137143
return interval;
138144
}
@@ -162,7 +168,7 @@ public synchronized void setEstimator(QuantileEstimator quantileEstimator) {
162168
* Runnable used to periodically roll over the internal
163169
* {@link SampleQuantiles} every interval.
164170
*/
165-
private static class RolloverSample implements Runnable {
171+
static class RolloverSample implements Runnable {
166172

167173
MutableQuantiles parent;
168174

hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/metrics2/util/TestSampleQuantiles.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Map;
2525
import java.util.Random;
2626

27+
import org.apache.hadoop.metrics2.lib.MutableInverseQuantiles;
2728
import org.junit.Before;
2829
import org.junit.Test;
2930

@@ -118,4 +119,40 @@ public void testQuantileError() throws IOException {
118119
}
119120
}
120121
}
122+
123+
/**
124+
* Correctness test that checks that absolute error of the estimate for inverse quantiles
125+
* is within specified error bounds for some randomly permuted streams of items.
126+
*/
127+
@Test
128+
public void testInverseQuantiles() throws IOException {
129+
SampleQuantiles estimator = new SampleQuantiles(MutableInverseQuantiles.inverseQuantiles);
130+
final int count = 100000;
131+
Random r = new Random(0xDEADDEAD);
132+
Long[] values = new Long[count];
133+
for (int i = 0; i < count; i++) {
134+
values[i] = (long) (i + 1);
135+
}
136+
// Do 10 shuffle/insert/check cycles
137+
for (int i = 0; i < 10; i++) {
138+
System.out.println("Starting run " + i);
139+
Collections.shuffle(Arrays.asList(values), r);
140+
estimator.clear();
141+
for (int j = 0; j < count; j++) {
142+
estimator.insert(values[j]);
143+
}
144+
Map<Quantile, Long> snapshot;
145+
snapshot = estimator.snapshot();
146+
for (Quantile q : MutableInverseQuantiles.inverseQuantiles) {
147+
long actual = (long) (q.quantile * count);
148+
long error = (long) (q.error * count);
149+
long estimate = snapshot.get(q);
150+
System.out.println(String.format("For inverse quantile %f " +
151+
"Expected %d with error %d, estimated %d",
152+
(1 - q.quantile), actual, error, estimate));
153+
assertThat(estimate <= actual + error).isTrue();
154+
assertThat(estimate >= actual - error).isTrue();
155+
}
156+
}
157+
}
121158
}

hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/metrics/DataNodeMetrics.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ public DataNodeMetrics(String name, String sessionId, int[] intervals,
258258
"ramDiskBlocksLazyPersistWindows" + interval + "s",
259259
"Time between the RamDisk block write and disk persist in ms",
260260
"ops", "latency", interval);
261-
readTransferRateQuantiles[i] = registry.newQuantiles(
261+
readTransferRateQuantiles[i] = registry.newInverseQuantiles(
262262
"readTransferRate" + interval + "s",
263263
"Rate at which bytes are read from datanode calculated in bytes per second",
264264
"ops", "rate", interval);

0 commit comments

Comments
 (0)