Published on

KServe+Istio로 LLM 추론 A/B·카나리 배포 실전

Authors

서빙 중인 LLM을 교체할 때 가장 무서운 순간은 “모델은 좋아졌는데 운영 지표가 나빠지는” 구간입니다. 특히 LLM은 모델 버전이 바뀌면 지연 시간(latency), 토큰 생성 속도, 비용, 품질(정답률/선호도), 안전성(유해 응답 비율)까지 한 번에 흔들립니다. 그래서 배포 전략은 단순 롤링 업데이트보다, 트래픽을 통제하면서 비교할 수 있는 A/B·카나리 방식이 훨씬 안전합니다.

이 글에서는 KServe로 LLM 추론 서비스를 운영한다는 전제에서, Istio를 이용해 트래픽을 “의도적으로” 쪼개고(가중치/헤더/쿠키), 관측하고, 문제가 생기면 즉시 되돌리는 흐름을 구성합니다.

전체 아키텍처: KServe는 서빙, Istio는 트래픽 제어

  • KServe: InferenceService로 모델 서빙 워크로드(파드/스케일링/리비전)를 관리합니다.
  • Istio: VirtualServiceDestinationRule로 L7 라우팅(가중치 분배, 헤더 기반 라우팅, 재시도/타임아웃)을 담당합니다.

핵심은 “모델 버전별로 서로 다른 KServe 서비스(혹은 리비전)를 만들고”, “Istio가 그 사이 트래픽을 분기”하도록 만드는 것입니다.

왜 LLM에서 더 중요할까

LLM은 다음 특성 때문에 카나리의 가치가 큽니다.

  • 지연 시간 분포가 길다: 평균보다 p95, p99가 더 중요합니다.
  • 비용이 트래픽에 비례: 카나리 비율이 곧 비용입니다.
  • 품질 평가는 오프라인만으로 부족: 실사용 프롬프트 분포가 다릅니다.
  • 안전성 회귀 가능: 특정 도메인 프롬프트에서만 사고가 납니다.

배포 모델: A/B와 카나리의 차이

  • 카나리: 신규 버전에 트래픽을 1% → 5% → 10% → 50%처럼 점진적으로 늘리며 안정성을 확인합니다.
  • A/B: 두 버전을 일정 비율로 고정 분배하여 품질/비용/지연을 비교합니다. 제품 실험(실사용자 그룹 비교)에 적합합니다.

Istio 관점에서는 둘 다 “라우팅 규칙”이고, 차이는 운영 정책(증분 확대 vs 고정 실험)입니다.

1) KServe로 v1, v2 InferenceService 준비

가장 단순한 형태는 모델 버전마다 InferenceService를 분리하는 방식입니다. 아래 예시는 v1, v2를 각각 다른 이름으로 배포합니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: llm-v1
  namespace: llm
spec:
  predictor:
    containers:
      - name: kserve-container
        image: ghcr.io/acme/llm-server:1.0.0
        ports:
          - containerPort: 8080
        env:
          - name: MODEL_ID
            value: "acme-llm-v1"
---
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: llm-v2
  namespace: llm
spec:
  predictor:
    containers:
      - name: kserve-container
        image: ghcr.io/acme/llm-server:2.0.0
        ports:
          - containerPort: 8080
        env:
          - name: MODEL_ID
            value: "acme-llm-v2"

운영에서는 다음도 함께 고려하세요.

  • 스케일링: 카나리에서 minReplicas를 0으로 두면 콜드스타트가 실험을 망칩니다. 최소 1~2개는 고정하거나 워밍업을 넣습니다.
  • 리소스: LLM은 CPU/메모리뿐 아니라 GPU, maxTokens, 배치 전략에 따라 병목이 달라집니다.
  • 동시성 제한: 과도한 동시 요청이 지연을 폭발시켜 A/B 비교가 무의미해질 수 있습니다.

2) Istio DestinationRule로 버전 서브셋 정의

Istio는 보통 “하나의 서비스에 대해 서브셋(subset)”을 정의하고, VirtualService에서 subset으로 라우팅합니다. KServe가 만드는 서비스 이름은 설치/설정에 따라 다를 수 있지만, 개념적으로는 v1/v2 각각을 목적지로 삼으면 됩니다.

여기서는 “외부에 노출되는 단일 호스트”를 llm.llm.svc.cluster.local로 가정하고, 내부적으로 v1/v2로 나눈다고 생각하면 이해가 쉽습니다.

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: llm-dr
  namespace: llm
spec:
  host: llm.llm.svc.cluster.local
  subsets:
    - name: v1
      labels:
        app: llm
        version: v1
    - name: v2
      labels:
        app: llm
        version: v2
  trafficPolicy:
    loadBalancer:
      simple: LEAST_REQUEST

중요 포인트:

  • subset 라벨은 실제 파드 라벨과 일치해야 합니다. KServe가 생성한 Deployment 또는 Pod 라벨을 확인하고 맞추세요.
  • LLM은 요청당 처리 시간이 길어 LEAST_REQUESTROUND_ROBIN보다 tail latency에 유리한 경우가 많습니다.

3) 카나리: 가중치 기반 라우팅

카나리는 가장 단순하게 “v1 95%, v2 5%”처럼 가중치로 시작합니다.

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: llm-vs
  namespace: llm
spec:
  hosts:
    - llm.llm.svc.cluster.local
  http:
    - name: canary
      route:
        - destination:
            host: llm.llm.svc.cluster.local
            subset: v1
          weight: 95
        - destination:
            host: llm.llm.svc.cluster.local
            subset: v2
          weight: 5
      timeout: 30s
      retries:
        attempts: 2
        perTryTimeout: 15s
        retryOn: "5xx,connect-failure,refused-stream"

LLM에서 타임아웃과 재시도는 신중하게

  • 생성형 응답은 20~60초가 흔합니다. timeout을 너무 짧게 잡으면 정상 요청을 실패로 만듭니다.
  • 재시도는 비용을 2배로 만들 수 있습니다. 특히 스트리밍 응답에서 중간 끊김이 잦다면 “재시도” 대신 “클라이언트 재연결/재개” 설계를 먼저 검토하세요.

4) A/B: 헤더 기반 고정 분기(사용자 일관성)

가중치 라우팅은 요청 단위로 섞이기 때문에, 같은 사용자가 여러 번 호출하면 v1/v2가 뒤섞일 수 있습니다. 제품 실험에서는 사용자 단위로 고정 분기가 필요합니다.

가장 쉬운 방법은 클라이언트(게이트웨이/백엔드)에서 실험 헤더를 붙이는 것입니다.

  • x-experiment: llm-v2면 v2로 강제
  • 그 외는 v1
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: llm-vs
  namespace: llm
spec:
  hosts:
    - llm.llm.svc.cluster.local
  http:
    - name: ab-v2
      match:
        - headers:
            x-experiment:
              exact: "llm-v2"
      route:
        - destination:
            host: llm.llm.svc.cluster.local
            subset: v2
          weight: 100
    - name: ab-default
      route:
        - destination:
            host: llm.llm.svc.cluster.local
            subset: v1
          weight: 100

사용자 해시 기반 분기 팁

헤더를 직접 넣기 어렵다면, 앞단(예: API Gateway, BFF)에서 userId를 해시해 10%만 x-experiment: llm-v2를 주입하는 방식이 현실적입니다.

  • 장점: 사용자 일관성 확보
  • 단점: 실험 로직이 애플리케이션에 들어감

5) “품질”을 어떻게 비교할까: 관측 지표 설계

카나리/AB에서 가장 흔한 실패는 “CPU, 메모리, 5xx만 보고 성공 선언”하는 것입니다. LLM은 품질이 핵심이므로, 최소한 아래 지표를 함께 보세요.

필수 운영 지표(서빙 관점)

  • 요청 성공률: HTTP 2xx 비율, 429(레이트리밋) 비율
  • 지연 시간: p50/p95/p99
  • 토큰 처리량: 초당 생성 토큰 수, 입력 토큰 대비 출력 토큰 비율
  • 비용 지표: 요청당 평균 출력 토큰, GPU 사용률

품질/안전 지표(제품 관점)

  • 사용자 피드백 기반 선호도(thumbs up/down)
  • 자동 평가: 사내 평가셋에 대한 정답률/루브릭 점수
  • 안전 필터 트리거율: 금칙어/PII/유해 카테고리

가능하면 v1/v2 라우팅 결과를 로그에 남겨야 합니다.

  • 예: 응답 헤더에 x-model-version: v1 또는 v2를 붙이고, 엣지/백엔드에서 수집

Istio는 응답 헤더 추가도 가능하지만, LLM 서버가 직접 넣는 편이 디버깅에 유리합니다.

6) 롤백 전략: “즉시 0%”로 되돌릴 수 있어야 함

카나리의 장점은 문제가 보이면 v2 비중을 0으로 내리면 된다는 점입니다. 이때 중요한 건 “롤백이 빠르고 확실해야 한다”는 것입니다.

  • VirtualService에서 v2 weight를 0으로
  • 또는 헤더 매칭 규칙을 제거
  • 장애가 심각하면 v2 InferenceService 자체를 scale down

GitOps를 쓴다면 라우팅 YAML 변경이 곧 배포입니다. 다만 Sync가 꼬이면 롤백이 늦어집니다. 운영 중 Argo CD가 라우팅 리소스에 대해 드리프트/헬스체크를 제대로 보고하는지 점검해 두세요.

7) LLM 특화 운영 이슈: 스티키 세션, 스트리밍, 커넥션

스트리밍 응답과 프록시 버퍼링

SSE나 chunked 스트리밍을 쓰면, 중간 프록시가 버퍼링을 해버려 “스트리밍인데 한 번에 도착”하는 문제가 생길 수 있습니다. Istio Ingress Gateway, Envoy 설정, 상위 L7 프록시(Nginx 등)의 버퍼링 옵션을 확인하세요.

  • 증상: 첫 토큰까지 시간이 비정상적으로 길어짐
  • 해결: 버퍼링 비활성화, 적절한 idle timeout 설정

스티키가 필요한 경우

대부분의 LLM 추론은 stateless라 스티키가 필요 없지만, 다음 상황에서는 필요할 수 있습니다.

  • 세션 캐시를 파드 로컬에만 두는 경우
  • speculative decoding, KV cache 공유를 파드 단위로 최적화한 경우

이때는 “사용자 기반 헤더 분기”가 사실상 스티키 역할을 하며, subset 라우팅과 궁합이 좋습니다.

8) 실전 운영 체크리스트

배포 전

  • v2는 최소 레플리카로 워밍업(콜드스타트 제거)
  • 동일한 리소스 제한/요청으로 공정 비교
  • 프롬프트/응답 로그에 버전 태깅
  • p95/p99 기준의 SLO 정의

카나리 진행

  • 1%에서 최소 30분~수시간 관찰(트래픽 패턴 반영)
  • 5xx뿐 아니라 429, 타임아웃 비율을 별도로 확인
  • 비용(출력 토큰) 급증 여부 확인

이상 징후

  • v2 비중 즉시 0으로
  • 원인 분석 후 재시도(모델 자체 문제인지, 런타임/리소스 문제인지 분리)

성능 문제를 추적할 때는 애플리케이션만 보지 말고, 빌드/이미지 레이어 캐시 문제로 배포 시간이 늘어나거나(결국 롤백이 늦어짐) CI가 병목이 되는지도 같이 점검하는 편이 좋습니다.

9) 예시: 단계별 카나리 전환을 GitOps로 관리

운영에서는 보통 아래처럼 단계별로 PR을 나눕니다.

  1. weight: 99/1
  2. weight: 95/5
  3. weight: 90/10
  4. weight: 50/50 (A/B로 전환해 품질 비교)
  5. weight: 0/100 (완전 전환)

이 과정에서 “실험 설계”가 중요합니다. 예를 들어 v2가 더 똑똑하지만 응답이 길어져 비용이 증가할 수 있습니다. 그때는 단순 정답률만 보지 말고 “비용 대비 품질”로 의사결정해야 합니다.

마무리

KServe는 LLM 추론 워크로드를 표준화된 형태로 배포하게 해주고, Istio는 그 위에서 트래픽을 정교하게 제어할 수 있게 해줍니다. 두 조합의 강점은 “모델을 바꾸는 행위”를 “트래픽 정책 변경”으로 분리해, 안전한 실험과 빠른 롤백을 가능하게 만든다는 점입니다.

실전에서는 가중치 라우팅으로 카나리 안정성을 확보한 뒤, 헤더 기반 A/B로 사용자 단위 품질을 비교하는 흐름이 가장 많이 쓰입니다. 여기에 p95/p99, 토큰 비용, 안전 지표까지 함께 묶어 관측하면, LLM 배포를 ‘감’이 아니라 데이터로 운영할 수 있습니다.