Ramp는 각자 자체 할당량을 부과하는 수십 개의 서드파티 API(Finicity, Teller, Zendesk, Salesforce)를 호출하고, 전체 사용자 기반에 걸쳐 주기적인 Celery 작업을 실행하며, 인바운드 트래픽을 IP가 아닌 인증된 사용자 기준으로 속도 제한하고자 했습니다. 이 세 가지 요구는 모두 같은 결핍을 가리켰습니다. 즉, 단일 워커 내에서만이 아니라 모든 워커와 서버에 걸쳐 정확한 속도 제한기가 필요했습니다.

Ramp의 기존 접근 방식은 Celery에 내장된 작업별 속도 제한에 의존했습니다. 그 제한은 워커별로 적용될 뿐 전역적으로 적용되지 않았으므로, 진정한 시스템 전체 속도를 강제하려면 엔지니어가 큐의 워커 수와 오토스케일링 동작을 알고 원하는 제한을 워커들에 수동으로 나눠야 했고, 그 수치는 워커 수가 바뀌는 순간 낡은 값이 되었습니다. 특정 Celery 프리페치 구성에서는 속도 제한된 작업이 워커가 새 작업을 가져오는 것을 막아 전체 큐를 정체시킬 수도 있었습니다.

Ramp는 고정 윈도, 슬라이딩 윈도, 누수 버킷 알고리즘을 검토한 뒤 GCRA(Generic Cell Rate Algorithm)를 선택했습니다. 이는 전체 요청 로그 대신 키당 하나의 계산된 타임스탬프만 저장하면 되는 누수 버킷 변형입니다. Ramp는 이를 Redis에 대해 구현하면서, 참조 구현에 있는 경쟁 조건의 틈을 메우기 위해 분산 잠금을 추가했고, 머신 간 드리프트를 피하기 위해 각 앱 서버의 시계 대신 Redis의 서버 시간을 사용했습니다.

그 결과 Ramp가 세 가지 사용 사례 모두에 적용할 수 있는 단일 속도 제한 프레임워크가 만들어졌습니다. 즉, 서드파티 API 호출, 전역 Celery 작업 스로틀링, 사용자별 인바운드 API 제한이며, 엔지니어가 워커 수를 전혀 고려할 필요가 없었습니다.

Redis로 뒷받침되는 글로벌 GCRA 속도 제한기

키당 하나의 TAT 값을 저장하는 것은 슬라이딩 윈도의 전체 요청 로그보다 훨씬 저렴하므로, 제한기는 Ramp의 요청량에서도 빠르게 유지됩니다.

요청을 처리하는 앱 서버가 아니라 Redis 자체에서 현재 시간을 읽음으로써, 시계 드리프트가 일관성 없는 적용의 원인이 되는 것을 제거합니다.

확인 후 설정(check-and-set) 주변의 Redis 잠금은 두 동시 요청이 모두 낡은 TAT를 읽고 둘 다 승인될 수 있는 경쟁 조건을 막습니다.

일회성 제한기 대신 하나의 범용 프레임워크를 구축함으로써, 동일한 코드가 중복 로직 없이 서드파티 할당량, Celery 작업 스로틀링, 사용자별 API 제한에 모두 쓰일 수 있었습니다.

프로세스별로만 적용되는 속도 제한은 전역 제한을 근사할 뿐이며, 카운터를 공유되고 잠금 가능한 저장소에 중앙화하는 것이 그 수치를 실제로 말하는 대로 의미 있게 만드는 방법입니다.

Ramp는 GCRA-on-Redis 제한기를 게시물에서 설명한 세 가지 사용 사례, 즉 전역 서드파티 API 할당량, Celery 작업 스로틀링, 인바운드 사용자별 API 제한에 단위 테스트와 수동 검증을 거쳐 배포했으며, 추가 사용 사례가 생기면 동일한 프레임워크를 새로운 속도 제한 알고리즘으로 확장할 계획이라고 밝혔습니다.

근거 확인하기

출처

  1. Rate limiting with Redis Ramp Engineering (Builders Blog) · builders.ramp.com ·