제3자 도구가 갑자기 요청을 제한하면, 재시도가 왜 오히려 눈사태를 키우는가
← 전체 목차 · 이전: 다중 도구를 병렬 호출할 때, 대화 상태는 먼저 갱신하나 나중에 갱신하나 · 다음: 도구가 200을 반환해도, 그 안의 값이 틀릴 수 있다면 무엇을 검증해야 하는가
"타임아웃 → 재시도 → 실패 → 몇 초 대기 → 다시 시도"는 방어가 아니라 공격이 될 수 있다. 실패한 작업들이 마침 레이트 리밋 회복 구간에 동시에 재시도를 걸면, 막 회복 중이던 인터페이스를 순간적으로 다시 가득 채워 버린다. 방어가 다음 라운드 장애의 진원지로 뒤바뀌는 것이다.
레이트 리밋 눈사태의 다섯 가지 실패 시나리오
레이트 리밋 눈사태는 "도구가 에러를 반환하면 곧 장애"라는 단순한 그림이 아니라, 레이트 리밋이 트리거된 이후의 연쇄 반응으로 인해 Agent 서비스 전체가 불가용해지는 현상이다.
| 시나리오 | 내용 |
|---|
| 순간 고동시성이 레이트 리밋을 트리거 | 실패한 작업들이 동시에 재시도해 재시도 폭풍을 형성, 하류 도구를 다시 무너뜨림 |
| 등급별 제한의 파급 | 일부 인터페이스만 제한됐는데 전체 작업 체인이 실패 |
| 다중 Agent 공유 쿼터 경쟁 | 저우선 작업이 고우선 작업의 몫을 잠식 |
| 제한 식별자 부재 | 시스템 장애를 레이트 리밋으로 오인해 반복 재시도 |
| 제한 해제 후 트래픽 돌발 | 회복 직후 재차 레이트 리밋 트리거 |
토큰 버킷 쿼터 관리 + 서킷 브레이커 등급별 강등
agent_tool_circuit 테이블로 도구별 상태를 관리한다.
tool_id 도구 식별자
biz_type 업무 유형
token_bucket 토큰 버킷 (쿼터)
circuit_status 서킷 상태
degrade_level 강등 등급
priority_weight 우선순위 가중치
error_count 오류 카운트
실행 링크:
1. 도구 호출 전 → 토큰 버킷에서 쿼터를 먼저 취득, 취득해야만 호출 허용
2. 취득 실패 → 대기열에 진입, priority_weight 순으로 정렬
3. 서킷 브레이커가 오류율을 집계 → 임계값 도달 시 자동으로 서킷 상태 전환
4. 등급별로 강등 전략을 실행
| 제한 정도 | 강등 동작 |
|---|
| 경도 제한 | 자동으로 대체 도구로 전환 |
| 중도 제한 | 캐시된 결과를 반환 |
| 중차단 | 비핵심 작업을 직접 거부 |
서킷 브레이커 상태는 5단계로 흐른다.
정상 → 경계 → 차단 → 강등 → 반개방 회복
동시성 안전: 매 상태 변경은 "현재 오류율 + 시간 윈도우"를 기준으로 해야 하며, 데이터베이스 행 잠금으로 상태 유일성을 보장한다. 동시 상태 변경 시 영향 행 수가 0이면 상태 요동을 회피한 것으로 본다.
제한 식별자가 없을 때 — 오류 코드 클러스터링 + 응답 시간 적합 모델
도구 쪽에서 명확한 레이트 리밋 표시를 주지 않는 경우가 실제로는 흔하다. 단일 오류 코드에 의존하지 않고, 오류 코드 클러스터링 + 응답 시간 적합 모델로 제한 여부를 간접 추론한다.
프로덕션 실전 디테일
| 문제 | 처리 |
|---|
| 데이터 일관성 | 쿼터 관리와 도구 호출을 같은 스케줄 링크 안에서 처리 |
| 장기 저장 | 서킷 기록은 장기 보존해 제한 전략을 지속 개선; 완료 후 7일 초과한 장애 기록은 이력 테이블로 이관, 단 이관 기간 동안 장애가 재발하지 않음을 보장 |
| 모니터링 지표 | 제한 트리거율, 재시도 성공률, 서킷 지속 시간, 강등 작업 비중, 쿼터 사용률 |
| 자체 장애 시 강등 | 토큰 버킷 서비스 자체가 불가용하면 "단일 머신 제한 + 핵심 작업 우선"으로 강등, 사후 트래픽 복기와 쿼터 교정 병행 |
핵심 통찰
- 재시도는 방어이자 동시에 공격이 될 수 있다 — 실패 작업이 레이트 리밋 회복 구간에 동시에 재시도하면, 막 회복 중인 인터페이스를 순간적으로 다시 가득 채운다. 재시도 방안의 좋고 나쁨을 가르는 기준은 "재시도가 있는가"가 아니라 "그 재시도가 다음 장애의 진원이 되는가"이다.
- 레이트 리밋 눈사태를 정의부터 바로잡아야 답이 맞는다 — 이를 "도구 에러"라는 점상 장애로 이해하면 해법은 catch 하나에 머문다. "제한 트리거 이후의 연쇄 반응이 서비스 전체를 불가용하게 만드는 것"이라고 범위를 다시 잡아야 방어가 체계가 된다.
- 제한에 등급이 있으면 강등에도 등급이 있어야 한다 — 조회 인터페이스는 살아있고 쓰기 인터페이스만 막히는 상황에서 "도구 전체가 불가용"이라는 이진 가정은 틀렸다. 경도-전환, 중도-캐시, 중차단-거부 세 단계가 이진 가정보다 훨씬 촘촘하다.
- 공유 쿼터 풀은 본질적으로 제한 문제가 아니라 스케줄링 문제다 — 다중 업무 라인이 하나의 쿼터를 공유할 때 저우선 작업이 고우선의 몫을 잠식하는 것은 "제한이 부실해서"가 아니라 "우선순위가 없어서"다. 그래서 방안에는
priority_weight와 "취득 실패 시 우선순위 대기열 진입"이 반드시 있어야 한다.
- 제한 상태는 약속이 아니라 추론으로 얻어야 할 때가 있다 — 도구 쪽이 명시적 제한 표시를 주지 않는 것은 일상이다. 오류 코드 클러스터링 + 응답 시간 적합 모델은 단일 신호가 아니라 통계적 특징에서 상태를 복원하는 방식이다.
- 서킷 브레이커의 본질은 동시성 유일성을 반드시 보장해야 하는 상태 머신이다 — 상태 전이 자체는 어렵지 않다. 어려운 것은 동시 접근에서의 상태 요동이다. "현재 오류율+시간 윈도우 기준 변경, 행 잠금으로 유일성, 영향 행 수 0으로 요동 회피"까지 말해야 진짜 구현 경험이다.
- 레이트 리밋 관리 컴포넌트 자체도 무너질 수 있다 — 토큰 버킷 서비스가 불가용할 때 "단일 머신 제한 + 핵심 작업 우선"으로 강등하고 사후 복기·교정을 한다. 의존이 건강할 때만 성립하는 안정성 방안은 안정성 방안이 아니다.
엔지니어링 실전 Tips
- "영향 행 수 0"을 동시성 판정 근거로 쓸 것: 서킷 브레이커 상태 변경은 행 잠금 + 영향 행 수로 변경 권한을 획득했는지 판단하는 것이 상태 요동을 막는 가장 직접적인 구현이다.
- 강등 동작을 미리 번호로 매길 것:
degrade_level 필드로 경/중/중차단 세 등급 각각의 확정 동작(대체 도구/캐시/거부)을 미리 정해 두어야 장애 시 자동화가 가능하다.
- 쿼터를 못 얻은 요청은 우선순위 대기열로 보낼 것, 무한 재시도로 보내지 말 것: 전자는 스케줄링이고 후자는 재시도 폭풍의 씨앗이다.
- 이관은 "재발 없음"을 전제로 할 것: 7일 지난 정상 장애 기록만 이관하고, 이관하는 동안 장애가 재발하지 않는지 반드시 확인해야 한다.
- 쿼터 관리와 도구 호출을 같은 스케줄 링크에 둘 것: 두 동작이 분리되면 "쿼터를 취득한 것"과 "호출을 발기하는 것" 사이에 틈이 생겨 쿼터가 다른 요청에 먼저 소진될 수 있다.
- 모니터링 다섯 지표를 세트로 볼 것: 제한 트리거율, 재시도 성공률, 서킷 지속 시간, 강등 작업 비중, 쿼터 사용률 — 하나만 보면 "재시도 성공률은 높은데 쿼터 사용률이 이미 가득 찬" 붕괴 직전 상태를 놓친다.
- 면접·설계 검토 답변 순서: 범위와 실패 시나리오 정의 → 핵심 방안(토큰 버킷+서킷 등급 강등) → 프로덕션 디테일(상태 추론, 이관, 모니터링, 자체 장애 강등) 순으로 답하면 빠뜨림이 없다.