Skip to content

added sliding window histogram #200

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 6, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,37 +21,27 @@
import com.netflix.servo.monitor.NumberGauge;
import org.HdrHistogram.Histogram;

import java.util.concurrent.TimeUnit;

/**
* Gauge that wraps a {@link Histogram} and when it's polled returns a particular percentage
*/
public class HdrHistogramGauge extends NumberGauge {
private static final long TIMEOUT = TimeUnit.MINUTES.toMillis(1);
private final Histogram histogram;
private final SlidingWindowHistogram histogram;
private final double percentile;
private volatile long lastCleared = System.currentTimeMillis();
private final Runnable slide;

public HdrHistogramGauge(MonitorConfig monitorConfig, Histogram histogram, double percentile) {
public HdrHistogramGauge(MonitorConfig monitorConfig, SlidingWindowHistogram histogram, double percentile, Runnable slide) {
super(monitorConfig);
this.histogram = histogram;
this.percentile = percentile;
this.slide = slide;

DefaultMonitorRegistry.getInstance().register(this);
}

@Override
public Long getValue() {
long value = histogram.getValueAtPercentile(percentile);
if (System.currentTimeMillis() - lastCleared > TIMEOUT) {
synchronized (histogram) {
if (System.currentTimeMillis() - lastCleared > TIMEOUT) {
histogram.reset();
lastCleared = System.currentTimeMillis();
}
}
}

long value = histogram.aggregateHistogram().getValueAtPercentile(percentile);
slide.run();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could lead to unintentional sliding of the window. eg: If a user action fetches the value (eg: from a JMX console), it will reset the histogram.

Copy link
Member Author

@robertroeser robertroeser Dec 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slide is passed in as a closure, and that method has a 1 minute timeout

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but do you intend to slide (and in the other class, today it rotates the histogram) on every get ?

Copy link
Member Author

@robertroeser robertroeser Dec 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes - it'll only rotate now when the the timeout expires

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, didn't see the timeout condition, thanks!

return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
* Gauge that wraps a {@link Histogram} and when its polled returns it's max
*/
public class HdrHistogramMaxGauge extends NumberGauge {
private final Histogram histogram;
private final SlidingWindowHistogram histogram;

public HdrHistogramMaxGauge(MonitorConfig monitorConfig, Histogram histogram) {
public HdrHistogramMaxGauge(MonitorConfig monitorConfig, SlidingWindowHistogram histogram) {
super(monitorConfig);
this.histogram = histogram;

Expand All @@ -36,6 +36,6 @@ public HdrHistogramMaxGauge(MonitorConfig monitorConfig, Histogram histogram) {

@Override
public Long getValue() {
return histogram.getMaxValue();
return histogram.aggregateHistogram().getMaxValue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
* Gauge that wraps a {@link Histogram} and when its polled returns it's min
*/
public class HdrHistogramMinGauge extends NumberGauge {
private final Histogram histogram;
private final SlidingWindowHistogram histogram;

public HdrHistogramMinGauge(MonitorConfig monitorConfig, Histogram histogram) {
public HdrHistogramMinGauge(MonitorConfig monitorConfig, SlidingWindowHistogram histogram) {
super(monitorConfig);
this.histogram = histogram;

Expand All @@ -36,6 +36,6 @@ public HdrHistogramMinGauge(MonitorConfig monitorConfig, Histogram histogram) {

@Override
public Long getValue() {
return histogram.getMinValue();
return histogram.aggregateHistogram().getMinValue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@

import com.netflix.servo.monitor.MonitorConfig;
import com.netflix.servo.tag.Tag;
import org.HdrHistogram.ConcurrentHistogram;
import org.HdrHistogram.Histogram;

import java.util.Arrays;
import java.util.List;
Expand All @@ -29,7 +27,11 @@
* The buckets are min, max, 50%, 90%, 99%, 99.9%, and 99.99%
*/
public class HdrHistogramServoTimer {
private final Histogram histogram = new ConcurrentHistogram(TimeUnit.MINUTES.toNanos(1), 2);
private final SlidingWindowHistogram histogram = new SlidingWindowHistogram();

private static final long TIMEOUT = TimeUnit.MINUTES.toMillis(1);

private volatile long lastCleared = System.currentTimeMillis();

private HdrHistogramMinGauge min;

Expand All @@ -46,31 +48,26 @@ public class HdrHistogramServoTimer {
private HdrHistogramGauge p99_99;

private HdrHistogramServoTimer(String label) {
histogram.setAutoResize(true);

min = new HdrHistogramMinGauge(MonitorConfig.builder(label + "_min").build(), histogram);
max = new HdrHistogramMaxGauge(MonitorConfig.builder(label + "_max").build(), histogram);
min = new HdrHistogramMinGauge(MonitorConfig.builder(label).withTag("value", "min").build(), histogram);
max = new HdrHistogramMaxGauge(MonitorConfig.builder(label).withTag("value", "max").build(), histogram);

p50 = new HdrHistogramGauge(MonitorConfig.builder(label + "_p50").build(), histogram, 50);
p90 = new HdrHistogramGauge(MonitorConfig.builder(label + "_p90").build(), histogram, 90);
p99 = new HdrHistogramGauge(MonitorConfig.builder(label + "_p99").build(), histogram, 99);
p99_9 = new HdrHistogramGauge(MonitorConfig.builder(label + "_p99_9").build(), histogram, 99.9);
p99_99 = new HdrHistogramGauge(MonitorConfig.builder(label + "_p99_99").build(), histogram, 99.99);
p50 = new HdrHistogramGauge(MonitorConfig.builder(label).withTag("value", "p50").build(), histogram, 50, this::slide);
p90 = new HdrHistogramGauge(MonitorConfig.builder(label).withTag("value", "p90").build(), histogram, 90, this::slide);
p99 = new HdrHistogramGauge(MonitorConfig.builder(label).withTag("value", "p99").build(), histogram, 99, this::slide);
p99_9 = new HdrHistogramGauge(MonitorConfig.builder(label).withTag("value", "p99_9").build(), histogram, 99.9, this::slide);
p99_99 = new HdrHistogramGauge(MonitorConfig.builder(label).withTag("value", "p99_99").build(), histogram, 99.99, this::slide);
}


private HdrHistogramServoTimer(String label, List<Tag> tags) {
histogram.setAutoResize(true);


min = new HdrHistogramMinGauge(MonitorConfig.builder(label + "_min").withTags(tags).build(), histogram);
max = new HdrHistogramMaxGauge(MonitorConfig.builder(label + "_max").withTags(tags).build(), histogram);

p50 = new HdrHistogramGauge(MonitorConfig.builder(label + "_p50").withTags(tags).build(), histogram, 50);
p90 = new HdrHistogramGauge(MonitorConfig.builder(label + "_p90").withTags(tags).build(), histogram, 90);
p99 = new HdrHistogramGauge(MonitorConfig.builder(label + "_p99").withTags(tags).build(), histogram, 99);
p99_9 = new HdrHistogramGauge(MonitorConfig.builder(label + "_p99_9").withTags(tags).build(), histogram, 99.9);
p99_99 = new HdrHistogramGauge(MonitorConfig.builder(label + "_p99_99").withTags(tags).build(), histogram, 99.99);
min = new HdrHistogramMinGauge(MonitorConfig.builder(label).withTag("value", "min").withTags(tags).build(), histogram);
max = new HdrHistogramMaxGauge(MonitorConfig.builder(label).withTag("value", "min").withTags(tags).build(), histogram);

p50 = new HdrHistogramGauge(MonitorConfig.builder(label).withTag("value", "p50").withTags(tags).build(), histogram, 50, this::slide);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will rotate the histogram for each slide of min, max, etc. Is that what you intend?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get min and max don't reset the histogram

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad. You are correct that min and max don't but 50,90,99,99_9,99_99 all do.

p90 = new HdrHistogramGauge(MonitorConfig.builder(label).withTag("value", "p90").withTags(tags).build(), histogram, 90, this::slide);
p99 = new HdrHistogramGauge(MonitorConfig.builder(label).withTag("value", "p90").withTags(tags).build(), histogram, 99, this::slide);
p99_9 = new HdrHistogramGauge(MonitorConfig.builder(label).withTag("value", "p99_9").withTags(tags).build(), histogram, 99.9, this::slide);
p99_99 = new HdrHistogramGauge(MonitorConfig.builder(label).withTag("value", "p99_99").withTags(tags).build(), histogram, 99.99, this::slide);
}

public static HdrHistogramServoTimer newInstance(String label) {
Expand All @@ -87,6 +84,7 @@ public static HdrHistogramServoTimer newInstance(String label, List<Tag> tags) {

/**
* Records a value for to the histogram and updates the Servo counter buckets
*
* @param value the value to update
*/
public void record(long value) {
Expand Down Expand Up @@ -120,4 +118,12 @@ public Long getP99_9() {
public Long getP99_99() {
return p99_99.getValue();
}

private synchronized void slide() {
if (System.currentTimeMillis() - lastCleared > TIMEOUT) {
histogram.rotateHistogram();
lastCleared = System.currentTimeMillis();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.reactivesocket.loadbalancer.servo.internal;

import org.HdrHistogram.ConcurrentHistogram;
import org.HdrHistogram.Histogram;

import java.io.PrintStream;
import java.util.ArrayDeque;
import java.util.concurrent.TimeUnit;

/**
* Wraps HdrHistogram to create a sliding window of n histogramQueue. Default window number is five.
*/
public class SlidingWindowHistogram {
private volatile Histogram liveHistogram;

private final ArrayDeque<Histogram> histogramQueue;

private final Object LOCK = new Object();

public SlidingWindowHistogram() {
this(5);
}

public SlidingWindowHistogram(final int numOfWindows) {
if (numOfWindows < 2) {
throw new IllegalArgumentException("number of windows must be greater than 1");
}
this.histogramQueue = new ArrayDeque<>(numOfWindows - 1);
this.liveHistogram = createHistogram();

for (int i = 0; i < numOfWindows - 1; i++) {
histogramQueue.offer(createHistogram());
}
}

private static Histogram createHistogram() {
ConcurrentHistogram histogram = new ConcurrentHistogram(TimeUnit.MINUTES.toNanos(1), 2);
histogram.setAutoResize(true);
return histogram;
}

/**
* Records a value to the in window liveHistogram
*
* @param value value to record
*/
public void recordValue(long value) {
liveHistogram.recordValue(value);
}

/**
* Slides the Histogram window. Pops a Histogram off a queue, resets it, and places the old
* on in the queue.
*/
public void rotateHistogram() {
synchronized (LOCK) {
Histogram onDeck = histogramQueue.poll();
if (onDeck != null) {
onDeck.reset();
Histogram old = liveHistogram;
liveHistogram = onDeck;
histogramQueue.offer(old);
}
}
}

/**
* Aggregates the Histograms into a single Histogram and returns it.
*
* @return Aggregated liveHistogram
*/
public Histogram aggregateHistogram() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since gets will most likely not be concurrent, done by the poller, do we need threadlocal isolation of aggregation? Synchronized aggregation may be simpler and efficient? Also, you are already synchronizing additions to the aggregate.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it's be bad because sync would block the aggregations. One line 86 it adds the live one, and then in the sync block it adds the ones in the queue.
I can replace the thread local with a new object.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the sync will block aggregations but one histogram will be used by one metric which will have one poller and hence there would only be a single thread calling aggregate isn't?

Histogram aggregate = createHistogram();

synchronized (LOCK) {
aggregate.add(liveHistogram);
histogramQueue
.forEach(aggregate::add);
}

return aggregate;
}

/**
* Prints HdrHistogram to System.out
*/
public void print() {
print(System.out);
}

public void print(PrintStream printStream) {
aggregateHistogram().outputPercentileDistribution(printStream, 1000.0);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.reactivesocket.loadbalancer.servo.internal;

import org.HdrHistogram.Histogram;
import org.junit.Assert;
import org.junit.Test;

public class SlidingWindowHistogramTest {
@Test
public void test() {
SlidingWindowHistogram slidingWindowHistogram = new SlidingWindowHistogram(2);

for (int i = 0; i < 100_000; i++) {
slidingWindowHistogram.recordValue(i);
}

slidingWindowHistogram.print();
slidingWindowHistogram.rotateHistogram();
Histogram histogram =
slidingWindowHistogram.aggregateHistogram();

long totalCount = histogram.getTotalCount();
Assert.assertTrue(totalCount == 100_000);

long p90 = histogram.getValueAtPercentile(90);
Assert.assertTrue(p90 < 100_000);

for (int i = 0; i < 100_000; i++) {
slidingWindowHistogram.recordValue(i * 10_000);
}

slidingWindowHistogram.print();
slidingWindowHistogram.rotateHistogram();

histogram =
slidingWindowHistogram.aggregateHistogram();

p90 = histogram.getValueAtPercentile(90);
Assert.assertTrue(p90 >= 100_000);

for (int i = 0; i < 100_000; i++) {
slidingWindowHistogram.recordValue(i);
}

slidingWindowHistogram.print();
slidingWindowHistogram.rotateHistogram();

for (int i = 0; i < 100_000; i++) {
slidingWindowHistogram.recordValue(i);
}

slidingWindowHistogram.print();

histogram =
slidingWindowHistogram.aggregateHistogram();

totalCount = histogram.getTotalCount();
Assert.assertTrue(totalCount == 200_000);

p90 = histogram.getValueAtPercentile(90);
Assert.assertTrue(p90 < 100_000);
}
}