- Published on
KServe로 vLLM 서빙 503·Cold Start 줄이기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스에 가까운 KServe 배포 모델은 운영 편의성이 큰 대신, LLM처럼 초기화 비용이 큰 워크로드에서는 503 과 Cold Start가 쉽게 발생합니다. 특히 vLLM은 엔진 초기화, 모델 가중치 로딩, KV cache 준비, GPU 메모리 워밍업까지 여러 단계가 겹치기 때문에 “컨테이너는 떴는데 요청은 실패” 같은 구간이 길어지기 쉽습니다.
이 글에서는 KServe로 vLLM을 서빙할 때 가장 흔한 503 시나리오를 원인별로 나누고, KServe 및 Knative 네트워킹 특성까지 고려해 재현 가능한 해결책을 정리합니다. 롤링 배포나 카나리까지 함께 운영한다면 KServe로 LLM 추론 서버 롤링배포·카나리 실전 글도 같이 보시면 전체 운영 흐름이 깔끔하게 연결됩니다.
503이 나는 지점부터 정확히 쪼개기
KServe 환경에서 503 은 대체로 아래 중 하나입니다.
- Activator 또는 queue-proxy가 백엔드를 못 찾는 경우
- 스케일 투 제로 상태에서 첫 요청이 들어왔는데, 아직 Pod가 Ready가 아니어서 라우팅 대상이 없음
- Pod는 Ready인데 vLLM 앱이 아직 준비가 안 된 경우
- readinessProbe가 너무 느슨하거나, 반대로 너무 엄격해서 계속 NotReady
- 요청은 들어왔지만 vLLM이 타임아웃으로 끊기는 경우
- Ingress, gateway, service mesh, client 타임아웃이 모델 로딩 시간보다 짧음
- 스토리지에서 모델 다운로드가 지연되거나 실패하는 경우
- S3, PVC, Hugging Face 다운로드가 느리거나 권한 문제로 재시도하다가 지연
즉, “503을 없애자”는 목표는 보통 두 가지로 바뀝니다.
- 첫 요청이 와도 즉시 처리 가능한 최소 인스턴스 를 유지한다
- 첫 요청이 초기화를 트리거하더라도 요청이 끊기지 않도록 준비 상태와 타임아웃을 맞춘다
KServe에서 vLLM Cold Start가 유독 긴 이유
vLLM은 일반적인 FastAPI 앱보다 초기화 비용이 큽니다.
- 모델 가중치 로딩: 수 GB에서 수십 GB
- GPU 메모리 할당과 커널 워밍업
- 토크나이저 로딩
- 텐서 병렬, paged attention 준비
게다가 KServe는 Knative 기반일 때 queue-proxy 를 통해 트래픽이 들어오므로, readiness가 늦으면 곧바로 “라우팅 불가” 상황이 됩니다. 이때 클라이언트는 503 을 받거나, Ingress 타임아웃으로 504 를 받습니다.
해결 전략 1: scale-to-zero를 끄거나 최소 1 유지
가장 확실한 방법은 “Cold Start를 없애는 것” 입니다.
최소 레플리카 유지
Knative 기반 KServe에서는 주로 annotation으로 최소 스케일을 고정합니다.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: vllm-llm
spec:
predictor:
annotations:
autoscaling.knative.dev/minScale: "1"
autoscaling.knative.dev/maxScale: "3"
autoscaling.knative.dev/target: "1"
containers:
- name: vllm
image: vllm/vllm-openai:latest
args:
- "--model"
- "/models"
- "--host"
- "0.0.0.0"
- "--port"
- "8000"
ports:
- containerPort: 8000
minScale: "1"로 첫 요청503의 대부분이 사라집니다.- 비용이 부담되면
minScale을 특정 시간대에만 올리는 방식도 고려할 수 있습니다.
동시성 목표 조정
LLM은 단순 QPS보다 “동시 요청” 에 민감합니다. target 을 너무 높게 잡으면 한 Pod에 과부하가 걸려 응답 지연이 늘고, 지연이 늘면 타임아웃으로 503 또는 504 로 관측될 수 있습니다.
- 작은 모델:
target을 2~5 - 큰 모델 또는 긴 컨텍스트:
target을 1~2
해결 전략 2: readinessProbe를 vLLM 준비 상태에 맞추기
Pod가 Ready로 표시되는 순간부터 KServe는 트래픽을 전달합니다. 그런데 vLLM은 프로세스가 떠도 모델이 아직 로딩 중일 수 있습니다. 이 간극을 readinessProbe로 메워야 합니다.
OpenAI 호환 엔드포인트 기반 readiness
vLLM OpenAI 서버는 보통 GET /v1/models 가 비교적 안정적입니다. 모델 로딩이 끝나야 정상 응답이 나오도록 구성하는 것이 핵심입니다.
readinessProbe:
httpGet:
path: /v1/models
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 60
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 6
startupProbe:
httpGet:
path: /v1/models
port: 8000
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 120
startupProbe를 쓰면 초기 로딩이 길어도 kubelet이 “죽었다”고 판단하지 않습니다.readinessProbe를 너무 빡세게 잡으면 계속 NotReady가 되어 오히려503이 늘 수 있으니, 로딩 최악 시간을 기준으로failureThreshold를 크게 잡습니다.
만약 Pod가 1분마다 재시작된다면 probe 설정이 원인일 가능성이 큽니다. 이 케이스는 EKS Pod 1분마다 재시작? livenessProbe 실패 해결 글의 체크리스트가 그대로 적용됩니다.
해결 전략 3: 타임아웃을 “모델 로딩 시간” 에 맞추기
Cold Start를 완전히 없애지 못한다면, 첫 요청이 끊기지 않게 타임아웃을 늘려야 합니다.
체크 포인트는 3군데입니다.
- 클라이언트 타임아웃
- Ingress 또는 Gateway 타임아웃
- Knative queue-proxy 관련 timeout
환경에 따라 다르지만, ALB Ingress를 쓰는 경우 기본 idle timeout이 60초라서 LLM 초기화가 60초를 넘으면 첫 요청이 끊길 수 있습니다. 이때는 Ingress 타임아웃을 늘려야 합니다. 관련 맥락은 EKS ALB Ingress 504(60초) idle_timeout 해결 글이 도움이 됩니다.
추가로, 스트리밍 응답을 쓰는 경우에도 중간에 데이터가 안 나오면 idle timeout으로 끊길 수 있으니 “첫 토큰까지 시간” 을 기준으로 설계해야 합니다.
해결 전략 4: 모델 다운로드 병목 제거
Cold Start의 상당 부분은 “모델을 어디서 어떻게 가져오느냐” 에서 결정됩니다.
권장 우선순위
- 노드 로컬 캐시 또는 이미지에 포함
- PVC에 미리 적재
- 오브젝트 스토리지에서 initContainer로 프리페치
- 앱 컨테이너가 부팅 후 다운로드 (가장 느리고 실패에 취약)
initContainer로 S3에서 모델 프리페치
initContainers:
- name: model-prefetch
image: amazon/aws-cli:2.15.0
command:
- sh
- -c
- |
aws s3 sync s3://my-bucket/models/llm /models --only-show-errors
volumeMounts:
- name: model-volume
mountPath: /models
containers:
- name: vllm
image: vllm/vllm-openai:latest
args:
- "--model"
- "/models"
volumeMounts:
- name: model-volume
mountPath: /models
volumes:
- name: model-volume
persistentVolumeClaim:
claimName: llm-model-pvc
여기서 S3 접근이 403 이나 AccessDenied 로 막히면 initContainer가 무한 재시도하며 전체가 늦어집니다. EKS에서 IRSA를 쓰는 경우 EKS IRSA 설정했는데 AccessDenied 뜰 때 점검 체크리스트대로 신뢰 정책과 서비스어카운트 연결을 먼저 확인하세요.
해결 전략 5: 워밍업 트래픽을 “의도적으로” 넣기
minScale 을 0으로 유지해야 하는 비용 제약이 있다면, 워밍업을 자동화해서 “사용자가 첫 요청을 맞는” 상황을 줄일 수 있습니다.
CronJob으로 주기적 워밍업
아래는 OpenAI 호환 chat/completions 로 짧은 요청을 보내는 예시입니다.
apiVersion: batch/v1
kind: CronJob
metadata:
name: vllm-warmup
spec:
schedule: "*/5 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: curl
image: curlimages/curl:8.5.0
args:
- -sS
- -X
- POST
- http://vllm-llm.default.example.com/v1/chat/completions
- -H
- "Content-Type: application/json"
- -d
- |
{"model":"my-llm","messages":[{"role":"user","content":"ping"}],"max_tokens":1,"temperature":0}
포인트는 “긴 생성” 이 아니라 “엔진이 살아있음을 유지할 정도의 최소 토큰” 입니다.
운영에서 자주 놓치는 디버깅 포인트
1) 503 이 어디서 나오는지 로그로 분리
- Ingress 컨트롤러 로그에서
503인지 - Knative activator 또는 queue-proxy에서
503인지 - vLLM 애플리케이션이
5xx를 내는지
원인별로 해결책이 완전히 달라집니다.
2) “Ready인데 느린” 상태는 리소스 부족일 가능성
모델 로딩이 지나치게 오래 걸리면 다음을 의심하세요.
- GPU 메모리 부족으로 스왑성 동작 또는 재시도
- PVC IOPS 부족
- 오브젝트 스토리지 다운로드 대역폭 제한
- 노드가 과밀해서 이미지 pull이 느림
3) 배포 중 503은 카나리/롤링 설정과도 연관
트래픽 분산 중 새 Revision이 아직 준비되지 않았는데 라우팅이 일부 넘어가면 503 이 튈 수 있습니다. 이때는 단계적 전환, 준비 완료 후 전환, 최소 레플리카 유지가 핵심입니다. 해당 주제는 위에서 링크한 롤링/카나리 글에서 더 깊게 다룹니다.
추천 조합: 비용과 안정성의 균형안
- 비용보다 안정성이 중요:
autoscaling.knative.dev/minScale: "1"고정 +startupProbe넉넉히 - 비용이 중요:
minScale: "0"유지 + 워밍업 CronJob + Ingress 타임아웃 상향 + 모델 프리페치 - 트래픽 스파이크가 잦음:
maxScale을 충분히 크게,target을 낮게, 모델은 PVC 또는 로컬 캐시
마무리
KServe에서 vLLM 503 과 Cold Start는 “한 가지 설정”으로 해결되지 않습니다. 스케일 정책, probe 설계, 네트워크 타임아웃, 모델 스토리지 경로가 서로 맞물려서 발생합니다.
실전에서 가장 효과가 큰 순서대로 정리하면 다음과 같습니다.
minScale로 Cold Start 자체를 제거하거나 최소화startupProbe와 readinessProbe로 “트래픽 투입 시점” 을 정확히 제어- Ingress 및 클라이언트 타임아웃을 초기화 시간에 맞춰 상향
- 모델 다운로드를 initContainer 또는 PVC로 앞당겨 병목 제거
- 비용 제약이 있으면 워밍업 트래픽으로 체감 Cold Start를 줄이기
이 5가지만 제대로 맞춰도, 운영 중 관측되는 503 의 대부분은 안정적으로 사라지거나 의미 있는 수준으로 감소합니다.