본문 바로가기

AI 개발

LiteLLM Load Balancing 완전 정복 1편 — Router 구조와 라우팅 전략 6가지

반응형

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 시리즈 완결

  1. ✅ Router 구조와 라우팅 전략 6가지  https://cell-devlog.tistory.com/273
  2. ✅ 폴백 전략과 장애 대응 https://cell-devlog.tistory.com/274
  3. ✅ 프로덕션 배포: Redis + Proxy 서버 https://cell-devlog.tistory.com/275
  4. ✅ 고급 라우팅과 실전 아키텍처 https://cell-devlog.tistory.com/276
반응형