Theory
네트워크
Rate Limiter

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
      • 유휴 시간 동안 토큰을 축적할 수 있어, 순간적인 요청 폭증(버스트)을 수용 가능.
      • 요청이 초과될 경우 즉시 차단되며, 초과 요청은 다음 토큰 생성 주기에 처리 가능.
  • 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)으로 분리하여 간단한 계산으로 대규모 요청을 관리

Reference