속도 제한과 배압 제어
서론
쌍십일(광군제) 자정, 수억 명의 사용자가 동시에 몰려드는데 -- 서버가 버틸 수 있을까? 모든 시스템에는 처리 능력의 상한선이 있습니다. 요청량이 시스템 수용 능력을 초과할 때 제어하지 않으면, 결과는 모두가 서비스를 이용할 수 없게 된다는 것입니다. 속도 제한과 배압은 시스템이 "압도당하지 않도록" 보호하는 두 가지 방어선입니다.
이 글에서 배울 내용
이 장을 마치면 다음을 얻게 됩니다:
- 속도 제한의 필요성: 시스템을 보호하기 위해 일부 요청을 능동적으로 거부해야 하는 이유
- 속도 제한 알고리즘: 토큰 버킷, 누출 버킷, 슬라이딩 윈도우 세 가지 핵심 알고리즘의 원리와 차이
- 배압 메커니즘: 업스트림 속도가 다운스트림을 초과할 때의 처리 전략
- 다층 속도 제한: 클라이언트에서 게이트웨이, 서비스까지의 다층 속도 제한 아키텍처
- 실전 능력: 어떤 시나리오에서 어떤 속도 제한 전략을 선택해야 하는지 파악
| 장 | 내용 | 핵심 개념 |
|---|---|---|
| 제 1장 | 속도 제한이 필요한 이유 | 눈사태 효과, 서비스 보호 |
| 제 2장 | 속도 제한 알고리즘 | 토큰 버킷, 누출 버킷, 슬라이딩 윈도우 |
| 제 3장 | 배압 제어 | 버퍼, 폐기 전략, 탄력적 확장 |
| 제 4장 | 다층 속도 제한 아키텍처 | 클라이언트, 게이트웨이, 서버 |
| 제 5장 | 실전 및 선택 | Nginx, Redis, Sentinel |
0. 전경도: 왜 사용자를 "거부"해야 하는가?
이것은 매우 직관에 반하는 것 같습니다 -- 모든 사용자에게 잘 서비스해야 하지 않나요? 하지만 현실은: 일부 요청을 거부하지 않으면, 모든 요청이 실패하게 됩니다.
100명만 앉을 수 있는 레스토랑에 갑자기 1000명이 몰려든다고 상상해 보세요. 속도 제한을 하지 않으면, 1000명이 모두 식사를 할 수 있는 것이 아니라 주방이 붕괴하고 웨이터가 마비되어 1000명 모두 식사를 할 수 없게 됩니다. 올바른 방법은 입구에서 줄을 서서 속도 제한을 하여, 100명을 먼저 들여보내고 나머지는 대기시키는 것입니다.
속도 제한의 핵심 목표
- 시스템 보호: 과부하로 인한 서비스 완전 불가 상태 방지
- 공정한 분배: 수락된 요청이 정상적으로 처리되도록 보장
- 우아한 성능 저하: 속도 제한된 요청은 명확한 429 상태 코드를 받으며, 타임아웃이나 500 오류가 아닙니다
1. 속도 제한 알고리즘: 세 가지 고전적 방안
속도 제한의 핵심 문제는: 단위 시간당 최대 몇 개의 요청을 통과시킬 것인가? 입니다. 다양한 알고리즘은 정확도, 버스트 트래픽 처리, 구현 복잡도 측면에서 각각의 장단점이 있습니다.
| 알고리즘 | 원리 | 버스트 트래픽 | 정확도 | 구현 복잡도 |
|---|---|---|---|---|
| 토큰 버킷 | 고정 속도로 토큰을 발행, 요청이 토큰을 소비 | 허용(버킷에 비축분이 있는 경우) | 높음 | 중간 |
| 누출 버킷 | 요청이 대기열에 들어가 고정 속도로 처리 | 허용하지 않음(완전히 평활화) | 높음 | 중간 |
| 슬라이딩 윈도우 | 윈도우 내 요청 수 통계 | 부분 허용 | 비교적 높음 | 낮음 |
| 고정 윈도우 | 시간 윈도우별 카운트 | 경계에서 버스트 가능 | 낮음 | 가장 낮음 |
어떤 알고리즘을 선택할까?
- API 속도 제한: 토큰 버킷이 가장 일반적이며, 합리적인 버스트 트래픽을 허용
- 트래픽 셰이핑: 누출 버킷은 일정한 출력 속도가 필요한 시나리오에 적합
- 간단한 카운팅: 슬라이딩 윈도우는 구현이 간단하여 대부분의 웹 애플리케이션에 적합
2. 배압 제어: 업스트림이 다운스트림보다 빠를 때
속도 제한이 해결하는 것은 "외부 요청이 너무 많은" 문제이고, 배압(Backpressure)이 해결하는 것은 "내부 구성 요소 간 속도 불일치" 문제입니다.
생산자가 데이터를 생성하는 속도가 지속적으로 소비자가 데이터를 처리하는 속도를 초과하면, 중간의 버퍼가 계속 팽창하여 결국 메모리 오버플로우나 데이터 손실을 초래합니다. 배압 메커니즘은 소비자가 생산자에게 "속도를 늦추라"고 역통지할 수 있게 합니다.
배압의 네 가지 전략
- 폐기(Drop): 버퍼가 가득 차면 새 데이터나 오래된 데이터를 폐기하며, 실시간성이 높지만 손실을 허용할 수 있는 시나리오에 적합
- 차단(Block): 생산자를 일시 정지시키고, 소비자가 처리를 마친 후 계속 진행하며, 데이터 손실이 불가능한 시나리오에 적합
- 샘플링(Sample): 일부 데이터만 처리하며, 고빈도 데이터 스트림에 적합
- 탄력적 확장(Scale): 소비자 수를 동적으로 증가시키며, 클라우드 네이티브 환경에 적합
3. 다층 속도 제한 아키텍처
프로덕션 환경에서 속도 제한은 한 곳에서만 하는 것으로는 충분하지 않으며, 다층 방어가 필요하며 각 계층은 다른 세분성의 문제를 해결합니다.
| 계층 | 위치 | 속도 제한 세분성 | 도구 |
|---|---|---|---|
| 클라이언트 | 프론트엔드/앱 | 버튼 디바운스, 요청 쓰로틀 | lodash.throttle, debounce |
| CDN/WAF | 엣지 노드 | IP 수준, 지역 수준 | Cloudflare Rate Limiting |
| API 게이트웨이 | 입구 게이트웨이 | 라우팅 수준, 사용자 수준 | Nginx limit_req, Kong |
| 서버 | 애플리케이션 내부 | 인터페이스 수준, 리소스 수준 | Sentinel, Resilience4j |
| 데이터베이스 | 스토리지 계층 | 연결 수, QPS | 연결 풀 설정, 슬로우 쿼리 서킷 브레이커 |
속도 제한의 HTTP 사양
속도 제한된 요청은 429 Too Many Requests 상태 코드를 반환해야 하며, 응답 헤더에 다음을 포함해야 합니다:
Retry-After: 클라이언트에게 재시도까지 권장 대기 시간(초 또는 날짜)X-RateLimit-Limit: 속도 제한 상한X-RateLimit-Remaining: 남은 할당량X-RateLimit-Reset: 할당량 초기화 시간
4. 실전 선택
| 시나리오 | 추천 방안 | 설명 |
|---|---|---|
| Nginx 입구 속도 제한 | limit_req_zone | 누출 버킷 알고리즘 기반, 설정이 간단 |
| 분산 속도 제한 | Redis + Lua 스크립트 | 토큰 버킷 또는 슬라이딩 윈도우, 다중 인스턴스 카운트 공유 |
| Java 마이크로서비스 | Sentinel / Resilience4j | 서킷 브레이커, 성능 저하, 핫스팟 속도 제한 지원 |
| Node.js API | express-rate-limit | 사용이 간편하고 Redis 스토리지 지원 |
| Go 서비스 | golang.org/x/time/rate | 표준 라이브러리 토큰 버킷 구현 |
요약
속도 제한과 배압은 시스템 안정성을 보호하는 두 가지 핵심 방어선입니다. 속도 제한은 외부 트래픽의 유입 속도를 제어하고, 배압은 내부 구성 요소의 처리 속도를 조정합니다.
이 장의 핵심 포인트를 되돌아보세요:
- 속도 제한의 필요성: 일부 요청을 거부하지 않으면 모든 요청이 실패합니다
- 세 가지 핵심 알고리즘: 토큰 버킷(버스트 허용), 누출 버킷(완전 평활화), 슬라이딩 윈도우(간단하고 정확)
- 배압 메커니즘: 폐기, 차단, 샘플링, 확장 네 가지 전략
- 다층 방어: 클라이언트에서 데이터베이스까지, 각 계층은 다른 세분성의 문제를 해결
- 429 사양: 속도 제한 시 표준 상태 코드와 속도 제한 헤더 정보 반환
더 읽어보기
- Stripe의 속도 제한 실천 - 결제 시스템의 속도 제한 설계
- Nginx limit_req 문서 - Nginx 속도 제한 모듈
- Alibaba Sentinel - 분산 서비스를 위한 트래픽 제어 구성 요소
- Resilience4j - Java 경량 내결함성 라이브러리
- Token Bucket 알고리즘 상세 설명 - 토큰 버킷 알고리즘의 수학적 원리