속도 제한(Rate Limiting)은 해결된 문제라고 생각했다. 그러다 AI 에이전트를 프로덕션 환경에 적용하게 되었다
요약
AI 에이전트를 프로덕션 환경에 배포할 때 발생하는 속도 제한(Rate Limiting) 문제를 다루고 있습니다. 기존의 `governor`는 단일 파드에서만 작동하며, Redis를 사용한 공유 카운터 방식은 네트워크 지연과 복잡성을 야기합니다. 필자는 이러한 한계를 극복하기 위해 rateguard라는 새로운 크레이트를 개발했습니다.
핵심 포인트
- AI 에이전트 프로덕션 배포 시 속도 제한 관리가 핵심 과제입니다.
- 단일 파드용 `governor`는 수평 확장 환경에서 효과적이지 않습니다.
- Redis 기반 공유 카운터는 네트워크 지연과 복잡성을 증가시킵니다.
- rateguard는 UDP 통신을 통해 로컬에서 분산된 공유 제한을 구현합니다.
어떤 서비스의 요청을 특정 속도로 유지하는 것은 나에게 항상 간단해 보였고, 이 문제는 다소 과장되었다고 생각했다. 그러다가 나는 AI 에이전트와 밀접하게 작업하기 시작했다.
나는 황홀경 단계(euphoria phase)를 거쳤다. 즉, 에이전트를 회사 데이터에 연결하면 실제 통찰력을 생성하기 시작하는 것이다. 하지만 이 해결책들이 프로덕션 규모로 이동할 때 문제가 발생한다. 우리는 Google Cloud에서 많은 부분을 운영하는데, 갑자기 여러 애플리케이션이 할당량(quota)을 초과하면서 느려진다. GCP 서비스와 에이전트 뒤에 있는 모델 모두에서 자원이 무서운 속도로 소모된다. 토큰은 단순히 비싼 것을 넘어, 금으로 무게를 재야 할 만큼 가치가 높아진다.
그래서 주요 과제는 의존하는 서비스 호출을 계산하고 제한하는 것이 된다.
governor: 완벽하다, 단일 파드(pod) 내에서만
Rust의 경우 명확한 답이 있다. 바로 governor이다. 이것은 훌륭한 크레이트(crate)이며, 자신이 말하는 것과 정확히 같은 일을 한다. 즉, 하나의 프로세스 내에서 요청을 제한한다.
하지만 서비스가 Kubernetes에서 수평적으로 확장되는 순간 그 즐거움은 사라진다. 각 파드(pod)는 자체적인 제한만 강제할 뿐, 다른 파드에 대해서는 아무것도 알지 못하다. 예를 들어, API가 분당 600개의 요청을 제공하고, 당신이 governor를 초당 10으로 설정했다고 가정하자. 8개의 파드가 있다면, 전체 시스템은 최대 초당 80개를 전송한다. 서비스 제공자는 429 에러(Too Many Requests)의 폭풍으로 응답하고, 재시도(retries)가 쌓이며, 아무런 효과도 없는 호출에 대해 비용을 지불하게 된다.
Redis: 작동하지만, 제발 좀...
표준 해결책은 Redis에 공유 카운터(shared counter)를 두는 것이다. 그리고 이것은 작동한다. 하지만 정말이지—작은 것 하나하나 때문에 우리가 얼마나 많은 추가 서비스를 구축해야 하는가? 모든 호출마다 네트워크 홉(network hop)이 하나 더 생기고, 배포하고, 확장하고, 모니터링하며, 알람을 받는 컴포넌트가 하나 더 생긴다. Redis가 느리면, 제한기 뒤에 있는 모든 것이 느려진다. Redis가 다운되면, 당신은 '제한 없음'과 '서비스 없음' 중 하나를 선택해야 한다.
이 문제는 너무 흔해서 이 두 가지 옵션만 있을 수는 없다고 느꼈다. 그래서 나는 이를 위한 크레이트를 작성했다.
rateguard가 하는 일
rateguard는 키당 하나의 공유 제한(shared limit)을 여러 인스턴스 그룹(fleet of instances)에 제공합니다. 이 과정에서 Redis나 중앙 서비스, 또는 요청 경로상의 네트워크 호출이 필요 없습니다.
모든 파드(pod)는 Guard를 내장하고 있습니다. 모든 결정은 로컬에서 약 25 나노초 만에 이루어집니다. 백그라운드에서는 파드들이 UDP를 통해 서로 통신하며 제한을 어떻게 분배할지 합의합니다.
아웃바운드 호출의 패턴은 다음과 같습니다: 호출하기 전에 문의하고, 지시받으면 기다립니다.
use rateguard::{Decision, Guard};
let guard = Guard::builder()
...
8개의 파드든 80개든, 전체 그룹은 초당 10회의 호출을 유지합니다.
작동 방식은 네 가지 라인으로 요약할 수 있습니다:
- 수요에 따른 공유: 각 파드는 자신이 시도하는 호출 횟수를 다른 파드들에게 알리고(gossips), 그 제한의 비례적인 부분을 가져갑니다. 바쁜 파드가 더 많이 받고, 유휴 상태인 파드는 사용하지 않는 할당량을 차지하지 않습니다.
- 바쁜 키만 조정: 활동이 적은 키는 로컬에서 작은 고정된 할당량으로 실행되며 네트워크 트래픽을 전혀 발생시키지 않습니다.
- 호출 경로의 실패 방지: 다른 파드들이 사라져도, 각 파드는 알고 있던 마지막 할당량을 계속 적용합니다. 네트워크 분할(network split)이 발생했을 때 어떤 일이 일어날지는 설정(HoldDown, Quorum 또는 Optimistic)으로 지정되어 있으며, 각각에 대해 명시된 경계가 있습니다.
- 분산 시스템처럼 테스트: 이 프로토콜은 결정론적 시뮬레이터에서 테스트됩니다. 파티션, 30% 패킷 손실, GC 일시 중지(GC pauses), 순환 재시작(rolling restarts) 등이 포함되며, README에는 CI가 최신 상태로 유지하는 정확도 보고서가 첨부되어 있습니다.
아직 하지 않는 것 (yet)
제가 제한 사항을 말씀드리는 편이 낫겠습니다:
- 토큰이 아닌 호출(call) 횟수를 계산합니다. 만약 LLM 할당량(quota)이 분당 토큰(tokens per minute) 단위라면, rateguard는 각 호출 비용이 얼마인지가 아니라 얼마나 자주 호출할 수 있는지를 제한합니다. 가중치 기반 확인(Weighted checks)은 제가 고려하고 있는 기능이니 필요하신지 알려주세요.
- Guard당 하나의 제한(limit)을 가집니다. Guard의 모든 키는 0.1 단위로 동일한 제한을 공유하며, 서로 다른 할당량은 서로 다른 Guard를 의미합니다.
- 현재 Seeds는 IP 주소입니다. DNS 이름으로 Kubernetes headless service를 통해 참여하는 기능은 이미 기여자(contributor)로부터 풀 리퀘스트(pull request)가 들어와 검토 중입니다.
- 신뢰 네트워크 전용입니다. Gossip 프로토콜은 암호화되거나 인증되지 않았으므로, 포트는 클러스터 내부에 유지해야 합니다.
- 정확한 계산을 위한 것은 아닙니다. 청구(Billing), 선불 크레딧(prepaid credits) 등 추가 호출 한 번에 실제 비용이 발생하는 모든 곳에서는 여전히 중앙 카운터가 필요합니다.
사용해 보세요
[dependencies]
rateguard = "0.1"
버전 0.1이며, 피드백을 받고 싶습니다. 특히 에이전트와 동일한 할당량 문제로 고군분투하는 분들과, 이 기능이 살아남지 못하는 실패 시나리오를 발견하는 모든 분들의 의견을 기다립니다.
📦 crates.io/crates/rateguard
💻 github.com/donmiro/rateguard
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기