Published on

KServe+Istio로 GPU 모델 카나리 배포·롤백

Authors

GPU 추론 모델은 배포 실패 비용이 큽니다. 새 이미지가 뜨지 않거나(드라이버, CUDA, 의존성), 성능이 미묘하게 나빠지거나(레이턴시, OOM), 특정 입력에서만 폭발하는 문제가 흔합니다. 그래서 한 번에 100% 교체가 아니라 소량 트래픽으로 검증 후 점진 확대가 정석입니다.

Kubernetes 환경에서는 KServe가 모델 서빙의 표준 추상화(InferenceService)를 제공하고, Istio는 L7 트래픽 제어(가중치 라우팅, 헤더 기반 라우팅, 미러링)를 제공합니다. 이 글에서는 KServe + Istio 조합으로 GPU 모델을 카나리 배포하고, 문제가 생기면 즉시 롤백하는 구체적인 설계를 다룹니다.

또한 카나리 중 자주 겪는 장애인 CrashLoopBackOff/프로브 오탐, GPU OOM 등을 어떻게 관찰하고 롤백 트리거로 연결할지도 함께 정리합니다.

전체 아키텍처: KServe가 “서빙”, Istio가 “트래픽”

핵심은 역할 분리입니다.

  • KServe: 모델 버전별 워크로드(Deployment/Knative Service), 스케일링, GPU 리소스 요청, predictor/transformer 구조
  • Istio: 두 버전의 엔드포인트 사이 트래픽 분할(예: 95/5), 특정 사용자만 카나리로 라우팅, 장애 시 100% 이전 버전으로 즉시 복구

실전에서는 보통 다음 2가지 방식 중 하나를 씁니다.

  1. 두 개의 InferenceService를 병렬로 띄우고 Istio로 분할
  • model-v1, model-v2를 각각 독립 서비스로 운영
  • Istio VirtualService에서 가중치 라우팅
  • 장점: 버전 간 완전 격리, 롤백이 단순(가중치만 되돌림)
  • 단점: 리소스가 2배 가까이 필요(특히 GPU)
  1. 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으로 즉시 차단” + “리소스 회수”

롤백은 두 단계로 나누면 안전합니다.

  1. 즉시 롤백(트래픽 차단): Istio VirtualService에서 v2 weight를 0으로
  2. 사후 처리(리소스 회수): 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로 빠집니다.

대응:

  • readinessProbelivenessProbe를 분리
  • startupProbe를 지원하는 런타임이면 적극 사용
  • 초기 로딩 시간을 반영해 initialDelaySeconds/failureThreshold 조정

프로브 오탐 패턴과 튜닝은 아래 글에서 더 자세히 다뤘습니다.

2) GPU OOM: 카나리에서 “특정 입력”만 터지는 문제

새 버전이 더 큰 컨텍스트를 받거나, 배치 전략이 바뀌거나, 정밀도가 FP16에서 FP32로 올라가면 VRAM이 급격히 증가합니다. 카나리에서는 “평균”보다 “최악 케이스”가 중요합니다.

대응 체크:

  • 입력 상한(토큰/해상도/배치) 강제
  • 모델 로딩 옵션(예: torch.set_default_dtype, autocast) 점검
  • 메모리 최적화 옵션(xFormers, 타일링 등) 적용

이미지/확산 모델 계열이라면 아래 최적화 전략이 직접적으로 도움이 됩니다.

3) 레이턴시 회귀: “성공률은 유지되는데 느려짐”

카나리에서 흔한 함정은 에러율만 보고 통과시키는 것입니다. GPU 모델은 느려지면 곧바로 비용 증가(더 많은 GPU 필요)로 이어집니다.

권장 게이트:

  • v2의 p95 레이턴시가 v1 대비 +10% 이상이면 자동 중단
  • 큐잉 지연(서버 내부 배치 대기)이 증가하는지 확인

Istio의 텔레메트리(요청 지표)와 애플리케이션 레벨 지표(모델 추론 시간)를 함께 봐야 원인을 분리할 수 있습니다.

운영 팁: 카나리 절차를 “반복 가능한 런북”으로 만들기

실무에서는 아래 순서로 런북을 고정해두면, 야간 장애에서도 흔들리지 않습니다.

  1. v2 배포(트래픽 0)
  • InferenceService 생성
  • 준비 완료 확인(Ready=True)
kubectl -n ml wait --for=condition=Ready inferenceservice/gpu-model-v2 --timeout=600s
  1. 내부 QA만 v2로(헤더 라우팅)
  • x-canary: 1로만 유입
  • OOM/재시작/레이턴시 확인
  1. 95/5 카나리 시작
  • 5분~30분 관찰(트래픽 규모에 따라)
  1. 50/50, 0/100 단계적 확대
  • 각 단계에서 SLO 통과 확인
  1. 최종 전환 후 v1 유지 기간 설정
  • 즉시 삭제하지 말고 “핫 스탠바이”로 하루 유지하는 조직도 많습니다
  1. 문제 시 롤백
  • Istio weight를 즉시 100/0으로
  • v2 삭제 또는 원인 분석 후 재배포

마무리: KServe와 Istio 조합의 장점

  • KServe는 모델 서빙을 표준화해 배포 단위를 단순화합니다.
  • Istio는 트래픽을 “정교하게” 제어해 카나리/롤백을 빠르고 안전하게 만듭니다.
  • GPU 모델의 리스크(초기 로딩, OOM, 레이턴시 회귀)를 카나리 단계에서 작은 비용으로 흡수할 수 있습니다.

다음 단계로는 Argo Rollouts 같은 컨트롤러를 붙여 지표 기반 자동 승격/자동 롤백까지 연결하는 것을 추천합니다. 다만 자동화 이전에, 이 글의 구성처럼 “두 버전 병렬 + Istio 라우팅”을 먼저 안정화하면 대부분의 GPU 서빙 사고를 크게 줄일 수 있습니다.