Published on

KServe vLLM 배포 503·HPA 미작동 원인 7가지

Authors

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로 라우팅하는 구성

문제는 클러스터에 설치된 구성과 InferenceServicepredictor 설정이 엇갈리면 요청이 503으로 떨어진다는 점입니다. 특히 다음 패턴이 흔합니다.

  • kubectl get ksvc는 생성되었는데, 실제 RevisionNotReady
  • VirtualService가 생성되지 않거나, 게이트웨이/호스트가 다름
  • 외부에서 호출하는 도메인이 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 설치 방식에 맞게 InferenceServiceingress/deploymentMode 관련 값을 정렬
  • Istio를 쓰는 클러스터라면 GatewayVirtualServicehosts가 실제 호출 도메인과 일치하는지 확인
  • Knative를 쓰는 클러스터라면 ksvcrevision readiness를 먼저 통과시키기

원인 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 실행 커맨드의 --portcontainerPort를 일치
  • 서비스 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>

해결:

  • requestslimits를 함께 설정해 스케줄링 안정화
  • 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 hpafailed 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 엔드포인트가 없어서 라우팅이 실패하고, 동시에 메트릭/스케일 타깃이 어긋나서 확장이 안 되는 형태로 함께 터지는 경우가 많습니다.

정리하면 우선순위는 다음이 실전에서 가장 효율적입니다.

  1. endpoints가 채워지는지로 503의 바닥 원인(파드 vs 라우팅)을 분리
  2. readiness/liveness와 포트 불일치부터 제거
  3. HPA는 kubectl describe hpa의 이벤트를 기준으로 메트릭 파이프라인과 타깃을 교정

원하시면 사용 중인 구성(istio/knative 여부, InferenceService YAML, HPA YAML, kubectl describe 출력 일부)을 기준으로, 위 7가지 중 어디에 해당하는지 빠르게 매칭해서 수정안을 구체적으로 제안해드릴게요. 단, 본문에 부등호가 들어간 로그/경로는 공유 시 반드시 백틱으로 감싸주세요.