- Published on
KServe로 GPU 모델 롤링업데이트 - 503 0건 만들기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서빙 중인 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: 60과 periodSeconds: 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 요청 처리 시간 확보
그리고 terminationGracePeriodSeconds는 sleep보다 크게 잡아야 합니다.
terminationGracePeriodSeconds: 120
타임아웃도 함께 올리기
GPU 추론은 요청당 시간이 길 수 있어, 배포 순간 tail latency가 늘면 상위에서 503으로 관측되기도 합니다. InferenceService의 timeout을 모델 특성에 맞게 조정하세요.
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을 어느 정도 유지하는 편이 좋습니다.
minReplicas를1이상으로 유지- 트래픽 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이 남는 경우가 많습니다.
- readiness가 “모델 추론 가능”을 의미한다
- liveness는 readiness와 분리되어 있다
preStop으로 Ready 해제 후 draining 대기가 있다terminationGracePeriodSeconds가 충분히 크다minReplicas가1이상이다- Canary로 트래픽을 점진적으로 전환한다
- 이미지/모델 다운로드가 병목이 아니다
- 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_atwarmup_completed_at
이 두 시각이 readiness 통과 시점과 일치하면, “트래픽이 언제부터 들어왔는지”를 명확히 증명할 수 있습니다.
마무리
KServe에서 GPU 모델을 롤링 업데이트할 때 503을 0건으로 만들려면, 단순히 minReplicas만 올리는 접근으로는 부족합니다. readiness를 진짜 준비 상태로 만들고, 종료를 느리게(draining) 만들며, Canary로 전환을 쪼개는 것이 가장 확실한 조합입니다.
이 조합을 적용하면 배포 순간의 짧은 공백(특히 cold start, 모델 로딩, SIGTERM 처리)이 대부분 사라지고, 503은 “가끔 보이는 이벤트”가 아니라 “관측되지 않는 상태”에 가까워집니다.