- Published on
KServe vLLM 배포 503·HPA 미작동 원인 7가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
KServe로 vLLM을 배포하면 겉보기엔 단순합니다. InferenceService 하나 만들고, 트래픽이 오면 알아서 뜨고, HPA가 부하에 맞춰 늘어나길 기대하죠. 그런데 실전에서는 503이 반복되거나, 요청은 늘었는데도 HPA가 0에서 꿈쩍도 안 하는 경우가 꽤 흔합니다.
이 글은 KServe + vLLM 조합에서 특히 자주 발생하는 장애 패턴을 503 원인과 HPA 미작동 원인으로 나눠, 재현 가능한 관점에서 7가지로 정리합니다. 각 항목마다 확인 명령과 수정 포인트를 함께 제공합니다.
참고로, 운영에서 “뭔가 돌아야 하는데 안 돈다” 류의 문제는 관측 지점이 중요합니다. 쿠버네티스도 마찬가지라서, 접근을 로그·환경·권한으로 쪼개어 점검하는 습관이 큰 도움이 됩니다. 리눅스에서 비슷한 유형의 점검 루틴은 리눅스 cron 미실행? PATH·메일로그·권한 점검 글의 체크리스트와 사고방식이 꽤 닮아 있습니다.
진단을 시작하기 전에: 최소 관측 포인트
아래 네 가지는 503과 HPA 이슈를 분리하는 데 필수입니다.
# 1) InferenceService 상태
kubectl get inferenceservice -A
kubectl describe inferenceservice -n <ns> <name>
# 2) 실제 라우팅(istio/knative)
kubectl get ksvc -n <ns>
kubectl get route -n <ns>
kubectl get virtualservice -n <ns>
# 3) 엔드포인트가 있는지
kubectl get endpoints -n <ns>
# 4) HPA/메트릭 파이프라인
kubectl get hpa -A
kubectl describe hpa -n <ns> <hpa-name>
kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes" | head
여기서부터는 “503이 어디서 나는지”와 “HPA가 어떤 메트릭을 못 읽는지”를 분리해서 들어가면 됩니다.
원인 1) KServe 트래픽 라우팅 계층 불일치(istio vs knative)
KServe는 환경에 따라 라우팅 계층이 달라집니다.
RawDeployment모드로 직접Service를 붙이는 구성- Knative 기반으로
KService를 만들고 activator/queue-proxy를 거치는 구성 - Istio
VirtualService로 라우팅하는 구성
문제는 클러스터에 설치된 구성과 InferenceService의 predictor 설정이 엇갈리면 요청이 503으로 떨어진다는 점입니다. 특히 다음 패턴이 흔합니다.
kubectl get ksvc는 생성되었는데, 실제Revision이NotReadyVirtualService가 생성되지 않거나, 게이트웨이/호스트가 다름- 외부에서 호출하는 도메인이 Istio
Gateway에 바인딩되지 않음
확인 포인트:
kubectl get ksvc -n <ns>
kubectl describe ksvc -n <ns> <name>
kubectl get virtualservice -n <ns>
kubectl describe virtualservice -n <ns> <name>
kubectl get gateway -A
해결 방향:
- KServe 설치 방식에 맞게
InferenceService의ingress/deploymentMode관련 값을 정렬 - Istio를 쓰는 클러스터라면
Gateway와VirtualService의hosts가 실제 호출 도메인과 일치하는지 확인 - Knative를 쓰는 클러스터라면
ksvc및revisionreadiness를 먼저 통과시키기
원인 2) readiness/liveness 프로브가 vLLM의 “워밍업 특성”과 충돌
vLLM은 첫 로딩에서 다음 작업을 수행합니다.
- 모델 가중치 로딩
- GPU 메모리 할당
- KV cache 준비
- 토크나이저 로딩
이 구간이 길면, readiness 프로브가 너무 공격적인 설정일 때 컨테이너가 Ready가 되기 전에 재시작을 반복합니다. 이 경우 라우팅 계층에서는 “엔드포인트가 없다”로 판단해 503을 반환합니다.
자주 보이는 징후:
- Pod 이벤트에
Readiness probe failed반복 CrashLoopBackOff는 아니지만 재시작 카운트가 증가kubectl get endpoints에 주소가 비어 있음
확인:
kubectl get pod -n <ns>
kubectl describe pod -n <ns> <pod>
kubectl logs -n <ns> <pod> --previous
kubectl get endpoints -n <ns>
해결:
- readiness 프로브에
initialDelaySeconds를 충분히 크게 - 모델 로딩이 끝났을 때만 성공하는 헬스 엔드포인트를 사용
- liveness는 더 보수적으로(너무 빨리 죽이지 않기)
예시(개념):
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 120
periodSeconds: 5
failureThreshold: 12
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 300
periodSeconds: 10
failureThreshold: 6
주의: 실제 vLLM 이미지/서빙 래퍼에 따라 헬스 경로는 다를 수 있으니, “항상 200을 주는 경로”를 붙이면 오히려 장애를 숨깁니다.
원인 3) containerPort/Service targetPort 불일치로 인한 503
KServe는 내부적으로 Service를 만들고 라우팅을 구성합니다. 이때 vLLM이 실제로 리슨하는 포트(예: 8000)와, 컨테이너 선언 또는 서비스의 targetPort가 다르면 다음이 발생합니다.
- Pod는 Ready인데, 서비스 라우팅은 죽어 있음
- Envoy/queue-proxy가 upstream 연결 실패로
503반환
확인:
kubectl get svc -n <ns>
kubectl describe svc -n <ns> <svc>
kubectl exec -n <ns> <pod> -- sh -c "netstat -lntp || ss -lntp"
해결:
- vLLM 실행 커맨드의
--port와containerPort를 일치 - 서비스
targetPort가 실제 컨테이너 포트로 향하는지 확인
이 문제는 특히 “이미지 기본 포트는 8000인데, 차트 값은 8080으로 박혀 있는” 상황에서 자주 터집니다.
원인 4) 리소스 요청/제한 미스매치로 스케줄은 됐지만 실제론 OOM 또는 GPU 할당 실패
503이 네트워크 계층 문제가 아니라, 애플리케이션이 정상 구동을 못 해서 생기는 경우도 많습니다. vLLM은 GPU 메모리 압박이 큰 편이라 아래 상황이 흔합니다.
resources.limits만 있고requests가 없어, 노드에 과밀 배치- GPU는 잡았지만 CPU/RAM이 부족해 토크나이저/서버가 OOM
nvidia.com/gpu요청은 했는데, 노드 드라이버/런타임 이슈로 CUDA 초기화 실패
확인:
kubectl describe pod -n <ns> <pod>
kubectl logs -n <ns> <pod>
# 노드 자원과 파드 리소스 확인
kubectl top node
kubectl top pod -n <ns>
해결:
requests와limits를 함께 설정해 스케줄링 안정화- vLLM의 동시성,
max_model_len, KV cache 설정을 리소스에 맞게 조정 - GPU 노드의 디바이스 플러그인 및 런타임 상태 점검
원인 5) Knative scale-to-zero 및 activator/queue-proxy 병목으로 인한 콜드스타트 503
Knative 기반 KServe에서는 트래픽이 없으면 0으로 줄어드는 구성이 흔합니다. vLLM은 콜드스타트가 길어질 수 있어, 다음 패턴이 생깁니다.
- 첫 요청이 activator에서 대기하다가 타임아웃
- 모델 로딩 중 readiness 실패로 계속 준비가 안 됨
- 결과적으로 사용자 입장에서는 첫 요청이
503또는 타임아웃
확인 포인트:
kubectl get ksvc -n <ns>
kubectl describe ksvc -n <ns> <name>
# knative-serving 네임스페이스에서 activator 로그 확인
kubectl logs -n knative-serving deploy/activator
해결:
- scale-to-zero를 끄거나 최소 레플리카를 유지
- 요청 타임아웃을 vLLM 로딩 시간에 맞게 확장
- readiness 프로브를 콜드스타트 특성에 맞게 조정
운영 관점에서는 “첫 요청이 중요한지”에 따라 결정을 다르게 가져가야 합니다. 챗봇처럼 상시 응답이 필요하면 최소 1개는 항상 띄우는 편이 안전합니다.
원인 6) HPA가 볼 메트릭이 없다: metrics-server/adapter 미설치 또는 권한 문제
HPA가 안 도는 가장 흔한 이유는 단순합니다. 메트릭이 없어서입니다.
- CPU/메모리 기반 HPA인데
metrics-server가 없음 - Prometheus 기반 커스텀 메트릭인데 adapter가 없음
- RBAC 때문에 HPA 컨트롤러가 메트릭 API를 못 읽음
증상:
kubectl describe hpa에failed to get cpu utilization또는unable to fetch metrics류 메시지kubectl top pod자체가 동작하지 않음
확인:
kubectl get apiservice | grep metrics
kubectl top pod -n <ns>
kubectl describe hpa -n <ns> <hpa>
해결:
- CPU/메모리 HPA면
metrics-server를 먼저 정상화 - 커스텀 메트릭이면 Prometheus Adapter 설치 및 규칙 설정
- HPA가 참조하는
scaleTargetRef가 실제 워크로드를 가리키는지 확인
GitOps로 배포한다면, 설치는 됐는데 drift나 값 충돌로 일부 리소스가 누락되는 경우도 있습니다. 이런 류의 배포 상태 꼬임은 Argo CD Sync Failed - drift·Helm 값·RBAC 해결에서 다룬 방식으로 원인을 빠르게 좁힐 수 있습니다.
원인 7) HPA 타깃이 잘못됨: KServe가 만든 리소스와 HPA가 스케일하는 리소스가 다름
KServe는 InferenceService 하나로 여러 하위 리소스를 생성합니다. 환경에 따라 스케일 대상이 다음 중 하나가 됩니다.
- Knative
Revision또는KService - RawDeployment 모드의
Deployment
그런데 사용자가 별도로 HPA를 만들면서 scaleTargetRef를 잘못 잡으면 이런 일이 벌어집니다.
- HPA는 열심히 계산하지만 스케일이 적용되지 않음
- 혹은 스케일은 되는데 트래픽 라우팅이 다른 리소스를 보고 있어 효과가 없음
확인:
kubectl get deploy -n <ns>
kubectl get rs -n <ns>
kubectl get ksvc -n <ns>
kubectl describe hpa -n <ns> <hpa>
해결:
- KServe 설치 모드에 맞는 “정답 스케일 타깃”을 먼저 확정
- Knative 기반이면 HPA 대신 Knative Pod Autoscaler 설정이 더 자연스러운 경우가 많음
- RawDeployment면 표준 HPA가 잘 맞음
예시(HPA가 Deployment를 스케일하는 형태):
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: vllm-hpa
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: vllm-predictor
minReplicas: 1
maxReplicas: 5
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
핵심은 “트래픽이 실제로 들어가는 워크로드”를 스케일해야 한다는 점입니다.
503과 HPA를 한 번에 잡는 실전 체크리스트
1) 503 경로를 먼저 확정
- 외부 호출
503인지, 클러스터 내부 호출도503인지 분리 Service직접 호출과 라우팅 계층(istio/knative) 호출을 분리
# 같은 네임스페이스에 임시 디버그 파드
kubectl run -n <ns> tmp --image=curlimages/curl:8.5.0 -it --rm -- sh
# 서비스 내부 호출
curl -v http://<svc-name>.<ns>.svc.cluster.local:8000/health
2) 엔드포인트 유무로 “라우팅 문제 vs 파드 문제”를 분리
kubectl get endpoints -n <ns> <svc-name> -o yaml
- 엔드포인트가 비어 있으면 readiness/라벨 셀렉터/포트 불일치부터
- 엔드포인트가 있으면 istio/knative 라우팅 설정부터
3) HPA는 describe가 답을 말해준다
kubectl describe hpa -n <ns> <hpa>
- 이벤트에 메트릭 수집 실패가 있으면 메트릭 파이프라인 문제
- 메트릭은 보이는데 스케일이 안 되면 타깃 리소스 문제
마무리
KServe에서 vLLM을 운영할 때 503과 HPA 미작동은 별개의 문제처럼 보여도, 실제로는 Ready 엔드포인트가 없어서 라우팅이 실패하고, 동시에 메트릭/스케일 타깃이 어긋나서 확장이 안 되는 형태로 함께 터지는 경우가 많습니다.
정리하면 우선순위는 다음이 실전에서 가장 효율적입니다.
endpoints가 채워지는지로 503의 바닥 원인(파드 vs 라우팅)을 분리- readiness/liveness와 포트 불일치부터 제거
- HPA는
kubectl describe hpa의 이벤트를 기준으로 메트릭 파이프라인과 타깃을 교정
원하시면 사용 중인 구성(istio/knative 여부, InferenceService YAML, HPA YAML, kubectl describe 출력 일부)을 기준으로, 위 7가지 중 어디에 해당하는지 빠르게 매칭해서 수정안을 구체적으로 제안해드릴게요. 단, 본문에 부등호가 들어간 로그/경로는 공유 시 반드시 백틱으로 감싸주세요.