Skip to content

Commit 1cf038d

Browse files
author
Ray Mattingly
committed
FixedIntervalRateLimiter support for a shorter refill interval
1 parent 0763a74 commit 1cf038d

File tree

4 files changed

+130
-14
lines changed

4 files changed

+130
-14
lines changed

hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/FixedIntervalRateLimiter.java

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,65 @@
2121
import org.apache.yetus.audience.InterfaceAudience;
2222
import org.apache.yetus.audience.InterfaceStability;
2323

24+
import org.apache.hbase.thirdparty.com.google.common.base.Preconditions;
25+
2426
/**
2527
* With this limiter resources will be refilled only after a fixed interval of time.
2628
*/
2729
@InterfaceAudience.Private
2830
@InterfaceStability.Evolving
2931
public class FixedIntervalRateLimiter extends RateLimiter {
32+
33+
/**
34+
* The FixedIntervalRateLimiter can be harsh from a latency/backoff perspective, which makes it
35+
* difficult to fully and consistently utilize a quota allowance. By configuring the
36+
* {@link #RATE_LIMITER_REFILL_INTERVAL_MS} to a lower value you will encourage the rate limiter
37+
* to throw smaller wait intervals for requests which may be fulfilled in timeframes shorter than
38+
* the quota's full interval. For example, if you're saturating a 100MB/sec read IO quota with a
39+
* ton of tiny gets, then configuring this to a value like 100ms will ensure that your retry
40+
* backoffs approach ~100ms, rather than 1sec. Be careful not to configure this too low, or you
41+
* may produce a dangerous amount of retry volume.
42+
*/
43+
public static final String RATE_LIMITER_REFILL_INTERVAL_MS =
44+
"hbase.quota.rate.limiter.refill.interval.ms";
45+
3046
private long nextRefillTime = -1L;
47+
private final long refillInterval;
48+
49+
public FixedIntervalRateLimiter() {
50+
this(DEFAULT_TIME_UNIT);
51+
}
52+
53+
public FixedIntervalRateLimiter(long refillInterval) {
54+
super();
55+
Preconditions.checkArgument(getTimeUnitInMillis() >= refillInterval,
56+
String.format("Refill interval %s must be less than or equal to TimeUnit millis %s",
57+
refillInterval, getTimeUnitInMillis()));
58+
this.refillInterval = refillInterval;
59+
}
3160

3261
@Override
3362
public long refill(long limit) {
3463
final long now = EnvironmentEdgeManager.currentTime();
64+
if (nextRefillTime == -1) {
65+
nextRefillTime = now + refillInterval;
66+
return limit;
67+
}
3568
if (now < nextRefillTime) {
3669
return 0;
3770
}
38-
nextRefillTime = now + super.getTimeUnitInMillis();
39-
return limit;
71+
long diff = refillInterval + now - nextRefillTime;
72+
long refills = diff / refillInterval;
73+
nextRefillTime = now + refillInterval;
74+
long refillAmount = refills * getRefillIntervalAdjustedLimit(limit);
75+
return Math.min(limit, refillAmount);
4076
}
4177

4278
@Override
4379
public long getWaitInterval(long limit, long available, long amount) {
80+
// adjust the limit based on the refill interval
81+
limit = getRefillIntervalAdjustedLimit(limit);
82+
4483
if (nextRefillTime == -1) {
4584
return 0;
4685
}
@@ -62,7 +101,11 @@ public long getWaitInterval(long limit, long available, long amount) {
62101
if (diff % limit == 0) {
63102
extraRefillsNecessary--;
64103
}
65-
return nextRefillInterval + (extraRefillsNecessary * super.getTimeUnitInMillis());
104+
return nextRefillInterval + (extraRefillsNecessary * refillInterval);
105+
}
106+
107+
private long getRefillIntervalAdjustedLimit(long limit) {
108+
return (long) Math.ceil(refillInterval / (double) getTimeUnitInMillis() * limit);
66109
}
67110

68111
// This method is for strictly testing purpose only

hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/RateLimiter.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
+ "are mostly synchronized...but to me it looks like they are totally synchronized")
3636
public abstract class RateLimiter {
3737
public static final String QUOTA_RATE_LIMITER_CONF_KEY = "hbase.quota.rate.limiter";
38-
private long tunit = 1000; // Timeunit factor for translating to ms.
38+
public static final long DEFAULT_TIME_UNIT = 1000;
39+
40+
private long tunit = DEFAULT_TIME_UNIT; // Timeunit factor for translating to ms.
3941
private long limit = Long.MAX_VALUE; // The max value available resource units can be refilled to.
4042
private long avail = Long.MAX_VALUE; // Currently available resource units
4143

@@ -157,7 +159,7 @@ public synchronized long getWaitIntervalMs(final long amount) {
157159
* @param amount the number of required resources, a non-negative number
158160
* @return true if there are enough available resources, otherwise false
159161
*/
160-
private boolean isAvailable(final long amount) {
162+
protected boolean isAvailable(final long amount) {
161163
if (isBypass()) {
162164
return true;
163165
}

hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/TimeBasedLimiter.java

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,17 @@ private TimeBasedLimiter() {
4949
conf.getClass(RateLimiter.QUOTA_RATE_LIMITER_CONF_KEY, AverageIntervalRateLimiter.class)
5050
.getName())
5151
) {
52-
reqsLimiter = new FixedIntervalRateLimiter();
53-
reqSizeLimiter = new FixedIntervalRateLimiter();
54-
writeReqsLimiter = new FixedIntervalRateLimiter();
55-
writeSizeLimiter = new FixedIntervalRateLimiter();
56-
readReqsLimiter = new FixedIntervalRateLimiter();
57-
readSizeLimiter = new FixedIntervalRateLimiter();
58-
reqCapacityUnitLimiter = new FixedIntervalRateLimiter();
59-
writeCapacityUnitLimiter = new FixedIntervalRateLimiter();
60-
readCapacityUnitLimiter = new FixedIntervalRateLimiter();
52+
long refillInterval = conf.getLong(FixedIntervalRateLimiter.RATE_LIMITER_REFILL_INTERVAL_MS,
53+
RateLimiter.DEFAULT_TIME_UNIT);
54+
reqsLimiter = new FixedIntervalRateLimiter(refillInterval);
55+
reqSizeLimiter = new FixedIntervalRateLimiter(refillInterval);
56+
writeReqsLimiter = new FixedIntervalRateLimiter(refillInterval);
57+
writeSizeLimiter = new FixedIntervalRateLimiter(refillInterval);
58+
readReqsLimiter = new FixedIntervalRateLimiter(refillInterval);
59+
readSizeLimiter = new FixedIntervalRateLimiter(refillInterval);
60+
reqCapacityUnitLimiter = new FixedIntervalRateLimiter(refillInterval);
61+
writeCapacityUnitLimiter = new FixedIntervalRateLimiter(refillInterval);
62+
readCapacityUnitLimiter = new FixedIntervalRateLimiter(refillInterval);
6163
} else {
6264
reqsLimiter = new AverageIntervalRateLimiter();
6365
reqSizeLimiter = new AverageIntervalRateLimiter();

hbase-server/src/test/java/org/apache/hadoop/hbase/quotas/TestRateLimiter.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
package org.apache.hadoop.hbase.quotas;
1919

2020
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertFalse;
2122
import static org.junit.Assert.assertNotEquals;
23+
import static org.junit.Assert.assertTrue;
2224

2325
import java.util.concurrent.TimeUnit;
2426
import org.apache.hadoop.hbase.HBaseClassTestRule;
@@ -427,4 +429,71 @@ public void testLimiterCompensationOverflow() throws InterruptedException {
427429
avgLimiter.consume(-80);
428430
assertEquals(limit, avgLimiter.getAvailable());
429431
}
432+
433+
@Test
434+
public void itRunsFullWithPartialRefillInterval() {
435+
RateLimiter limiter = new FixedIntervalRateLimiter(100);
436+
limiter.set(10, TimeUnit.SECONDS);
437+
assertEquals(0, limiter.getWaitIntervalMs());
438+
439+
// Consume the quota
440+
limiter.consume(10);
441+
442+
// Need to wait 1s to acquire another resource
443+
long waitInterval = limiter.waitInterval(10);
444+
assertTrue(900 < waitInterval);
445+
assertTrue(1000 >= waitInterval);
446+
// We need to wait 2s to acquire more than 10 resources
447+
waitInterval = limiter.waitInterval(20);
448+
assertTrue(1900 < waitInterval);
449+
assertTrue(2000 >= waitInterval);
450+
451+
limiter.setNextRefillTime(limiter.getNextRefillTime() - 1000);
452+
// We've waited the full interval, so we should now have 10
453+
assertEquals(0, limiter.getWaitIntervalMs(10));
454+
assertEquals(0, limiter.waitInterval());
455+
}
456+
457+
@Test
458+
public void itRunsPartialRefillIntervals() {
459+
RateLimiter limiter = new FixedIntervalRateLimiter(100);
460+
limiter.set(10, TimeUnit.SECONDS);
461+
assertEquals(0, limiter.getWaitIntervalMs());
462+
463+
// Consume the quota
464+
limiter.consume(10);
465+
466+
// Need to wait 1s to acquire another resource
467+
long waitInterval = limiter.waitInterval(10);
468+
assertTrue(900 < waitInterval);
469+
assertTrue(1000 >= waitInterval);
470+
// We need to wait 2s to acquire more than 10 resources
471+
waitInterval = limiter.waitInterval(20);
472+
assertTrue(1900 < waitInterval);
473+
assertTrue(2000 >= waitInterval);
474+
// We need to wait 0<=x<=100ms to acquire 1 resource
475+
waitInterval = limiter.waitInterval(1);
476+
assertTrue(0 < waitInterval);
477+
assertTrue(100 >= waitInterval);
478+
479+
limiter.setNextRefillTime(limiter.getNextRefillTime() - 500);
480+
// We've waited half the interval, so we should now have half available
481+
assertEquals(0, limiter.getWaitIntervalMs(5));
482+
assertEquals(0, limiter.waitInterval());
483+
}
484+
485+
@Test
486+
public void itRunsRepeatedPartialRefillIntervals() {
487+
RateLimiter limiter = new FixedIntervalRateLimiter(100);
488+
limiter.set(10, TimeUnit.SECONDS);
489+
assertEquals(0, limiter.getWaitIntervalMs());
490+
// Consume the quota
491+
limiter.consume(10);
492+
for (int i = 0; i < 100; i++) {
493+
limiter.setNextRefillTime(limiter.getNextRefillTime() - 100); // free 1 resource
494+
limiter.consume(1);
495+
assertFalse(limiter.isAvailable(1)); // all resources consumed
496+
assertTrue(limiter.isAvailable(0)); // not negative
497+
}
498+
}
430499
}

0 commit comments

Comments
 (0)