We Added Proxies and the Limit Grew
한국어 원문으로 표시합니다.
한 줄 요약
전역 속도 제한은 세는 곳을 프록시 밖 한곳에 둔다. Envoy 는 요청마다 설명자(descriptor)를 만들어 속도 제한 서비스에 gRPC 로 묻고, 서비스는 Redis 의 카운터를 올려 허용·초과를 답한다. 그래서 프록시가 몇 대든 한도는 하나다.
왜 이게 필요했나
로컬 속도 제한(토큰 버킷)은 빠르고 의존성이 없다. 대신 버킷이 Envoy 한 대 안에 있다. 공식 문서도 로컬 제한의 버킷은 기본으로 "Envoy 프로세스 하나" 에 걸린다고 밝힌다. 인그레스 프록시를 두 대에서 여섯 대로 늘리는 순간, "고객 하나당 시간당 1,000번" 이던 약속이 6,000번이 된다. 오토스케일링이 붙으면 한도가 트래픽을 따라 스스로 늘어난다 — 막으려던 폭주가 올수록 문이 넓어지는 셈이다.
API 판매처럼 전체에 걸린 약속이 있으면 셀 곳이 하나여야 한다. 그것이 전역 속도 제한 서비스이고, Envoy 쪽에는 그 서비스에 묻는 HTTP 필터(envoy.filters.http.ratelimit)가 있다. 서비스 쪽 참조 구현이 envoyproxy/ratelimit 이다.
어떻게 동작하나
1. 라우트가 설명자를 만든다. 필터는 스스로 무엇을 셀지 모른다. 라우트(또는 가상 호스트)의 rate_limits 가 요청에서 값을 뽑아 설명자를 만든다.
rate_limits:
- actions:
- request_headers: { header_name: x-plan, descriptor_key: plan }
# x-plan: free 인 요청 → 설명자 [("plan", "free")]
헤더가 없으면 설명자가 만들어지지 않고, 그 요청은 제한을 받지 않는다. 가상 호스트에 적은 rate_limits 는 그 호스트의 모든 라우트에 걸리며, 라우트에 자기 rate_limits 가 있을 때만 대신 쓰인다.
2. 서비스가 규칙과 맞춘다. 서비스 설정은 도메인 하나에 설명자 규칙 목록이다.
| 설정 | 뜻 |
|---|---|
domain: edge |
Envoy 필터의 domain 과 같아야 이 규칙을 쓴다 |
key: plan, value: free |
설명자가 정확히 이 쌍일 때 |
unit: hour, requests_per_unit: 3 |
시간 창마다 3번 |
3. Redis 가 센다. 서비스는 창마다 열쇠 하나(edge_plan_free_<창 시작 시각>)를 INCRBY 로 올리고 창 길이만큼 TTL 을 건다. 서비스 자체는 상태가 없으므로 여러 대를 띄워도 된다 — 한도는 Redis 열쇠 하나에 모인다.
4. 결과가 응답에 드러난다. 초과면 Envoy 가 429 를 낸다. enable_x_ratelimit_headers 를 켜면 x-ratelimit-limit·x-ratelimit-remaining·x-ratelimit-reset 헤더가 붙어, 클라이언트가 얼마나 남았는지 알 수 있다.
서비스가 죽으면 failure_mode_deny 가 정한다. 기본값 false 는 통과다. 속도 제한은 보호 장치라, 그것이 고장 났다고 서비스를 멈추지는 않겠다는 선택이다. 대신 그 순간은 통계 cluster.<업스트림>.ratelimit.error·failure_mode_allowed 에만 남는다.
둘을 함께 쓴다. 전역 제한은 요청마다 네트워크 왕복이 하나 더 들고, 그 서비스가 새 병목이 된다. 그래서 로컬 제한을 앞에 두어 폭주를 먼저 거르고, 전역 제한은 약속을 지키는 데 쓴다. 공식 문서가 권하는 조합도 이것이다.
현장에서 만나는 모습
"프록시를 늘렸더니 고객 한도가 늘었습니다." 로컬 제한만 쓴 경우다. 한도를 대수로 나눠 적어 두는 방법은 오토스케일링 앞에서 무너진다.
"한도 설정을 바꿨는데 적용이 안 됩니다." 설명자가 규칙과 한 글자라도 다르면 규칙이 걸리지 않고, 걸리지 않은 요청은 제한 없이 지나간다. 서비스의 /rlconfig 로 규칙이 읽혔는지, Envoy 가 어떤 설명자를 보내는지를 나눠 확인한다.
"Redis 장애 동안 아무도 막히지 않았습니다." 기본값이 통과라서다. 이것이 의도라면 통계에 알림을 걸고, 의도가 아니라면 failure_mode_deny: true 로 바꾸되 그러면 Redis 가 모든 요청의 가용성을 쥔다는 것을 함께 받아들여야 한다.
공식 문서: Global rate limiting · Rate limit filter · Local rate limit filter · envoyproxy/ratelimit
다음 실습에서 할 것
Redis 와 참조 구현 속도 제한 서비스를 파드 안에 띄우고, 같은 서비스를 부르는 Envoy 두 대를 세운다. 두 대에 번갈아 보낸 요청이 한도 하나를 나눠 쓰는 것과, 같은 요청을 로컬 토큰 버킷으로 보내면 대마다 따로 세어지는 것을 나란히 센다. 마지막으로 서비스를 죽여 기본값이 통과라는 것을 응답과 통계로 확인한다.