- Published on
KServe+Knative로 GPU 모델 무중단 카나리 배포
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
왜 KServe+Knative로 GPU 모델 카나리를 하나
GPU 추론 서비스는 일반 웹 서비스보다 배포 리스크가 큽니다. 모델 파일이 크고 콜드 스타트가 길며, GPU 자원은 비싸고 수가 제한적이라 롤아웃 중 잠깐의 과부하도 바로 장애로 이어집니다. 또한 모델 교체는 단순 코드 배포가 아니라 정확도/편향/지연시간 같은 품질 지표까지 함께 검증해야 합니다.
KServe는 모델 서빙에 필요한 추상화(모델 아티팩트 로딩, 프로토콜, 오토스케일, 버저닝)를 제공하고, 내부적으로 Knative Serving을 사용해 리비전 기반 배포와 트래픽 분할을 수행합니다. 즉, GPU 모델을 무중단으로 올리면서 일부 트래픽만 새 모델로 보내고, 지표가 나쁘면 즉시 되돌리는 카나리 전략을 비교적 단순한 선언형 설정으로 구현할 수 있습니다.
이 글에서는 다음 목표를 달성합니다.
- GPU 기반 InferenceService를 운영 중인 상태에서
- 새 모델 버전을 별도 리비전으로 띄우고
- 트래픽을
90/10 -> 50/50 -> 100/0처럼 점진적으로 전환하며 - 준비 지연(모델 다운로드, TensorRT 엔진 로딩 등)과 GPU 스케줄링 병목을 고려해
- 실패 시 빠르게 롤백하는 운영 패턴을 정리합니다.
모델 최적화(예: ONNX, TensorRT)로 콜드 스타트를 줄이는 방법은 별도 글인 파이썬 CNN·Transformer ONNX+TensorRT 10배 튜닝도 함께 참고하면 배포 안정성이 크게 좋아집니다.
아키텍처 개요: 리비전과 트래픽 스플리팅
구성 요소를 한 문장으로 요약하면 다음과 같습니다.
- KServe InferenceService: 모델 서빙 CRD
- Knative Service/Revision: 배포 단위(리비전)와 트래픽 라우팅
- Queue-Proxy: 요청 버퍼링/동시성/프로빙 등 Knative 런타임
- Autoscaler(KPA/HPA): 요청 기반 스케일링(기본은 Knative Pod Autoscaler)
카나리 배포에서 핵심은 “새 모델은 새 리비전”이라는 점입니다. InferenceService의 predictor 스펙을 바꾸면 Knative가 새 Revision을 만들고, 트래픽을 기존/신규 리비전으로 분배할 수 있습니다.
운영 관점에서 아래 3가지를 분리해 생각하면 설계가 쉬워집니다.
- 배포 단위: 리비전(immutable)
- 전환 단위: 트래픽 비율(가변)
- 안전장치: readiness, minScale, maxScale, PDB, 관측 지표
사전 준비 체크리스트(GPU 환경에서 특히 중요)
1) GPU 스케줄링과 리소스 요청
resources.limits에nvidia.com/gpu: 1처럼 GPU를 명시해야 합니다.- CPU/메모리를 너무 낮게 잡으면 모델 로딩 중 OOM이나 스로틀링으로 readiness가 늦어집니다.
2) 콜드 스타트 완화: minScale과 워밍업
카나리 리비전이 10% 트래픽을 받기 시작할 때 첫 요청이 곧 워밍업이 되면 지연시간이 튀고 타임아웃이 터집니다. GPU 모델은 특히 심각합니다.
- 카나리 시작 전, 신규 리비전을
minScale=1로 올려 미리 모델을 로드 - 또는 트래픽을 0%로 둔 상태에서 내부 워밍업 호출(관리용 Job) 실행
3) 모델 아티팩트 다운로드 경로
모델이 S3/GCS 같은 원격 스토리지에 있다면, 배포 순간마다 수 GB 다운로드가 발생할 수 있습니다.
- 노드 로컬 캐시, 이미지에 번들링, 혹은 PV 캐시 전략을 고려
- 네트워크가 느리면 readiness가 길어져 롤아웃이 지연됨
4) 타임아웃과 데드라인 전파
콜드 스타트/워밍업 구간에서 gRPC/HTTP 타임아웃이 지나치게 짧으면 “실제는 준비 중인데 클라이언트가 먼저 포기”하는 형태로 장애가 관측됩니다. 데드라인 설계는 gRPC 타임아웃 지옥 탈출 - 데드라인 전파 설계도 같이 보면 좋습니다.
기본 InferenceService 예제(GPU + TensorRT 컨테이너 가정)
아래는 KServe InferenceService 예시입니다. 핵심은 GPU 리소스, 프로브(ready), 그리고 Knative 어노테이션(minScale)입니다.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: triton-llm
namespace: ml-serving
annotations:
autoscaling.knative.dev/minScale: "1"
autoscaling.knative.dev/maxScale: "4"
autoscaling.knative.dev/target: "5"
spec:
predictor:
containers:
- name: kserve-container
image: nvcr.io/nvidia/tritonserver:24.01-py3
args:
- tritonserver
- --model-repository=/models
- --http-port=8080
- --grpc-port=9000
ports:
- containerPort: 8080
name: http1
resources:
limits:
cpu: "4"
memory: 16Gi
nvidia.com/gpu: "1"
requests:
cpu: "2"
memory: 12Gi
readinessProbe:
httpGet:
path: /v2/health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 12
livenessProbe:
httpGet:
path: /v2/health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
volumeMounts:
- name: model-repo
mountPath: /models
volumes:
- name: model-repo
persistentVolumeClaim:
claimName: triton-model-pvc
포인트:
minScale을 1로 두면 “0으로 스케일 다운”이 방지되어 콜드 스타트 리스크가 줄어듭니다.target은 동시성 목표치입니다. GPU 모델은 동시성을 너무 높이면 오히려 지연시간이 폭증하므로 작은 값부터 시작합니다.- readiness는 “모델 로딩 완료 후 성공”하도록 잡아야 합니다. 단순히 프로세스가 떠있다는 신호로는 부족합니다.
카나리 배포: 리비전 생성과 트래픽 분할
KServe는 내부적으로 Knative Service를 만들고, Knative는 Revision을 생성합니다. 카나리 배포 절차는 다음 흐름으로 진행합니다.
- 기존 모델 리비전(예:
rev-a) 100% - 새 모델 스펙으로 업데이트하여 신규 리비전(예:
rev-b) 생성 - 트래픽을
90/10으로 분할 - 지표 확인 후 점진적으로 확대
- 문제 있으면 즉시
100/0으로 되돌리기
1) 새 모델로 업데이트(새 리비전 유도)
예를 들어 모델 저장소 경로 또는 이미지 태그를 바꾸면 새 리비전이 생성됩니다.
kubectl -n ml-serving apply -f inference-service.yaml
kubectl -n ml-serving get revisions
2) 트래픽 분할 설정
트래픽 분할은 Knative Service(ksvc) 레벨에서 수행하는 것이 직관적입니다. KServe가 만든 Knative Service 이름을 확인한 뒤 패치합니다.
kubectl -n ml-serving get ksvc
리비전 이름을 확인합니다.
kubectl -n ml-serving get revisions --sort-by=.metadata.creationTimestamp
이제 90/10 카나리로 전환합니다.
kubectl -n ml-serving patch ksvc triton-llm \
--type merge \
-p '{"spec":{"traffic":[
{"revisionName":"triton-llm-00001","percent":90},
{"revisionName":"triton-llm-00002","percent":10}
]}}'
주의: 리비전 이름은 예시이며 환경마다 다릅니다.
3) 단계적 확대
운영에서는 보통 아래처럼 “관측 가능한 구간”을 둡니다.
- 10%: 기능/정확도/에러율/지연시간의 급격한 악화 여부
- 50%: GPU 사용률 포화 및 큐잉 지연 여부
- 100%: 완전 전환 후에도 안정적인지
각 단계는 동일한 patch로 비율만 바꿉니다.
무중단을 위한 핵심: 준비 완료 전 트래픽을 받지 않게 하기
무중단 카나리에서 가장 흔한 장애는 “새 리비전이 아직 모델 로딩 중인데 트래픽이 흘러 들어가서 5xx/타임아웃이 발생”하는 패턴입니다.
이를 막는 3종 세트를 같이 적용합니다.
1) readinessProbe를 모델 준비 완료 기준으로
앞서 예시처럼 Triton의 ready 엔드포인트를 사용하거나, 커스텀 서버라면 모델 로드 완료 시점에만 200을 반환하도록 구현합니다.
2) minScale로 카나리 리비전 선기동
카나리 리비전이 만들어진 직후 minScale=1이면 즉시 Pod이 떠서 모델을 로드합니다. 트래픽을 0%로 두고도 워밍업이 가능합니다.
3) 클라이언트 타임아웃을 콜드 스타트 현실에 맞추기
특히 gRPC는 deadline exceeded가 관측되면 서버 문제인지 클라이언트 데드라인 문제인지 헷갈립니다. 원인 분해는 Go gRPC context deadline exceeded 원인·해결에서 제시하는 체크리스트가 도움이 됩니다.
GPU 모델에서 카나리 시 흔히 겪는 함정과 대응
1) 트래픽 10%인데 GPU는 100%가 되는 현상
원인 후보:
- 요청이 무거워서 10%만으로도 GPU가 포화
- 동시성 설정이 과도해 큐잉과 배치가 꼬임
- 모델이 커져서 latency가 증가
대응:
- Knative
target(동시성 목표)을 낮추고,maxScale을 올려 수평 확장 여지를 확보 - 서버 내부 배치/스레드 설정을 조정
- 모델 최적화(엔진 빌드, 정밀도 FP16/INT8)로 latency 자체를 낮춤
2) 카나리 리비전이 스케줄링이 안 됨
원인 후보:
- GPU 노드 부족
nvidia.com/gpu요청 수가 과다- 노드 셀렉터/테인트 톨러레이션 미설정
대응:
- GPU 노드 풀 오토스케일러 설정 확인
nodeSelector/tolerations로 GPU 노드에만 스케줄되게 강제
예시:
spec:
predictor:
containers:
- name: kserve-container
resources:
limits:
nvidia.com/gpu: "1"
nodeSelector:
node.kubernetes.io/instance-type: gpu
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
3) 롤백했는데도 장애가 지속됨
원인 후보:
- 기존 리비전이 이미 scale down 되었거나
- 트래픽은 되돌렸지만 클라이언트 캐시/리졸브가 늦거나
- 외부 의존성(모델 스토리지, 인증, DNS)이 병목
대응:
- 안정 리비전에도
minScale=1을 유지해 “항상 즉시 응답 가능한 용량” 확보 - 관측 지표를 “서버 5xx” 뿐 아니라 “큐 길이/대기시간”까지 포함
관측(Observability): 카나리 성공/실패를 판정하는 지표
카나리 배포는 결국 “새 리비전이 더 좋거나 최소한 나쁘지 않다”를 데이터로 증명하는 과정입니다. GPU 모델에서는 아래를 기본 세트로 권장합니다.
- 에러율: HTTP 5xx, gRPC status 비율
- 지연시간: p50/p95/p99, 특히 p99
- 준비 시간: Pod 생성부터 readiness까지 걸린 시간
- GPU 지표: GPU utilization, memory usage, throttling
- 큐/동시성: queue-proxy 대기시간, in-flight 요청 수
실무에서는 “카나리 10% 구간에서 p99가 기준 대비 2배 이상 상승” 같은 자동 중단 조건을 걸어두면 안전합니다.
빠른 롤백 전략(리비전 기반)
Knative 트래픽 분할의 장점은 롤백이 매우 빠르다는 점입니다. 새 리비전에 문제가 있으면 트래픽을 즉시 0%로 만들면 됩니다.
kubectl -n ml-serving patch ksvc triton-llm \
--type merge \
-p '{"spec":{"traffic":[
{"revisionName":"triton-llm-00001","percent":100},
{"revisionName":"triton-llm-00002","percent":0}
]}}'
여기서 중요한 운영 팁:
- 실패 리비전을 바로 삭제하기보다, 원인 분석을 위해 잠시 남겨두는 편이 좋습니다.
- 단, GPU 비용이 크면
minScale을 실패 리비전에만 0으로 내려 비용을 차단합니다.
운영 자동화: 점진적 트래픽 전환 스크립트 예시
아래는 리비전 이름을 받아 90/10 -> 50/50 -> 100/0로 전환하는 단순 스크립트 예시입니다.
#!/usr/bin/env bash
set -euo pipefail
NS="ml-serving"
KSVC="triton-llm"
STABLE_REV="$1"
CANARY_REV="$2"
step() {
local stable="$1"; local canary="$2"
kubectl -n "$NS" patch ksvc "$KSVC" --type merge \
-p '{"spec":{"traffic":[
{"revisionName":"'"$STABLE_REV"'","percent":'"$stable"'},
{"revisionName":"'"$CANARY_REV"'","percent":'"$canary"'}
]}}'
}
step 90 10
sleep 300
step 50 50
sleep 600
step 0 100
실전에서는 sleep 대신 Prometheus 쿼리 결과나 SLO 게이트를 붙여 “지표가 기준을 만족할 때만 다음 단계로 진행”하도록 만듭니다.
정리: GPU 모델 카나리의 성공 조건
KServe+Knative 조합으로 GPU 모델을 무중단 카나리 배포하려면, YAML만 맞추는 것보다 아래 운영 조건이 더 중요합니다.
- readiness는 모델 준비 완료를 의미해야 한다
- 카나리 리비전은 트래픽 전환 전에 미리 떠서 워밍업돼야 한다(
minScale) - 동시성/스케일링은 GPU 포화 특성을 반영해 보수적으로 시작한다
- 판정 지표를 p99 지연시간과 GPU 포화까지 포함해 설계한다
- 롤백은 트래픽 0%로 즉시 수행하고, 원인 분석을 위한 흔적은 남긴다
이 5가지만 지키면, “새 모델이 준비되는 동안 기존 모델이 안정적으로 서비스하고, 준비가 끝난 순간부터만 새 모델이 트래픽을 받는” 형태의 진짜 무중단 카나리를 구현할 수 있습니다.