Rate Limit 은 오토 스케일링이 반응하기 전 정상 인스턴스의 자원이 고갈되지 않게 보호하는 장치다. 부하 스파이크가 새 인스턴스의 준비 시간보다 빠르게 도착할 때, 일부 요청을 일찍 거절해서 정상 인스턴스가 한계에 도달하지 않도록 막는다.
Rate Limit 을 어떻게 셀지는 알고리즘의 문제고, 어디서 셀지는 보호 계층의 문제다. 두 기준이 독립처럼 보이지만, 보호 계층이 가능한 알고리즘의 선택지를 먼저 좁힌다. 알고리즘을 고르기 전에 계층부터 정해야 하는 이유다.
보호 계층
L4, L7, Application 세 계층이 후보다. 식별 정밀도와 알고리즘 선택지가 계층을 따라 함께 넓어진다.
L4 (Load Balancer 단)는 가장 외부에 위치한다. TCP 연결 단위로 카운팅하며, 식별 기준은 IP 정도가 한계다. 처리 비용이 낮아 빠른 대신, 카운팅 단위가 거칠어 단순한 알고리즘만 적용 가능하다. 클라이언트가 NAT 뒤라면 식별이 더 거칠어진다.
L7 (Gateway 또는 Sidecar)는 HTTP 단위로 처리한다. 헤더, 경로, 토큰 같은 애플리케이션 식별자를 카운팅 키로 쓸 수 있다. 사용자 단위, API 단위로 카운트가 분리되므로 알고리즘 선택지가 넓어진다. 마이크로서비스 환경에서는 사이드카(Envoy 등)가 우선 후보다.
Application 계층은 비즈니스 컨텍스트까지 본다. 사용자 등급별 다른 제한, 특정 엔드포인트만 보호, 인증된 토큰 종류별 분리 같은 결정이 가능하다. 가장 정밀한 만큼 가장 무겁고, 인스턴스마다 카운터가 분산되는 비용까지 따라온다.
세 계층은 트레이드오프 관계가 명확하다. 외부 계층일수록 식별이 거친 대신 처리 비용이 낮고, Application 쪽으로 갈수록 정밀해지지만 무거워진다.
계층 선택이 알고리즘 후보 공간을 좁히는 이유는 식별 정밀도와 카운팅 단위에 있다. L4 처럼 IP/연결 단위 식별만 가능한 환경에서는 Sliding Window 같은 정밀 알고리즘이 식별 키의 모호함 때문에 정밀도의 이점을 잃는다. 반대로 Application 처럼 인증된 사용자 단위 식별이 가능하면 모든 알고리즘이 의미 있게 동작한다. 식별 정밀도가 곧 알고리즘의 효용을 결정한다. 다음 절의 알고리즘 비교는 이 계층 선택 이후의 후속 결정이다.
알고리즘
주로 쓰이는 알고리즘은 Token Bucket, Fixed Window, Leaky Bucket, Sliding Window 다. burst 허용 여부로 두 그룹으로 나뉜다.
burst 허용 그룹
Token Bucket 은 일정 속도로 토큰이 채워지는 버킷을 두고, 요청이 들어올 때마다 토큰을 하나 소모한다. 토큰이 있으면 통과, 없으면 거절. 한동안 요청이 없으면 토큰이 쌓이고, 그만큼 짧은 burst 가 허용된다. 평소 한가하다가 짧게 몰리는 워크로드에 적합하다.
Fixed Window 는 시간 윈도우 안의 카운트만 셈한다. 윈도우 안에서는 burst 가 허용되고, 윈도우가 바뀌면 카운트가 리셋된다. 구현이 가장 단순한 대신, 윈도우 경계에서 burst 가 두 번 가능한 약점이 있다 (윈도우 끝 직전과 다음 윈도우 시작 직후에 몰린 요청이 모두 통과).
burst 제거 그룹
Leaky Bucket 은 일정 속도로만 요청이 흘러나가는 큐 비유다. 입력이 burst 로 들어와도 출력 속도는 평탄해진다. 다운스트림이 일정 속도까지만 처리 가능할 때 그 속도에 맞춰야 한다면 우선 후보다. 외부 결제 게이트웨이로 보내는 호출이 대표적인 사례다.
Sliding Window 는 시간 윈도우를 이동시키며 카운트한다. 윈도우 경계라는 개념이 없으니 Fixed Window 의 경계 burst 문제가 사라진다. 정밀도는 가장 높지만, 각 요청의 타임스탬프를 따로 보관해야 하므로 메모리와 계산 비용이 가장 무겁다.
두 그룹의 선택은 다운스트림이 burst 를 흡수할 수 있느냐로 결정된다. 흡수 가능하면 burst 허용 그룹으로 운영 단순성을 가져가고, 흡수 불가능하면 burst 제거 그룹으로 평탄화를 보장한다.
도구 매핑
계층과 알고리즘이 모든 조합으로 가능한 건 아니다. 실전 도구에서는 특정 조합이 굳어져 있다.
| 계층 | 대표 도구 | 자연스러운 알고리즘 | 비고 |
|---|---|---|---|
| L4 | Nginx (limit_req) | Leaky Bucket | 연결 단위 처리에 적합 |
| L7 Sidecar | Istio / Envoy | Token Bucket | HTTP 헤더 기반 식별 |
| Application | Resilience4j (Java) | Cycle 기반 | 비즈니스 컨텍스트 기반 |
| Application | Bucket4j (Java) | Token Bucket | 분산 백엔드 연동 가능 |
Nginx 의 limit_req 모듈은 Leaky Bucket 으로 동작한다. 연결을 받아 일정 속도로 흘려보내는 구조가 Leaky 의 출력 평탄화와 직접 대응하기 때문이다. burst 옵션으로 짧은 입력 burst 흡수도 허용한다.
Istio / Envoy 의 Rate Limit 필터가 Token Bucket 을 기본으로 한 이유는 HTTP 헤더 단위로 식별된 클라이언트별 burst 허용이 게이트웨이 환경의 일반적 요구사항이기 때문이다. 사이드카 자체는 로컬 모드(단일 인스턴스 내)와 글로벌 모드(외부 RLS 서버 연동)를 모두 제공한다.
Resilience4j 의 RateLimiter 모듈은 cycle 기반 카운팅으로 동작한다. limitRefreshPeriod 마다 limitForPeriod 만큼의 permission 이 리셋되는 구조로, 토큰을 누적하는 Token Bucket 과 다르게 cycle 경계에서 카운트가 일괄 갱신된다. 메소드 단위 보호에서 단순한 cycle 카운팅이 충분한 시나리오에 적합하며, Circuit Breaker, Retry 등과 같은 컴포넌트 모음의 일부로 제공된다.
Bucket4j 는 Token Bucket 전용 라이브러리다. 분산 환경에서 Redis 같은 백엔드로 카운터를 공유하는 형태를 지원하며, 단일 JVM 보호가 아닌 클러스터 전체 보호가 필요할 때의 후보다.
도구별로 굳어진 조합을 정리하면, L4 에서는 Leaky 가 우세하고, L7 의 Sidecar 와 Application 의 분산 보호(Bucket4j)에서는 Token 이 우세하다. Resilience4j 같은 cycle 기반 도구는 단일 JVM 안에서 단순 카운팅이 충분한 시나리오에 적합하다. Sliding Window 가 표에서 빠진 건 도구 차원에서 기본으로 채택되는 경우가 드물기 때문이다 (자체 구현이거나 분산 카운터 위에 구성하는 형태가 일반적).
결정 순서
위 정리에서 한 가지 흐름이 보인다. 알고리즘은 트래픽 모양 단독으로 결정되는 것이 아니라, 계층을 먼저 정한 뒤 그 계층이 허용하는 후보 안에서 트래픽 모양으로 좁혀진다.
- 비즈니스 컨텍스트 기반 보호가 필요한가 → Application 계층 → Token Bucket
- HTTP 단위 식별로 충분한가 → L7 → Token Bucket (로컬 또는 글로벌)
- 연결 단위 보호로 충분한가 → L4 → Leaky Bucket
이 위에서 burst 허용 여부가 알고리즘을 마지막으로 좁히는 기준이 된다.
보호가 필요한 의존성 앞단에 장치를 둘 때 알고리즘 비교부터 시작하면 도구의 선택지가 좁아진 채로 알고리즘을 고르게 된다. 계층 결정이 선행해야 알고리즘의 후보 공간이 열린다.
참고
- 광고 시스템 장애 회고 — 공유 의존성과 단일 장애점 — Rate Limit 이 cascade 의 시작점을 차단한 실제 사례.