서버 튜닝
서버 튜닝 (Server Tuning)
이 페이지는 기본값으로 충분하지 않을 때만 참고하세요. 대부분의 배포는 프로덕션 체크리스트에 설명된 대로 pod당 Uvicorn 워커 하나를 실행하고 수평 확장해야 해요. 아래 옵션들이 의미 있는 경우는 하나의 컨테이너에 여러 워커를 넣거나, 프록시에서 TLS를 종료하거나, HTTP/2를 제공하거나, 호스트에 config 파일을 마운트할 수 없을 때예요. 모든 플래그는 CLI 참조를 참고하세요.
Uvicorn vs Gunicorn
LiteLLM Proxy는 기본적으로 Uvicorn에서 실행돼요. --run_gunicorn을 넘기면 Uvicorn 워커 프로세스(uvicorn.workers.UvicornWorker)를 관리하는 프로세스 매니저로 Gunicorn이 시작돼요. 두 경우 모두 애플리케이션 코드는 여전히 Uvicorn에서 실행돼요. 차이는 어떤 프로세스가 워커를 관리하고 재활용(recycle)하느냐예요.
| Uvicorn (기본) | Gunicorn (--run_gunicorn) |
언제 사용할까 | |
|---|---|---|---|
| 언제 사용할까 | 거의 모든 배포, 특히 pod당 워커 하나인 Kubernetes에 권장 | 단일 컨테이너에서 여러 워커를 실행할 때, 성숙한 프로세스 매니저가 워커를 관리·재활용하게 하려는 경우 | |
| 워커 재활용 | Uvicorn의 limit_max_requests |
Gunicorn이 수년간 제공해 온 검증된 메커니즘인 max_requests |
|
| 프로세스 감독 | Uvicorn의 내장 멀티프로세스 매니저 | Gunicorn의 arbiter. 워커가 종료될 때 하나씩 재시작 |
권장 사항
Kubernetes에서는 pod당 Uvicorn 워커 하나를 실행하고, 수직 확장(워커 수 늘리기)보다 수평 확장(pod 늘리기)을 하세요. pod당 단일 프로세스는 부하 아래에서 레이턴시를 예측 가능하게 유지하고, Horizontal Pod Autoscaler가 프로덕션 체크리스트의 임계값을 정확히 사용하게 하며, Kubernetes가 pod를 하나씩 드레인하므로 롤링 재시작을 무중단으로 만들어요. 반드시 하나의 컨테이너에 여러 워커를 넣어야 할 때만 Gunicorn을 사용하세요.
워커 재활용
지속 부하 아래에서 점진적 메모리 증가가 보인다면, 고정된 요청 수 이후 각 워커를 재활용해 메모리 사용량을 제한하세요. --max_requests_before_restart는 기본 서버에서 Uvicorn의 limit_max_requests로, --run_gunicorn 아래에서 Gunicorn의 max_requests로 매핑돼요. CLI 플래그나 환경 변수로 설정하세요:
# CLI
CMD ["--port", "4000", "--config", "./proxy_server_config.yaml", "--num_workers", "1", "--max_requests_before_restart", "10000"]
# or ENV (for deployment manifests / containers)
export MAX_REQUESTS_BEFORE_RESTART=10000
tip
하나의 컨테이너에서 여러 워커를 실행하고 --max_requests_before_restart에 의존한다면 --run_gunicorn을 선호하세요. Gunicorn의 max_requests 재활용은 Uvicorn보다 성숙하고, arbiter가 워커를 하나씩 재시작하므로 워커가 교체되는 동안에도 pod가 트래픽을 계속 서빙할 수 있어요.
# Multiple workers in one container, with Gunicorn-managed recycling
CMD ["--port", "4000", "--config", "./proxy_server_config.yaml", "--num_workers", "4", "--run_gunicorn", "--max_requests_before_restart", "10000"]
여러 워커가 함께 부팅하고 비슷한 양의 트래픽을 서빙하면 거의 동시에 요청 임계값에 도달해 나란히 재활용되며, 한 번에 용량 일부가 빠져요. --max_requests_before_restart_jitter를 추가해 각 워커의 임계값에 [0, jitter] 범위의 랜덤 값을 오프셋하면 재시작이 동기화되지 않고 분산돼요. 이는 Uvicorn의 limit_max_requests_jitter(uvicorn>=0.41.0 필요)와 Gunicorn의 max_requests_jitter에 매핑되고, --max_requests_before_restart 없이는 효과가 없어요.
# Stagger recycling so workers don't all restart at once
CMD ["--port", "4000", "--config", "./proxy_server_config.yaml", "--num_workers", "4", "--run_gunicorn", "--max_requests_before_restart", "10000", "--max_requests_before_restart_jitter", "1000"]
재시작을 무중단으로 유지
재시작이 "무중단(hitless)"이라는 것은 진행 중인 요청이 프로세스가 종료되기 전에 끝나서 어떤 클라이언트도 연결 끊김을 보지 못한다는 뜻이에요. 프로덕션에서 두 가지 경우가 중요해요:
워커 재활용(--max_requests_before_restart에서). 두 서버 모두 재활용되는 워커에서 새 연결 수락을 중단하고, 종료 전에 미해결 요청을 드레인한 뒤 교체 워커가 시작돼요. Gunicorn은 추가로 SIGTERM 시 graceful_timeout(기본 30초)까지 진행 중 요청을 보장해요. pod당 워커 하나면 재활용이 그 pod의 용량을 잠시 줄이므로, 로드 밸런서가 우회할 수 있도록 수평 확장을 권장해요.
롤링 배포와 pod 재시작(Kubernetes). 서버에만 의존하지 말고 오케스트레이션 계층에서 재시작을 무중단으로 만드세요:
- 새 pod가 이전 pod보다 먼저 Ready가 되도록
RollingUpdate전략(Deployment 기본값)을 사용하세요. /health/readiness에 readiness 프로브를 유지해서 Kubernetes가 서빙할 수 있는 pod에만 트래픽을 보내게 하고, 종료가 시작되면 즉시 라우팅을 중단하게 하세요.terminationGracePeriodSeconds를 예상 최장 요청보다 충분히 크게 설정하세요(LiteLLM 요청 타임아웃 기본 600초; 권장 구성 참고). 종료 시 Kubernetes는SIGTERM을 보내고, Uvicorn과 Gunicorn은 모두 우아하게 종료하며 진행 중 요청을 드레인한 뒤 나가요.- 선택적으로 작은
preStop훅(예:sleep 5)을 추가해 서버가 종료를 시작하기 전에 로드 밸런서가 pod를 등록 해제할 시간을 주면, 종료 중인 pod에 트래픽이 여전히 도달하는 짧은 창을 없앨 수 있어요.
무중단 롤링 재시작을 위한 Kubernetes Deployment 스니펫
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0 # never drop below desired replica count
maxSurge: 1 # add one new pod at a time
template:
spec:
terminationGracePeriodSeconds: 620 # > your longest request (request_timeout: 600)
containers:
- name: litellm
readinessProbe:
httpGet:
path: /health/readiness
port: 4000
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5"]
프록시에서 TLS
(로드 밸런서가 아닌) 프록시 자체가 TLS를 종료하는 경우 키와 인증서 경로를 전달하세요:
docker run docker.litellm.ai/berriai/litellm:latest \
--ssl_keyfile_path ssl_test/keyfile.key \
--ssl_certfile_path ssl_test/certfile.crt
Hypercorn으로 HTTP/2
HTTP/2를 서빙하려면 hypercorn이 설치된 이미지를 만들고 --run_hypercorn을 전달하세요:
FROM docker.litellm.ai/berriai/litellm:latest
WORKDIR /app
COPY config.yaml .
RUN chmod +x ./docker/entrypoint.sh
EXPOSE 4000/tcp
RUN uv add hypercorn
CMD ["--port", "4000", "--config", "config.yaml"]
docker run \
-v $(pwd)/proxy_config.yaml:/app/config.yaml \
-p 4000:4000 \
-e DATABASE_URL=postgresql://<user>:***@<host>:<port>/<dbname> \
-e LITELLM_MASTER_KEY="sk-<paste-a-long-random-key>" \
your_custom_docker_image \
--config /app/config.yaml \
--run_hypercorn
제공자로의 아웃바운드 HTTP/2
v1.103.0부터 사용 가능해요.
위 서버 플래그들은 클라이언트에서 LiteLLM까지의 구간에만 영향을 줘요. LiteLLM에서 LLM 제공자로의 호출은 기본 aiohttp transport에 HTTP/2 클라이언트가 없어서 기본적으로 HTTP/1.1을 사용해요. TLS 위에서 LiteLLM이 제공자와 HTTP/2를 협상하게 하려면 litellm_settings 아래 http2: true(또는 LITELLM_HTTP2 환경 변수)를 설정하세요. ALPN으로 h2를 제공하지 않는 업스트림은 자동으로 HTTP/1.1로 폴백하고, 일반 http:// 업스트림은 HTTP/1.1을 유지해요.
litellm_settings:
http2: true
이 설정을 켜면 제공자 트래픽이 aiohttp 대신 httpx를 통해 흘러요. httpx는 더 높은 HTTP/1.1 처리량 때문에 기본값으로 선택됐어요. 플래그를 켠 채 로드 테스트를 먼저 하고, 플릿 전체에 적용하세요. litellm.client_session이나 litellm.aclient_session으로 직접 넘긴 클라이언트는 있는 그대로 사용되며 HTTP/2로 바뀌지 않아요. aiohttp_openai/ 제공자 배포는 항상 aiohttp를 사용하고 HTTP/1.1을 유지해요. 그런 요청에 플래그가 켜져 있으면 LiteLLM이 경고를 로그로 남겨요.
Granian ASGI 서버 [Beta]
Beta 기능이에요.
--run_granian은 베타예요. Uvicorn이 여전히 기본 서버예요. 더 높은 게이트웨이 처리량이 필요하거나 uvicorn에서 부하 아래 불안정이 보일 때 Granian을 시도하고, 이슈는 GitHub에 보고하세요.
Granian은 Rust 기반 ASGI 서버예요. LiteLLM 벤치마크에서 같은 워커 수로 uvicorn보다 RPS 10~20 개선, 지속 부하 아래 더 안정적인 레이턴시, 더 낮은 오류율을 보여줬어요(PR #26027 참고). --num_workers로 처리량을 확장하세요.
docker run docker.litellm.ai/berriai/litellm:latest \
--config /app/config.yaml \
--port 4000 \
--run_granian \
--num_workers 4
Granian으로 TLS를 켤 때는 --ssl_certfile_path와 --ssl_keyfile_path가 모두 필요해요. Granian에서 지원되지 않는 것: --max_requests_before_restart(요청별 워커 재활용은 Gunicorn 사용)와 --ciphers(Hypercorn 전용). CLI 서버 백엔드 옵션을 참고하세요.
워커별 접수 제어 (admission control)
이벤트 루프가 포화된 워커는 계속 연결을 수락해요. 그래서 부하 급증 동안 호출자는 과부하 신호 없이 몇 초를 기다리게 되고, (같은 루프에서 실행되는) liveness 프로브가 충분히 느려져서 Kubernetes가 pod를 재시작하고 부하를 나머지 복제본으로 밀어 넣어요. 접수 제어는 각 워커 프로세스가 받는 작업량에 하드 캡을 두고, 초과분을 클라이언트가 재시도할 수 있는 명시적이고 빠른 503으로 바꿔요.
general_settings:
max_in_flight_requests_per_worker: 64 # requests being processed at once, per worker process
max_queued_requests_per_worker: 64 # requests waiting for a slot; defaults to the in-flight cap
admission_queue_timeout_seconds: 1.0 # a queued request is rejected after waiting this long
max_in_flight_requests_per_worker가 설정될 때까지 이 기능은 꺼져 있어요. 요청이 도착했을 때 워커에 빈 슬롯이 있으면 즉시 실행돼요. 그렇지 않으면 슬롯이 비거나 타임아웃이 지날 때까지 큐에서 기다려요. 큐가 이미 가득 찼거나 대기가 타임아웃되면 클라이언트는 다음을 받아요:
HTTP/1.1 503 Service Unavailable
retry-after: 1
{"error":{"message":"Worker at capacity: 64 in-flight, 64 queued requests. Retry later.","type":"overloaded_error","code":"503"}}
슬롯은 전체 응답 동안 잡히므로, 스트리밍 완성은 마지막 청크가 보내질 때까지 진행 중 요청 하나로 집계되고, 클라이언트 연결 해제(큐에 있든 진행 중이든)는 즉시 슬롯을 해제해요. 프로브와 메트릭 경로(/health/liveliness, /health/liveness, /health/readiness, /health/readiness/details, /health/backlog, /health/drain, /metrics)는 게이트를 우회하므로, 과부하 워커도 멈춘 프로세스와 달리 liveness 프로브에 빨리 응답해요. 거부는 인증 전에 일어나므로 지출 로그에서 키에 귀속되지 않아요. 한도는 첫 요청에서 읽히므로 변경하려면 재시작이 필요해요.
캡은 워커 프로세스별이고 uvicorn과 Granian에서 동일하게 동작해요. --num_workers 4와 max_in_flight_requests_per_worker: 64로 시작한 pod는 최대 256개의 동시 요청을 접수하고, N개 복제본 배포는 그 N배를 접수하므로, 배포 총량이 아니라 측정한 워커별 처리량으로 크기를 정하세요. HPA와 잘 어울려요. 이미 포화된 복제본은 레이턴시를 쌓지 않고 503으로 부하를 떨어뜨리고, 새 복제본이 올라오는 동안요. 이는 Redis를 통해 조정되는 배포 전체 한도인 global_max_parallel_requests를 보완해요. Redis 의존 없이 단일 이벤트 루프가 물에 잠기지 않게 하려면 워커별 한도를, 제공자에 걸린 총 부하를 제한하려면 전역 한도를 사용하세요.
/health/backlog(필드 in_flight_requests, admitted_requests, queued_requests, rejected_requests)나 Prometheus 메트릭 litellm_admission_admitted_requests, litellm_admission_queued_requests, litellm_admission_rejected_requests_total{reason="queue_full"|"queue_timeout"}로 모니터링하세요. Pod health 메트릭 참고. queue_timeout 거부가 꾸준히 나오면 워커가 포화된 것이므로 복제본이 더 필요하고, queue_full 거부는 큐가 감당할 수 있는 것보다 더 빨리 급증이 도착한다는 뜻이므로 큐 크기를 늘리거나 용량을 추가하세요.
Keepalive 타임아웃
기본 5초예요. 요청 사이에 연결은 이 기간 내에 새 데이터를 받아야 하고, 그렇지 않으면 연결이 끊겨요.
docker run docker.litellm.ai/berriai/litellm:latest \
--keepalive_timeout 75
또는 env var로 KEEPALIVE_TIMEOUT=75를 설정하세요.
S3 또는 GCS에서 config.yaml 로드
배포 서비스(AWS Fargate, Railway 등)에서 config 파일을 마운트할 수 없을 때 사용하세요. LiteLLM은 시작 시 버킷에서 config.yaml을 읽어요.
- GCS Bucket
- s3
docker run --name litellm-proxy \
-e DATABASE_URL=<database_url> \
-e LITELLM_CONFIG_BUCKET_TYPE="gcs" \
-e LITELLM_CONFIG_BUCKET_NAME="litellm-proxy" \
-e LITELLM_CONFIG_BUCKET_OBJECT_KEY="proxy_config.yaml" \
-p 4000:4000 \
docker.litellm.ai/berriai/litellm:latest
docker run --name litellm-proxy \
-e DATABASE_URL=<database_url> \
-e LITELLM_CONFIG_BUCKET_NAME="litellm-proxy" \
-e LITELLM_CONFIG_BUCKET_OBJECT_KEY="litellm_proxy_config.yaml" \
-p 4000:4000 \
docker.litellm.ai/berriai/litellm:latest
실시간 모델 가격 가져오기 비활성화
콜드 스타트가 길거나 네트워크 이그레스 제한이 있다면 LITELLM_LOCAL_MODEL_COST_MAP="True"를 설정해서 시작 시 가격을 가져오는 대신 번들된 모델 가격 파일을 사용하세요.
출처: 문서
더 알아보기 (Learn more)
- 프로덕션 체크리스트와 워커 크기 조정 가이드 읽어보기
- Prometheus Pod health 메트릭과 admission 제어 모니터링 방법 확인하기