Published on

KServe로 LLM 추론 서버 롤링배포·카나리 실전

Authors
Binance registration banner

서빙 중인 LLM 추론 서버는 단순히 Deployment 롤링업데이트만으로는 운영 안정성을 보장하기 어렵습니다. 모델 로딩 시간이 길고, 워밍업이 필요하며, 버전 간 출력 품질 회귀(regression)도 자주 발생합니다. 이 글에서는 KServe를 기준으로 롤링 배포(무중단에 가까운 교체)카나리(점진 트래픽 전환) 를 실제로 적용하는 방법을 정리합니다.

또한 LLM 특성상 배포 전략만큼 중요한 것이 클라이언트 리트라이/백오프 입니다. 카나리 전환 중 일시적인 429나 타임아웃이 발생할 수 있으므로, 호출 측에서도 방어적으로 설계해야 합니다. 관련해서는 OpenAI 429/RateLimitError 실전 백오프·리트라이 글의 패턴을 내부 API 호출에도 동일하게 적용하는 것을 권장합니다.

전제: KServe에서 “배포”가 의미하는 것

KServe의 핵심 리소스는 InferenceService 입니다. 일반적인 패턴은 다음과 같습니다.

  • InferenceService 하나가 “모델 엔드포인트” 역할을 한다
  • predictor 스펙을 바꾸면 KServe가 내부적으로 새 Revision을 만들고 트래픽을 라우팅한다
  • 카나리 트래픽 분배는 canaryTrafficPercent 같은 필드로 제어한다

단, KServe는 내부적으로 Knative Serving을 쓰는 구성이 많고(환경에 따라 다름), 이 경우 Revision 기반 트래픽 분배 가 매우 강력합니다. 반대로 Knative 없이 “RawDeployment” 모드로 쓰는 경우에는 카나리 기능/행동이 제한될 수 있습니다. 운영 환경이 어떤 모드인지 먼저 확인하세요.

체크리스트

  • KServe 설치가 Knative 기반인지 확인
  • 서비스 메시에 의존하는지(Istio 등) 확인
  • GPU 노드풀, 스케줄링, 이미지 풀 정책, 볼륨(모델 스토리지) 경로 확인

LLM 추론 서버에서 롤링 배포가 어려운 이유

LLM은 다음 이유로 일반 웹서비스보다 배포 리스크가 큽니다.

  1. 콜드 스타트가 길다: 모델 다운로드, 텐서 로딩, CUDA 그래프 준비 등으로 수십 초에서 수분
  2. 메모리/GPU 메모리 압박: 새 파드가 뜨는 동안 구 파드가 살아있으면 순간적으로 GPU가 부족해 스케줄링이 지연
  3. 워밍업 필요: 첫 요청이 느리거나 타임아웃
  4. 출력 품질 회귀: 같은 프롬프트에 대한 응답이 달라져 장애로 인식될 수 있음

따라서 “한 번에 갈아끼우기” 보다는, 새 버전을 충분히 준비시킨 뒤 트래픽을 천천히 이동시키는 카나리가 더 적합합니다.

기본 InferenceService 예시(단일 버전)

아래는 KServe InferenceService 의 단일 버전 예시입니다. LLM 서빙 런타임은 환경마다 다르지만, 여기서는 컨테이너 기반 커스텀 서버(예: vLLM, TGI, llama.cpp 서버 등)를 올린다고 가정합니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: llm-infer
  namespace: ml
spec:
  predictor:
    containers:
      - name: server
        image: ghcr.io/acme/llm-server:1.0.0
        ports:
          - containerPort: 8080
        env:
          - name: MODEL_ID
            value: mistral-7b-instruct
          - name: MAX_TOKENS
            value: "512"
        resources:
          limits:
            nvidia.com/gpu: "1"
            cpu: "4"
            memory: 16Gi
          requests:
            nvidia.com/gpu: "1"
            cpu: "2"
            memory: 12Gi

이 상태에서 image 태그만 바꾸면 “업데이트” 는 되지만, LLM 특성상 다음을 같이 설계해야 합니다.

  • 준비 상태(readinessProbe)가 진짜로 “추론 가능” 을 의미하도록 구성
  • 종료 시 그레이스풀 셧다운으로 진행 중 요청을 안전하게 마무리
  • 배포 중 에러율을 관측하고 자동으로 전환을 멈출 수 있는 장치

롤링 배포: 준비 상태와 종료 처리부터 잡기

1) readinessProbe를 “모델 로딩 완료” 기준으로

단순히 HTTP 포트가 열렸다고 Ready로 두면, 첫 수십 요청이 실패하거나 지연됩니다. 서버에 /readyz 같은 엔드포인트를 두고, 모델 로딩과 워밍업이 끝난 뒤에만 200 을 반환하게 하세요.

readinessProbe:
  httpGet:
    path: /readyz
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5
  timeoutSeconds: 2
  failureThreshold: 12
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  timeoutSeconds: 2
  failureThreshold: 6
  • readinessProbe 는 “트래픽 받아도 됨”
  • livenessProbe 는 “프로세스가 망가졌는지”

LLM 서버는 일시적으로 느려질 수 있으니 timeoutSeconds 를 너무 공격적으로 잡지 않는 편이 낫습니다.

2) terminationGracePeriodSeconds와 preStop 훅

진행 중인 스트리밍 응답(SSE)이나 긴 추론 요청이 있다면, 파드 종료 시점에 연결이 끊기며 장애로 보입니다.

terminationGracePeriodSeconds: 120
lifecycle:
  preStop:
    httpGet:
      path: /drain
      port: 8080
  • /drain 호출 시 새 요청을 받지 않도록 서버를 “드레인 모드” 로 전환
  • 이후 그레이스 기간 동안 기존 요청을 마무리

3) PDB로 동시 축출 방지

노드 드레인이나 업그레이드로 파드가 한꺼번에 내려가면 LLM은 바로 용량 부족이 납니다.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: llm-infer-pdb
  namespace: ml
spec:
  minAvailable: 1
  selector:
    matchLabels:
      serving.kserve.io/inferenceservice: llm-infer

카나리 배포: KServe 트래픽 분할로 안전하게 전환

1) 카나리 트래픽 퍼센트 지정

KServe는 새 스펙이 적용되면 새 Revision을 만들고, canaryTrafficPercent 로 트래픽을 분할할 수 있습니다.

아래는 1.1.0 이미지를 카나리로 올리고, 10%만 먼저 보내는 예시입니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: llm-infer
  namespace: ml
spec:
  predictor:
    canaryTrafficPercent: 10
    containers:
      - name: server
        image: ghcr.io/acme/llm-server:1.1.0
        ports:
          - containerPort: 8080
        env:
          - name: MODEL_ID
            value: mistral-7b-instruct

이 방식의 핵심은 “구 버전은 그대로 유지” 한 채로, 새 버전이 충분히 안정적일 때까지 트래픽만 조절한다는 점입니다.

2) 단계적 전환 런북 예시

운영에서는 보통 아래처럼 올립니다.

  • 10%로 시작, 15분 관측
  • 25%로 확대, 30분 관측
  • 50%로 확대, 1시간 관측
  • 100% 전환

관측 지표는 최소한 다음을 보세요.

  • p95, p99 latency
  • HTTP 5xx 비율
  • 429 비율(큐잉/레이트리밋)
  • GPU utilization, GPU memory, KV cache hit/miss(가능하면)
  • 토큰 생성 속도(tokens/sec)

카나리 중 429 가 늘면 “서버가 나쁨” 일 수도 있지만, “새 버전이 더 많은 메모리를 먹어서 동시성이 줄었음” 같은 용량 문제일 수도 있습니다. 이때는 호출 측도 백오프/리트라이가 필요합니다. 패턴은 Anthropic Claude 429 레이트리밋 재시도 설계법 과 동일하게 가져가면 됩니다.

실전: 카나리 품질 검증(회귀 테스트)까지 자동화

LLM은 “응답이 나온다” 만으로는 부족합니다. 카나리 트래픽 중 일부를 검증용 프롬프트 세트 로 돌려 품질을 점수화하거나, 최소한 “금지어/형식 위반” 같은 정책 위반을 탐지해야 합니다.

1) 간단한 검증 잡(Job) 예시

아래는 카나리 전환 직후, 내부에서 엔드포인트를 호출해 스모크 테스트를 수행하는 Job 예시입니다.

apiVersion: batch/v1
kind: Job
metadata:
  name: llm-infer-smoke
  namespace: ml
spec:
  backoffLimit: 0
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: curl
          image: curlimages/curl:8.6.0
          command:
            - sh
            - -c
            - |
              set -e
              URL="http://llm-infer.ml.svc.cluster.local/v1/chat/completions"
              payload='{"model":"mistral-7b-instruct","messages":[{"role":"user","content":"Say hello in one word."}],"max_tokens":8}'
              code=$(curl -s -o /tmp/out.json -w "%{http_code}" -H "Content-Type: application/json" -d "$payload" "$URL")
              test "$code" = "200"
              cat /tmp/out.json | head -c 400

운영에서는 이 결과를 CI 파이프라인에서 받아서, 실패 시 카나리 퍼센트를 올리지 않도록 게이트를 두는 식으로 확장합니다.

2) “형식 고정” 이 필요한 경우

LLM 응답을 JSON으로 고정해서 파싱하는 시스템이라면, 배포 중 형식이 깨지는 순간이 곧 장애입니다. 이 경우 모델/프롬프트 변경과 함께 “엄격한 스키마 강제” 전략이 필요합니다. 외부 LLM API 사례지만, 원리는 동일하므로 OpenAI JSON Schema 응답 깨짐, strict 모드로 막기 의 접근을 내부 추론 서버에도 적용해 보세요.

GPU 환경에서 카나리 시 자주 터지는 함정 4가지

1) 순간 GPU 부족으로 새 Revision이 스케줄링 실패

카나리는 “구 버전 유지 + 신 버전 추가” 구조라서, GPU가 딱 맞게 운영 중이면 새 파드가 뜰 자리가 없습니다.

대응:

  • 카나리용 여유 GPU를 미리 확보
  • 또는 카나리 파드를 더 작은 모델/낮은 동시성 설정으로 먼저 검증

2) 모델 다운로드로 네트워크/스토리지 병목

신 버전 파드가 동시에 뜨며 모델을 내려받으면, 레지스트리나 오브젝트 스토리지가 병목이 됩니다.

대응:

  • 노드 로컬 캐시(예: 이미지 프리풀, 모델 캐시 PV)
  • initContainer로 모델 선다운로드 후 본 컨테이너 시작

3) 워밍업 누락으로 카나리 트래픽에서만 지연 폭발

새 Revision이 Ready가 됐어도, 첫 추론이 느릴 수 있습니다.

대응:

  • /readyz 가 워밍업까지 포함하도록 구현
  • 또는 KServe가 트래픽을 주기 전에 워밍업 잡을 먼저 실행

4) 스트리밍 응답 중단

SSE/웹소켓 유사 스트리밍을 쓰면, 파드 교체 시 커넥션이 끊기기 쉽습니다.

대응:

  • 드레인 모드 + 충분한 그레이스 기간
  • 클라이언트 재연결 로직
  • 가능하면 요청을 짧게 쪼개거나, 서버가 resume 토큰을 지원하도록 설계

운영 팁: “카나리 실패” 를 빠르게 롤백하는 방법

카나리의 장점은 롤백이 빠르다는 점입니다.

  • canaryTrafficPercent0 으로 내려서 즉시 구 버전 100%로 복귀
  • 문제 원인을 찾기 전까지 새 Revision을 유지하되 트래픽만 차단

예시(개념):

kubectl -n ml patch inferenceservice llm-infer --type merge -p '{"spec":{"predictor":{"canaryTrafficPercent":0}}}'

또한 관측 지표에서 아래 조건이 충족되면 자동 롤백하도록 구성할 수 있습니다.

  • 5분 이동평균 5xx 가 기준치 초과
  • p99 지연이 기준치 초과
  • 429 가 급증

마무리: KServe 카나리의 본질은 “트래픽 제어 + 검증 자동화”

KServe를 쓰면 LLM 추론 서버 배포에서 가장 위험한 구간인 “새 버전 투입” 을 트래픽 분할로 완충할 수 있습니다. 하지만 성공의 핵심은 배포 버튼이 아니라 다음 3가지입니다.

  1. readinessProbe 가 진짜 준비 완료를 의미하도록 만들기
  2. 드레인/그레이스풀 셧다운으로 연결 끊김 최소화
  3. 카나리 단계마다 성능 지표와 품질 검증을 자동화하고, 실패 시 즉시 0% 로 롤백

이 3가지를 갖추면, LLM처럼 무겁고 민감한 워크로드도 “자주, 안전하게” 업데이트할 수 있습니다.