Published on

KServe+Istio로 LLM 카나리 배포와 자동 롤백

Authors

서빙 중인 LLM을 무중단으로 업데이트하려면 단순히 새 파드를 띄우는 것만으로는 부족합니다. LLM은 모델 파일 크기, 워밍업 시간, GPU 메모리 압박, 토큰 생성 지연 같은 변수가 커서, 배포 직후에는 정상처럼 보여도 실제 트래픽에서 지연이 급증하거나 에러가 튀는 일이 흔합니다.

이 글에서는 KServeInferenceService로 모델 버전을 관리하고, Istio로 트래픽을 canary에 점진적으로 분배한 뒤, Prometheus 지표를 기준으로 자동 롤백까지 연결하는 구성을 단계별로 설명합니다. 운영 관점에서 자주 부딪히는 함정(워밍업, readiness, 지표 선택, 롤백 트리거)을 함께 다룹니다.

관련해서 배포 직후 CrashLoopBackOff나 프로브 실패로 카나리가 바로 죽는 경우가 많습니다. 이 부분은 아래 글의 체크리스트가 그대로 도움이 됩니다.

목표 아키텍처

구성 요소와 역할은 다음과 같습니다.

  • KServe InferenceService
    • 모델 서빙 리소스의 선언적 관리
    • predictorcanaryTrafficPercent로 카나리 퍼센트 제어
  • Istio
    • KServe가 생성하는 서비스 앞단에서 라우팅/관측/리트라이/타임아웃 정책 적용
    • (선택) VirtualService/DestinationRule로 세밀한 트래픽 정책
  • Prometheus + (선택) Grafana
    • SLO 지표 수집: p95 latency, 5xx, GPU OOM 징후 등
  • Argo Rollouts 또는 Flagger
    • 지표 기반 분석(analysis) 후 자동 승격/자동 롤백

핵심은 “트래픽 분할”과 “분석/판정”을 분리하는 것입니다.

  • 트래픽 분할: KServe/Istio
  • 판정 및 자동화: Argo Rollouts/Flagger

사전 준비

1) 네임스페이스와 사이드카

Istio를 사용한다면 네임스페이스에 사이드카 주입을 켭니다.

kubectl create ns llm
kubectl label ns llm istio-injection=enabled

GPU 노드/런타임은 환경마다 다르지만, LLM은 워밍업이 길어 readinessProbe 설계가 특히 중요합니다. 단순 HTTP 200 체크만으로는 “첫 토큰 지연” 문제를 잡지 못합니다.

2) 관측 지표 설계(롤백 트리거)

카나리에서 흔히 보는 실패 모드는 다음 3가지입니다.

  1. p95/p99 latency 급증 (특히 첫 토큰, 혹은 긴 컨텍스트)
  2. 5xx 증가 (모델 로딩 실패, OOM, upstream timeout)
  3. OOM/재시작 증가 (GPU 메모리 파편화 포함)

LLM은 입력 길이 분포가 바뀌면 지연이 급격히 바뀌므로, 단순 평균(latency avg)은 잘못된 결론을 내기 쉽습니다. 최소 p95를 기준으로 하세요.

KServe로 LLM InferenceService 만들기

아래 예시는 KServe InferenceService를 이용해 v1 모델을 서빙하고, v2를 카나리로 붙이는 형태입니다.

주의: 본문에서 부등호 문자가 그대로 노출되면 MDX에서 문제가 될 수 있으니, 예시의 제네릭/화살표/태그 형태는 모두 코드 블록 안에서만 사용합니다.

1) v1(Stable) 배포

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: llm-textgen
  namespace: llm
  annotations:
    sidecar.istio.io/inject: "true"
spec:
  predictor:
    containers:
      - name: kserve-container
        image: ghcr.io/your-org/llm-server:1.0.0
        ports:
          - containerPort: 8080
        env:
          - name: MODEL_ID
            value: "your-llm-v1"
        resources:
          requests:
            cpu: "2"
            memory: "8Gi"
            nvidia.com/gpu: "1"
          limits:
            cpu: "4"
            memory: "16Gi"
            nvidia.com/gpu: "1"
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 2
          failureThreshold: 12
        livenessProbe:
          httpGet:
            path: /health/live
            port: 8080
          initialDelaySeconds: 60
          periodSeconds: 10
          timeoutSeconds: 2
          failureThreshold: 6

적용:

kubectl apply -f inferenceservice.yaml
kubectl -n llm get inferenceservice llm-textgen -w

2) v2(Canary) 붙이기

KServe는 predictor.canaryTrafficPercent로 안정 버전과 카나리 버전의 트래픽 비율을 분할할 수 있습니다. 일반적으로 10부터 시작해 2550100 순으로 올립니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: llm-textgen
  namespace: llm
spec:
  predictor:
    canaryTrafficPercent: 10
    containers:
      - name: kserve-container
        image: ghcr.io/your-org/llm-server:2.0.0
        ports:
          - containerPort: 8080
        env:
          - name: MODEL_ID
            value: "your-llm-v2"
        resources:
          requests:
            cpu: "2"
            memory: "8Gi"
            nvidia.com/gpu: "1"
          limits:
            cpu: "4"
            memory: "16Gi"
            nvidia.com/gpu: "1"

여기서 중요한 점은 “containers에 적은 내용이 카나리(새 버전)”라는 것입니다. 기존 stable은 KServe가 내부적으로 유지하며, 트래픽 비율로 분배합니다.

Istio로 LLM 트래픽 정책 잡기(타임아웃/리트라이)

LLM은 요청 시간이 길어질 수 있어 기본 타임아웃이 너무 짧으면 504가 늘고, 반대로 너무 길면 장애 시 회복이 느려집니다.

1) VirtualService로 요청 타임아웃 설정

KServe가 생성한 라우트를 그대로 쓰더라도, Istio 레벨에서 타임아웃을 명시하는 것이 좋습니다.

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: llm-textgen
  namespace: llm
spec:
  hosts:
    - llm-textgen.llm.svc.cluster.local
  http:
    - timeout: 120s
      retries:
        attempts: 2
        perTryTimeout: 40s
        retryOn: gateway-error,connect-failure,refused-stream,reset
      route:
        - destination:
            host: llm-textgen.llm.svc.cluster.local

리트라이는 LLM에서는 신중해야 합니다. 생성형 요청은 비용이 크고, 서버가 이미 일부 토큰을 생성한 뒤 끊기는 케이스도 있어 “중복 생성”이 발생할 수 있습니다. 가능하면 클라이언트에 idempotency key를 두고, 서버가 동일 키에 대해 중복 실행을 방지하는 편이 안전합니다.

자동 롤백: Argo Rollouts로 지표 기반 판정

KServe 자체도 카나리 퍼센트를 조절할 수 있지만, “지표를 보고 자동으로 승격/롤백”까지 하려면 별도의 컨트롤러가 필요합니다. 여기서는 Argo RolloutsAnalysisTemplate을 이용해 Prometheus 쿼리 결과로 실패 시 롤백하는 패턴을 소개합니다.

구현 방식은 두 가지가 있습니다.

  1. Argo Rollouts가 직접 워크로드를 카나리로 관리
  2. KServe의 카나리 퍼센트는 유지하되, Rollouts(또는 워크플로)가 canaryTrafficPercent를 단계적으로 패치하고 분석

LLM 서빙은 KServe 리소스가 중심이므로, 2번(패치 기반)이 현실적인 경우가 많습니다.

1) Prometheus 지표 예시

아래는 “카나리 트래픽이 들어간 뒤, 5분 동안 p95가 임계치를 넘으면 실패” 같은 룰을 만들 때 자주 쓰는 형태입니다. 실제 메트릭 이름은 환경(Envoy, 앱 커스텀 메트릭)에 맞게 조정하세요.

  • p95 latency(Envoy 기반 예시):
histogram_quantile(0.95,
  sum(rate(istio_request_duration_milliseconds_bucket{destination_service="llm-textgen.llm.svc.cluster.local"}[2m]))
  by (le)
)
  • 5xx 비율:
sum(rate(istio_requests_total{destination_service="llm-textgen.llm.svc.cluster.local",response_code=~"5.."}[2m]))
/
sum(rate(istio_requests_total{destination_service="llm-textgen.llm.svc.cluster.local"}[2m]))

2) AnalysisTemplate 예시

apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: llm-canary-slo
  namespace: llm
spec:
  metrics:
    - name: p95-latency
      interval: 30s
      failureLimit: 2
      provider:
        prometheus:
          address: http://prometheus-server.monitoring.svc.cluster.local
          query: |
            histogram_quantile(0.95,
              sum(rate(istio_request_duration_milliseconds_bucket{destination_service="llm-textgen.llm.svc.cluster.local"}[2m]))
              by (le)
            )
      successCondition: result[0] <= 1500
    - name: http-5xx-rate
      interval: 30s
      failureLimit: 1
      provider:
        prometheus:
          address: http://prometheus-server.monitoring.svc.cluster.local
          query: |
            sum(rate(istio_requests_total{destination_service="llm-textgen.llm.svc.cluster.local",response_code=~"5.."}[2m]))
            /
            sum(rate(istio_requests_total{destination_service="llm-textgen.llm.svc.cluster.local"}[2m]))
      successCondition: result[0] <= 0.01

여기서 successCondition의 숫자(예: 1500ms, 1%)는 서비스 SLO와 트래픽 특성에 맞춰 잡아야 합니다. LLM은 프롬프트 길이/출력 길이에 따라 지연이 크게 달라지므로, 가능하면 “동일한 입력 분포”의 카나리 트래픽을 확보하거나, 최소한 route를 분리해 특정 엔드포인트(예: /generate)만 분석 대상으로 삼는 것이 좋습니다.

카나리 퍼센트 단계적 적용과 자동 롤백(패치 워크플로)

Argo Rollouts가 KServe 리소스를 직접 컨트롤하지 않는 구성이라면, kubectl patch(또는 GitOps 툴)로 canaryTrafficPercent를 올리고, 각 단계마다 AnalysisRun을 실행해 실패 시 되돌리는 방식이 단순하고 강력합니다.

1) 단계별 패치 스크립트 예시

set -euo pipefail

NS=llm
SVC=llm-textgen

apply_canary() {
  local p=$1
  kubectl -n "$NS" patch inferenceservice "$SVC" --type merge -p "{\"spec\":{\"predictor\":{\"canaryTrafficPercent\":$p}}}"
}

rollback_canary() {
  kubectl -n "$NS" patch inferenceservice "$SVC" --type merge -p '{"spec":{"predictor":{"canaryTrafficPercent":0}}}'
}

wait_analysis() {
  # 실제로는 AnalysisRun 생성 후 상태를 watch
  # 여기서는 예시로 5분 대기
  sleep 300
}

for p in 10 25 50 100; do
  echo "Set canary to ${p}%"
  apply_canary "$p"

  echo "Run analysis"
  wait_analysis

  # 이 부분에 Prometheus 쿼리 체크나 AnalysisRun 결과 확인을 붙인다.
  # 실패면 rollback_canary 후 종료.

done

운영에서는 위 스크립트를 CI 파이프라인(예: GitHub Actions)이나 Argo Workflows로 감싸고, 분석 결과를 기준으로 rollback_canary를 호출하도록 만듭니다.

LLM 카나리에서 특히 중요한 운영 포인트 6가지

1) 워밍업과 캐시

LLM 서버는 첫 요청이 가장 느립니다. 카나리 10%를 주었는데도 p95가 튀는 이유가 “실제 부하”가 아니라 “워밍업 요청”일 수 있습니다.

  • 배포 직후 synthetic warmup(대표 프롬프트 몇 개)을 먼저 수행
  • KV cache, tokenizer 초기화, CUDA 그래프 캡처 등 모델 서버 특성에 맞춘 워밍업 절차를 readiness 이전에 끝내기

2) readinessProbe는 “모델 로딩 완료”를 보장해야 함

프로세스가 떠있다고 준비 완료가 아닙니다. 최소한 다음을 만족해야 합니다.

  • 모델 가중치 로딩 완료
  • GPU 메모리 할당 성공
  • 첫 토큰 생성까지 가능한 상태

그렇지 않으면 Istio가 트래픽을 보내고, 카나리가 5xx를 내며 분석에서 실패합니다.

3) 타임아웃은 “업스트림+프록시+클라이언트”가 일치해야 함

  • 클라이언트 타임아웃: 예를 들어 90초
  • Istio timeout: 120초
  • 서버 내부 timeout: 110초

처럼 계층별로 모순이 없게 맞춰야 합니다. 중간 계층이 먼저 끊으면 서버는 계속 생성하고, 비용만 나가며, 클라이언트는 실패로 인지하는 최악의 상황이 됩니다.

4) 롤백 조건은 지표 1개가 아니라 2~3개 조합

LLM은 트래픽 분포 변화로 latency가 흔들리기 쉬워, p95 latency 하나만으로 롤백하면 과민반응이 됩니다.

추천 조합:

  • p95 latency + 5xx rate
  • p95 latency + pod restarts(또는 OOM 지표)

5) 카나리 트래픽은 “대표성”이 있어야 함

특정 고객/특정 엔드포인트만 카나리에 들어가면, 일반 트래픽에서 터질 문제가 감춰집니다. 가능하면 랜덤 샘플링(퍼센트 기반) + 최소 트래픽량을 확보하세요.

6) 모델 메모리 이슈는 배포보다 먼저 잡아야 함

카나리 배포로 잡히는 문제도 있지만, “애초에 v2가 GPU 메모리에 안 올라감” 같은 문제는 카나리 이전에 해결해야 합니다. 로컬/스테이징에서 8bit/4bit 로딩, 텐서 병렬, offload 전략을 미리 검증하세요.

장애 시나리오로 보는 자동 롤백 흐름

  1. v2 배포 후 canaryTrafficPercent=10
  2. 2~5분 동안 지표 수집
  3. 다음 중 하나라도 발생하면 실패로 판정
    • p95 latency가 임계치 초과가 연속 N
    • 5xx rate1% 초과
    • 파드 재시작 급증
  4. 실패 시 canaryTrafficPercent=0으로 패치(즉시 stable 100%)
  5. 원인 분석
    • 모델 로딩 실패 로그
    • GPU OOM
    • Istio timeout/리트라이로 인한 중복 실행

이 방식의 장점은 롤백이 “이미 검증된 stable로 트래픽을 되돌리는 것”이라 빠르고 안전하다는 점입니다.

마무리

KServe+Istio 조합은 LLM처럼 배포 리스크가 큰 워크로드에서 특히 강력합니다. KServe가 모델 서빙 리소스를 표준화하고, Istio가 트래픽/정책/관측을 담당하며, Argo Rollouts(또는 Flagger)가 지표 기반 자동화를 붙이면 “점진 배포 + 자동 롤백”이 운영 가능한 형태로 완성됩니다.

다음 단계로는 카나리 분석에 “토큰 단가(초당 토큰 처리량)”나 “첫 토큰 지연(TTFT)” 같은 LLM 특화 지표를 추가하고, 입력 길이 버킷별 SLO를 분리하면 롤백 판정의 정확도가 크게 올라갑니다.