(WIP) Bucket4j
최근 Rate Limiter 처리를 위해 Bucket4j를 사용했습니다. 관련 내용을 정리하고 공유합니다.
1. Token Bucket
Bucket4j는 소개에서 명시된것처럼 Token Bucket 방식을 사용하는 Rate Limiter 구현체 입니다.
2. 코드 분석
2.1 Token Consume
토큰을 사용하려고 시도할때 버킷에 토큰 채우기를 시도합니다. 나노세컨드 기반으로 이전 토큰 사용 시간으로부터 채울 토큰수를 계산하고 토큰을 리필합니다. 현재 버킷에 존재하는 토큰량과 사용해야할 토큰량을 비교하고 처리합니다.
@Override
protected boolean tryConsumeImpl(long tokensToConsume) {
long currentTimeNanos = timeMeter.currentTimeNanos();
lock.lock();
try {
state.refillAllBandwidth(currentTimeNanos);
long availableToConsume = state.getAvailableTokens();
if (tokensToConsume > availableToConsume) {
return false;
}
state.consume(tokensToConsume);
return true;
} finally {
lock.unlock();
}
}
2.2 Refill
Bucket4j는 여러가지 Refill 전략을 제공합니다.
public void refillAllBandwidth(long currentTimeNanos) {
Bandwidth[] bandwidths = configuration.getBandwidths();
for (int i = 0; i < bandwidths.length; i++) {
refill(i, bandwidths[i], currentTimeNanos);
}
}
private void refill(int bandwidthIndex, Bandwidth bandwidth, long currentTimeNanos) {
long previousRefillNanos = getLastRefillTimeNanos(bandwidthIndex);
if (currentTimeNanos <= previousRefillNanos) {
return;
}
// 주기적 리필 방식인경우
if (bandwidth.isRefillIntervally()) {
// currentTimeNanos에 리필이 되어야하는 마지막 나노초까지의 타임스탬프를 지정
long incompleteIntervalCorrection = (currentTimeNanos - previousRefillNanos) % bandwidth.getRefillPeriodNanos();
currentTimeNanos -= incompleteIntervalCorrection;
}
if (currentTimeNanos <= previousRefillNanos) {
return;
} else {
setLastRefillTimeNanos(bandwidthIndex, currentTimeNanos);
}
final long capacity = bandwidth.getCapacity();
final long refillPeriodNanos = bandwidth.getRefillPeriodNanos();
final long refillTokens = bandwidth.getRefillTokens();
final long currentSize = getCurrentSize(bandwidthIndex);
// 버킷이 가득찼는지 검사
if (currentSize >= capacity) {
// can come here if forceAddTokens has been used
return;
}
long durationSinceLastRefillNanos = currentTimeNanos - previousRefillNanos;
long newSize = currentSize;
// 마지막 리필시간으로부터 현재 시간까지의 기간이 리필되어야하는 기간보다 큰경우, 즉 리필이 수행되어야하는경우
if (durationSinceLastRefillNanos > refillPeriodNanos) {
long elapsedPeriods = durationSinceLastRefillNanos / refillPeriodNanos;
long calculatedRefill = elapsedPeriods * refillTokens;
// 기존 버킷의 토큰수 + 리필되어야하는 토큰수
newSize += calculatedRefill;
// 리필후 토큰사이즈가 버킷의 토큰 용량보다 큰경우 용량 사이즈로 리필하고 종료
if (newSize > capacity) {
resetBandwidth(bandwidthIndex, capacity);
return;
}
// 리필후 토큰사이즈가 현재 사이즈보다 작은경우 오버플로가 발생한것으로 판단하고 버킷 용량 사이즈로 리필하고 종료
if (newSize < currentSize) {
// arithmetic overflow happens. This mean that tokens reached Long.MAX_VALUE tokens.
// just reset bandwidth state
resetBandwidth(bandwidthIndex, capacity);
return;
}
durationSinceLastRefillNanos %= refillPeriodNanos;
}
long roundingError = getRoundingError(bandwidthIndex);
long dividedWithoutError = multiplyExactOrReturnMaxValue(refillTokens, durationSinceLastRefillNanos);
long divided = dividedWithoutError + roundingError;
if (divided < 0 || dividedWithoutError == Long.MAX_VALUE) {
// arithmetic overflow happens.
// there is no sense to stay in integer arithmetic when having deal with so big numbers
long calculatedRefill = (long) ((double) durationSinceLastRefillNanos / (double) refillPeriodNanos * (double) refillTokens);
newSize += calculatedRefill;
roundingError = 0;
} else {
// 리필되어야하는 토큰수
long calculatedRefill = divided / refillPeriodNanos;
if (calculatedRefill == 0) {
roundingError = divided;
} else {
newSize += calculatedRefill;
roundingError = divided % refillPeriodNanos;
}
}
if (newSize >= capacity) {
resetBandwidth(bandwidthIndex, capacity);
return;
}
if (newSize < currentSize) {
// arithmetic overflow happens. This mean that bucket reached Long.MAX_VALUE tokens.
// just reset bandwidth state
resetBandwidth(bandwidthIndex, capacity);
return;
}
// 새로계산된 토큰수로 버킷 토큰수 설정
setCurrentSize(bandwidthIndex, newSize);
setRoundingError(bandwidthIndex, roundingError);
}
References
- https://bucket4j.com
- https://redis.io/learn/howtos/ratelimiting
- https://github.com/bucket4j/bucket4j
- https://www.mimul.com/blog/about-rate-limit-algorithm/
- https://en.wikipedia.org/wiki/Token_bucket
- https://smudge.ai/blog/ratelimit-algorithms