콘텐츠로 이동

사용자별 동시 처리 중인 요청 상한: 설계 기록

이 문서는 사용자, 키, 팀별 동시 처리 중인(in-flight) 요청 상한을 기록합니다 (이슈 #2383, 에픽 #2362). 구현되었으며 기본값은 꺼짐입니다.

배경

속도 제한은 초당 요청 수 + 버스트입니다(RateLimitSettings). 버킷은 요청 시점에 소모되고 윈도우 동안 리필되지만, 테넌트의 요청 중 얼마나 많은 것이 아직 스트리밍 중인지는 알려주지 않습니다. 이 상한이 생기기 전의 동시성 경계는 테넌트별이 아니었습니다.

  • server.max_concurrent_requests (src/http/middleware/admission.rs, #1173): 라우터 전체에 하나뿐인 in-flight 상한입니다. 허가는 스트림 전체를 포함해 요청 전체 동안 유지되며, 상한을 넘는 요청은 503과 Retry-After로 즉시 거부됩니다. 한 테넌트가 여전히 모든 허가를 차지할 수 있습니다.
  • fallback.fallback_policy.max_concurrent_dials_per_backend: 다이얼 전용이며 폴백 홉에만 적용됩니다.
  • 백엔드별 HTTP/2 SETTINGS_MAX_CONCURRENT_STREAMS: 한 사용자가 소진할 수 있습니다. 긴 스트림이 스트림 상한을 채우면 이후 요청이 HTTP 클라이언트 안에서 수 분을 기다립니다.

키별 속도 재정의(api_keys[].rate_limit, 분당 요청 수)는 있지만, 처리 중인 요청 수가 아니라 도착률을 제한합니다. 이 페이지는 멀티 테넌트 한도 적용 순서의 2단계를 자세히 다룹니다.

출시된 설계

  • 설정 표면: 수락 시점 테넌트 차원 per_api_key, per_user, per_team, per_client의 max_in_flight_requests(정수, 0 = 무제한, 기본값)입니다. global(라우터 전체 상주 한도는 server.max_concurrent_requests 사용), per_backend(수락 시임 없음), per_model(테넌트 식별자가 아님)에서는 무시되며, 무시되는 차원에 설정하면 config validate가 경고합니다.
  • In-flight 추적: 버킷 저장소 옆의 테넌트별 카운터 맵이며, 해당 차원의 요청 수 버킷과 같은 식별자로 키를 잡습니다. 요청 수와 토큰 예산 검사를 모두 통과한 뒤 설정된 차원마다 슬롯 하나를 잡으므로, 거부된 요청은 슬롯을 차지하지 않습니다.
  • 해제: 슬롯은 응답 본문에 붙는 RAII 가드가 보유하며, server.max_concurrent_requests가 사용하는 요청 전체 허가 생명주기와 같습니다. 따라서 스트림은 마지막 프레임까지 슬롯을 붙잡고, 핸들러 실패나 클라이언트 연결 끊김은 가드를 드롭해 슬롯을 해제합니다. 회계는 레플리카별입니다. 분산 카운터는 임대가 필요하고 죽은 프로세스가 반납할 수 없으므로, N개 레플리카 함대는 인메모리 요청 버킷처럼 설정 상한의 최대 N배까지 허용합니다.
  • 상한 초과 동작: 거부하며 큐잉하지 않습니다. 거부는 429와 limit_type: "inflight", api_key/user/team/client를 밝히는 error.details.inflight_dimension, 고정 Retry-After: 1입니다(라우터는 스트림 종료 시점을 추정하지 않음). 할당량 수치는 per_api_key에만 보고하고, 사용자/팀/클라이언트 카운트는 공유되므로 다른 공유 버킷과 같이 수치를 숨깁니다. 공정 큐잉은 여전히 별도 설계입니다(이슈 #2397, #2423).
  • 바이패스: whitelist / bypass_keys 전체 면제는 이 상한도 건너뜁니다. 범위 지정 바이패스는 새 inflight 클래스를 지정합니다(이슈 #2426).
  • 관측성: rate_limit_inflight_requests{dimension, tenant}가 현재 보유 수(0이 되면 시리즈 제거), rate_limit_inflight_refusals_total{dimension, tenant}가 거부 수를 보고하며, 거부는 rate_limit_exceeded_total{limit_type="inflight"}에도 집계됩니다.

해결된 질문

  • 기본 상한은 0(꺼짐)이고, 팀 차원은 per-team 요청 수 버킷과 같은 로컬 해석 팀 식별자(organization_id)로 키를 잡습니다.
  • 거부 응답은 per-backend 상한이 사용하는 것과 같은 error.details 엔벨로프에서 한도 유형을 밝힙니다.