Published on

KServe로 GPU 모델 롤링업데이트 - 503 0건 만들기

Authors

서빙 중인 GPU 모델을 교체할 때 가장 흔한 장애는 503 Service Unavailable입니다. 특히 KServe는 Knative 기반 트래픽 라우팅과 Pod 스케일링이 결합되어 있어, 새 Pod가 준비되기 전 트래픽이 이동하거나 기존 Pod가 너무 빨리 내려가면서 짧은 공백이 생기기 쉽습니다.

이 글은 “GPU 모델 롤링 업데이트에서 503을 0건으로 만들기”를 목표로, KServe에서 실제로 효과가 큰 설정을 원인별로 분해해 적용하는 방법을 정리합니다. 핵심은 한 가지가 아니라 아래 조합입니다.

  • 새 Revision이 실제로 추론 가능해진 뒤에만 트래픽을 받게 하기
  • 기존 Revision을 충분히 오래 유지해 연결 draining을 보장하기
  • GPU 특유의 모델 로딩/컴파일 지연을 워밍업으로 흡수하기
  • Canary로 “조금씩” 넘기고, 실패 시 즉시 되돌리기

운영 중 성능/안정성 이슈를 같이 다루는 글로는 리눅스 OOM Killer로 프로세스 죽을 때 원인 추적도 함께 보면 좋습니다. GPU 노드에서 메모리/페이지 캐시 압박으로 예기치 않은 종료가 나면 503이 폭증할 수 있습니다.

503이 발생하는 대표 시나리오 (GPU 모델 기준)

1) readiness가 “프로세스 떴음” 수준이라 너무 빨리 Ready

많은 서빙 컨테이너가 HTTP 서버가 뜨면 readiness를 통과시키는데, GPU 모델은 실제로는 다음이 끝나야 합니다.

  • 가중치 로딩 (수백 MB~수십 GB)
  • TensorRT / XLA / CUDA 커널 초기화
  • 첫 요청 시 JIT 컴파일, 메모리 풀 생성

readiness가 이를 반영하지 못하면, 트래픽이 새 Pod로 이동한 직후 첫 요청들이 실패하면서 503이 발생합니다.

2) 기존 Pod가 SIGTERM 이후 너무 빨리 내려감

Knative/Kubernetes는 롤링 업데이트 시 기존 Pod에 종료 시그널을 보내고 일정 시간 뒤 강제 종료합니다. 이때

  • 기존 연결이 아직 처리 중인데 컨테이너가 종료
  • 큐 프록시가 트래픽을 끊는 타이밍이 빨라 요청이 유실

같은 이유로 503이 생깁니다.

3) scale-to-zero, 동시성 설정이 GPU에 비현실적

GPU는 cold start가 매우 비싸서 scale-to-zero나 과도한 동시성은 업데이트 타이밍과 겹칠 때 장애 확률을 올립니다.

  • 스케일 다운 직후 새 Revision 준비 전 공백
  • 동시성 과다로 OOM 또는 latency 폭증 후 타임아웃

목표 상태: “트래픽은 준비된 Pod만 받고, 종료는 느리게”

무중단에 근접한 배포의 핵심은 간단히 말해 다음 2줄입니다.

  • 트래픽 전환은 늦게: 새 Revision이 실제 추론 준비가 끝난 뒤에만
  • 기존 Revision 종료는 느리게: in-flight 요청이 다 끝날 때까지

이를 위해 KServe 리소스, Pod 스펙, 프로브, Knative 애노테이션을 함께 만집니다.

KServe InferenceService 기본 예시 (GPU)

아래는 InferenceService에 GPU를 붙이고, 롤링 업데이트에 필요한 최소한의 “안전장치”를 넣는 예시입니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: llm-gpu
  namespace: model
spec:
  predictor:
    minReplicas: 1
    maxReplicas: 3
    containerConcurrency: 1
    timeout: 300
    model:
      modelFormat:
        name: pytorch
      storageUri: s3://my-bucket/models/llm-v2/
      resources:
        limits:
          nvidia.com/gpu: 1
          cpu: "4"
          memory: "16Gi"
        requests:
          nvidia.com/gpu: 1
          cpu: "2"
          memory: "12Gi"
    podSpec:
      terminationGracePeriodSeconds: 120
      containers:
        - name: kserve-container
          env:
            - name: NVIDIA_VISIBLE_DEVICES
              value: all

여기서 중요한 값은 minReplicas, containerConcurrency, terminationGracePeriodSeconds입니다.

  • minReplicas: 1은 업데이트 순간에 “0으로 떨어지는” 상황을 방지합니다.
  • containerConcurrency: 1은 GPU 메모리 변동이 큰 모델에서 안정성을 높입니다(필요 시 점진적으로 올리세요).
  • terminationGracePeriodSeconds는 아래 preStop과 함께 draining 시간을 확보합니다.

1단계: readiness를 “모델 추론 가능”으로 정의하기

권장: 실제 추론 워밍업이 끝났을 때만 Ready

가장 확실한 방법은 컨테이너 내부에 healthz/ready 엔드포인트를 두고, 다음 조건을 만족할 때만 200을 반환하게 하는 것입니다.

  • 모델 로딩 완료
  • GPU 메모리 할당 및 커널 초기화 완료
  • (가능하면) 더미 입력 1회 추론 성공

FastAPI 기반이라면 아래처럼 구현할 수 있습니다.

from fastapi import FastAPI
import threading

app = FastAPI()
ready = False


def warmup():
    global ready
    # 1) 모델 로딩
    # 2) GPU 초기화
    # 3) 더미 추론 1회
    # 예: model.generate(dummy)
    ready = True


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


@app.get("/healthz/ready")
def health_ready():
    return {"ready": ready} if ready else ("not ready", 503)

그리고 Kubernetes readinessProbe로 이 엔드포인트를 체크합니다.

readinessProbe:
  httpGet:
    path: /healthz/ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 2
  timeoutSeconds: 1
  failureThreshold: 60

failureThreshold: 60periodSeconds: 2 조합이면 최대 120초까지 “아직 준비 안 됨”을 허용합니다. GPU 모델이 큰 경우 이 정도 버퍼가 실제로 필요합니다.

liveness는 “프로세스 생존”으로 분리

readiness와 liveness를 같은 조건으로 두면, 워밍업이 길어질 때 liveness가 실패해 재시작 루프에 빠질 수 있습니다.

  • liveness: 서버가 응답하는지
  • readiness: 모델이 추론 가능한지

이렇게 분리하세요.

2단계: 종료 시점에 트래픽을 끊지 말고 draining 하기

preStop으로 “종료 전에 먼저 Ready 해제”

종료 시그널을 받으면 즉시 readiness를 실패로 바꿔 “새 요청 유입”을 막고, 잠깐 대기 후 종료하는 패턴이 효과적입니다.

lifecycle:
  preStop:
    exec:
      command:
        - /bin/sh
        - -c
        - |
          echo "preStop: mark not ready";
          curl -sf -X POST http://127.0.0.1:8080/admin/disable-ready || true;
          echo "preStop: draining...";
          sleep 30

애플리케이션에 /admin/disable-ready 같은 내부 엔드포인트를 두고, 호출되면 ready = False로 바꾸면 됩니다.

  • 즉시 Ready 해제: 새로운 요청 유입 차단
  • sleep 30: in-flight 요청 처리 시간 확보

그리고 terminationGracePeriodSecondssleep보다 크게 잡아야 합니다.

terminationGracePeriodSeconds: 120

타임아웃도 함께 올리기

GPU 추론은 요청당 시간이 길 수 있어, 배포 순간 tail latency가 늘면 상위에서 503으로 관측되기도 합니다. InferenceServicetimeout을 모델 특성에 맞게 조정하세요.

spec:
  predictor:
    timeout: 300

3단계: Canary 트래픽으로 “조금씩” 넘기기

KServe는 Knative Revision 기반으로 트래픽을 나눌 수 있습니다. 운영에서 가장 안전한 방식은

  • 새 모델 v2를 배포하되
  • 처음에는 1~5%만 보내서
  • 지표(에러율, 지연, GPU 메모리)를 확인한 뒤
  • 점진적으로 100%로 올리는 것

입니다.

구체적인 트래픽 분할은 환경마다 다르지만, 핵심은 “한 번에 100% 스위치”를 피하는 것입니다. 특히 GPU 모델은 워밍업 이후에도 첫 몇 분간 캐시/메모리 풀 안정화로 지연이 출렁일 수 있습니다.

배포 자동화 파이프라인에서 Canary 단계가 길어지면 CI 시간도 늘 수 있는데, 캐시가 꼬여 빌드가 느려진 상황이라면 GitHub Actions 캐시 무효화로 빌드 느림 해결처럼 파이프라인 자체를 먼저 안정화하는 것도 중요합니다.

4단계: scale-down을 늦춰 “구 Revision”을 안전망으로 유지

GPU 모델에서 503을 0에 가깝게 만들려면, 새 Revision이 안정화될 때까지 구 Revision을 어느 정도 유지하는 편이 좋습니다.

  • minReplicas1 이상으로 유지
  • 트래픽 100% 전환 직후에도 구 Revision이 바로 0이 되지 않게 유도

Knative 기반에서는 유휴 시간 후 scale-to-zero가 동작할 수 있으므로, 운영 정책상 scale-to-zero를 끄거나 유휴 시간을 늘리는 선택을 검토하세요. 비용 최적화보다 “무중단”이 우선인 워크로드(결제, 실시간 추천, 상담봇 등)에서는 특히 그렇습니다.

5단계: GPU 특유의 병목을 업데이트 전에 제거하기

(1) 이미지 Pull 시간

대형 이미지, 프라이빗 레지스트리, 노드 캐시 미스는 업데이트 때 공백을 만듭니다.

  • 이미지 사이즈 줄이기
  • 노드에 이미지 프리풀(daemonset)
  • 레지스트리 네트워크/인증 병목 제거

(2) 모델 다운로드 시간

storageUri가 S3나 GCS인 경우, 모델 다운로드가 readiness 이전에 끝나야 합니다.

  • 모델 아티팩트를 노드 로컬 캐시로 프리로드
  • 멀티파트 다운로드 최적화
  • 압축 포맷과 로딩 속도 튜닝

(3) 메모리/스왑/페이지 캐시 압박

GPU 노드에서 CPU 메모리도 부족하면, 로딩 중 OOM으로 죽고 재시작하면서 503이 반복됩니다. 이 경우 커널 로그와 컨테이너 종료 코드를 함께 봐야 합니다. 관련해서는 리눅스 OOM Killer로 프로세스 죽을 때 원인 추적에서 소개한 방식대로 dmesg, cgroup 메모리 이벤트, 종료 시그널을 추적하세요.

운영 체크리스트: “503 0건”에 가까워지는 조건

아래 항목 중 하나라도 빠지면, 배포 시점에 간헐적 503이 남는 경우가 많습니다.

  1. readiness가 “모델 추론 가능”을 의미한다
  2. liveness는 readiness와 분리되어 있다
  3. preStop으로 Ready 해제 후 draining 대기가 있다
  4. terminationGracePeriodSeconds가 충분히 크다
  5. minReplicas1 이상이다
  6. Canary로 트래픽을 점진적으로 전환한다
  7. 이미지/모델 다운로드가 병목이 아니다
  8. GPU/CPU 메모리 헤드룸이 충분하다

배포 후 검증: 503을 “관측”하고 “재현” 가능하게 만들기

무중단은 설정만으로 끝나지 않습니다. 관측이 없으면 503이 0인지도 확신하기 어렵습니다.

  • Ingress 또는 Gateway 레벨에서 5xx 비율
  • KServe/Knative Revision별 요청 수, 에러 수
  • Pod 이벤트에서 Killing, Unhealthy, Readiness probe failed
  • 컨테이너 로그에서 워밍업 완료 시각과 첫 요청 시각

가능하면 배포 직후 5분 동안은 다음을 자동으로 실행하세요.

kubectl -n model get pods -w
kubectl -n model describe pod -l serving.kserve.io/inferenceservice=llm-gpu

그리고 애플리케이션 로그에 아래 두 줄은 반드시 남기세요.

  • warmup_started_at
  • warmup_completed_at

이 두 시각이 readiness 통과 시점과 일치하면, “트래픽이 언제부터 들어왔는지”를 명확히 증명할 수 있습니다.

마무리

KServe에서 GPU 모델을 롤링 업데이트할 때 503을 0건으로 만들려면, 단순히 minReplicas만 올리는 접근으로는 부족합니다. readiness를 진짜 준비 상태로 만들고, 종료를 느리게(draining) 만들며, Canary로 전환을 쪼개는 것이 가장 확실한 조합입니다.

이 조합을 적용하면 배포 순간의 짧은 공백(특히 cold start, 모델 로딩, SIGTERM 처리)이 대부분 사라지고, 503은 “가끔 보이는 이벤트”가 아니라 “관측되지 않는 상태”에 가까워집니다.