|
1 | 1 | package com.netflix.concurrency.limits.limit; |
2 | 2 |
|
| 3 | +import java.util.concurrent.ThreadLocalRandom; |
3 | 4 | import java.util.concurrent.TimeUnit; |
4 | 5 | import java.util.function.Function; |
5 | 6 |
|
|
12 | 13 | import com.netflix.concurrency.limits.MetricRegistry.SampleListener; |
13 | 14 | import com.netflix.concurrency.limits.internal.EmptyMetricRegistry; |
14 | 15 | import com.netflix.concurrency.limits.internal.Preconditions; |
| 16 | +import com.netflix.concurrency.limits.limit.functions.Log10RootFunction; |
15 | 17 |
|
16 | 18 | /** |
17 | 19 | * Limiter based on TCP Vegas where the limit increases by alpha if the queue_use is small ({@literal <} alpha) |
|
26 | 28 | public class VegasLimit implements Limit { |
27 | 29 | private static final Logger LOG = LoggerFactory.getLogger(VegasLimit.class); |
28 | 30 |
|
| 31 | + private static final Function<Integer, Integer> LOG10 = Log10RootFunction.create(0); |
| 32 | + |
| 33 | + private static final int DISABLED = -1; |
| 34 | + |
29 | 35 | public static class Builder { |
30 | 36 | private int initialLimit = 20; |
31 | 37 | private int maxConcurrency = 1000; |
32 | 38 | private MetricRegistry registry = EmptyMetricRegistry.INSTANCE; |
33 | 39 | private double smoothing = 1.0; |
34 | 40 |
|
35 | | - private Function<Integer, Integer> alpha = (limit) -> 3; |
36 | | - private Function<Integer, Integer> beta = (limit) -> 6; |
37 | | - private Function<Double, Double> increaseFunc = (limit) -> limit + 1; |
38 | | - private Function<Double, Double> decreaseFunc = (limit) -> limit - 1; |
| 41 | + private Function<Integer, Integer> alphaFunc = (limit) -> 3 * LOG10.apply(limit.intValue()); |
| 42 | + private Function<Integer, Integer> betaFunc = (limit) -> 6 * LOG10.apply(limit.intValue()); |
| 43 | + private Function<Integer, Integer> thresholdFunc = (limit) -> LOG10.apply(limit.intValue()); |
| 44 | + private Function<Double, Double> increaseFunc = (limit) -> limit + LOG10.apply(limit.intValue()); |
| 45 | + private Function<Double, Double> decreaseFunc = (limit) -> limit - LOG10.apply(limit.intValue()); |
| 46 | + private int probeMultiplier = 30; |
| 47 | + |
| 48 | + private Builder() { |
| 49 | + } |
| 50 | + |
| 51 | + /** |
| 52 | + * The limiter will probe for a new noload RTT every probeMultiplier * current limit |
| 53 | + * iterations. Default value is 30. |
| 54 | + * @param probeMultiplier |
| 55 | + * @return Chinable builder |
| 56 | + */ |
| 57 | + public Builder probeMultiplier(int probeMultiplier) { |
| 58 | + this.probeMultiplier = probeMultiplier; |
| 59 | + return this; |
| 60 | + } |
39 | 61 |
|
40 | 62 | public Builder alpha(int alpha) { |
41 | | - this.alpha = (ignore) -> alpha; |
| 63 | + this.alphaFunc = (ignore) -> alpha; |
| 64 | + return this; |
| 65 | + } |
| 66 | + |
| 67 | + public Builder threshold(Function<Integer, Integer> threshold) { |
| 68 | + this.thresholdFunc = threshold; |
42 | 69 | return this; |
43 | 70 | } |
44 | 71 |
|
45 | 72 | public Builder alpha(Function<Integer, Integer> alpha) { |
46 | | - this.alpha = alpha; |
| 73 | + this.alphaFunc = alpha; |
47 | 74 | return this; |
48 | 75 | } |
49 | 76 |
|
50 | 77 | public Builder beta(int beta) { |
51 | | - this.beta = (ignore) -> beta; |
| 78 | + this.betaFunc = (ignore) -> beta; |
52 | 79 | return this; |
53 | 80 | } |
54 | 81 |
|
55 | 82 | public Builder beta(Function<Integer, Integer> beta) { |
56 | | - this.beta = beta; |
| 83 | + this.betaFunc = beta; |
57 | 84 | return this; |
58 | 85 | } |
59 | 86 |
|
@@ -125,49 +152,77 @@ public static VegasLimit newDefault() { |
125 | 152 | private final double smoothing; |
126 | 153 | private final Function<Integer, Integer> alphaFunc; |
127 | 154 | private final Function<Integer, Integer> betaFunc; |
| 155 | + private final Function<Integer, Integer> thresholdFunc; |
128 | 156 | private final Function<Double, Double> increaseFunc; |
129 | 157 | private final Function<Double, Double> decreaseFunc; |
130 | 158 | private final SampleListener rttSampleListener; |
| 159 | + private int probeMultiplier; |
| 160 | + private int probeCountdown; |
131 | 161 |
|
132 | 162 | private VegasLimit(Builder builder) { |
133 | 163 | this.estimatedLimit = builder.initialLimit; |
134 | 164 | this.maxLimit = builder.maxConcurrency; |
135 | | - this.alphaFunc = builder.alpha; |
136 | | - this.betaFunc = builder.beta; |
| 165 | + this.alphaFunc = builder.alphaFunc; |
| 166 | + this.betaFunc = builder.betaFunc; |
137 | 167 | this.increaseFunc = builder.increaseFunc; |
138 | 168 | this.decreaseFunc = builder.decreaseFunc; |
| 169 | + this.thresholdFunc = builder.thresholdFunc; |
139 | 170 | this.smoothing = builder.smoothing; |
| 171 | + this.probeMultiplier = builder.probeMultiplier; |
| 172 | + this.probeCountdown = nextProbeCountdown(); |
140 | 173 |
|
141 | 174 | this.rttSampleListener = builder.registry.registerDistribution(MetricIds.MIN_RTT_NAME); |
142 | | - |
| 175 | + } |
| 176 | + |
| 177 | + private int nextProbeCountdown() { |
| 178 | + int max = (int) (probeMultiplier * estimatedLimit); |
| 179 | + return ThreadLocalRandom.current().nextInt(max / 2, max); |
143 | 180 | } |
144 | 181 |
|
145 | 182 | @Override |
146 | 183 | public synchronized void update(SampleWindow sample) { |
147 | 184 | long rtt = sample.getCandidateRttNanos(); |
148 | 185 | Preconditions.checkArgument(rtt > 0, "rtt must be >0 but got " + rtt); |
149 | 186 |
|
| 187 | + final int queueSize = (int) Math.ceil(estimatedLimit * (1 - (double)rtt_noload / rtt)); |
| 188 | + |
| 189 | + if (probeCountdown != DISABLED && probeCountdown-- <= 0) { |
| 190 | + LOG.debug("Probe MinRTT {}", TimeUnit.NANOSECONDS.toMicros(rtt) / 1000.0); |
| 191 | + probeCountdown = nextProbeCountdown(); |
| 192 | + rtt_noload = rtt; |
| 193 | + return; |
| 194 | + } |
| 195 | + |
150 | 196 | if (rtt_noload == 0 || rtt < rtt_noload) { |
151 | 197 | LOG.debug("New MinRTT {}", TimeUnit.NANOSECONDS.toMicros(rtt) / 1000.0); |
152 | 198 | rtt_noload = rtt; |
| 199 | + return; |
153 | 200 | } |
154 | 201 |
|
155 | 202 | rttSampleListener.addSample(rtt_noload); |
156 | 203 |
|
157 | 204 | double newLimit; |
158 | | - final int queueSize = (int) Math.ceil(estimatedLimit * (1 - (double)rtt_noload / rtt)); |
| 205 | + // Treat any drop (i.e timeout) as needing to reduce the limit |
159 | 206 | if (sample.didDrop()) { |
160 | 207 | newLimit = decreaseFunc.apply(estimatedLimit); |
161 | | - } else if (sample.getMaxInFlight() + queueSize < estimatedLimit) { |
| 208 | + // Prevent upward drift if not close to the limit |
| 209 | + } else if (sample.getMaxInFlight() * 2 < estimatedLimit) { |
162 | 210 | return; |
163 | 211 | } else { |
164 | 212 | int alpha = alphaFunc.apply((int)estimatedLimit); |
165 | 213 | int beta = betaFunc.apply((int)estimatedLimit); |
| 214 | + int threshold = this.thresholdFunc.apply((int)estimatedLimit); |
166 | 215 |
|
167 | | - if (queueSize < alpha) { |
| 216 | + // Aggressive increase when no queuing |
| 217 | + if (queueSize <= threshold) { |
| 218 | + newLimit = estimatedLimit + beta; |
| 219 | + // Increase the limit if queue is still manageable |
| 220 | + } else if (queueSize < alpha) { |
168 | 221 | newLimit = increaseFunc.apply(estimatedLimit); |
| 222 | + // Detecting latency so decrease |
169 | 223 | } else if (queueSize > beta) { |
170 | 224 | newLimit = decreaseFunc.apply(estimatedLimit); |
| 225 | + // We're within he sweet spot so nothing to do |
171 | 226 | } else { |
172 | 227 | return; |
173 | 228 | } |
|
0 commit comments