Published on

KServe LLM 서빙 503·스케일0 지연 해결법

Authors

서빙 트래픽이 들쭉날쭉한 LLM 서비스에서 KServe의 scale-to-zero는 비용을 크게 줄여주지만, 첫 요청이 503으로 떨어지거나 응답이 수십 초 이상 지연되는 문제가 자주 발생합니다. 특히 GPU 노드 프로비저닝, 대형 모델 가중치 다운로드, 워밍업 미흡이 겹치면 “살아나긴 하는데 첫 요청은 실패” 같은 체감이 생깁니다.

이 글은 KServe 기반 LLM 서빙에서 503과 스케일0 지연을 어디서 발생하는지 레이어별로 쪼개서 진단하고, 재현 가능한 설정 변경으로 해결하는 방법을 정리합니다.

증상 패턴 정리: 503은 어디서 나오나

KServe는 보통 Knative Serving 위에서 동작합니다. 요청 경로를 단순화하면 다음과 같습니다.

  • Ingress Gateway 또는 Kourier/Istio
  • Knative Activator(스케일0일 때)
  • Revision Pod의 queue-proxy
  • 사용자 컨테이너(예: vLLM, TGI, Triton, custom server)

503은 주로 아래 상황에서 발생합니다.

  1. 스케일0에서 스케일업 중: Activator가 백엔드가 준비되지 않았다고 판단해 503을 반환
  2. 프로브 실패: readiness/liveness가 계속 실패해 트래픽이 붙지 못함
  3. 컨테이너는 떴지만 모델 로딩이 끝나지 않음: 서버 포트는 열렸으나 실제 추론 준비가 안 된 상태
  4. 리소스 부족: GPU 할당 실패, 이미지 풀 실패, 노드 부족으로 Pod가 Pending에 머무름

이미지 풀/권한 문제로 Pod가 올라오지 못하는 케이스도 흔합니다. 이 경우 503은 “결과”이고, 실제 원인은 이벤트에 남습니다. 관련 진단 루틴은 EKS Pod ImagePullBackOff 401 해결 가이드K8s Pod ImagePullBackOff - ECR 403 해결 가이드도 함께 참고하면 좋습니다.

1단계: 503의 원인 레이어를 로그로 식별

Knative/Activator에서 503이 나는지 확인

KServe가 Knative를 사용 중이라면 Activator 로그가 가장 먼저입니다.

kubectl -n knative-serving get pods -l app=activator
kubectl -n knative-serving logs -l app=activator --tail=200

Activator 로그에 “no ready endpoints” 류 메시지가 반복되면, 스케일업은 시작됐지만 Revision Pod가 Ready가 되지 못한 상태입니다.

Revision Pod 상태와 이벤트 확인

InferenceService가 만든 Revision을 찾기 어렵다면, 우선 KServe 리소스부터 따라갑니다.

kubectl get inferenceservice -A
kubectl describe inferenceservice -n <namespace> <name>

describe 출력에서 URL, predictor deployment/revision 정보를 확인한 뒤 Pod 이벤트를 봅니다.

kubectl -n <namespace> get pods
kubectl -n <namespace> describe pod <pod-name>
kubectl -n <namespace> get events --sort-by=.metadata.creationTimestamp | tail -n 50

여기서 Pending, ImagePullBackOff, CrashLoopBackOff, Readiness probe failed 같은 단서가 대부분 나옵니다. CrashLoopBackOff로 이어진다면 원인별 진단은 Kubernetes CrashLoopBackOff 10가지 원인과 15분 진단를 같이 보면 빠릅니다.

2단계: scale-to-zero 콜드스타트 지연의 핵심 원인 4가지

LLM은 일반 웹앱과 달리 콜드스타트 비용이 큽니다. 지연을 구성요소로 분해하면 대략 아래입니다.

  1. 노드 준비 시간: GPU 노드가 없으면 Cluster Autoscaler 또는 Karpenter가 노드를 띄움
  2. 이미지 Pull: 수 GB 이미지면 수십 초가 걸릴 수 있음
  3. 모델 가중치 다운로드/마운트: S3, PVC, HF Hub에서 가져오면 병목이 큼
  4. 엔진 워밍업: vLLM/TGI가 그래프 컴파일, KV 캐시 준비, 첫 토큰 지연

따라서 “KServe 설정만” 만져서는 한계가 있고, 노드/이미지/모델/서버를 같이 최적화해야 합니다.

3단계: 503 방지용 최소 설정: 프로브와 타임아웃을 현실화

readinessProbe를 “포트 오픈”이 아니라 “모델 준비 완료”로

LLM 서버가 포트만 열고 모델 로딩 중이면, readiness가 너무 빨리 성공해 트래픽이 붙고 실패할 수 있습니다. 반대로 readiness가 너무 엄격하면 스케일업이 늦어져 Activator가 503을 오래 반환합니다.

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

  • 서버가 /health/ready 같은 엔드포인트에서 “모델 로딩 완료”를 반환
  • 최소한 /v1/models 같은 모델 목록 API가 정상 응답할 때 Ready

아래는 KServe InferenceService에서 predictor 컨테이너에 프로브를 주는 예시입니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: llm
  namespace: ai
spec:
  predictor:
    containers:
      - name: user-container
        image: myrepo/vllm:latest
        ports:
          - containerPort: 8000
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 8000
          initialDelaySeconds: 5
          periodSeconds: 2
          timeoutSeconds: 1
          failureThreshold: 60
        livenessProbe:
          httpGet:
            path: /health/live
            port: 8000
          initialDelaySeconds: 30
          periodSeconds: 10

포인트는 failureThreshold를 늘려서 “모델 로딩이 길어도 죽지 않게” 하고, readiness 기준을 모델 준비 완료로 맞추는 것입니다.

Knative 요청 타임아웃 조정

콜드스타트가 길면 첫 요청이 라우팅되기 전에 타임아웃으로 끊길 수 있습니다. Knative는 리비전 단위 timeout을 어노테이션으로 줄 수 있습니다.

metadata:
  annotations:
    serving.knative.dev/timeoutSeconds: "300"

이 값은 무작정 늘리기보다, “최악의 콜드스타트 시간”을 측정해서 그보다 약간 크게 잡는 게 좋습니다.

4단계: scale-to-zero는 유지하되 503을 줄이는 트래픽 전략

Activator가 버티는 동안 첫 요청을 실패시키지 않기

현실적으로 콜드스타트 중에는 응답이 늦어집니다. 사용자 경험을 위해 다음 중 하나를 선택합니다.

  • 클라이언트 재시도: 첫 요청 503을 재시도/백오프로 흡수
  • 프론트 프록시 큐잉: API Gateway에서 큐잉 또는 서킷브레이커

LLM API 호출 측 재시도는 특히 중요합니다. OpenAI 호환 API를 제공한다면, 레이트리밋뿐 아니라 일시적 503도 같은 방식으로 다루는 게 실전적입니다. 재시도/백오프 설계는 OpenAI 429/Rate Limit 대응 - 재시도·백오프·큐잉에서 다룬 패턴을 그대로 가져올 수 있습니다.

예시로, Python에서 503에 지수 백오프를 적용합니다.

import time
import random
import requests

URL = "https://llm.example.com/v1/chat/completions"

def post_with_retry(payload, max_attempts=6):
    for attempt in range(1, max_attempts + 1):
        r = requests.post(URL, json=payload, timeout=60)
        if r.status_code < 500:
            return r

        # 5xx: exponential backoff + jitter
        sleep = min(2 ** attempt, 30) + random.random()
        time.sleep(sleep)

    return r

서버가 스케일업되는 동안 1~2회 정도의 재시도로 대부분 흡수됩니다.

5단계: “스케일0 지연” 자체를 줄이는 6가지 실전 최적화

1) minScale로 완전 0을 피하는 하이브리드

비용이 조금 들더라도, 특정 시간대나 특정 모델은 minScale=1이 체감 품질을 크게 올립니다. KServe/Knative 조합에 따라 어노테이션이 달라질 수 있지만, 일반적으로는 Knative autoscaling 설정을 씁니다.

metadata:
  annotations:
    autoscaling.knative.dev/minScale: "1"
    autoscaling.knative.dev/maxScale: "5"

트래픽이 낮아도 최소 1개 Pod를 유지해 503과 첫 토큰 지연을 거의 제거합니다.

2) GPU 노드 웜풀 유지

스케일0의 가장 큰 적은 “GPU 노드가 없다”입니다. 해결책은 두 가지입니다.

  • GPU 노드를 1대 이상 상시 유지
  • Karpenter 사용 시 GPU 노드 프로비저닝을 빠르게 만들고 AMI/드라이버 준비를 최적화

운영에서는 “모델 Pod는 0으로 내려가도 GPU 노드는 1대 유지”가 비용 대비 효과가 좋습니다.

3) 이미지 Pull 최적화

  • 이미지 사이즈 줄이기(불필요한 빌드 아티팩트 제거)
  • 노드에 이미지 프리풀(daemonset로 crictl pull)
  • 레지스트리 네트워크 병목 제거

이미지 풀 실패나 지연이 의심되면 Pod 이벤트에 Pulling image 구간이 얼마나 오래 걸리는지 확인하세요.

4) 모델 가중치 배포 전략 바꾸기

대형 LLM에서 가장 큰 지연은 가중치입니다. 대표적인 전략은 아래입니다.

  • PVC에 미리 다운로드: Pod 시작 시 다운로드를 없애고 마운트만 수행
  • 노드 로컬 캐시: 동일 노드에서 재기동 시 재다운로드 방지
  • 오브젝트 스토리지 + initContainer: 시작 전에 검증된 경로로 동기화

initContainer로 모델을 미리 받아두는 예시입니다.

spec:
  predictor:
    containers:
      - name: user-container
        image: myrepo/tgi:latest
        volumeMounts:
          - name: model
            mountPath: /models
    volumes:
      - name: model
        persistentVolumeClaim:
          claimName: llm-model-pvc

핵심은 “서빙 컨테이너가 뜨는 동안 다운로드를 하지 않게” 만드는 것입니다.

5) 서버 워밍업 엔드포인트 추가

모델 로딩이 끝난 직후 첫 요청이 느리면, readiness가 성공한 뒤에 워밍업을 한 번 수행해두는 방식이 효과적입니다.

  • Pod 시작 시 postStart 훅에서 간단한 프롬프트 1회 실행
  • 또는 별도 워머 Job이 서비스 URL을 주기적으로 호출

주의할 점은 워밍업이 과하면 비용이 늘고, 토큰 생성이 길면 readiness 타이밍과 충돌할 수 있으니 아주 짧게 합니다.

6) 동시성(concurrency)과 큐 설정

LLM은 동시성이 높아지면 지연이 급증합니다. Knative는 컨테이너 동시성 제한을 둘 수 있습니다.

metadata:
  annotations:
    autoscaling.knative.dev/target: "1"
    autoscaling.knative.dev/metric: "concurrency"

이렇게 하면 Pod 하나가 처리하는 동시 요청 수를 낮춰 품질을 유지하고, 대신 더 빨리 스케일아웃하도록 유도할 수 있습니다.

6단계: 체크리스트로 빠르게 수렴하기

운영에서 시간을 아끼려면 아래 순서로 체크하면 됩니다.

  1. kubectl describe inferenceservice 에서 Ready 조건과 URL 확인
  2. Activator 로그에서 no ready endpoints 여부 확인
  3. Revision Pod 이벤트에서 Pending 원인 확인
  4. readinessProbe가 “모델 준비 완료” 기준인지 확인
  5. 콜드스타트가 노드, 이미지, 모델, 워밍업 중 어디가 병목인지 시간 측정
  6. 비용 허용 범위 내에서 minScale=1 또는 GPU 노드 웜풀 적용

결론: 503은 설정 문제가 아니라 “준비 신호” 문제인 경우가 많다

KServe에서 LLM을 scale-to-zero로 운영할 때 503은 단순 장애라기보다, 트래픽이 들어왔는데 아직 준비가 안 됐다는 신호로 발생하는 경우가 많습니다. 따라서 해결도 “무조건 스케일0 끄기”가 아니라,

  • 준비 완료를 정확히 표현하는 프로브
  • 콜드스타트 구간을 버티는 타임아웃과 재시도
  • 노드, 이미지, 모델 배포를 포함한 엔드투엔드 최적화

이 3가지를 함께 맞추는 방향이 가장 현실적입니다.

다음 단계로는, 실제 콜드스타트 시간을 노드 준비, 이미지 pull, 모델 mount, 모델 load, 첫 토큰으로 계측해서 병목을 수치로 잡아두면 튜닝이 훨씬 빨라집니다.