- Published on
KServe+Istio로 GPU 모델 카나리 배포·롤백
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
GPU 추론 모델은 배포 실패 비용이 큽니다. 새 이미지가 뜨지 않거나(드라이버, CUDA, 의존성), 성능이 미묘하게 나빠지거나(레이턴시, OOM), 특정 입력에서만 폭발하는 문제가 흔합니다. 그래서 한 번에 100% 교체가 아니라 소량 트래픽으로 검증 후 점진 확대가 정석입니다.
Kubernetes 환경에서는 KServe가 모델 서빙의 표준 추상화(InferenceService)를 제공하고, Istio는 L7 트래픽 제어(가중치 라우팅, 헤더 기반 라우팅, 미러링)를 제공합니다. 이 글에서는 KServe + Istio 조합으로 GPU 모델을 카나리 배포하고, 문제가 생기면 즉시 롤백하는 구체적인 설계를 다룹니다.
또한 카나리 중 자주 겪는 장애인 CrashLoopBackOff/프로브 오탐, GPU OOM 등을 어떻게 관찰하고 롤백 트리거로 연결할지도 함께 정리합니다.
- 관련 참고: K8s CrashLoopBackOff - liveness probe 오탐 해결
- 관련 참고: Stable Diffusion VRAM OOM - xFormers·VAE 타일링
전체 아키텍처: KServe가 “서빙”, Istio가 “트래픽”
핵심은 역할 분리입니다.
- KServe: 모델 버전별 워크로드(Deployment/Knative Service), 스케일링, GPU 리소스 요청, predictor/transformer 구조
- Istio: 두 버전의 엔드포인트 사이 트래픽 분할(예: 95/5), 특정 사용자만 카나리로 라우팅, 장애 시 100% 이전 버전으로 즉시 복구
실전에서는 보통 다음 2가지 방식 중 하나를 씁니다.
- 두 개의 InferenceService를 병렬로 띄우고 Istio로 분할
model-v1,model-v2를 각각 독립 서비스로 운영- Istio
VirtualService에서 가중치 라우팅 - 장점: 버전 간 완전 격리, 롤백이 단순(가중치만 되돌림)
- 단점: 리소스가 2배 가까이 필요(특히 GPU)
- KServe의 canary 기능(내부 트래픽 분할)을 활용
- KServe가 제공하는 카나리 스펙을 사용
- 장점: KServe 레벨에서 일관된 UX
- 단점: 클러스터/설치 구성에 따라 제약이 있고, Istio 고급 라우팅(헤더/사용자군)이 필요하면 다시 Istio를 섞게 됨
이 글은 가장 통제력이 높은 1) 방식을 기준으로 설명합니다. GPU는 비싸지만, 카나리 기간을 짧게 가져가면 “안전 비용”으로 충분히 합리화됩니다.
사전 준비 체크리스트
1) Istio 사이드카 주입과 mTLS 정책
- 네임스페이스에
istio-injection=enabled라벨 적용 - mTLS가
STRICT인 경우, KServe/Knative 구성 요소와 통신이 막히지 않는지 확인
kubectl label namespace ml istio-injection=enabled
kubectl get peerauthentication -A
2) KServe 설치 모드(서버리스/Raw Deployment)
KServe는 환경에 따라 Knative 기반 서버리스 모드 또는 Raw Deployment 모드로 운영됩니다. GPU 워크로드는 다음을 특히 확인하세요.
- 노드에 NVIDIA 드라이버/디바이스 플러그인 설치
- GPU 스케줄링을 위한
nodeSelector/tolerations/runtimeClassName필요 여부 - 콜드스타트가 치명적이면
minReplicas를 1 이상으로
3) 관측 지표: “롤백 결정을 자동화”하려면
카나리의 목적은 정량 지표로 안전하게 전환하는 것입니다. 최소한 아래는 준비해두는 것을 권장합니다.
- 성공률(HTTP 2xx, gRPC OK)
- p95/p99 레이턴시
- GPU 메모리 사용량, OOM 이벤트
- 재시작 횟수(
CrashLoopBackOff)
Prometheus/Grafana가 없다면, 최소한 kubectl describe pod 이벤트와 컨테이너 로그에서 OOM/프로브 실패를 빠르게 확인하는 루틴을 만들어야 합니다.
예시: GPU 모델 v1/v2 InferenceService 정의
아래는 ml 네임스페이스에 v1/v2를 각각 배포하는 예시입니다. 모델 서버는 TorchServe/TFServing/커스텀 서버 등 무엇이든 가능하지만, 여기서는 “컨테이너 이미지로 서빙”하는 형태로 단순화합니다.
v1 InferenceService
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: gpu-model-v1
namespace: ml
spec:
predictor:
containers:
- name: predictor
image: registry.example.com/ml/gpu-model:1.0.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "2"
memory: "8Gi"
nvidia.com/gpu: "1"
limits:
cpu: "4"
memory: "16Gi"
nvidia.com/gpu: "1"
env:
- name: MODEL_NAME
value: "gpu-model"
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
v2 InferenceService(카나리 후보)
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: gpu-model-v2
namespace: ml
spec:
predictor:
containers:
- name: predictor
image: registry.example.com/ml/gpu-model:2.0.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "2"
memory: "8Gi"
nvidia.com/gpu: "1"
limits:
cpu: "4"
memory: "16Gi"
nvidia.com/gpu: "1"
env:
- name: MODEL_NAME
value: "gpu-model"
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
배포 후 주소를 확인합니다.
kubectl -n ml get inferenceservice
kubectl -n ml get svc | grep gpu-model
KServe가 만드는 서비스 이름/호스트는 설치 모드에 따라 다를 수 있습니다. 이후 Istio 라우팅에서는 “클러스터 내부 DNS 이름”을 대상으로 잡으면 가장 단순합니다(예: gpu-model-v1-predictor.ml.svc.cluster.local).
Istio로 95/5 카나리 트래픽 분할
이제 외부/내부 클라이언트가 호출하는 “단일 엔드포인트”를 만들고, 그 뒤에서 v1/v2로 나눕니다.
구성은 보통 아래 3종 세트입니다.
Gateway: 외부 인바운드(선택)VirtualService: 라우팅 규칙DestinationRule: 서브셋(버전) 정의 및 트래픽 정책
DestinationRule: v1/v2 서브셋 정의
아래 예시는 “단일 서비스 이름”을 앞단에 두고 서브셋으로 나누는 전형적인 패턴입니다. 여기서는 gpu-model이라는 “논리 호스트”로 묶고, 실제 라우팅은 subset별로 각기 다른 host로 보내는 방식 대신, VirtualService에서 라우트 목적지를 직접 v1/v2 서비스로 지정하는 형태를 사용합니다(더 직관적이고 KServe 환경 차이를 덜 탑니다).
즉, DestinationRule은 생략 가능하지만, 연결 풀/아웃라이어 탐지 같은 정책을 넣고 싶다면 권장됩니다.
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: gpu-model-dr
namespace: ml
spec:
host: gpu-model.ml.svc.cluster.local
trafficPolicy:
connectionPool:
http:
http2MaxRequests: 1000
maxRequestsPerConnection: 100
outlierDetection:
consecutive5xxErrors: 5
interval: 5s
baseEjectionTime: 30s
maxEjectionPercent: 50
VirtualService: 95%는 v1, 5%는 v2
host/gateways는 환경에 맞게 조정하세요. 내부 전용이면 gateways 없이 메시 내부 라우팅만 적용할 수도 있습니다.
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: gpu-model-vs
namespace: ml
spec:
hosts:
- gpu-model.ml.svc.cluster.local
http:
- route:
- destination:
host: gpu-model-v1-predictor.ml.svc.cluster.local
port:
number: 80
weight: 95
- destination:
host: gpu-model-v2-predictor.ml.svc.cluster.local
port:
number: 80
weight: 5
timeout: 30s
retries:
attempts: 2
perTryTimeout: 10s
retryOn: 5xx,connect-failure,refused-stream
이제 클라이언트는 gpu-model.ml.svc.cluster.local만 호출하면 되고, Istio가 95/5로 분할합니다.
“특정 사용자만” 카나리: 헤더 기반 라우팅
가중치 카나리는 전체 트래픽 일부를 무작위로 흘립니다. 하지만 GPU 모델은 입력 분포에 따라 성능/메모리 특성이 달라서, 내부 QA 트래픽만 v2로 보내고 싶을 때가 많습니다.
예: 요청 헤더 x-canary: 1이 있으면 v2로 100% 라우팅.
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: gpu-model-vs
namespace: ml
spec:
hosts:
- gpu-model.ml.svc.cluster.local
http:
- match:
- headers:
x-canary:
exact: "1"
route:
- destination:
host: gpu-model-v2-predictor.ml.svc.cluster.local
port:
number: 80
weight: 100
- route:
- destination:
host: gpu-model-v1-predictor.ml.svc.cluster.local
port:
number: 80
weight: 100
이 패턴은 “내부 검증” 단계에서 특히 유용합니다. 검증이 끝나면 위 규칙을 95/5, 50/50, 0/100으로 점진 변경하면 됩니다.
롤백 전략: “가중치 0으로 즉시 차단” + “리소스 회수”
롤백은 두 단계로 나누면 안전합니다.
- 즉시 롤백(트래픽 차단): Istio
VirtualService에서 v2 weight를 0으로 - 사후 처리(리소스 회수): v2 InferenceService를 scale down 또는 삭제
즉시 롤백: v2 weight를 0으로
kubectl -n ml patch virtualservice gpu-model-vs --type='json' -p='[
{"op":"replace","path":"/spec/http/0/route/0/weight","value":100},
{"op":"replace","path":"/spec/http/0/route/1/weight","value":0}
]'
json patch 경로는 YAML 구조에 따라 달라질 수 있으니, 먼저 아래로 현재 스펙을 확인하세요.
kubectl -n ml get virtualservice gpu-model-vs -o yaml
리소스 회수: v2를 0으로 스케일(가능한 경우)
KServe/Knative 조합에서는 minReplicas 설정에 따라 0으로 내려갈 수도, 못 내려갈 수도 있습니다. Raw Deployment 모드라면 HPA/replica 조정으로 내릴 수 있습니다.
가장 단순한 방법은 “카나리 종료 후 v2 삭제”입니다.
kubectl -n ml delete inferenceservice gpu-model-v2
GPU 노드가 빡빡한 환경이라면, 카나리 중에도 v2를 항상 띄워두기 어렵습니다. 이 경우 “헤더 기반 라우팅 + 짧은 검증 윈도우”로 운영하고, 검증 시간 외에는 v2를 내리는 식으로 비용을 줄입니다.
카나리 실패의 대표 원인과 대응 포인트
1) CrashLoopBackOff: 프로브 오탐으로 ‘정상인데 죽는’ 경우
모델 서버는 초기 로딩이 길고(GPU 메모리 로드, 가중치 로딩), 첫 요청 전까지 준비가 늦을 수 있습니다. 이때 livenessProbe가 너무 공격적이면, 준비 중인 프로세스를 계속 죽여 CrashLoopBackOff로 빠집니다.
대응:
readinessProbe와livenessProbe를 분리startupProbe를 지원하는 런타임이면 적극 사용- 초기 로딩 시간을 반영해
initialDelaySeconds/failureThreshold조정
프로브 오탐 패턴과 튜닝은 아래 글에서 더 자세히 다뤘습니다.
2) GPU OOM: 카나리에서 “특정 입력”만 터지는 문제
새 버전이 더 큰 컨텍스트를 받거나, 배치 전략이 바뀌거나, 정밀도가 FP16에서 FP32로 올라가면 VRAM이 급격히 증가합니다. 카나리에서는 “평균”보다 “최악 케이스”가 중요합니다.
대응 체크:
- 입력 상한(토큰/해상도/배치) 강제
- 모델 로딩 옵션(예:
torch.set_default_dtype,autocast) 점검 - 메모리 최적화 옵션(xFormers, 타일링 등) 적용
이미지/확산 모델 계열이라면 아래 최적화 전략이 직접적으로 도움이 됩니다.
3) 레이턴시 회귀: “성공률은 유지되는데 느려짐”
카나리에서 흔한 함정은 에러율만 보고 통과시키는 것입니다. GPU 모델은 느려지면 곧바로 비용 증가(더 많은 GPU 필요)로 이어집니다.
권장 게이트:
- v2의 p95 레이턴시가 v1 대비
+10%이상이면 자동 중단 - 큐잉 지연(서버 내부 배치 대기)이 증가하는지 확인
Istio의 텔레메트리(요청 지표)와 애플리케이션 레벨 지표(모델 추론 시간)를 함께 봐야 원인을 분리할 수 있습니다.
운영 팁: 카나리 절차를 “반복 가능한 런북”으로 만들기
실무에서는 아래 순서로 런북을 고정해두면, 야간 장애에서도 흔들리지 않습니다.
- v2 배포(트래픽 0)
InferenceService생성- 준비 완료 확인(
Ready=True)
kubectl -n ml wait --for=condition=Ready inferenceservice/gpu-model-v2 --timeout=600s
- 내부 QA만 v2로(헤더 라우팅)
x-canary: 1로만 유입- OOM/재시작/레이턴시 확인
- 95/5 카나리 시작
- 5분~30분 관찰(트래픽 규모에 따라)
- 50/50, 0/100 단계적 확대
- 각 단계에서 SLO 통과 확인
- 최종 전환 후 v1 유지 기간 설정
- 즉시 삭제하지 말고 “핫 스탠바이”로 하루 유지하는 조직도 많습니다
- 문제 시 롤백
- Istio weight를 즉시 100/0으로
- v2 삭제 또는 원인 분석 후 재배포
마무리: KServe와 Istio 조합의 장점
- KServe는 모델 서빙을 표준화해 배포 단위를 단순화합니다.
- Istio는 트래픽을 “정교하게” 제어해 카나리/롤백을 빠르고 안전하게 만듭니다.
- GPU 모델의 리스크(초기 로딩, OOM, 레이턴시 회귀)를 카나리 단계에서 작은 비용으로 흡수할 수 있습니다.
다음 단계로는 Argo Rollouts 같은 컨트롤러를 붙여 지표 기반 자동 승격/자동 롤백까지 연결하는 것을 추천합니다. 다만 자동화 이전에, 이 글의 구성처럼 “두 버전 병렬 + Istio 라우팅”을 먼저 안정화하면 대부분의 GPU 서빙 사고를 크게 줄일 수 있습니다.