Rate Limiting과 백프레셔
알고리즘
| 알고리즘 | 동작 | 특징 |
|---|
| 고정 윈도우 | 구간별 카운트 | 단순, 경계에서 버스트 2배 |
| 슬라이딩 윈도우 | 최근 N초 기준 | 정확, 비용 높음 |
| 토큰 버킷 | 토큰 충전과 소비 | 평균 제한 + 버스트 허용 |
| Leaky bucket | 일정 속도로 유출 | 처리량 평탄화 |
초과 응답 규약
429 Too Many Requests
Retry-After: 30
한도의 기준을 먼저 정한다. 사용자, API 키, IP 중 무엇인가. IP 기준은 NAT 뒤 다수 사용자를 한 덩어리로 묶는다.
백프레셔와의 차이
- Rate limiting은 외부 유입량을 정책으로 제한한다
- 백프레셔는 내부 처리 능력을 초과한 부하를 상류로 되밀어낸다
- 수단: 큐 길이 상한, 커넥션과 스레드풀 상한, 타임아웃, 부하 차단(load shedding)
무엇을 기준으로 세나
같은 알고리즘이라도 무엇마다 세느냐가 정책의 실체입니다.
| 기준 | 맞는 곳 | 문제 |
|---|
| 사용자 | 로그인한 API | 로그인 전 경로에 못 쓴다 |
| 주소 | 로그인 전 경로 | 같은 회선 뒤의 여러 사람이 함께 걸린다 |
| API 키 | 서버끼리 부르는 경로 | 키 하나를 여러 서비스가 나눠 쓰면 뒤섞인다 |
| 그 둘의 조합 | 로그인 시도 같은 민감한 곳 | 저장할 키가 많아진다 |
둘째가 실제로 사고를 냅니다. 회사나 학교에서 여러 사람이 같은 주소로 나가면 한 명 때문에
전체가 막힙니다. 그래서 주소 기준은 한도를 넉넉히 둡니다.
막을 것과 미룰 것
거절과 기다리게 하기는 다른 도구입니다.
사용자가 보는 요청은 거절하고 언제 다시 오라고 알려 준다
배경에서 도는 일은 거절보다 천천히 흘려보내는 쪽이 낫다
둘째가 백프레셔입니다. 배치를 거절하면 그 일이 사라지고, 흘려보내면 늦더라도 끝납니다.
사용자 요청은 반대입니다. 오래 기다리게 하는 것이 거절보다 나쁩니다.
실무 포인트
- 무한 큐는 지연을 무한히 키운다. 큐를 두면 상한과 초과 시 정책을 같이 정한다
- 분산 환경의 카운터는 공유 저장소에 둔다. 인스턴스별 카운트는 실제 한도가 인스턴스 수만큼 커진다
- 서버의
Retry-After와 클라이언트의 지터 백오프는 짝이다. 한쪽만 있으면 재시도 폭주가 남는다
- 제한을 걸기 전에 어떤 트래픽이 얼마나 오는지 계측한다. 근거 없는 한도는 정상 사용자를 자른다
Q.특정 고객사 트래픽이 전체 API를 마비시켰습니다. 어떤 제어 장치를 넣겠습니까?
한 고객이 전체를 마비시켰다는 것은 격리가 없다는 뜻입니다. 순서대로 넣습니다.
| 장치 | 무엇을 막나 |
|---|
| 고객사별 한도 | 한 고객이 전체 용량을 다 쓰는 것 |
| 동시 처리 수 제한 | 느린 요청이 워커를 전부 점유하는 것 |
| 격벽 | 한 고객의 부하가 다른 고객 경로로 번지는 것 |
| 큐 길이 상한과 거절 | 무한히 쌓이다 한꺼번에 터지는 것 |
| 우선순위 | 중요한 트래픽이 뒤로 밀리는 것 |
첫 줄이 즉시 효과가 있고, 둘째 줄이 진짜 원인일 때가 많습니다. 요청 수는 적은데
하나하나가 오래 걸리는 경우, 초당 요청 한도로는 못 막습니다. 동시에 처리 중인 건수를
고객사별로 제한해야 워커가 남습니다.
한도를 정할 때는 전체 용량을 고객 수로 나누지 않습니다. 대부분은 한도 근처도 안 쓰므로,
평소 사용량의 몇 배를 한도로 두고 전체 합이 용량을 넘지 않는지만 확인합니다.
거절할 때는 언제 다시 오라고 알려 줘야 합니다. 그 정보가 없으면 상대가 즉시 재시도해서
부하가 더 몰립니다.
흔한 실수: 서버를 증설해서 넘기는 것. 격리가 없으면 늘어난 용량도 같은 고객이
가져갑니다. 나누는 장치가 먼저이고 증설은 그 다음입니다.
Q.토큰 버킷과 고정 윈도우 중 무엇을 택하고 왜 그런가요?
| 항목 | 고정 윈도우 | 토큰 버킷 |
|---|
| 구현 | 가장 단순하다. 카운터 하나 | 토큰 수와 갱신 시각을 들고 있어야 한다 |
| 순간 몰림 | 경계에서 두 배까지 통과한다 | 버킷 크기까지만 |
| 짧은 폭주 허용 | 못 한다 | 버킷에 모아 둔 만큼 허용한다 |
| 사용자 체감 | 창이 바뀔 때까지 전부 막힌다 | 조금씩 계속 통과한다 |
고정 윈도우의 경계 문제가 결정적입니다. 분당 100이면, 59초에 100개와 61초에 100개가
통과해 2초 사이에 200개가 들어옵니다. 한도를 건 이유가 순간 부하를 막는 것이었다면
그 목적을 못 지킵니다.
그래서 대개 토큰 버킷을 고릅니다. 평소에 안 쓴 만큼 토큰이 쌓여 짧은 폭주를 허용하고,
그 상한이 버킷 크기로 명확합니다. API 클라이언트의 실제 사용 패턴(평소 조용하다가
배치 때 몰림)과도 맞습니다.
고정 윈도우가 맞는 자리도 있습니다. 정확히 "시간당 N회" 를 계약으로 파는 경우입니다.
과금과 한도가 같은 기준이어야 설명이 쉽습니다.
흔한 실수: 경계 문제를 알면서 "그 정도는 괜찮다" 고 넘기는 것. 클라이언트가 창 경계에
맞춰 몰아 보내도록 스스로 최적화하면, 그 두 배가 매 창마다 반복됩니다.
Q.서버 인스턴스가 여러 대일 때 rate limit을 어떻게 정확히 적용하나요?
인스턴스마다 따로 세면 실제 한도는 한도 곱하기 대수가 됩니다. 그래서 카운터를
공유해야 합니다.
| 방식 | 정확도 | 대가 |
|---|
| 인스턴스별 카운터 | 낮다. 대수만큼 초과 | 네트워크 비용 0 |
| 공용 저장소 카운터 | 높다 | 요청마다 왕복이 붙는다 |
| 한도를 대수로 나눠 배분 | 중간 | 부하가 고르지 않으면 낭비된다 |
| 지역 카운터 + 주기 동기화 | 중간에서 높음 | 구현이 복잡하다 |
공용 저장소가 기본입니다. 원자적 증가와 만료를 한 번에 처리하면 왕복 한 번으로
끝나고, 그 비용은 대개 감당할 만합니다. 스크립트로 증가와 확인을 묶어 원자적으로
처리하는 것이 핵심입니다.
그 저장소가 죽었을 때를 정해야 합니다. 막을 것인가 통과시킬 것인가. 통과시키면
한도가 사라지고, 막으면 전체 서비스가 멈춥니다. 대개는 통과시키되 인스턴스별 보수적
한도로 내려앉는 쪽을 씁니다.
왕복이 부담이면 지역 카운터를 두고 주기적으로 합치는 방식이 있습니다. 정확도를 조금
포기하는 대신 지연이 거의 없습니다. 한도가 과금 기준이 아니라 보호 장치라면 이
거래가 맞습니다.
흔한 실수: 정확도를 위해 요청마다 여러 번 왕복하는 것. 한도 장치가 지연의 원인이
되면 본말이 뒤집힙니다. 보호가 목적이면 약간의 초과는 허용해도 됩니다.
Q.큐를 늘리는 것이 근본 해결이 아닌 이유를 설명해주세요
큐는 속도 차이를 흡수하지만 만들어 내지는 못합니다. 들어오는 속도가 처리 속도보다
계속 크면, 큐 길이는 무한히 늘어나고 결국 터집니다.
늘리면 생기는 일입니다.
| 결과 | 설명 |
|---|
| 지연이 길어진다 | 큐 길이가 곧 대기 시간이다. 100만 건이 쌓이면 그만큼 늦게 처리된다 |
| 실패를 늦게 안다 | 받아 놓고 못 처리하는 상태가 길어진다 |
| 터질 때 크게 터진다 | 메모리나 디스크가 한계에 닿는 순간 한꺼번에 잃는다 |
| 처리해도 의미가 없다 | 이미 지난 알림, 만료된 요청을 뒤늦게 처리한다 |
마지막 줄이 큐를 늘리는 것이 해가 되는 이유입니다. 인증 번호를 5분 뒤에 배달하면
도움이 아니라 혼란입니다.
근본 해결은 셋뿐입니다. 처리 속도를 올리거나, 들어오는 속도를 줄이거나(거절), 일의
양 자체를 줄이는 것입니다. 큐는 순간 몰림을 평탄하게 만드는 용도이지 지속적인
불균형의 해법이 아닙니다.
그래서 큐에는 길이 상한을 둡니다. 상한에 닿으면 거절하고, 그 거절이 앞단으로 전해져
들어오는 속도를 늦추게 만듭니다. 문제를 감추지 않고 드러내는 쪽이 안전합니다.
흔한 실수: 큐가 쌓이는 것을 보고 큐 용량부터 키우는 것. 증상이 잠시 사라지고
문제를 발견하는 시점만 뒤로 밀립니다. 그 사이 밀린 양은 더 커집니다.
먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.
읽었으면 문제로 확인해보세요
API 설계 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.