Rate Limiter 알고리즘 정리

서버와 데이터베이스 등 리소스는 유한하다. 요청이 몰리거나 급증하면 부하가 리소스를 무너뜨리기 전에 유입량을 제어할 무언가가 필요하다.

그 역할을 하는 것이 Rate Limiter다. 클라이언트나 서비스가 보내는 트래픽의 처리율을 제어하는 장치다.

사용 사례

Rate Limiter는 여러가지 목적으로 사용할 수 있다.

목적은 다양하고, 서버 측 리소스 보호는 그중 하나일 뿐이다. Rate Limiter가 결국 하는 일은 단위시간당 허용하는 요청의 양을 제어하는 것이다.

알고리즘

그렇다면 어떤 기준으로 요청량을 세고, 최대치를 제한할 것인가? 일반적으로 사용되는 처리율 제한 알고리즘으로는 아래와 같은 알고리즘들이 존재한다.

각 알고리즘 마다 장단점이 있고, 적합한 영역도 다를 수 있다. IP 단위로 요청량 제한을 하는 시스템을 전제로 각 알고리즘을 살펴 보자.

Token Bucket

버킷, 즉 바구니가 있고 일정 속도로 바구니에는 토큰이 채워진다. 바구니가 꽉 차면 더 이상 토큰을 채우지 않는다. 요청이 들어오면 버킷에 있는 토큰을 하나 꺼내고 요청을 통과시킨다. 버킷이 비어 있으면 요청은 버려진다.

 refill: r tokens/sec
        │
        ▼
   ┌─────────┐   overflow (bucket full) → token discarded
   │ ● ● ● ● │ ◀─ capacity = b
   │ ● ● ●   │
   │ ● ●     │   tokens = currently remaining
   └────┬────┘
        │ 1 request = 1 token consumed
        ▼
  request ──▶ has token? ──yes──▶ pass
                  │
                  no ──▶ reject (429)

특징

장점

단점

Leaky Bucket

요청 처리율이 고정이다. 요청은 큐에 도착하고, 큐에 빈 자리가 있으면 삽입하고, 없으면 버려진다. 요청 처리율 만큼 매 tick에 큐에서 요청을 꺼내 처리한다. 즉 서버 측에서 시간당 처리할 수 있는 건수를 정해두고 최대 그 건수까지만 처리하는 알고리즘이다.

  requests (irregular, bursty)
     │ │ │  │    │
     ▼ ▼ ▼  ▼    ▼
   ┌───────────────┐
   │ ▒▒▒▒▒▒▒▒▒▒▒▒▒ │ ◀─ new requests rejected when queue is full
   │ ▒▒▒▒▒▒▒▒▒     │    (capacity = queue size)
   │ ▒▒▒▒          │
   └───────┬───────┘
           │  leaks at a constant rate (leak rate = r)
           ▼
       ─ ─ ─ ─ ─ ─   uniform output (burst smoothed out)

특징

장점

단점

참고: meter 방식

참고로 Leaky Bucket은 여기서 설명한 queue 방식 외에 meter 방식으로도 구현한다. meter 방식은 요청을 큐에 쌓지 않고, 버킷의 수위(카운터)만 관리한다. 요청이 오면 수위를 1 올리고, 수위는 시간에 따라 일정 속도(r)로 줄어든다. 수위가 capacity를 넘으면 요청을 거절한다. 요청을 지연시키지 않고 즉시 통과/거절을 판단하므로, 사실상 Token Bucket과 같은 동작이 된다. (NGINX의 limit_req가 이 방식이며, burst/delay 옵션으로 queue처럼 지연 처리도 할 수 있다.)

Fixed Window Counter

타임라인을 고정된 간격의 window로 나누고 각 윈도우마다 counter를 센다.

  limit = 5 / window (e.g. 1 min)

  window 1              window 2              window 3
  ┌─────────────────┬─────────────────┬─────────────────┐
  │ ● ● ● ● ●   ✕ ✕ │ ● ●             │ ● ● ● ●         │
  │ count=5 (full)  │ count=2         │ count=4         │
  └─────────────────┴─────────────────┴─────────────────┘
 00:00             01:00             02:00             03:00
                     ▲
                     └ counter reset to 0 at the boundary

윈도우 경계에서 윈도우 크기 기준으로 limit보다 큰 요청이 통과될 수 있다.

Problem: bursts on both sides of a boundary let 2×limit through
  ┌────────────┬────────────┐
  │       ●●●●●│●●●●●       │  ← 10 requests pass between 00:59 and 01:01
  └────────────┴────────────┘

특징

장점

단점

Sliding Window Log

요청의 타임스탬프를 추적한다. 새 요청이 오면 윈도우를 벗어난 만료된 타임스탬프를 제거하고, 새 요청의 타임스탬프를 로그에 추가한다. 로그의 크기가 허용치보다 같거나 작으면 요청을 전달하고 이외에는 거부한다. 이 방식에서는 거부된 요청의 타임스탬프도 로그에 남기는데, 계속 두드리는 클라이언트가 실제로 멈추기 전까지는 다시 통과하지 못하게 하기 위해서다.

  limit = 5 / 60s,  now = 100s

  log at the previous request (t = 99)
  ◀──────────── window (39, 99] ────────────▶
  ┌──────┬──────┬──────┬──────┬──────┬──────┐
  │  40  │  42  │  58  │  71  │  90  │  99  │
  └──┬───┴──────┴──────┴──────┴──────┴──────┘
     │
     └─ ≤ now-60 = 40 → expired, removed

  remove 40, append the new request's timestamp (100)
         ┌──────┬──────┬──────┬──────┬──────┬──────┐
         │  42  │  58  │  71  │  90  │  99  │ 100  │
         └──────┴──────┴──────┴──────┴──────┴──────┘
         ◀─────────── window (40, 100] ────────────▶
                count = 6 > 5 → new request rejected

특징

장점

단점

Sliding Window Counter

Fixed Window Counter를 개선해 카운터 2개만으로 Sliding Window Log를 근사하는 방식이다. Window=1분이라고 할 때, 최근 1분간의 요청 수를 다음과 같이 추정한다.

estimate = 현재 window 내 요청 수 (지금 들어온 요청 제외)
         + 직전 window 내 요청 수 × 직전 window가 최근 1분과 겹치는 비율

estimate < limit이면 들어온 요청을 통과시킨다.

  limit = 5 / 60s,  window size 60s,  now = 01:20 (1/3 into the current window)

   previous (00:00~01:00)    current (01:00~02:00)
  ┌────────────────────────┬────────────────────────┐
  │ prev = 6               │ curr = 2               │
  └────────────────────────┴────────────────────────┘
          ◀─ sliding window (60s) ─▶
          └──── overlap ───┘
        00:20            01:00   01:20

  overlap  = 1 - 1/3 = 2/3

  estimate = curr + prev × overlap
           = 2    + 6    × 2/3   = 6  ≥ 5 → rejected

특징

장점

단점

한도 초과 요청의 처리

클라이언트에게 처리율 제한에 걸렸다는 것을 통보할 때는 429 Too Many Requests 상태 코드와 함께, 언제 다시 요청할 수 있는지를 Retry-After 헤더로 알려준다(RFC 6585). X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset 같은 헤더도 널리 쓰이지만(GitHub, Twitter 등) 표준이 아닌 관례이며, IETF에서는 이를 표준화한 RateLimit, RateLimit-Policy 헤더를 draft-ietf-httpapi-ratelimit-headers로 논의 중이다.