Published on

KServe LLM 503 해결 - autoscaling·readiness 진단

Authors

서빙이 잘 되다가도 트래픽이 조금만 튀면 KServe 추론 엔드포인트가 503 Service Unavailable 를 뱉는 경우가 있습니다. 특히 LLM은 콜드스타트(모델 로딩, GPU 초기화, KV cache 준비)가 길고, 동시성 제어가 까다로워서 autoscalingreadiness 가 조금만 어긋나도 503이 급증합니다.

이 글은 KServe(대개 Knative Serving 기반)에서 LLM 추론 503을 만났을 때, 어디부터 어떤 순서로 확인해야 가장 빨리 원인을 좁힐 수 있는지에 초점을 맞춥니다. 결론부터 말하면, 503은 “애플리케이션이 죽어서”가 아니라 “라우팅 계층이 지금은 보낼 곳이 없다고 판단해서” 나는 경우가 훨씬 많습니다.

관련해서 추론 서버 자체 튜닝이 필요하다면 TorchServe 관점의 503/워커/메모리 이슈도 함께 참고할 만합니다: TorchServe 503·OOM·워커 튜닝 실전 가이드

503이 나는 지점부터 구분하기

KServe에서 외부 요청이 들어와 503이 되기까지의 경로는 보통 다음 중 하나입니다.

  1. Ingress 또는 Gateway 레벨에서 업스트림이 없어서 503
  2. Knative queue-proxy가 백엔드 컨테이너로 전달하지 못해 503
  3. 백엔드 컨테이너는 떠 있지만 readiness가 False 라서 라우팅에서 제외되어 503
  4. 백엔드가 과부하로 타임아웃 또는 연결 리셋이 나고, 그 결과가 503으로 표면화

핵심은 “누가 503을 응답했는지”를 로그로 확인하는 것입니다.

가장 먼저 보는 명령어

kubectl -n ${NS} get inferenceservice
kubectl -n ${NS} describe inferenceservice ${ISVC}

# KServe가 만든 Knative Service 확인
kubectl -n ${NS} get ksvc
kubectl -n ${NS} describe ksvc ${KSVC}

# Revision / Pod 상태
kubectl -n ${NS} get revision
kubectl -n ${NS} get pod -l serving.knative.dev/service=${KSVC} -o wide
kubectl -n ${NS} describe pod ${POD}

describe 출력에서 특히 아래를 봅니다.

  • Conditions 에서 ReadyFalse 인 이유
  • 최근 이벤트에 Readiness probe failed, ContainerCreating, FailedScheduling, Back-off restarting failed container 같은 힌트
  • Autoscaler 관련 이벤트(스케일 업/다운)

CrashLoop이 섞여 있다면 readiness 이전에 컨테이너 생존성부터 해결해야 합니다. 이 경우는 아래 글의 체크리스트가 그대로 적용됩니다: Kubernetes CrashLoopBackOff 원인 7가지·즉시복구

KServe/Knative에서 503이 자주 생기는 구조적 이유

1) 스케일이 0 으로 내려갔다가 콜드스타트가 길어지는 경우

Knative는 기본적으로 유휴 시 스케일을 0 까지 내릴 수 있습니다. LLM은 모델 로딩이 수십 초에서 수분까지 갈 수 있어, 이 동안 요청이 들어오면 다음 문제가 생깁니다.

  • 스케일 업 트리거는 됐지만 Pod가 아직 Ready 가 아님
  • queue-proxy가 버퍼링할 수 있는 한계를 넘으면 503
  • Ingress의 업스트림이 아직 준비되지 않아 503

해결 방향은 두 갈래입니다.

  • 스케일 0 을 막고 최소 레플리카를 유지
  • 콜드스타트 동안 요청을 충분히 버퍼링할 수 있게 timeouts/queue 설정 조정

2) readiness가 “프로세스 up”이 아니라 “모델 준비 완료”를 반영하지 못하는 경우

LLM 컨테이너가 HTTP 서버는 먼저 뜨고, 실제 모델 로딩은 백그라운드로 진행하는 패턴이 흔합니다. 이때 readiness probe가 단순히 GET /healthz 로 200을 반환하면, 라우터는 “보낼 준비 완료”로 오판합니다.

그 결과:

  • 처음 몇 요청이 모델 로딩 중에 들어와 타임아웃
  • 추론 엔진이 초기화 중이라 5xx
  • 라우터/클라이언트 레벨에서는 503으로 관측

readiness는 반드시 “모델이 메모리에 올라가고, 첫 토큰 생성까지 가능한 상태”를 의미해야 합니다.

3) 동시성 설정 불일치로 큐가 터지는 경우

Knative는 containerConcurrency 를 기준으로 동시 요청을 제어합니다. LLM 서버는 내부적으로도 max_concurrencynum_workers 같은 제한이 있습니다.

  • Knative는 동시 N 개를 허용
  • 실제 엔진은 동시에 M 개만 처리 가능 (M 이 더 작음)

이 불일치가 있으면 큐가 급격히 쌓이고, 지연이 늘다가 503이 발생합니다.

진단 1: 실제로 스케일링이 따라가고 있는가

현재 스케일 전략이 KPA인지 HPA인지 확인

Knative의 기본은 KPA(Knative Pod Autoscaler)입니다. 환경에 따라 HPA를 쓰기도 합니다.

kubectl -n ${NS} get podautoscaler
kubectl -n ${NS} describe podautoscaler ${PA}

kubectl -n ${NS} get hpa
kubectl -n ${NS} describe hpa ${HPA}
  • KPA면 panic window, stable window, target concurrency 같은 개념이 중요합니다.
  • HPA면 CPU/GPU/커스텀 메트릭 지연이 중요합니다.

스케일 업이 느린 흔한 원인

  1. GPU 노드가 부족해서 FailedScheduling
  2. 이미지 pull이 느림(대형 이미지)
  3. 모델 다운로드를 런타임에 수행(시작마다 네트워크 의존)
  4. 초기화 시간이 길어 readiness가 늦게 True

특히 3번은 NAT 비용과도 연결됩니다. 모델을 매번 외부에서 당겨오면 지연뿐 아니라 egress 비용이 튈 수 있습니다. 비용 관점은 아래 글이 진단 프레임을 제공해줍니다: VPC NAT Gateway 비용 폭증 10분 진단·절감

진단 2: readiness probe가 LLM 현실을 반영하는가

권장 패턴: “모델 준비 완료” 엔드포인트 분리

애플리케이션 내부에 다음 상태를 분리하는 것을 권장합니다.

  • live : 프로세스가 살아있음
  • ready : 모델 로딩 완료, 추론 가능

예시(파이썬 FastAPI):

from fastapi import FastAPI
import threading

app = FastAPI()

model_ready = False

def load_model():
    global model_ready
    # 무거운 모델 로딩
    # tokenizer = ...
    # model = ...
    model_ready = True

@app.on_event("startup")
def startup():
    t = threading.Thread(target=load_model, daemon=True)
    t.start()

@app.get("/health/live")
def live():
    return {"ok": True}

@app.get("/health/ready")
def ready():
    if not model_ready:
        # readiness는 200을 주면 안 됨
        return ("not ready", 503)
    return {"ready": True}

이때 KServe/Knative가 사용하는 readiness probe가 /health/ready 를 보도록 맞춰야 합니다.

초기화가 긴 LLM에서 probe 파라미터가 너무 공격적인 경우

기본 probe 설정이 짧으면, 로딩 중에 실패가 누적되어 재시작 루프로 들어가거나, 준비가 되기 전에 트래픽이 붙었다가 실패합니다.

KServe InferenceService 예시(개념적으로 이해하기 위한 YAML이며, 실제 필드는 런타임에 따라 다를 수 있습니다):

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: llm
spec:
  predictor:
    containers:
      - name: user-container
        image: myrepo/llm-server:latest
        ports:
          - containerPort: 8080
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 8080
          # 모델 로딩이 120초 걸리면 그 이상으로 잡아야 함
          initialDelaySeconds: 10
          periodSeconds: 5
          timeoutSeconds: 2
          failureThreshold: 60
        livenessProbe:
          httpGet:
            path: /health/live
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 2
          failureThreshold: 6

포인트는 readiness 를 쉽게 True 로 만들지 말고, 대신 failureThreshold 를 충분히 크게 잡아 “정상적인 로딩 시간”을 허용하는 것입니다.

진단 3: queue-proxy/동시성/타임아웃이 병목인가

LLM은 “요청 1개가 오래 걸리는” 워크로드라, 동시성 설정이 503에 직결됩니다.

확인할 설정 축

  • containerConcurrency : Pod 하나가 동시에 처리할 요청 수
  • 요청 타임아웃(클라이언트, Ingress, Knative Revision)
  • 스트리밍 응답 여부(스트리밍이면 프록시 타임아웃과 궁합)

containerConcurrency 를 너무 크게 잡으면 GPU 메모리와 KV cache가 터지거나 지연이 폭증합니다. 너무 작게 잡으면 스케일 아웃이 잦아지고, 스케일링 지연 동안 503이 늘 수 있습니다.

실전 권장 접근

  1. 먼저 엔진이 안정적으로 처리 가능한 동시성을 측정
  2. 그 값에 맞춰 containerConcurrency 를 설정
  3. 오토스케일 타깃을 동시성 기반으로 조정

엔진 최적화(4bit, FlashAttention 등)로 “요청당 시간”을 줄이면 503도 같이 줄어듭니다. 로컬/단일 노드 기준이지만 병목 이해에 도움이 됩니다: Transformers 로컬 LLM 느림·OOM, 4bit+FlashAttn2

자주 나오는 원인별 처방전

1) 스케일 0 콜드스타트로 503

증상

  • 유휴 후 첫 요청이 자주 503
  • Pod 생성 이벤트가 503 시점과 겹침

처방

  • 최소 레플리카 유지(스케일 0 방지)
  • 모델을 이미지에 포함하거나, 노드 로컬 캐시/퍼시스턴트 볼륨으로 다운로드 비용 제거
  • 초기화 시간을 반영해 readiness를 보수적으로

2) readiness가 너무 빨리 True

증상

  • Pod는 Ready 로 보이는데 첫 요청들이 실패
  • 서버 로그에 model loading 중 에러 또는 타임아웃

처방

  • readiness 엔드포인트를 “모델 로딩 완료”로 정의
  • 워밍업 요청을 startup 단계에서 수행(가능하면)

3) 동시성 과다로 큐 적체 후 503

증상

  • 트래픽 증가 시 p95/p99 지연이 먼저 튄 뒤 503 발생
  • GPU 메모리 사용량이 급상승

처방

  • containerConcurrency 를 엔진 한계에 맞춤
  • 배치/동적 배치가 있다면 설정하되, 지연과의 트레이드오프 측정
  • 요청 크기 제한(최대 토큰, 최대 입력 길이) 강제

4) 스케줄링 지연(특히 GPU)

증상

  • 이벤트에 FailedScheduling 이 보임
  • GPU 노드 오토스케일이 늦음

처방

  • GPU 노드풀 최소 수량 확보
  • Pod priority, node affinity, taints/tolerations 재점검
  • 이미지 크기 최적화 및 registry pull 가속

503을 줄이는 운영 체크리스트

아래 순서로 보면 대부분의 케이스가 빠르게 정리됩니다.

  1. 503 응답 주체 확인: Ingress 로그, queue-proxy 로그, 앱 로그
  2. ksvcrevisionConditions 에서 Ready 가 왜 False 인지 확인
  3. Pod 이벤트에서 FailedScheduling 또는 probe 실패 여부 확인
  4. readiness가 “모델 준비 완료”를 보장하는지 점검
  5. containerConcurrency 와 엔진 내부 동시성 제한이 일치하는지 점검
  6. 스케일 0 을 쓸지 말지 결정(LLM은 대체로 최소 레플리카 권장)
  7. 모델 다운로드/초기화를 런타임에서 제거해 콜드스타트 단축

마무리

KServe에서 LLM 503은 단순 장애라기보다 “스케일링과 준비 상태의 계약이 깨졌을 때” 나타나는 신호인 경우가 많습니다. 특히 LLM은 콜드스타트가 길고 요청이 무거워서, 웹 API에서 흔히 쓰는 readiness/오토스케일 기본값이 그대로는 잘 맞지 않습니다.

우선 Ready 조건과 Pod 이벤트로 “라우팅이 왜 제외됐는지”를 확인하고, readiness를 모델 준비 완료 기준으로 재정의한 뒤, 동시성과 스케일 정책을 맞추면 503의 상당수를 구조적으로 없앨 수 있습니다.