Rate Limiter 알고리즘 종류
1. Leaky Bucket
설명
- 일정한 속도로 물(요청)을 배출하는 방식
- 버킷(큐)에 요청을 넣고, 일정 속도로 요청을 처리
- 초과 요청은 버려지거나 대기
장점
- 요청 처리의 균등화
- QoS(Quality of Service) 보장
단점
-
처리 속도 고정 (유연성 부족)
- 항상 일정한 처리 속도로 동작하며, 폭발적인 요청(버스트)을 처리하지 못함
- 유휴 시간 동안 처리량을 축적하지 못함
-
즉시 폐기 (재시도 불가)
- 초과된 요청은 즉시 버려지며, 복구나 대기 메커니즘이 없음
- 중요한 요청도 손실될 위험이 큼
-
비효율적인 리소스 활용
- 요청이 없던 유휴 시간 동안에도 처리 능력을 낭비
- 이후 폭발적인 요청이 들어와도 처리 불가
다른 알고리즘과의 차이
- Token Bucket
- 유휴 시간동안 쌓인 버킷을 토대로 폭발적인 요청(버스트)를 처리 가능
- Sliding Window Log
- 애초에 모든 로그를 기록하기 때문에 초과한 요청이 완전히 버려졌다고 보기 어려움
- Sliding Window Counter
- 임시 큐에 초과된 요청을 임시로 저장되서 다음 슬라이드가 시작될 때 이를 우선적으로 처리가능
구현
public class LeakyBucket {
private int capacity; // 버킷의 최대 크기 (허용 가능한 최대 요청 수)
private long lastDripTime; // 마지막으로 물이 새어나간(처리된) 시각
private int water; // 현재 버킷에 쌓인 물의 양 (현재 처리 대기 중인 요청 수)
private int dripRate; // 초당 물이 새어나가는 속도 (요청 처리 속도)
/**
* LeakyBucket 생성자
* @param capacity 버킷의 최대 크기
* @param dripRate 초당 처리할 수 있는 요청의 수
*/
public LeakyBucket(int capacity, int dripRate) {
this.capacity = capacity; // 버킷의 최대 크기 설정
this.dripRate = dripRate; // 초당 처리 속도 설정
this.lastDripTime = System.currentTimeMillis(); // 객체 생성 시점의 시간을 기록
this.water = 0; // 초기 버킷은 비어 있음
}
/**
* 요청이 허용 가능한지 확인
* @return 요청이 허용되면 true, 그렇지 않으면 false 반환
*/
public synchronized boolean allowRequest() {
long currentTime = System.currentTimeMillis(); // 현재 시각 기록
long elapsed = currentTime - lastDripTime; // 마지막 처리 이후 경과 시간 계산
// 경과 시간에 따라 새어나간 물의 양 계산
// (예: 초당 5 요청 처리 가능하면, 2초 경과 시 10 요청 처리)
int leaked = (int) (elapsed * dripRate / 1000);
// 경과 시간 동안 새어나간 물을 반영하여 현재 물 양 갱신
// 버킷의 물은 0 이하로 내려갈 수 없으므로 Math.max로 보호
water = Math.max(0, water - leaked);
// 마지막 물 새어나간 시각을 현재 시각으로 갱신
lastDripTime = currentTime;
if (water < capacity) {
// 현재 버킷의 물 양이 최대 크기보다 작다면 요청을 허용
water++; // 새 요청을 처리하기 위해 버킷에 물을 추가
return true; // 요청 허용
} else {
return false; // 요청 거부 (버킷이 가득 찼기 때문)
}
}
}
2. Token Bucket
설명
- 토큰을 버킷에 담아두고 요청이 올 때마다 토큰을 소모해서 요청 처리
- 매초마다 Token이 생성됨
- 토큰 부족 시 요청 차단
장점
- 버스트 처리 가능(초과 토큰 사용)
- 유동적인 요청 처리 가능
단점
- 구현 복잡성
- 버스트 초과 요청은 차단
구현
public class TokenBucket {
private int capacity; // Bucket capacity
private int tokens; // Current token count
private long lastRefillTime;
private int refillRate; // Tokens added per second
public TokenBucket(int capacity, int refillRate) {
this.capacity = capacity;
this.refillRate = refillRate;
this.tokens = capacity;
this.lastRefillTime = System.currentTimeMillis();
}
public synchronized boolean allowRequest() {
refill();
if (tokens > 0) {
tokens--;
return true;
}
return false; // Request denied
}
private void refill() {
long currentTime = System.currentTimeMillis();
long elapsed = currentTime - lastRefillTime;
int refillTokens = (int) (elapsed * refillRate / 1000);
if (refillTokens > 0) {
tokens = Math.min(capacity, tokens + refillTokens);
lastRefillTime = currentTime;
}
}
}
3. Fixed Window Counter
설명
- 일정한 시간 창을 기준으로 요청을 제한
- 요청은 해당 시간 창에서 카운트됨
장점
- 구현이 간단하고 메모리 효율적임
단점
- 경계 문제(윈도우 경계에서 폭증 가능)
- 예를 들자면 0 ~ 100, 101 ~ 200 이렇게 구간이 나눠져있는데 51 ~ 100 사이에 요청이 몰려버릴 경우 알고리즘 상으로는 요청을 수행 가능하지만 실제 서버가 요청 수행이 불가능해질 수 있음
구현
import java.util.concurrent.atomic.AtomicInteger;
public class FixedWindow {
private int maxRequests;
private long windowStart;
private long windowDuration;
private AtomicInteger requestCount;
public FixedWindow(int maxRequests, long windowDurationMillis) {
this.maxRequests = maxRequests;
this.windowDuration = windowDurationMillis;
this.windowStart = System.currentTimeMillis();
this.requestCount = new AtomicInteger(0);
}
public synchronized boolean allowRequest() {
long currentTime = System.currentTimeMillis();
if (currentTime - windowStart >= windowDuration) {
windowStart = currentTime;
requestCount.set(0);
}
if (requestCount.incrementAndGet() <= maxRequests) {
return true;
} else {
return false; // Request denied
}
}
}
4. Sliding Window Log
설명
- 요청 타임스탬프를 기록해 가변적 윈도우를 사용
- 요청 시 타임스탬프 리스트를 갱신하며 제한 체크
장점
- 정확한 요청 제한 가능
- 시간 경계 문제 해결
단점
- 타임 스탬프 저장으로 인한 높은 메모리 사용량
구현
import java.util.LinkedList;
public class SlidingWindowLog {
private int maxRequests;
private long windowDuration;
private LinkedList<Long> requestTimestamps;
public SlidingWindowLog(int maxRequests, long windowDurationMillis) {
this.maxRequests = maxRequests;
this.windowDuration = windowDurationMillis;
this.requestTimestamps = new LinkedList<>();
}
public synchronized boolean allowRequest() {
long currentTime = System.currentTimeMillis();
while (!requestTimestamps.isEmpty() &&
currentTime - requestTimestamps.peek() >= windowDuration) {
requestTimestamps.poll();
}
if (requestTimestamps.size() < maxRequests) {
requestTimestamps.add(currentTime);
return true;
}
return false; // Request denied
}
}
5. Sliding Window Counter
설명
- 시간 창을 여러 구간으로 나누고 요청을 카운트
- 부분 윈도우의 합으로 요청 제한
장점
- 메모리 효율적
- 정확도와 성능의 균형
단점
- Slot을 얼마나 잘게 쪼개냐에 따라서 다르겠지만 Fixed Window와 같이 여전히 경계 문제가 발생할 수 있음
구현
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class SlidingWindowCounter {
private int maxRequests; // 최대 허용 요청 수
private long windowDuration; // 슬라이딩 윈도우의 전체 기간 (밀리초 단위)
private int splitParts; // 윈도우를 나눌 부분 수 (슬롯의 개수)
private ConcurrentMap<Long, Integer> counters; // 각 슬롯(windowKey)별 요청 수를 저장하는 맵
// 생성자: 윈도우 크기, 최대 요청 수, 나눌 부분 수 초기화
public SlidingWindowCounter(int maxRequests, long windowDurationMillis, int splitParts) {
this.maxRequests = maxRequests; // 최대 허용 요청 수 설정
this.windowDuration = windowDurationMillis; // 윈도우 지속 시간 설정
this.splitParts = splitParts; // 윈도우를 나눌 슬롯의 개수 설정
this.counters = new ConcurrentHashMap<>();
}
// 요청이 허용 가능한지 확인
public boolean allowRequest() {
long currentTime = System.currentTimeMillis(); // 현재 시간 (밀리초 단위)
long windowKey = currentTime / (windowDuration / splitParts);
// 현재 시간 기준으로 요청이 속한 윈도우 키 계산
// 현재 윈도우 슬롯의 요청 수 증가 (스레드 안전)
counters.merge(windowKey, 1, Integer::sum);
// 만료된 윈도우 슬롯 제거
counters.keySet().removeIf(key -> key < windowKey - splitParts);
// 전체 요청 수 합산
int totalCount = counters.values().stream().mapToInt(Integer::intValue).sum();
// 총 요청 수가 최대 허용 요청 수를 초과하지 않으면 요청 허용
return totalCount <= maxRequests;
}
}
Resilience4j의 Rate Limiter 알고리즘
- Resilience4j는 크게 두 종류의 구현체가 있음
- 대표적인 구현체인 Semaphore-Based Rate Limiter를 보자면
- 위와 같이 권한을 요청하고
- 이렇게 권한을 릴리즈해주는데 Semaphore를 활용한 Token Bucket Algorithm으로 동작하는 것 같음
각 알고리즘별 실제 사용사례
-
Token Bucket
- API Gateway: AWS API Gateway, Google Cloud API
- 유휴 시간 동안 토큰을 축적할 수 있어, 순간적인 요청 폭증(버스트)을 수용 가능.
- 요청이 초과될 경우 즉시 차단되며, 초과 요청은 다음 토큰 생성 주기에 처리 가능.
- API Gateway: AWS API Gateway, Google Cloud API
-
Leaky Bucket
- VoIP 서비스: 음성 데이터 전송 (Skype, Zoom)
- 네트워크 트래픽 관리: 라우터, 스위치의 QoS 설정
- 일정한 처리 속도 보장:
- 실시간 데이터 안정성:
-
Fixed Window Counter
- SNS 서비스의 API 제한: Twitter, Instagram
- 간단한 웹 애플리케이션: 소규모 API 서버
-
Sliding Window Log
- 금융 서비스: 카드 결제, 은행 거래 모니터링
- 보안 시스템: 로그인 시도 제한 (Brute Force 방지)
- 요청 타임스탬프를 기반으로 윈도우 내 모든 요청을 기록하여, 요청 수를 정밀하게 판단.
-
Sliding Window Counter
- 이커머스 플랫폼: 상품 조회 및 장바구니 API (Amazon)
- 대규모 트래픽 관리: CDN 서비스(Akamai, Cloudflare)
- 요청을 구간(Slot)으로 분리하여 간단한 계산으로 대규모 요청을 관리