- Published on
KServe로 LLM 추론 서버 롤링배포·카나리 실전
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서빙 중인 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은 다음 이유로 일반 웹서비스보다 배포 리스크가 큽니다.
- 콜드 스타트가 길다: 모델 다운로드, 텐서 로딩, CUDA 그래프 준비 등으로 수십 초에서 수분
- 메모리/GPU 메모리 압박: 새 파드가 뜨는 동안 구 파드가 살아있으면 순간적으로 GPU가 부족해 스케줄링이 지연
- 워밍업 필요: 첫 요청이 느리거나 타임아웃
- 출력 품질 회귀: 같은 프롬프트에 대한 응답이 달라져 장애로 인식될 수 있음
따라서 “한 번에 갈아끼우기” 보다는, 새 버전을 충분히 준비시킨 뒤 트래픽을 천천히 이동시키는 카나리가 더 적합합니다.
기본 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,p99latency- 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 토큰을 지원하도록 설계
운영 팁: “카나리 실패” 를 빠르게 롤백하는 방법
카나리의 장점은 롤백이 빠르다는 점입니다.
canaryTrafficPercent를0으로 내려서 즉시 구 버전 100%로 복귀- 문제 원인을 찾기 전까지 새 Revision을 유지하되 트래픽만 차단
예시(개념):
kubectl -n ml patch inferenceservice llm-infer --type merge -p '{"spec":{"predictor":{"canaryTrafficPercent":0}}}'
또한 관측 지표에서 아래 조건이 충족되면 자동 롤백하도록 구성할 수 있습니다.
- 5분 이동평균
5xx가 기준치 초과 p99지연이 기준치 초과429가 급증
마무리: KServe 카나리의 본질은 “트래픽 제어 + 검증 자동화”
KServe를 쓰면 LLM 추론 서버 배포에서 가장 위험한 구간인 “새 버전 투입” 을 트래픽 분할로 완충할 수 있습니다. 하지만 성공의 핵심은 배포 버튼이 아니라 다음 3가지입니다.
readinessProbe가 진짜 준비 완료를 의미하도록 만들기- 드레인/그레이스풀 셧다운으로 연결 끊김 최소화
- 카나리 단계마다 성능 지표와 품질 검증을 자동화하고, 실패 시 즉시
0%로 롤백
이 3가지를 갖추면, LLM처럼 무겁고 민감한 워크로드도 “자주, 안전하게” 업데이트할 수 있습니다.