Published on

KServe v0.12 503와 콜드스타트 줄이는 실전 튜닝

Authors

KServe v0.12에서 추론 요청이 간헐적으로 503으로 떨어지거나, 첫 요청이 유독 느린(콜드스타트) 문제는 대부분 트래픽 라우팅 계층(게이트웨이/Knative/KServe queue-proxy)Pod 준비 상태(readiness)·모델 로딩 시간의 불일치에서 시작합니다.

특히 GPU 모델(Transformers, vLLM, TensorRT 등)은 컨테이너가 Ready가 된 뒤에도 실제 모델 로딩/워밍업이 끝나지 않아 첫 요청이 타임아웃으로 503이 되거나, 스케일-투-제로 이후 재기동 과정에서 지연이 커지는 패턴이 흔합니다.

이 글에서는 KServe v0.12 기준으로 503을 줄이고 콜드스타트를 완화하는 진단 순서설정/아키텍처 선택지를 실전 위주로 정리합니다.

1) KServe 503의 “어디서” 발생하는지 먼저 분리

503 Service Unavailable은 단일 원인이 아니라, 다음 중 하나가 “업스트림을 못 찾거나(엔드포인트 없음)”, “준비가 안 됐거나(Ready 아님)”, “시간 내 응답을 못 받았거나(타임아웃)”일 때 발생합니다.

1-1. 가장 흔한 503 경로

  • Gateway/Ingress: 라우팅 대상 서비스가 없거나 업스트림 연결 실패
  • Knative activator / queue-proxy: 스케일-투-제로에서 활성화 중이거나, 컨테이너 준비/응답 타임아웃
  • KServe predictor 컨테이너: 애플리케이션이 5xx를 직접 반환하거나, 모델 서버가 아직 로딩 중

따라서 “503이 어디서 찍히는지”를 로그로 분리해야 합니다.

1-2. 필수 로그/이벤트 확인

아래 커맨드로 InferenceService 이벤트, Knative 서비스 상태, Pod readiness 전환 시점을 먼저 확인합니다.

# InferenceService 상태/이벤트
kubectl -n ml get isvc
kubectl -n ml describe isvc my-model

# Knative Service/Revision 상태
kubectl -n ml get ksvc
kubectl -n ml get revision
kubectl -n ml describe ksvc my-model-predictor

# Pod 이벤트(스케줄링 지연, 이미지 풀, 볼륨 마운트 등)
kubectl -n ml get pods -l serving.kserve.io/inferenceservice=my-model
kubectl -n ml describe pod -l serving.kserve.io/inferenceservice=my-model

# queue-proxy / activator 로그(존재한다면)
kubectl -n ml logs deploy/activator -c activator --tail=200

여기서 다음 신호가 보이면 원인을 좁힐 수 있습니다.

  • RevisionMissing / NoReadyEndpoints 류: 엔드포인트 자체가 준비되지 않음
  • Readiness probe failed: 준비 프로브가 너무 엄격하거나 경로가 틀림
  • context deadline exceeded / upstream request timeout: 게이트웨이/프록시 타임아웃이 모델 로딩/첫 추론 시간보다 짧음

네트워크 타임아웃/데드라인 이슈의 진단 관점은 gRPC지만 HTTP에도 그대로 통합니다. 타임아웃이 어디 계층에서 걸리는지 분해하는 접근은 이 글도 참고할 만합니다: Go gRPC context deadline exceeded 원인 9가지

2) 콜드스타트의 3대 원인: 스케일-투-제로, 이미지/볼륨, 모델 로딩

콜드스타트는 “Pod가 뜨는 시간”만이 아니라 다음 합입니다.

  1. 스케일-투-제로에서 0 Pod → 1 Pod로 늘어나는 시간
  2. 이미지 pull, GPU 노드 스케줄링, PVC 마운트 시간
  3. 모델 다운로드/로딩 + 그래프 컴파일 + 첫 토큰/첫 배치 워밍업

따라서 해결도 1)~3) 각각에 걸어야 합니다.

3) 503 줄이기: 타임아웃과 프로브를 모델 특성에 맞추기

3-1. readiness/liveness는 “서버 기동”과 “모델 준비”를 분리

많은 모델 서버가 프로세스는 뜨지만 모델이 아직 준비되지 않은 상태가 길게 존재합니다. 이때 readiness가 너무 빨리 Ready로 바뀌면, 첫 요청이 들어와 모델 로딩 중 타임아웃 → 503으로 이어집니다.

권장 패턴은 다음 중 하나입니다.

  • 모델 준비 완료 시점까지 readiness를 fail 시키기
  • 또는 readiness는 가볍게 두되, 첫 요청 타임아웃을 충분히 늘리기

가능하다면 첫 번째가 더 안전합니다. 예를 들어 모델 서버가 /v1/health/ready 같은 엔드포인트를 제공한다면 readiness를 그쪽으로 연결합니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: my-model
  namespace: ml
spec:
  predictor:
    containers:
      - name: kserve-container
        image: myrepo/myserver:latest
        ports:
          - containerPort: 8080
        readinessProbe:
          httpGet:
            path: /v1/health/ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 2
          timeoutSeconds: 1
          failureThreshold: 60
        livenessProbe:
          httpGet:
            path: /v1/health/live
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
  • failureThreshold를 크게 잡는 이유: 모델 로딩이 1~2분 걸리는 환경에서 readiness가 계속 실패해도 “정상적인 준비 과정”으로 간주해야 하기 때문입니다.
  • liveness는 너무 공격적으로 두면 로딩 중 재시작 루프가 생깁니다.

3-2. 요청 타임아웃을 “첫 추론” 기준으로 재설계

KServe/Knative 경로에는 여러 타임아웃이 겹칩니다. 흔한 실수는:

  • 게이트웨이 타임아웃 30초
  • queue-proxy 타임아웃 30초
  • 애플리케이션 내부 타임아웃 15초

처럼 서로 다른 기본값이 섞여, 어느 계층에서 먼저 끊기는지 모른 채 503만 보게 됩니다.

운영 관점에서는 다음 2개를 분리하는 게 좋습니다.

  • 콜드스타트/워밍업 요청의 타임아웃(길게)
  • 정상 상태 P99 타임아웃(짧게)

이를 구현하는 방법은 대표적으로 2가지입니다.

  1. 스케일-투-제로를 끄거나 최소 레플리카를 유지해서 “콜드 요청” 자체를 없애기
  2. 워밍업 전용 엔드포인트/잡을 두고, 외부 요청은 준비 이후에만 받기(readiness로 차단)

4) 콜드스타트 줄이기: minScale, 초기 스케일, 동시성 튜닝

4-1. 스케일-투-제로를 피하는 가장 확실한 방법: minScale

실시간 트래픽(사용자가 기다리는 UI)이라면 스케일-투-제로는 비용 절감 대신 사용자 경험을 크게 해칠 수 있습니다. 이때는 최소 1개 Pod를 유지하는 것이 가장 단순하고 효과적입니다.

KServe는 Knative 애너테이션을 통해 최소 스케일을 지정할 수 있습니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: my-model
  namespace: ml
  annotations:
    autoscaling.knative.dev/minScale: "1"
    autoscaling.knative.dev/initialScale: "1"
spec:
  predictor:
    containers:
      - name: kserve-container
        image: myrepo/myserver:latest
  • minScale: "1"은 콜드스타트를 사실상 제거합니다.
  • initialScale은 배포 직후 트래픽 급증에서 503을 줄이는 데 도움이 됩니다.

4-2. 동시성(Concurrency)과 큐잉: 503을 “부하”로 착각하지 않기

LLM/비전 모델은 GPU 메모리와 KV 캐시, 배치 전략에 따라 적정 동시성이 크게 달라집니다. 동시성을 과도하게 올리면:

  • 지연이 폭증하고
  • queue-proxy에서 대기열이 길어지고
  • 결국 타임아웃으로 503이 납니다.

반대로 너무 낮추면 오토스케일이 과도하게 일어나 비용이 증가합니다.

모델이 OOM 또는 지연 폭증을 보인다면, 동시성보다 먼저 메모리/캐시 튜닝이 선행돼야 합니다. 로컬 LLM 기준이지만, “KV 캐시가 지연과 OOM의 핵심 변수”라는 점은 서버 추론에서도 동일합니다: Transformers 로컬 LLM OOM - 4-bit·KV 캐시 튜닝

5) 모델 로딩 시간을 줄이는 배포 패턴

5-1. 모델을 컨테이너 이미지에 굽기 vs 런타임 다운로드

콜드스타트의 가장 큰 변수는 “모델을 어디서 가져오느냐”입니다.

  • 런타임 다운로드(S3, GCS, HF Hub): 네트워크/권한/레이트리밋에 취약
  • 이미지에 포함: 이미지 크기 증가, 배포 느림, 하지만 실행 시점은 안정적
  • PVC에 미리 캐시: 노드 이동 시 마운트/캐시 정책에 따라 편차

실시간 서비스라면 다음 우선순위를 추천합니다.

  1. PVC 캐시 + 노드 로컬 캐시 최적화(가능하면)
  2. 또는 이미지에 모델 포함(배포 파이프라인이 받쳐준다면)
  3. 런타임 다운로드는 개발/실험 단계에서만

5-2. initContainer로 “다운로드와 준비”를 메인 컨테이너에서 분리

메인 컨테이너가 모델 다운로드까지 책임지면 readiness 설계가 복잡해지고, 재시작 시마다 같은 작업을 반복할 수 있습니다. initContainer로 모델을 준비해두면 메인 컨테이너는 “서빙”에만 집중할 수 있습니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: my-model
  namespace: ml
  annotations:
    autoscaling.knative.dev/minScale: "1"
spec:
  predictor:
    volumes:
      - name: model-cache
        persistentVolumeClaim:
          claimName: model-cache-pvc
    containers:
      - name: kserve-container
        image: myrepo/myserver:latest
        volumeMounts:
          - name: model-cache
            mountPath: /models
        env:
          - name: MODEL_PATH
            value: /models/my-model
    initContainers:
      - name: fetch-model
        image: curlimages/curl:8.5.0
        command:
          - sh
          - -c
          - |
            set -e
            test -f /models/my-model/READY && exit 0
            mkdir -p /models/my-model
            # 예: 오브젝트 스토리지에서 다운로드
            curl -L -o /models/my-model/model.bin "$MODEL_URL"
            echo ok > /models/my-model/READY
        env:
          - name: MODEL_URL
            valueFrom:
              secretKeyRef:
                name: model-url
                key: url
        volumeMounts:
          - name: model-cache
            mountPath: /models

핵심은:

  • initContainer에서 READY 마커를 만들고
  • 메인 컨테이너는 READY가 있는 경로만 로딩
  • 재시작 시 다운로드를 스킵

여기서 마커 문자열 READY처럼 부등호가 없는 값은 안전하지만, 경로/템플릿에 <> 형태를 쓰지 않도록 주의하세요(Next.js MDX 빌드 환경에서 JSX로 오인될 수 있음).

6) 워밍업(Pre-warm)으로 첫 요청 지연과 503을 동시에 줄이기

모델이 준비되자마자 첫 추론이 느린 이유는 다음이 겹치기 때문입니다.

  • CUDA 커널 로딩
  • 그래프/엔진 빌드(TensorRT 등)
  • 토크나이저/캐시 초기화
  • 첫 배치에서만 발생하는 메모리 할당

따라서 배포 직후 또는 스케일 아웃 직후에 워밍업 요청을 내부에서 1회 보내면 효과가 큽니다.

6-1. postStart 훅으로 간단 워밍업

lifecycle:
  postStart:
    exec:
      command:
        - sh
        - -c
        - |
          # 서버가 떠도 모델 준비까지 시간이 걸릴 수 있으니 재시도
          for i in $(seq 1 60); do
            curl -sf http://127.0.0.1:8080/v1/health/ready && break
            sleep 1
          done
          # 최소 워밍업 요청(예: 짧은 입력)
          curl -sf -X POST http://127.0.0.1:8080/v1/models/my-model:predict \
            -H 'Content-Type: application/json' \
            -d '{"inputs":"warmup"}' || true

주의점:

  • 워밍업이 무거우면 오히려 시작 시간을 늘립니다. “가장 가벼운 입력”으로 1회만.
  • 서버가 아직 바인딩 전이면 실패하므로, ready 체크 후 호출합니다.

6-2. CronJob 워밍업(스케일-투-제로 유지하면서 503 완화)

비용 때문에 minScale을 못 쓰는 경우, 트래픽 패턴이 정해져 있다면(예: 오전 9시 피크) CronJob으로 미리 깨우는 방식도 실무에서 자주 씁니다.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: warmup-my-model
  namespace: ml
spec:
  schedule: "55 8 * * 1-5"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: warmup
              image: curlimages/curl:8.5.0
              command:
                - sh
                - -c
                - |
                  # 외부 라우트(게이트웨이)로 호출해 activator까지 깨우기
                  curl -sf -X POST "$PREDICT_URL" \
                    -H 'Content-Type: application/json' \
                    -d '{"inputs":"warmup"}'
              env:
                - name: PREDICT_URL
                  value: "http://my-model.ml.example.com/v1/models/my-model:predict"

이 방식은 “사용자 첫 요청”을 “시스템 워밍업 요청”으로 바꿔 503 체감률을 낮춥니다.

7) GPU 스케줄링 지연이 503로 보일 때: 노드/리소스 전략

GPU Pod은 다음 이유로 Pending 시간이 길어집니다.

  • GPU 노드 부족
  • nodeSelector/taints/tolerations 불일치
  • 큰 이미지 pull
  • 디스크 압박으로 이미지 GC

이때 외부에서는 단순히 “엔드포인트 없음”으로 보여 503이 납니다.

운영 팁:

  • GPU 노드에 이미지 프리풀(daemonset) 적용
  • 모델 캐시 PVC를 해당 노드 풀에 가까운 스토리지로 구성
  • 오토스케일 정책이 GPU 노드를 충분히 빨리 늘릴 수 있는지 점검

8) 실전 체크리스트: 503·콜드스타트 개선 순서

  1. 503이 발생하는 계층을 로그로 특정(게이트웨이 vs queue-proxy vs 앱)
  2. readiness가 “모델 준비 완료”를 반영하는지 확인
  3. 타임아웃을 첫 추론 기준으로 재설정(짧은 기본값 방치 금지)
  4. minScale 또는 initialScale로 콜드스타트 자체를 제거/완화
  5. initContainer로 모델 준비를 분리하고 PVC 캐시로 재시작 비용 절감
  6. postStart 또는 CronJob으로 워밍업 추가
  7. GPU 스케줄링/이미지 pull 병목을 제거(프리풀, 노드풀, 스토리지)

9) 마무리: “503은 증상, 원인은 준비·시간·용량”

KServe v0.12에서 503과 콜드스타트는 대개 모델 서버 자체의 문제가 아니라, 준비 상태 신호(readiness)와 라우팅/타임아웃 정책, 그리고 스케일링 전략이 모델의 현실적인 로딩/추론 시간과 맞지 않을 때 발생합니다.

가장 효과가 큰 조합은 보통 다음입니다.

  • 실시간 서비스: minScale: "1" + readiness를 모델 준비 완료에 연결 + 가벼운 워밍업
  • 비용 우선 서비스: 스케일-투-제로 유지 + CronJob 워밍업 + 타임아웃 상향 + 캐시/PVC 최적화

환경(게이트웨이 종류, Knative 설정, 모델 서버)에 따라 세부 파라미터는 달라지니, 현재 사용 중인 KServe 설치 방식(예: Istio/Envoy 기반인지, Knative Serving 버전, GPU/모델 크기)과 함께 503 로그 샘플을 기준으로 튜닝 포인트를 더 좁혀가면 좋습니다.