LLM 프로덕션에서 단일 엔드포인트에 의존하는 순간 리스크가 시작됩니다. Azure OpenAI 리전이 다운되거나, OpenAI가 Rate Limit을 내뱉거나, 트래픽 폭증으로 TPM 한도가 터질 때 — 이 모든 상황을 코드 한 줄 바꾸지 않고 흡수하는 것이 LiteLLM Router입니다. 100개 이상의 LLM 프로바이더를 단일 OpenAI 호환 인터페이스로 추상화하면서, 6가지 라우팅 전략과 자동 폴백·재시도를 제공합니다. 이 편에서는 Router의 내부 구조와 6가지 전략 각각이 어떻게 동작하는지, 언제 무엇을 써야 하는지를 실전 코드와 함께 완전히 정리합니다.
이 포스트 한 줄 요약 → LiteLLM Router: 동일 model_name으로 여러 배포를 등록하면 자동 로드밸런싱 → 핵심 내부 구조: _model_index 딕셔너리 + CooldownCache + DualCache → 6가지 전략: simple-shuffle·least-busy·usage-based-routing·usage-based-routing-v2·latency-based-routing·cost-based-routing → 프로덕션 기본값: simple-shuffle — 최소 오버헤드, LiteLLM 공식 권장 → usage-based-routing: 공식 문서에서 프로덕션 비권장 — Redis 왕복으로 레이턴시 추가 → latency-based-routing: buffer로 실제 응답시간 기반 선택, 초기 cold start 주의 → weight 파라미터: 용량 차이 있는 배포에 트래픽 비율 지정 → 잘못된 전략명 입력 시 ValueError 즉시 발생 (런타임 방어)
LiteLLM Router가 존재하는 이유
프로덕션 LLM 시스템이 단일 배포에 의존하면 세 가지 문제가 동시에 발생합니다.
단일 엔드포인트의 한계:
1. Rate Limit (429)
Azure OpenAI 100K TPM 한도 → 트래픽 스파이크 시 전면 차단
2. 프로바이더 장애
특정 리전 다운 → 전체 서비스 영향
3. 비용 최적화 불가
항상 가장 비싼 모델만 사용
→ GPT-5.5가 필요 없는 요청도 GPT-5.5로 전송
LiteLLM Router는 이 세 가지를 한 번에 해결합니다. 같은 model_name으로 여러 배포를 등록하면 Router가 자동으로 트래픽을 분산합니다.
Router 내부 구조
Router 클래스가 어떻게 동작하는지 이해하면 전략 선택이 쉬워집니다.
litellm.Router 핵심 컴포넌트
┌─────────────────────────────────────────────────┐
│ Router │
│ │
│ _model_index: Dict │
│ {"claude-sonnet": [deploy_A, deploy_B, ...]} │ ← 모델명 → 배포 목록 매핑
│ │
│ CooldownCache │
│ {"deploy_A": cooldown_until_ts} │ ← 장애 배포 임시 제외
│ │
│ DualCache (in-memory + Redis) │
│ {"deploy_A_tpm": 45320, "deploy_B_rpm": 72} │ ← 사용량 추적
│ │
│ routing_strategy: "simple-shuffle" │ ← 선택 알고리즘
└─────────────────────────────────────────────────┘
요청이 들어왔을 때 흐름:
요청 도착
↓
_model_index에서 해당 model_name 배포 목록 조회
↓
CooldownCache 확인 → 쿨다운 중인 배포 제외
↓
나머지 배포 중 routing_strategy에 따라 선택
↓
선택된 배포로 API 호출
↓
실패 시: 재시도 → 폴백 (2편에서 상세 다룸)
Router 기본 설정
from litellm import Router
router = Router(
model_list=[
# 같은 model_name으로 여러 배포 등록 → 자동 로드밸런싱
{
"model_name": "claude-sonnet", # 코드에서 쓰는 이름
"litellm_params": {
"model": "claude-sonnet-4-6",
"api_key": "sk-ant-...",
},
"model_info": {"id": "claude-sonnet-primary"},
},
{
"model_name": "claude-sonnet", # 같은 이름 → 동일 그룹
"litellm_params": {
"model": "bedrock/anthropic.claude-sonnet-4-6-v1",
"aws_region_name": "us-east-1",
},
"model_info": {"id": "claude-sonnet-bedrock"},
},
{
"model_name": "claude-sonnet",
"litellm_params": {
"model": "bedrock/anthropic.claude-sonnet-4-6-v1",
"aws_region_name": "ap-northeast-1", # 다른 리전
},
"model_info": {"id": "claude-sonnet-bedrock-apne1"},
},
],
routing_strategy="simple-shuffle", # 전략 명시 (기본값과 동일)
num_retries=3,
retry_after=0,
allowed_fails=2,
cooldown_time=60, # 초 단위, 실패한 배포 제외 시간
)
# 사용: 일반 litellm.completion과 동일한 인터페이스
response = router.completion(
model="claude-sonnet", # 등록한 model_name
messages=[{"role": "user", "content": "안녕하세요"}],
)
model_name을 같게 설정하는 것이 핵심입니다. Router는 이 이름으로 배포 그룹을 형성하고 전략에 따라 내부에서 실제 배포를 선택합니다.
전략 1 — simple-shuffle (프로덕션 기본값)
원리: RPM/TPM이 설정돼 있으면 가중 랜덤 선택, 없으면 균등 랜덤 선택. 쿨다운 중인 배포는 제외 후 나머지에서 선택.
LiteLLM 공식 문서는 simple-shuffle을 프로덕션 기본값으로 명시적으로 권장하며, 최소 레이턴시 오버헤드로 최상의 성능을 제공한다고 설명합니다.
router = Router(
model_list=[
{
"model_name": "gpt-4o",
"litellm_params": {
"model": "azure/gpt-4o-east",
"api_base": "https://my-east.openai.azure.com",
"api_key": "...",
"rpm": 500, # 분당 요청 한도 → 가중치 계산에 사용
"tpm": 100000, # 분당 토큰 한도
},
},
{
"model_name": "gpt-4o",
"litellm_params": {
"model": "azure/gpt-4o-west",
"api_base": "https://my-west.openai.azure.com",
"api_key": "...",
"rpm": 1000, # 용량이 2배 → 2배 많은 트래픽 수신
"tpm": 200000,
},
},
],
routing_strategy="simple-shuffle",
)
RPM이 500:1000 비율이면 Router는 west 배포에 트래픽을 2배 더 보냅니다. 실패 시 동작:
실패한 배포 제외 → 나머지로 가중치 재정규화 → 재선택
→ 모든 배포 소진되거나 max_fallbacks 도달 시 에러
weight 파라미터로 RPM/TPM과 독립적으로 트래픽 비율을 직접 설정할 수도 있습니다.
{
"model_name": "gpt-4o",
"litellm_params": {
"model": "azure/gpt-4o-primary",
"api_base": "...",
"api_key": "...",
"weight": 3, # 전체 트래픽의 3/(3+1) = 75%
},
},
{
"model_name": "gpt-4o",
"litellm_params": {
"model": "openai/gpt-4o", # 백업으로 OpenAI 직접
"api_key": "...",
"weight": 1, # 전체 트래픽의 1/(3+1) = 25%
},
},
언제 쓸까: 거의 모든 프로덕션 상황. 다른 전략이 필요한 명확한 이유가 없다면 simple-shuffle이 기본값입니다.
전략 2 — least-busy (진행 중 요청 기반)
원리: 현재 처리 중인 요청(in-flight requests) 수가 가장 적은 배포를 선택합니다. LeastBusyLoggingHandler가 litellm.input_callbacks와 success/failure callbacks를 통해 각 배포의 진행 중 요청 수를 DualCache에 실시간 추적합니다.
router = Router(
model_list=[
{
"model_name": "claude-opus",
"litellm_params": {
"model": "claude-opus-4-7",
"api_key": "...",
},
"model_info": {"id": "opus-1"},
},
{
"model_name": "claude-opus",
"litellm_params": {
"model": "claude-opus-4-7",
"api_key": "...", # 다른 API 키 → 다른 rate limit 풀
},
"model_info": {"id": "opus-2"},
},
{
"model_name": "claude-opus",
"litellm_params": {
"model": "bedrock/anthropic.claude-opus-4-7",
},
"model_info": {"id": "opus-bedrock"},
},
],
routing_strategy="least-busy",
)
내부 동작:
요청 시작 → 선택된 배포의 request_count += 1 (캐시에 저장)
요청 완료 → request_count -= 1
다음 요청 → 가장 낮은 request_count를 가진 배포 선택
simple-shuffle vs least-busy:
simple-shuffle: 확률적 균등 분산 (RPM 기반 가중치)
→ 트래픽이 통계적으로 고르게 분산됨
→ 순간적인 쏠림 가능
least-busy: 실제 처리 중 부하 기반 선택
→ 긴 스트리밍 응답, 무거운 요청이 섞인 워크로드에 유리
→ 각 요청이 서로 다른 처리 시간을 가질 때 효과적
고동시성 워크로드에서 least-busy가 simple-shuffle보다 쏠림을 방지합니다. 단, 인메모리 카운터 동기화 오버헤드가 약간 있습니다.
전략 3 — usage-based-routing (공식 비권장)
원리: 분당 TPM 사용량이 가장 낮은 배포를 선택합니다. Redis에서 현재 분의 TPM/RPM 누적치를 redis.incr와 redis.mget으로 조회합니다.
LiteLLM 공식 문서는 usage-based routing을 프로덕션에서 권장하지 않는다고 명시합니다. Redis 작업으로 인한 레이턴시 추가가 고트래픽 시나리오에서 성능에 영향을 줍니다.
# ⚠️ 공식 문서에서 프로덕션 비권장
# Redis 왕복 레이턴시가 모든 요청에 추가됨
router = Router(
model_list=[...],
routing_strategy="usage-based-routing",
redis_host="your-redis-host",
redis_port=6379,
redis_password="...",
)
왜 비권장인가:
요청 처리 흐름 (usage-based-routing):
1. 요청 수신
2. Redis에서 모든 배포의 현재 분 TPM 조회 → +5~15ms
3. 가장 낮은 TPM 배포 선택
4. 실제 API 호출
5. 완료 후 Redis TPM 카운터 업데이트 → +5ms
추가 레이턴시: 매 요청마다 10~30ms
고트래픽(1000 RPS)에서: Redis가 병목으로 작동 가능
실제로 필요한 상황은 정확한 TPM 한도 준수가 비즈니스 요건인 경우입니다. 단순 부하 분산이 목적이라면 simple-shuffle이 훨씬 효율적입니다.
전략 4 — usage-based-routing-v2 (비동기 개선판)
원리: usage-based-routing의 비동기 구현입니다. redis.incr와 redis.mget을 비동기 호출로 처리해 이전 버전보다 레이턴시 오버헤드를 줄였습니다. 배포의 TPM/RPM 한도가 설정돼 있으면 초과한 배포를 필터링합니다.
router = Router(
model_list=[
{
"model_name": "gpt-4o",
"litellm_params": {
"model": "azure/gpt-4o",
"api_base": "https://...",
"api_key": "...",
"tpm": 100000, # 분당 10만 토큰 한도 명시
"rpm": 500,
},
},
{
"model_name": "gpt-4o",
"litellm_params": {
"model": "azure/gpt-4o-backup",
"api_base": "https://...",
"api_key": "...",
"tpm": 200000,
"rpm": 1000,
},
},
],
routing_strategy="usage-based-routing-v2",
redis_host="...",
)
# v2의 동작:
# TPM/RPM 한도가 있으면 초과한 배포를 먼저 제외
# 남은 배포 중 현재 사용량이 가장 낮은 것 선택
# 비동기 Redis 호출로 v1 대비 레이턴시 개선
RPM vs TPM 처리 차이:
RPM: 하드 한도 — Redis.incr로 정확히 추적, 한도 초과 즉시 차단
TPM: 소프트 한도 — LLM 응답 후 토큰 수 확인, 사전 추정 불가
→ TPM은 "다음 요청 전 체크" 방식으로 best-effort 시행
Azure의 경우 RPM = TPM / 6 공식이 성립합니다. Azure 배포 설정 시 이 비율을 참고해 한도를 설정합니다.
전략 5 — latency-based-routing (응답시간 기반)
원리: 각 배포의 평균 응답 시간을 추적하고, 가장 빠른 배포(또는 빠른 배포들 중 무작위)로 라우팅합니다. routing_strategy_args로 세밀하게 제어합니다.
router = Router(
model_list=[
{
"model_name": "llm-pool",
"litellm_params": {
"model": "claude-sonnet-4-6",
"api_key": "...",
},
"model_info": {"id": "anthropic-direct"},
},
{
"model_name": "llm-pool",
"litellm_params": {
"model": "bedrock/anthropic.claude-sonnet-4-6-v1",
"aws_region_name": "us-east-1",
},
"model_info": {"id": "bedrock-useast1"},
},
{
"model_name": "llm-pool",
"litellm_params": {
"model": "bedrock/anthropic.claude-sonnet-4-6-v1",
"aws_region_name": "ap-northeast-1",
},
"model_info": {"id": "bedrock-apne1"},
},
],
routing_strategy="latency-based-routing",
routing_strategy_args={
# 응답시간 평균 계산에 포함할 최근 시간 범위 (초)
"ttl": 60,
# 최저 응답시간 기준으로 이 버퍼(%) 이내 배포를 후보로 포함
# → 최저 응답시간이 500ms일 때 buffer=0.3이면
# 500ms × 1.3 = 650ms 이내 배포 모두 후보
# → 후보 중 랜덤 선택 (단일 배포 집중 방지)
"lowest_latency_buffer": 0.3,
},
)
Cold Start 문제와 해결:
라우터가 처음 시작되면 응답시간 데이터가 없습니다. 이때 latency-based-routing은 simple-shuffle처럼 동작합니다. 데이터가 누적될수록 실제 응답시간 기반 선택이 활성화됩니다.
# buffer=0 일 때 (항상 가장 빠른 배포만 선택)
# → 초기에 데이터 부족 시 특정 배포에 과부하 가능
# buffer=0.5 설정 권장: 상위 50% 버퍼 이내 배포를 후보로 포함
routing_strategy_args={
"ttl": 60,
"lowest_latency_buffer": 0.5, # 과부하 방지 버퍼
}
latency-based vs least-busy:
latency-based: "어느 배포가 과거에 빨랐나?" → 응답시간 이력 기반
least-busy: "지금 어느 배포가 한가한가?" → 현재 부하 기반
긴 스트리밍 응답이 많은 워크로드: least-busy 유리
응답 품질(속도)이 배포마다 크게 다른 경우: latency-based 유리
전략 6 — cost-based-routing (비용 기반)
원리: 등록된 배포 중 현재 토큰 단가가 가장 낮은 배포로 라우팅합니다. LiteLLM의 내장 비용 DB를 참조합니다. 동일 model_name 아래 서로 다른 모델을 등록할 때 특히 유용합니다.
router = Router(
model_list=[
{
"model_name": "fast-model",
"litellm_params": {
"model": "gpt-5.5", # 비쌈
"api_key": "...",
},
},
{
"model_name": "fast-model",
"litellm_params": {
"model": "claude-sonnet-4-6", # 중간
"api_key": "...",
},
},
{
"model_name": "fast-model",
"litellm_params": {
"model": "gemini-3.5-flash", # 저렴
"api_key": "...",
},
},
],
routing_strategy="cost-based-routing",
)
# → 동일 성능 대역에서 가장 저렴한 배포로 자동 라우팅
# → 비용 DB가 최신 상태인지 주기적 확인 권장
주의사항: cost-based-routing은 단가만 보고 품질 차이를 고려하지 않습니다. 서로 다른 모델을 같은 그룹에 넣으면 가장 저렴한 모델에 트래픽이 집중됩니다. 동일 모델의 여러 리전/배포를 비교하거나, 비용 최적화를 위한 배치 처리에 적합합니다.
전략 선택 가이드
ROUTING_STRATEGY_GUIDE = {
"simple-shuffle": {
"use_when": [
"거의 모든 프로덕션 상황 (기본값)",
"배포들이 동일하거나 유사한 용량",
"명확한 다른 요건이 없을 때",
],
"overhead": "최소 (레이턴시 추가 없음)",
"verdict": "✅ 기본값으로 유지",
},
"least-busy": {
"use_when": [
"요청마다 처리 시간이 크게 다를 때 (긴 스트리밍 혼재)",
"고동시성 워크로드에서 쏠림 방지",
],
"overhead": "낮음 (인메모리 카운터)",
"verdict": "✅ 스트리밍 헤비 워크로드에 권장",
},
"usage-based-routing": {
"use_when": [
"정확한 RPM/TPM 한도 준수 필요",
"⚠️ Redis 인프라 확보된 경우에만",
],
"overhead": "높음 (매 요청 Redis 왕복)",
"verdict": "❌ 공식 비권장, v2 사용 또는 simple-shuffle 대체",
},
"usage-based-routing-v2": {
"use_when": [
"정확한 TPM/RPM 한도 준수 필요",
"비동기 처리 가능한 환경",
],
"overhead": "중간 (비동기 Redis)",
"verdict": "⚠️ usage-based 써야 한다면 v2 선택",
},
"latency-based-routing": {
"use_when": [
"배포마다 응답 속도가 크게 다를 때",
"사용자 체감 레이턴시 최소화 목표",
"멀티 리전 배포 (리전별 RTT 차이 큼)",
],
"overhead": "낮음 (인메모리 응답시간 캐시)",
"verdict": "✅ 멀티 리전 + 레이턴시 민감 워크로드",
},
"cost-based-routing": {
"use_when": [
"동일 성능 대역 모델 간 비용 최적화",
"배치 처리 (품질보다 비용 우선)",
],
"overhead": "낮음",
"verdict": "✅ 비용 최적화 배치 작업",
},
}
잘못된 전략명 입력 방어
Router 클래스는 초기화 시 제공된 전략이 유효한지 검증합니다. "simple" 대신 "simple-shuffle"처럼 정확한 문자열이 필요하며, 잘못된 문자열은 ValueError를 즉시 발생시켜 런타임 실패를 방지합니다.
# ❌ 이렇게 쓰면 ValueError 즉시 발생
router = Router(
model_list=[...],
routing_strategy="simple", # "simple-shuffle" 이어야 함
)
# ValueError: Invalid routing strategy. Accepted values: ...
# ✅ 정확한 전략명 리스트
VALID_STRATEGIES = [
"simple-shuffle",
"least-busy",
"usage-based-routing",
"usage-based-routing-v2",
"latency-based-routing",
"cost-based-routing",
]
완성 예시 — 3티어 라우팅 설정
from litellm import Router
import os
router = Router(
model_list=[
# ── 고성능 티어 ──────────────────────────────
{
"model_name": "tier-high",
"litellm_params": {
"model": "claude-opus-4-7",
"api_key": os.environ["ANTHROPIC_API_KEY"],
"rpm": 200,
"tpm": 40000,
},
"model_info": {"id": "opus-primary"},
},
# ── 밸런스 티어 (기본값) ───────────────────────
{
"model_name": "tier-default",
"litellm_params": {
"model": "claude-sonnet-4-6",
"api_key": os.environ["ANTHROPIC_API_KEY"],
"rpm": 1000,
"tpm": 200000,
},
"model_info": {"id": "sonnet-anthropic"},
},
{
"model_name": "tier-default",
"litellm_params": {
"model": "bedrock/anthropic.claude-sonnet-4-6-v1",
"aws_region_name": "us-east-1",
"rpm": 1000,
"tpm": 200000,
},
"model_info": {"id": "sonnet-bedrock-east"},
},
{
"model_name": "tier-default",
"litellm_params": {
"model": "bedrock/anthropic.claude-sonnet-4-6-v1",
"aws_region_name": "eu-west-1",
"rpm": 800,
"tpm": 160000,
},
"model_info": {"id": "sonnet-bedrock-eu"},
},
# ── 경량 티어 ────────────────────────────────
{
"model_name": "tier-fast",
"litellm_params": {
"model": "claude-haiku-4-5-20251001",
"api_key": os.environ["ANTHROPIC_API_KEY"],
"rpm": 5000,
"tpm": 1000000,
},
"model_info": {"id": "haiku-primary"},
},
],
# 전략: 프로덕션 기본값
routing_strategy="simple-shuffle",
# 재시도: 실패 시 동일 그룹 내 다른 배포로 3회 재시도
num_retries=3,
retry_after=0,
# 쿨다운: 2회 실패한 배포는 60초 제외
allowed_fails=2,
cooldown_time=60,
# 타임아웃
timeout=30,
)
# 사용
response = await router.acompletion(
model="tier-default",
messages=[{"role": "user", "content": "요청 내용"}],
)
✅ 결론
전략 오버헤드 권장 상황
| simple-shuffle | ✅ 최소 | 거의 모든 프로덕션 → 기본값 유지 |
| least-busy | ✅ 낮음 | 긴 스트리밍 혼재, 고동시성 |
| latency-based-routing | ✅ 낮음 | 멀티 리전, 레이턴시 민감 |
| cost-based-routing | ✅ 낮음 | 배치 처리, 비용 최적화 |
| usage-based-routing-v2 | ⚠️ 중간 | 정확한 TPM/RPM 한도 준수 필요 시 |
| usage-based-routing | ❌ 높음 | 공식 비권장 — v2 또는 simple-shuffle로 대체 |
핵심은 단순합니다. 기본값인 simple-shuffle에서 시작하고, 구체적인 요건이 생겼을 때만 다른 전략으로 이동하세요. 이 원칙만 지켜도 대부분의 프로덕션 문제를 예방할 수 있습니다.
LiteLLM 시리즈 완결
- ✅ Router 구조와 라우팅 전략 6가지 https://cell-devlog.tistory.com/273
- ✅ 폴백 전략과 장애 대응 https://cell-devlog.tistory.com/274
- ✅ 프로덕션 배포: Redis + Proxy 서버 https://cell-devlog.tistory.com/275
- ✅ 고급 라우팅과 실전 아키텍처 https://cell-devlog.tistory.com/276
'AI 개발' 카테고리의 다른 글
| LiteLLM Load Balancing 3편 — 프로덕션 배포: Redis 연동, Proxy 서버, 예산 관리 (0) | 2026.05.26 |
|---|---|
| LiteLLM Load Balancing 2편 — 폴백 전략과 장애 대응 완전 가이드 (0) | 2026.05.26 |
| 회사에서 지금 몇 개의 AI 모델이 돌고 있나요 — AI-BOM이 뜨는 이유 (0) | 2026.05.26 |
| 코드베이스에 모델 ID 박아놨습니까 — 6월 15일 API retirement 완전 대응 가이드 (0) | 2026.05.26 |
| WebMCP 3편: Agentic SEO — 에이전트 시대에 웹사이트는 무엇이 달라져야 하나 (0) | 2026.05.26 |