
com.netflix.concurrency.limits.limit.VegasLimit Maven / Gradle / Ivy
package com.netflix.concurrency.limits.limit;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.netflix.concurrency.limits.Limit;
import com.netflix.concurrency.limits.MetricIds;
import com.netflix.concurrency.limits.MetricRegistry;
import com.netflix.concurrency.limits.MetricRegistry.SampleListener;
import com.netflix.concurrency.limits.internal.EmptyMetricRegistry;
import com.netflix.concurrency.limits.internal.Preconditions;
import com.netflix.concurrency.limits.limit.functions.Log10RootFunction;
/**
* Limiter based on TCP Vegas where the limit increases by alpha if the queue_use is small ({@literal <} alpha)
* and decreases by alpha if the queue_use is large ({@literal >} beta).
*
* Queue size is calculated using the formula,
* queue_use = limit − BWE×RTTnoLoad = limit × (1 − RTTnoLoad/RTTactual)
*
* For traditional TCP Vegas alpha is typically 2-3 and beta is typically 4-6. To allow for better growth and
* stability at higher limits we set alpha=Max(3, 10% of the current limit) and beta=Max(6, 20% of the current limit)
*/
public class VegasLimit implements Limit {
private static final Logger LOG = LoggerFactory.getLogger(VegasLimit.class);
private static final Function LOG10 = Log10RootFunction.create(0);
private static final int DISABLED = -1;
public static class Builder {
private int initialLimit = 20;
private int maxConcurrency = 1000;
private MetricRegistry registry = EmptyMetricRegistry.INSTANCE;
private double smoothing = 1.0;
private Function alphaFunc = (limit) -> 3 * LOG10.apply(limit.intValue());
private Function betaFunc = (limit) -> 6 * LOG10.apply(limit.intValue());
private Function thresholdFunc = (limit) -> LOG10.apply(limit.intValue());
private Function increaseFunc = (limit) -> limit + LOG10.apply(limit.intValue());
private Function decreaseFunc = (limit) -> limit - LOG10.apply(limit.intValue());
private int probeMultiplier = 30;
private Builder() {
}
/**
* The limiter will probe for a new noload RTT every probeMultiplier * current limit
* iterations. Default value is 30.
* @param probeMultiplier
* @return Chinable builder
*/
public Builder probeMultiplier(int probeMultiplier) {
this.probeMultiplier = probeMultiplier;
return this;
}
public Builder alpha(int alpha) {
this.alphaFunc = (ignore) -> alpha;
return this;
}
public Builder threshold(Function threshold) {
this.thresholdFunc = threshold;
return this;
}
public Builder alpha(Function alpha) {
this.alphaFunc = alpha;
return this;
}
public Builder beta(int beta) {
this.betaFunc = (ignore) -> beta;
return this;
}
public Builder beta(Function beta) {
this.betaFunc = beta;
return this;
}
public Builder increase(Function increase) {
this.increaseFunc = increase;
return this;
}
public Builder decrease(Function decrease) {
this.decreaseFunc = decrease;
return this;
}
public Builder smoothing(double smoothing) {
this.smoothing = smoothing;
return this;
}
public Builder initialLimit(int initialLimit) {
this.initialLimit = initialLimit;
return this;
}
@Deprecated
public Builder tolerance(double tolerance) {
return this;
}
public Builder maxConcurrency(int maxConcurrency) {
this.maxConcurrency = maxConcurrency;
return this;
}
@Deprecated
public Builder backoffRatio(double ratio) {
return this;
}
public Builder metricRegistry(MetricRegistry registry) {
this.registry = registry;
return this;
}
public VegasLimit build() {
return new VegasLimit(this);
}
}
public static Builder newBuilder() {
return new Builder();
}
public static VegasLimit newDefault() {
return newBuilder().build();
}
/**
* Estimated concurrency limit based on our algorithm
*/
private volatile double estimatedLimit;
private volatile long rtt_noload = 0;
/**
* Maximum allowed limit providing an upper bound failsafe
*/
private final int maxLimit;
private final double smoothing;
private final Function alphaFunc;
private final Function betaFunc;
private final Function thresholdFunc;
private final Function increaseFunc;
private final Function decreaseFunc;
private final SampleListener rttSampleListener;
private int probeMultiplier;
private int probeCountdown;
private VegasLimit(Builder builder) {
this.estimatedLimit = builder.initialLimit;
this.maxLimit = builder.maxConcurrency;
this.alphaFunc = builder.alphaFunc;
this.betaFunc = builder.betaFunc;
this.increaseFunc = builder.increaseFunc;
this.decreaseFunc = builder.decreaseFunc;
this.thresholdFunc = builder.thresholdFunc;
this.smoothing = builder.smoothing;
this.probeMultiplier = builder.probeMultiplier;
this.probeCountdown = nextProbeCountdown();
this.rttSampleListener = builder.registry.registerDistribution(MetricIds.MIN_RTT_NAME);
}
private int nextProbeCountdown() {
int max = (int) (probeMultiplier * estimatedLimit);
return ThreadLocalRandom.current().nextInt(max / 2, max);
}
@Override
public synchronized void update(SampleWindow sample) {
long rtt = sample.getCandidateRttNanos();
Preconditions.checkArgument(rtt > 0, "rtt must be >0 but got " + rtt);
final int queueSize = (int) Math.ceil(estimatedLimit * (1 - (double)rtt_noload / rtt));
if (probeCountdown != DISABLED && probeCountdown-- <= 0) {
LOG.debug("Probe MinRTT {}", TimeUnit.NANOSECONDS.toMicros(rtt) / 1000.0);
probeCountdown = nextProbeCountdown();
rtt_noload = rtt;
return;
}
if (rtt_noload == 0 || rtt < rtt_noload) {
LOG.debug("New MinRTT {}", TimeUnit.NANOSECONDS.toMicros(rtt) / 1000.0);
rtt_noload = rtt;
return;
}
rttSampleListener.addSample(rtt_noload);
double newLimit;
// Treat any drop (i.e timeout) as needing to reduce the limit
if (sample.didDrop()) {
newLimit = decreaseFunc.apply(estimatedLimit);
// Prevent upward drift if not close to the limit
} else if (sample.getMaxInFlight() * 2 < estimatedLimit) {
return;
} else {
int alpha = alphaFunc.apply((int)estimatedLimit);
int beta = betaFunc.apply((int)estimatedLimit);
int threshold = this.thresholdFunc.apply((int)estimatedLimit);
// Aggressive increase when no queuing
if (queueSize <= threshold) {
newLimit = estimatedLimit + beta;
// Increase the limit if queue is still manageable
} else if (queueSize < alpha) {
newLimit = increaseFunc.apply(estimatedLimit);
// Detecting latency so decrease
} else if (queueSize > beta) {
newLimit = decreaseFunc.apply(estimatedLimit);
// We're within he sweet spot so nothing to do
} else {
return;
}
}
newLimit = Math.max(1, Math.min(maxLimit, newLimit));
newLimit = (1 - smoothing) * estimatedLimit + smoothing * newLimit;
if ((int)newLimit != (int)estimatedLimit && LOG.isDebugEnabled()) {
LOG.debug("New limit={} minRtt={} ms winRtt={} ms queueSize={}",
(int)newLimit,
TimeUnit.NANOSECONDS.toMicros(rtt_noload) / 1000.0,
TimeUnit.NANOSECONDS.toMicros(rtt) / 1000.0,
queueSize);
}
estimatedLimit = newLimit;
}
@Override
public int getLimit() {
return (int)estimatedLimit;
}
@Override
public String toString() {
return "VegasLimit [limit=" + getLimit() +
", rtt_noload=" + TimeUnit.NANOSECONDS.toMicros(rtt_noload) / 1000.0 +
" ms]";
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy