반응형
배포는 끝이 아닙니다. TTFT가 언제 터질지 모르는 채로 운영하거나, 모델 버전 바꿀 때마다 트래픽 다 끊거나, GPU 노드가 밤새 idle로 켜져 있거나. 4편은 배포 이후 진짜 운영의 이야기입니다.
📌 핵심 요약
→ LLM 모니터링 핵심 4종: TTFT / TPOT / 큐 깊이 / KV캐시 — RPS만 보면 아무것도 모름
→ DCGM Exporter: GPU 하드웨어 메트릭(온도·전력·SM 가동률) Prometheus로 수집
→ terminationGracePeriodSeconds 기본 30초는 LLM에 사형선고 — 최소 300초로
→ 카나리 배포: 새 모델 10% 트래픽 → TTFT 비교 → 점진 전환, 문제 시 즉시 롤백
→ Spot + On-Demand 혼합: 학습은 Spot(60~70% 절감), 추론은 On-Demand
→ Karpenter WhenUnderutilized: 유휴 노드 자동 종료 → idle 비용 50~65% 절감
→ cost-per-token 메트릭: CTO가 봐야 할 진짜 숫자, 대부분 팀이 안 추적하고 있음
→ 실전 트러블슈팅 5종 패턴과 원인-해결책 한 번에 정리
실전1 — LLM 관찰가능성 스택 구성
[LLM 모니터링 3계층]
Layer 3: LLM 비즈니스 메트릭
→ TTFT p95, TPOT p95, cost-per-token, tokens/sec
Layer 2: 추론 서버 메트릭 (vLLM /metrics)
→ 큐 깊이, KV 캐시 점유율, 배치 크기, 처리 요청 수
Layer 1: GPU 하드웨어 메트릭 (DCGM Exporter)
→ SM 가동률, VRAM 사용량, 온도, 전력, PCIe 대역폭
# Prometheus + Grafana + DCGM 스택 한 번에 설치
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
# kube-prometheus-stack: Prometheus + Grafana + AlertManager 통합
helm install monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--create-namespace \
--set prometheus.prometheusSpec.retention=30d \
--set prometheus.prometheusSpec.retentionSize=50GB \
--set grafana.adminPassword=changeme \
--set grafana.persistence.enabled=true \
--set grafana.persistence.size=10Gi
# GPU Operator가 DCGM Exporter를 자동 배포함 (1편에서 설치했다면 이미 있음)
# 없다면:
helm upgrade gpu-operator nvidia/gpu-operator \
--namespace gpu-operator \
--set dcgmExporter.enabled=true \
--set dcgmExporter.serviceMonitor.enabled=true # Prometheus 자동 수집
# vllm-servicemonitor.yaml — vLLM 메트릭 Prometheus 수집 등록
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: vllm-monitor
namespace: ml-serving
labels:
release: monitoring # kube-prometheus-stack 레이블과 일치해야 함
spec:
selector:
matchLabels:
app: vllm-inference
endpoints:
- port: http
path: /metrics
interval: 15s # 15초마다 스크랩
scrapeTimeout: 10s
실전2 — 핵심 메트릭 4종: PromQL + Alert 규칙
전통적인 API 모니터링으로는 LLM 워크로드를 제대로 볼 수 없습니다. LLM 서빙은 추가적인 측정 축이 필요합니다. E2E 레이턴시(요청 수신 → 마지막 토큰), 토큰 간 레이턴시(스트리밍 UX에 치명적), 큐 깊이, cost-per-token이 그것입니다. RPS가 아닌 tokens/sec가 진짜 처리량 지표입니다.
# prometheus-rules.yaml — LLM 핵심 Alert 규칙
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: llm-inference-alerts
namespace: monitoring
labels:
release: monitoring
spec:
groups:
- name: llm-inference
interval: 30s
rules:
# ── 1. TTFT SLA 위반 ──────────────────────────────────
- alert: LLMHighTTFT
expr: |
histogram_quantile(0.95,
rate(vllm:time_to_first_token_seconds_bucket[5m])
) > 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "TTFT p95 > 500ms ({{ $value | humanizeDuration }})"
description: "추론 서버 응답 느림 — 스케일 아웃 또는 모델 최적화 필요"
- alert: LLMCriticalTTFT
expr: |
histogram_quantile(0.95,
rate(vllm:time_to_first_token_seconds_bucket[5m])
) > 2.0
for: 2m
labels:
severity: critical
annotations:
summary: "TTFT p95 > 2초 — SLA 심각 위반"
# ── 2. 추론 큐 폭발 ───────────────────────────────────
- alert: LLMQueueDepthHigh
expr: |
sum(vllm:num_requests_waiting) by (deployment) > 20
for: 3m
labels:
severity: warning
annotations:
summary: "대기 요청 {{ $value }}개 — 즉시 스케일 아웃 확인"
# ── 3. KV 캐시 포화 ───────────────────────────────────
- alert: LLMKVCacheHigh
expr: |
avg(vllm:gpu_cache_usage_perc) by (deployment) > 90
for: 2m
labels:
severity: warning
annotations:
summary: "KV 캐시 {{ $value }}% — OOM 임박"
# ── 4. GPU 온도 과열 (DCGM) ───────────────────────────
- alert: GPUHighTemperature
expr: DCGM_FI_DEV_GPU_TEMP > 85
for: 5m
labels:
severity: warning
annotations:
summary: "GPU 온도 {{ $value }}°C — 쿨링 시스템 확인"
# ── 5. GPU SM 가동률 저조 ─────────────────────────────
- alert: GPULowUtilization
expr: |
avg(DCGM_FI_DEV_GPU_UTIL) by (node) < 20
for: 30m
labels:
severity: info
annotations:
summary: "GPU 가동률 {{ $value }}% — 노드 통합 또는 Scale-down 고려"
# ── 6. cost-per-token 기록 규칙 ───────────────────────
# 1시간마다 토큰당 비용 계산 (GPU 비용 / 처리 토큰 수)
- record: llm:cost_per_token:1h
expr: |
(
sum(DCGM_FI_DEV_POWER_USAGE) * 0.00003 # 전력비 근사
+
on() vector(4.0) # 시간당 GPU 인스턴스 비용 ($4/h H100)
)
/
sum(rate(vllm:prompt_tokens_total[1h]) + rate(vllm:generation_tokens_total[1h]))
[핵심 DCGM 메트릭]
DCGM_FI_DEV_GPU_UTIL SM 가동률 (%) → 실제 연산 얼마나 하는지
DCGM_FI_DEV_MEM_COPY_UTIL 메모리 복사율 → 데이터 병목 감지
DCGM_FI_DEV_FB_USED VRAM 사용량 (MB) → OOM 사전 감지
DCGM_FI_DEV_GPU_TEMP 온도 (°C) → 하드웨어 이상 감지
DCGM_FI_DEV_POWER_USAGE 전력 (W) → 비용 계산 기준
DCGM_FI_DEV_NVLINK_BANDWIDTH NVLink 대역폭 → 멀티GPU 병목 감지
→ SM 가동률 낮은데 VRAM 높음: 모델 로드만 되고 idle — Scale-down 후보
→ SM 높은데 메모리 복사율 높음: 데이터 피드가 병목 — PVC I/O 확인
→ 온도 85°C+ 지속: 쿨링 불량 또는 파워 리미팅 — 하드웨어 점검
실전3 — Graceful Shutdown: 진행 중인 요청 안 죽이는 법
기본 terminationGracePeriodSeconds는 30초로 진행 중인 LLM 요청을 죽입니다. 500토큰 스트리밍 응답은 10~30초가 걸릴 수 있습니다. 배치 요청은 더 길 수 있습니다.
# vllm-deployment.yaml — Graceful Shutdown 설정
spec:
template:
spec:
# ── 핵심: 30초 → 300초로 늘림 ─────────────────────
terminationGracePeriodSeconds: 300 # 5분 = 진행 중 요청 완료 대기
containers:
- name: vllm
# ── preStop hook: 새 요청 차단 → 기존 요청 완료 대기
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- |
echo "Graceful shutdown 시작..."
# 1. /health 엔드포인트를 503으로 변경 → LB가 새 요청 안 보냄
# (vLLM은 SIGTERM 받으면 자동으로 새 요청 거부)
# 2. 현재 처리 중인 요청 완료 대기 (최대 240초)
timeout=240
elapsed=0
while [ $elapsed -lt $timeout ]; do
# 대기 중인 요청 수 확인
waiting=$(curl -s localhost:8000/metrics | \
grep 'vllm:num_requests_running' | \
awk '{print $2}' | head -1)
if [ -z "$waiting" ] || [ "$waiting" = "0" ]; then
echo "✅ 모든 요청 완료, 종료 진행"
break
fi
echo "⏳ 처리 중인 요청: $waiting개, 대기 중..."
sleep 5
elapsed=$((elapsed + 5))
done
echo "Graceful shutdown 완료"
# Service에서 종료 중인 Pod로 트래픽 차단
# Readiness probe가 실패하면 Service가 자동으로 해당 Pod 제외
# → preStop + readinessProbe 조합이 완벽한 Graceful shutdown
# 추가 보호: PodDisruptionBudget (3편에서 설명)
# minAvailable: 1 → 항상 최소 1개 Pod 유지
→ Graceful shutdown 전체 흐름:
1) Kubernetes가 SIGTERM 전송
2) preStop hook 실행 → 새 요청 거부 + 기존 요청 완료 대기
3) terminationGracePeriodSeconds 내에 preStop 완료
4) SIGKILL (강제 종료)
→ preStop 시간 < terminationGracePeriodSeconds 이어야 함
preStop 최대 240초 + 여유 60초 = terminationGracePeriod 300초
→ 7B 모델 평균 요청 시간 10~15초 → 300초면 충분
→ 70B 모델 배치 요청 수분 → terminationGracePeriod 600초 이상 고려
실전4 — 카나리 배포: 새 모델 무중단 전환
# 카나리 배포 전략: Kubernetes Service + 두 Deployment로 트래픽 분할
# ── Deployment 1: 현재 운영 버전 (90% 트래픽) ────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-llama3-8b-stable
namespace: ml-serving
spec:
replicas: 9 # 전체 10개 중 9개
selector:
matchLabels:
app: vllm-inference
track: stable
template:
metadata:
labels:
app: vllm-inference
track: stable
version: v1-llama3-8b # 현재 모델
spec:
containers:
- name: vllm
image: vllm/vllm-openai:v0.4.2
args:
- "--model=/model-cache/Llama-3-8B-Instruct"
# ... (생략)
---
# ── Deployment 2: 카나리 버전 (10% 트래픽) ───────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-llama3-8b-canary
namespace: ml-serving
spec:
replicas: 1 # 전체 10개 중 1개 → 10% 트래픽
selector:
matchLabels:
app: vllm-inference
track: canary
template:
metadata:
labels:
app: vllm-inference
track: canary
version: v2-llama3-8b-ft # 파인튜닝된 새 모델
spec:
containers:
- name: vllm
image: vllm/vllm-openai:v0.4.2
args:
- "--model=/model-cache/Llama-3-8B-FT-v2" # 새 모델
- "--served-model-name=llama3-8b" # 클라이언트는 같은 이름으로 호출
# ...
---
# ── Service: 두 Deployment에 동시에 트래픽 전달 ──────────
apiVersion: v1
kind: Service
metadata:
name: vllm-inference-svc
namespace: ml-serving
spec:
selector:
app: vllm-inference # track 레이블 없음 → 두 Deployment 모두 선택
ports:
- port: 80
targetPort: 8000
# 카나리 배포 운영 스크립트
# 1단계: 카나리 10% 배포
kubectl apply -f vllm-canary-deployment.yaml
# 2단계: TTFT 비교 모니터링 (5분)
echo "=== 안정 버전 TTFT ==="
kubectl exec -n monitoring deployment/prometheus -- \
promtool query instant \
'histogram_quantile(0.95, rate(vllm:time_to_first_token_seconds_bucket{track="stable"}[5m]))'
echo "=== 카나리 버전 TTFT ==="
kubectl exec -n monitoring deployment/prometheus -- \
promtool query instant \
'histogram_quantile(0.95, rate(vllm:time_to_first_token_seconds_bucket{track="canary"}[5m]))'
# 3단계: 카나리가 안정적이면 점진 전환
# stable: 9→7→5→3→1 / canary: 1→3→5→7→9 (각 단계 10분 관찰)
kubectl scale deployment vllm-llama3-8b-stable --replicas=7 -n ml-serving
kubectl scale deployment vllm-llama3-8b-canary --replicas=3 -n ml-serving
# (반복)
# 4단계: 문제 발생 시 즉시 롤백
kubectl scale deployment vllm-llama3-8b-canary --replicas=0 -n ml-serving
echo "❌ 카나리 롤백 완료"
# 5단계: 완전 전환
kubectl scale deployment vllm-llama3-8b-stable --replicas=0 -n ml-serving
kubectl scale deployment vllm-llama3-8b-canary --replicas=10 -n ml-serving
# → 카나리가 새 stable이 됨
→ 카나리 판단 기준:
✅ 전환 조건: 카나리 TTFT p95 < stable TTFT p95 × 1.2
❌ 롤백 조건: 카나리 오류율 > 1% or TTFT > 2배 이상
→ Kubernetes Service는 Pod 수 비율로 트래픽 분산
stable 9개 + canary 1개 → 약 90/10 분할 (완벽하지 않지만 실용적)
→ 정밀 트래픽 분할 필요 시: Istio VirtualService weight 사용
→ 10개 모델 이상 운영 팀: KServe InferenceService로 통합 관리
실전5 — Spot + Karpenter: 비용 최적화
# karpenter-nodepool.yaml — 학습용 Spot + 추론용 On-Demand 분리
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: gpu-training-spot
spec:
template:
metadata:
labels:
workload-class: training
karpenter.sh/capacity-type: spot
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"] # ← Spot 인스턴스만
- key: node.kubernetes.io/instance-type
operator: In
values:
# 여러 인스턴스 타입 분산 → Spot 중단 위험 분산
- p3.8xlarge # V100 4GPU
- p3.16xlarge # V100 8GPU
- p4d.24xlarge # A100 8GPU
- p4de.24xlarge # A100 8GPU (80GB)
taints:
- key: nvidia.com/gpu
effect: NoSchedule
- key: dedicated
value: gpu
effect: NoSchedule
disruption:
consolidationPolicy: WhenUnderutilized # 유휴 노드 자동 종료
consolidateAfter: 30m # 30분 후 종료 판단
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: gpu-inference-ondemand
spec:
template:
metadata:
labels:
workload-class: inference
karpenter.sh/capacity-type: on-demand
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"] # ← 추론은 On-Demand (중단 없음)
- key: node.kubernetes.io/instance-type
operator: In
values:
- g5.xlarge # A10G 1GPU (추론 최적)
- g5.2xlarge
- g5.4xlarge
taints:
- key: nvidia.com/gpu
effect: NoSchedule
- key: dedicated
value: gpu
effect: NoSchedule
disruption:
consolidationPolicy: WhenUnderutilized
consolidateAfter: 10m # 추론 노드는 더 빠르게 종료
# Spot 중단 대비: 학습 Job에 체크포인트 + 재시도
apiVersion: batch/v1
kind: Job
metadata:
name: llm-finetuning-spot
namespace: ml-training
spec:
backoffLimit: 10 # Spot 중단 시 자동 재시도
template:
spec:
terminationGracePeriodSeconds: 120 # AWS Spot: 2분 경고 후 중단
nodeSelector:
karpenter.sh/capacity-type: spot
containers:
- name: trainer
image: pytorch/pytorch:2.3.0-cuda12.1-cudnn8-runtime
command:
- python
- finetune.py
- --checkpoint-dir=/checkpoints
- --resume-from-checkpoint=latest # 재시작 시 마지막 체크포인트에서
lifecycle:
preStop: # 중단 신호 받으면 즉시 저장
exec:
command: ["python", "save_checkpoint.py", "--emergency"]
volumeMounts:
- name: checkpoints
mountPath: /checkpoints
resources:
limits:
nvidia.com/gpu: "4"
volumes:
- name: checkpoints
persistentVolumeClaim:
claimName: training-checkpoints-pvc
[비용 최적화 효과]
항목 절감율 비고
────────────────────────────────────────────
학습 → Spot 전환 60~70% 체크포인트 필수
Karpenter 유휴 종료 50~65% WhenUnderutilized 설정
추론 Time-slicing 50~75% 소형 모델 인퍼런스 환경
Scale-to-zero (개발) 전체 idle 프로덕션 cold start 주의
→ 현실적 절감 순서:
1순위: 학습 Spot 전환 (설정 간단, 효과 즉각)
2순위: Karpenter WhenUnderutilized (idle GPU 노드 자동 제거)
3순위: 추론 Time-slicing (소형 모델 개발 환경)
→ GPU H100 idle 비용: 하루 $30, 10개면 $300/일 = 월 $9,000 낭비
→ WhenUnderutilized 설정 하나로 이 중 70% 제거 가능
실전6 — 실전 트러블슈팅 5종 패턴
# ── 패턴 1: Pod OOMKilled (가장 흔함) ────────────────────
kubectl describe pod <pod-name> -n ml-serving
# Last State: Terminated, Reason: OOMKilled
# 원인 A: GPU VRAM 부족
# 확인:
kubectl exec <pod> -n ml-serving -- nvidia-smi --query-gpu=memory.used,memory.total --format=csv
# 해결:
# --gpu-memory-utilization 0.90 → 0.85로 낮추기
# 또는 --quantization awq 추가 (VRAM 50% 절감)
# 원인 B: /dev/shm 부족 (Tensor Parallelism 시)
# 해결: emptyDir medium: Memory 추가 (2편 참고)
# ── 패턴 2: Pod Pending 지속 ──────────────────────────────
kubectl describe pod <pod-name> -n ml-serving
# Events: 0/5 nodes available: 5 Insufficient nvidia.com/gpu
# 원인 A: GPU 노드 전부 포화
kubectl get nodes -o custom-columns="NAME:.metadata.name,GPU_ALLOC:.status.allocatable.nvidia\.com/gpu"
# 원인 B: Taint/Toleration 불일치
kubectl get node <gpu-node> -o json | jq '.spec.taints'
# → Deployment의 tolerations와 key/value 정확히 일치하는지 확인
# 원인 C: ResourceQuota 초과
kubectl describe resourcequota -n ml-serving
# → hard limits 대비 used 확인
# ── 패턴 3: TTFT 갑자기 급등 ─────────────────────────────
# 확인 순서:
# 1) 큐 깊이 확인
kubectl exec -n monitoring deploy/prometheus -- \
promtool query instant 'sum(vllm:num_requests_waiting)'
# 2) KV 캐시 확인
kubectl exec -n monitoring deploy/prometheus -- \
promtool query instant 'avg(vllm:gpu_cache_usage_perc)'
# KV 캐시 90%+ → --max-model-len 줄이거나 스케일 아웃
# 3) GPU SM 가동률 확인
kubectl exec -n monitoring deploy/prometheus -- \
promtool query instant 'avg(DCGM_FI_DEV_GPU_UTIL)'
# SM 낮은데 TTFT 높음 → PCIe 병목, 네트워크 이슈 가능성
# ── 패턴 4: 롤링 업데이트 중 요청 드롭 ───────────────────
# 원인: terminationGracePeriodSeconds 너무 짧음
# 확인:
kubectl get deployment vllm-llama3-8b -n ml-serving -o jsonpath=\
'{.spec.template.spec.terminationGracePeriodSeconds}'
# → 30이면 즉시 300으로 수정 (3편 Graceful Shutdown 참고)
# ── 패턴 5: GPU Driver 충돌 (노드 재부팅 후) ──────────────
kubectl get pods -n gpu-operator
# nvidia-driver-daemonset-xxx: CrashLoopBackOff
kubectl logs -n gpu-operator daemonset/nvidia-driver-daemonset
# "driver already loaded" or "kernel module conflict"
# 해결:
kubectl delete pod -n gpu-operator -l app=nvidia-driver
# GPU Operator가 자동 재시작 → 드라이버 재로드
# 커널 업데이트로 드라이버 버전 불일치 시:
helm upgrade gpu-operator nvidia/gpu-operator \
--namespace gpu-operator \
--set driver.version=535.104.05 # 커널에 맞는 버전 고정
[트러블슈팅 빠른 참조표]
증상 확인 명령 해결
────────────────────────────────────────────────────────────────────
OOMKilled nvidia-smi → VRAM 확인 --gpu-memory-utilization 낮춤
Pending (GPU 없음) describe pod → Events 확인 Toleration or ResourceQuota 확인
TTFT 급등 vllm:num_requests_waiting 스케일 아웃 or --max-model-len 축소
요청 드롭 terminationGracePeriodSeconds 300초로 증가
Driver CrashLoop gpu-operator 로그 확인 driver Pod 재시작 or 버전 고정
✅ 시리즈 4편 핵심 정리
✅ DCGM + vLLM /metrics + Prometheus: 3계층 관찰가능성 스택 완성
✅ Alert 5종: TTFT / 큐 깊이 / KV 캐시 / GPU 온도 / SM 가동률
✅ cost-per-token: 대부분 팀이 안 추적하는 진짜 비용 메트릭
✅ terminationGracePeriodSeconds 300초: 롤링 업데이트 중 요청 드롭 방지
✅ 카나리 배포: 새 모델 10% 트래픽 → TTFT 비교 → 점진 전환
✅ Spot 60~70% + Karpenter WhenUnderutilized 50~65% → 비용 현실적 절감
❌ Spot 학습 Job: 체크포인트 없이 쓰면 중단 시 처음부터
❌ 카나리 오류율 1% 초과 즉시 롤백 — 방치하면 10%가 전체에 영향
❌ DCGM ServiceMonitor 레이블이 kube-prometheus-stack과 불일치 시 수집 안 됨
❌ terminationGracePeriodSeconds < preStop 실행 시간이면 요청 강제 종료
관련글
반응형
'AI 개발' 카테고리의 다른 글
| xAI가 터미널 코딩 에이전트 시장에 뛰어들었다 — Grok Build CLI 완전 가이드 (0) | 2026.05.26 |
|---|---|
| Antigravity SDK 심화편—Managed Agents API·GCP 엔터프라이즈 연동·CI/CD 파이프라인 실전 구축: Antigravity 2.0 (0) | 2026.05.23 |
| K8s AI 워크로드 3편—KEDA 스케일링과 멀티테넌시, HPA가 LLM에 왜 안 되는지부터 (0) | 2026.05.23 |
| K8s AI 워크로드 2편—LLM 추론 서버 배포, vLLM·TGI·Triton 실전 Deployment 완전 가이드 (0) | 2026.05.23 |
| K8s AI 워크로드 1편—GPU 노드 설정과 인프라 기초, 비싼 GPU 낭비 없이 쓰는 법 (0) | 2026.05.23 |