본문 바로가기

AI 개발

K8s AI 워크로드 4편—프로덕션 관찰가능성·카나리 배포·비용 최적화, 운영에서 살아남는 법

반응형

 

배포는 끝이 아닙니다. 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 실행 시간이면 요청 강제 종료

관련글

 

반응형