Skip to content

Commit 76710e8

Browse files
authored
Implement LockFreeHistogram and use it for PerWorkerHistograms (#30769)
* Implement LockFreeHistogram and use it for PerWorkerHistograms * Address comments * Override update(double... values) * Address comments on multithread test * Address comments in Multithreaded test * Variable renaming in test
1 parent 54c2a72 commit 76710e8

6 files changed

Lines changed: 488 additions & 52 deletions

File tree

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
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.beam.runners.dataflow.worker;
19+
20+
import com.google.auto.value.AutoValue;
21+
import com.google.auto.value.extension.memoized.Memoized;
22+
import java.io.Serializable;
23+
import java.util.Optional;
24+
import java.util.concurrent.atomic.AtomicBoolean;
25+
import java.util.concurrent.atomic.AtomicLongArray;
26+
import java.util.concurrent.atomic.AtomicReference;
27+
import javax.annotation.concurrent.ThreadSafe;
28+
import org.apache.beam.sdk.annotations.Internal;
29+
import org.apache.beam.sdk.metrics.Histogram;
30+
import org.apache.beam.sdk.metrics.MetricName;
31+
import org.apache.beam.sdk.util.HistogramData;
32+
import org.apache.beam.sdk.values.KV;
33+
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.primitives.ImmutableLongArray;
34+
35+
/**
36+
* A lock free implementation of {@link org.apache.beam.sdk.metrics.Histogram}. This class supports
37+
* extracting delta updates with the {@link #getSnapshotAndReset} method.
38+
*/
39+
@ThreadSafe
40+
@Internal
41+
public final class LockFreeHistogram implements Histogram {
42+
private final HistogramData.BucketType bucketType;
43+
private final AtomicLongArray buckets;
44+
private final MetricName name;
45+
private final AtomicReference<OutlierStatistic> underflowStatistic;
46+
private final AtomicReference<OutlierStatistic> overflowStatistic;
47+
48+
/**
49+
* Whether this histogram has updates that have not been extracted by {@code getSnapshotAndReset}.
50+
* This values should be flipped to true AFTER recording a value, and flipped to false BEFORE
51+
* extracting a snapshot. This ensures that recorded values will always be seen by a future {@code
52+
* getSnapshotAndReset} call.
53+
*/
54+
private final AtomicBoolean dirty;
55+
56+
/** Create a histogram. */
57+
public LockFreeHistogram(KV<MetricName, HistogramData.BucketType> kv) {
58+
this.name = kv.getKey();
59+
this.bucketType = kv.getValue();
60+
this.buckets = new AtomicLongArray(bucketType.getNumBuckets());
61+
this.underflowStatistic =
62+
new AtomicReference<LockFreeHistogram.OutlierStatistic>(OutlierStatistic.EMPTY);
63+
this.overflowStatistic =
64+
new AtomicReference<LockFreeHistogram.OutlierStatistic>(OutlierStatistic.EMPTY);
65+
this.dirty = new AtomicBoolean(false);
66+
}
67+
68+
/**
69+
* Represents the sum and mean of a collection of numbers. Used to represent the
70+
* underflow/overflow statistics of a histogram.
71+
*/
72+
@AutoValue
73+
public abstract static class OutlierStatistic implements Serializable {
74+
abstract double sum();
75+
76+
public abstract long count();
77+
78+
public static final OutlierStatistic EMPTY = create(0, 0);
79+
80+
public static OutlierStatistic create(double sum, long count) {
81+
return new AutoValue_LockFreeHistogram_OutlierStatistic(sum, count);
82+
}
83+
84+
public OutlierStatistic combine(double value) {
85+
return create(sum() + value, count() + 1);
86+
}
87+
88+
public double mean() {
89+
if (count() == 0) {
90+
return 0;
91+
}
92+
return sum() / count();
93+
}
94+
}
95+
96+
/**
97+
* The snapshot of a histogram. The snapshot contains the overflow/underflow statistic, number of
98+
* values recorded in each bucket, and the BucketType of the underlying histogram.
99+
*/
100+
@AutoValue
101+
public abstract static class Snapshot {
102+
public abstract OutlierStatistic underflowStatistic();
103+
104+
public abstract OutlierStatistic overflowStatistic();
105+
106+
public abstract ImmutableLongArray buckets();
107+
108+
public abstract HistogramData.BucketType bucketType();
109+
110+
public static Snapshot create(
111+
OutlierStatistic underflowStatistic,
112+
OutlierStatistic overflowStatistic,
113+
ImmutableLongArray buckets,
114+
HistogramData.BucketType bucketType) {
115+
return new AutoValue_LockFreeHistogram_Snapshot(
116+
underflowStatistic, overflowStatistic, buckets, bucketType);
117+
}
118+
119+
@Memoized
120+
public long totalCount() {
121+
long count = 0;
122+
count += underflowStatistic().count();
123+
count += overflowStatistic().count();
124+
count += buckets().stream().sum();
125+
126+
return count;
127+
}
128+
}
129+
130+
/**
131+
* Extract a delta update of this histogram. Update represents values that have been recorded in
132+
* this histogram since the last time this method was called.
133+
*
134+
* <p>If this histogram is being updated concurrent to this method, then the returned snapshot is
135+
* not guarenteed to contain those updates. However, those updates are not dropped and will be
136+
* represented in a future call to this method.
137+
*
138+
* <p>If this histogram has not been updated since the last call to this method, an empty optional
139+
* is returned.
140+
*/
141+
public Optional<Snapshot> getSnapshotAndReset() {
142+
if (!dirty.getAndSet(false)) {
143+
return Optional.empty();
144+
}
145+
146+
ImmutableLongArray.Builder bucketsSnapshotBuilder =
147+
ImmutableLongArray.builder(buckets.length());
148+
for (int i = 0; i < buckets.length(); i++) {
149+
bucketsSnapshotBuilder.add(buckets.getAndSet(i, 0));
150+
}
151+
OutlierStatistic overflowSnapshot = overflowStatistic.getAndSet(OutlierStatistic.EMPTY);
152+
OutlierStatistic underflowSnapshot = underflowStatistic.getAndSet(OutlierStatistic.EMPTY);
153+
154+
return Optional.of(
155+
Snapshot.create(
156+
underflowSnapshot, overflowSnapshot, bucketsSnapshotBuilder.build(), bucketType));
157+
}
158+
159+
@Override
160+
public MetricName getName() {
161+
return name;
162+
}
163+
164+
private void updateInternal(double value) {
165+
double rangeTo = bucketType.getRangeTo();
166+
double rangeFrom = bucketType.getRangeFrom();
167+
if (value >= rangeTo) {
168+
recordTopRecordsValue(value);
169+
} else if (value < rangeFrom) {
170+
recordBottomRecordsValue(value);
171+
} else {
172+
recordInBoundsValue(value);
173+
}
174+
}
175+
176+
@Override
177+
public void update(double value) {
178+
updateInternal(value);
179+
dirty.set(true);
180+
}
181+
182+
@Override
183+
public void update(double... values) {
184+
for (double value : values) {
185+
updateInternal(value);
186+
}
187+
dirty.set(true);
188+
}
189+
190+
/** Record a inbounds value to the appropriate bucket. */
191+
private void recordInBoundsValue(double value) {
192+
int index = bucketType.getBucketIndex(value);
193+
if (index < 0 || index >= bucketType.getNumBuckets()) {
194+
return;
195+
}
196+
197+
buckets.getAndIncrement(index);
198+
}
199+
200+
/**
201+
* Record a new value in {@code overflowStatistic}. This method should only be called when a
202+
* Histogram is recording a value greater than the upper bound of it's largest bucket.
203+
*
204+
* @param value
205+
*/
206+
private void recordTopRecordsValue(double value) {
207+
OutlierStatistic original;
208+
do {
209+
original = overflowStatistic.get();
210+
} while (!overflowStatistic.compareAndSet(original, original.combine(value)));
211+
}
212+
213+
/**
214+
* Record a new value in {@code underflowStatistic}. This method should only be called when a
215+
* Histogram is recording a value smaller than the lowerbound bound of it's smallest bucket.
216+
*/
217+
private void recordBottomRecordsValue(double value) {
218+
OutlierStatistic original;
219+
do {
220+
original = underflowStatistic.get();
221+
} while (!underflowStatistic.compareAndSet(original, original.combine(value)));
222+
}
223+
}

runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/MetricsToPerStepNamespaceMetricsConverter.java

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ private static Optional<MetricValue> convertCounterToMetricValue(
7070
* @param outputHistogram
7171
*/
7272
private static void addOutlierStatsToHistogram(
73-
HistogramData inputHistogram, DataflowHistogramValue outputHistogram) {
74-
long overflowCount = inputHistogram.getTopBucketCount();
75-
long underflowCount = inputHistogram.getBottomBucketCount();
73+
LockFreeHistogram.Snapshot inputHistogram, DataflowHistogramValue outputHistogram) {
74+
long overflowCount = inputHistogram.overflowStatistic().count();
75+
long underflowCount = inputHistogram.underflowStatistic().count();
7676
if (underflowCount == 0 && overflowCount == 0) {
7777
return;
7878
}
@@ -81,12 +81,12 @@ private static void addOutlierStatsToHistogram(
8181
if (underflowCount > 0) {
8282
outlierStats
8383
.setUnderflowCount(underflowCount)
84-
.setUnderflowMean(inputHistogram.getBottomBucketMean());
84+
.setUnderflowMean(inputHistogram.underflowStatistic().mean());
8585
}
8686
if (overflowCount > 0) {
8787
outlierStats
8888
.setOverflowCount(overflowCount)
89-
.setOverflowMean(inputHistogram.getTopBucketMean());
89+
.setOverflowMean(inputHistogram.overflowStatistic().mean());
9090
}
9191
outputHistogram.setOutlierStats(outlierStats);
9292
}
@@ -99,8 +99,8 @@ private static void addOutlierStatsToHistogram(
9999
* Otherwise returns an empty optional.
100100
*/
101101
private static Optional<MetricValue> convertHistogramToMetricValue(
102-
MetricName metricName, HistogramData inputHistogram) {
103-
if (inputHistogram.getTotalCount() == 0L) {
102+
MetricName metricName, LockFreeHistogram.Snapshot inputHistogram) {
103+
if (inputHistogram.totalCount() == 0L) {
104104
return Optional.empty();
105105
}
106106

@@ -111,33 +111,31 @@ private static Optional<MetricValue> convertHistogramToMetricValue(
111111
}
112112

113113
DataflowHistogramValue outputHistogram = new DataflowHistogramValue();
114-
int numberOfBuckets = inputHistogram.getBucketType().getNumBuckets();
114+
int numberOfBuckets = inputHistogram.bucketType().getNumBuckets();
115115

116-
if (inputHistogram.getBucketType() instanceof HistogramData.LinearBuckets) {
116+
if (inputHistogram.bucketType() instanceof HistogramData.LinearBuckets) {
117117
HistogramData.LinearBuckets buckets =
118-
(HistogramData.LinearBuckets) inputHistogram.getBucketType();
118+
(HistogramData.LinearBuckets) inputHistogram.bucketType();
119119
Linear linearOptions =
120120
new Linear()
121121
.setNumberOfBuckets(numberOfBuckets)
122122
.setWidth(buckets.getWidth())
123123
.setStart(buckets.getStart());
124124
outputHistogram.setBucketOptions(new BucketOptions().setLinear(linearOptions));
125-
} else if (inputHistogram.getBucketType() instanceof HistogramData.ExponentialBuckets) {
125+
} else if (inputHistogram.bucketType() instanceof HistogramData.ExponentialBuckets) {
126126
HistogramData.ExponentialBuckets buckets =
127-
(HistogramData.ExponentialBuckets) inputHistogram.getBucketType();
127+
(HistogramData.ExponentialBuckets) inputHistogram.bucketType();
128128
Base2Exponent expoenntialOptions =
129129
new Base2Exponent().setNumberOfBuckets(numberOfBuckets).setScale(buckets.getScale());
130130
outputHistogram.setBucketOptions(new BucketOptions().setExponential(expoenntialOptions));
131131
} else {
132132
return Optional.empty();
133133
}
134134

135-
outputHistogram.setCount(inputHistogram.getTotalCount());
136-
List<Long> bucketCounts = new ArrayList<>(inputHistogram.getBucketType().getNumBuckets());
135+
outputHistogram.setCount(inputHistogram.totalCount());
136+
List<Long> bucketCounts = new ArrayList<>(inputHistogram.buckets().length());
137137

138-
for (int i = 0; i < inputHistogram.getBucketType().getNumBuckets(); i++) {
139-
bucketCounts.add(inputHistogram.getCount(i));
140-
}
138+
inputHistogram.buckets().forEach(val -> bucketCounts.add(val));
141139

142140
// Remove trailing 0 buckets.
143141
for (int i = bucketCounts.size() - 1; i >= 0; i--) {
@@ -167,7 +165,9 @@ private static Optional<MetricValue> convertHistogramToMetricValue(
167165
* stage, metrics namespace} pair.
168166
*/
169167
public static Collection<PerStepNamespaceMetrics> convert(
170-
String stepName, Map<MetricName, Long> counters, Map<MetricName, HistogramData> histograms) {
168+
String stepName,
169+
Map<MetricName, Long> counters,
170+
Map<MetricName, LockFreeHistogram.Snapshot> histograms) {
171171

172172
Map<String, PerStepNamespaceMetrics> metricsByNamespace = new HashMap<>();
173173
for (Entry<MetricName, Long> entry : counters.entrySet()) {
@@ -192,7 +192,7 @@ public static Collection<PerStepNamespaceMetrics> convert(
192192
stepNamespaceMetrics.getMetricValues().add(metricValue.get());
193193
}
194194

195-
for (Entry<MetricName, HistogramData> entry : histograms.entrySet()) {
195+
for (Entry<MetricName, LockFreeHistogram.Snapshot> entry : histograms.entrySet()) {
196196
MetricName metricName = entry.getKey();
197197
Optional<MetricValue> metricValue =
198198
convertHistogramToMetricValue(metricName, entry.getValue());

runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingStepMetricsContainer.java

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import javax.annotation.Nonnull;
3333
import org.apache.beam.runners.core.metrics.DistributionData;
3434
import org.apache.beam.runners.core.metrics.GaugeCell;
35-
import org.apache.beam.runners.core.metrics.HistogramCell;
3635
import org.apache.beam.runners.core.metrics.MetricsMap;
3736
import org.apache.beam.sdk.metrics.Counter;
3837
import org.apache.beam.sdk.metrics.Distribution;
@@ -71,8 +70,8 @@ public class StreamingStepMetricsContainer implements MetricsContainer {
7170
private MetricsMap<MetricName, DeltaDistributionCell> distributions =
7271
new MetricsMap<>(DeltaDistributionCell::new);
7372

74-
private MetricsMap<KV<MetricName, HistogramData.BucketType>, HistogramCell> perWorkerHistograms =
75-
new MetricsMap<>(HistogramCell::new);
73+
private MetricsMap<KV<MetricName, HistogramData.BucketType>, LockFreeHistogram>
74+
perWorkerHistograms = new MetricsMap<>(LockFreeHistogram::new);
7675

7776
private final Map<MetricName, Instant> perWorkerCountersByFirstStaleTime;
7877

@@ -267,8 +266,8 @@ private void deleteStaleCounters(
267266
@VisibleForTesting
268267
Iterable<PerStepNamespaceMetrics> extractPerWorkerMetricUpdates() {
269268
ConcurrentHashMap<MetricName, Long> counters = new ConcurrentHashMap<MetricName, Long>();
270-
ConcurrentHashMap<MetricName, HistogramData> histograms =
271-
new ConcurrentHashMap<MetricName, HistogramData>();
269+
ConcurrentHashMap<MetricName, LockFreeHistogram.Snapshot> histograms =
270+
new ConcurrentHashMap<MetricName, LockFreeHistogram.Snapshot>();
272271
HashSet<MetricName> currentZeroValuedCounters = new HashSet<MetricName>();
273272

274273
// Extract metrics updates.
@@ -283,11 +282,7 @@ Iterable<PerStepNamespaceMetrics> extractPerWorkerMetricUpdates() {
283282
});
284283
perWorkerHistograms.forEach(
285284
(k, v) -> {
286-
HistogramData val = v.getCumulative().getAndReset();
287-
if (val.getTotalCount() == 0) {
288-
return;
289-
}
290-
histograms.put(k.getKey(), val);
285+
v.getSnapshotAndReset().ifPresent(snapshot -> histograms.put(k.getKey(), snapshot));
291286
});
292287

293288
deleteStaleCounters(currentZeroValuedCounters, Instant.now(clock));

0 commit comments

Comments
 (0)