Published on

KServe+Knative로 LLM 추론 오토스케일·카나리

Authors

LLM 추론을 쿠버네티스 위에 올리면, 곧바로 두 가지 요구가 생깁니다. 첫째는 부하에 따라 자동으로 늘고 줄어드는 오토스케일(가능하면 유휴 시 0까지)이고, 둘째는 모델 교체를 안전하게 검증하는 카나리 배포입니다. KServe는 추론 서빙(모델 관리, 프로토콜, 관측성)을 표준화하고, Knative는 트래픽 라우팅과 요청 기반 스케일링을 제공합니다. 둘을 함께 쓰면 “모델 서버”와 “배포/스케일”을 깔끔하게 분리하면서도 운영 난이도를 낮출 수 있습니다.

이 글은 다음을 목표로 합니다.

  • KServe InferenceService를 Knative 기반으로 배포해 요청 기반 오토스케일을 구성
  • revision 간 트래픽 분할로 카나리(점진적) 롤아웃 수행
  • LLM 특유의 콜드스타트, GPU 자원, 동시성/큐잉 이슈를 함께 다룸

참고로, 추론 API가 gRPC라면 타임아웃과 리트라이가 카나리 구간에서 폭주를 만들 수 있습니다. 관련해서는 gRPC MSA에서 데드라인·리트라이 폭주 막는 법도 같이 보면 안정성 설계에 도움이 됩니다.

KServe와 Knative 역할 분담

KServe가 해주는 것

  • InferenceService라는 CRD로 모델 서빙을 선언적으로 정의
  • Predictor(모델 서버) 중심의 표준 엔드포인트 제공(HTTP/gRPC, v2 프로토콜 등)
  • 배포 형태를 Knative 또는 Raw Deployment로 선택 가능(여기서는 Knative 사용)
  • 메트릭/로깅/트레이싱 연동 지점 제공(설치 옵션에 따라 다름)

Knative가 해주는 것

  • Revision 기반 롤아웃(새 설정/새 이미지가 새 revision으로 생성)
  • 트래픽 분할(예: 90%는 기존, 10%는 신규)
  • 요청 기반 오토스케일(동시성, RPS, 큐 길이 등 기준)
  • minScale/maxScale로 스케일 범위 제어(유휴 시 0도 가능)

정리하면 KServe는 “추론 워크로드의 표준화된 선언”이고, Knative는 “그 워크로드를 얼마나 띄우고 어떻게 트래픽을 나눌지”를 담당합니다.

아키텍처: LLM 추론에서 중요한 지점

LLM 추론은 일반 웹 API와 달리 다음 특성이 두드러집니다.

  • 콜드스타트 비용: 모델 로딩, 토크나이저 초기화, GPU 메모리 워밍업
  • 자원 단위가 큼: GPU 1장 단위, 또는 MIG/슬라이스 단위로 스케일해야 함
  • 동시성/큐잉이 품질에 영향: 배치/큐가 길어지면 p95/p99 지연이 급등
  • 스트리밍 응답: SSE/웹소켓/스트리밍 gRPC를 쓰면 “요청 1개가 오래 붙잡는” 형태가 됨

따라서 오토스케일은 단순 CPU 사용률보다, 동시성(Concurrency) 또는 RPS 기반이 더 잘 맞습니다. 카나리는 단순 성공률뿐 아니라, 토큰 생성 속도(token/s), 첫 토큰 지연(TTFT), OOM, 5xx 같은 지표를 함께 봐야 합니다.

설치 전제(간단 체크리스트)

클러스터마다 설치 방식이 다르므로 여기서는 “어떤 컴포넌트가 필요하다”에 집중합니다.

  • Knative Serving 설치(트래픽 라우팅, 오토스케일)
  • KServe 설치(기본 배포 모드가 Knative를 사용하도록)
  • Ingress(예: Istio, Kourier) 및 도메인 설정
  • GPU 노드가 있다면 NVIDIA device plugin 및 런타임 설정

운영 중 권한 문제로 막히는 경우가 꽤 잦습니다. 쿠버네티스 인증/인가 이슈는 Kubernetes 401 Unauthorized 원인별 해결 가이드도 참고하세요.

KServe InferenceService로 LLM Predictor 배포

아래 예시는 “LLM 서버 이미지(예: vLLM/TGI/자체 서버)”를 Predictor로 올린다고 가정합니다. 핵심은 Knative annotation으로 스케일 정책과 트래픽 분할의 기반을 마련하는 것입니다.

주의: 본문에서 <> 류 문자가 그대로 나오면 MDX에서 JSX로 오인될 수 있어, 모든 예시는 코드 블록 또는 인라인 코드로만 표기합니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: llm-infer
  namespace: llm
  annotations:
    # Knative autoscaling: 동시성 기반(권장)
    autoscaling.knative.dev/class: kpa.autoscaling.knative.dev
    autoscaling.knative.dev/metric: concurrency
    autoscaling.knative.dev/target: "2"            # pod당 목표 동시 요청 수
    autoscaling.knative.dev/minScale: "0"          # 유휴 시 0까지(콜드스타트 감수)
    autoscaling.knative.dev/maxScale: "10"         # 상한
    autoscaling.knative.dev/scaleDownDelay: "60s"  # 급격한 축소 방지
spec:
  predictor:
    containers:
      - name: predictor
        image: ghcr.io/your-org/llm-server:1.0.0
        ports:
          - containerPort: 8080
        env:
          - name: MODEL_ID
            value: "your-model"
          - name: MAX_TOKENS
            value: "512"
        resources:
          requests:
            cpu: "2"
            memory: "8Gi"
            nvidia.com/gpu: "1"
          limits:
            cpu: "4"
            memory: "16Gi"
            nvidia.com/gpu: "1"

동시성 타깃을 어떻게 잡을까

  • GPU LLM은 보통 “동시 요청 수”가 곧 KV cache 압박과 지연 증가로 이어집니다.
  • target=2는 보수적인 시작점입니다. 모델/컨텍스트 길이/배치 전략에 따라 1이 적절할 수도 있습니다.
  • 스트리밍 응답이면 요청이 오래 유지되므로 동시성 타깃을 더 낮춰야 합니다.

minScale=0 vs minScale=1

  • minScale=0: 비용 절감 극대화, 대신 첫 요청이 느릴 수 있음(모델 로딩)
  • minScale=1: 항상 warm pod 유지, 비용 증가 대신 TTFT 안정

실무에서는 “업무 시간엔 minScale=1(또는 2), 야간엔 0” 같은 스케줄링을 별도로 두기도 합니다.

Knative Revision을 이용한 카나리 트래픽 분할

KServe가 Knative 기반으로 배포되면, 내부적으로 Knative Service/Revision이 만들어지고, 트래픽 분할은 Knative의 라우팅 레이어에서 처리됩니다.

카나리의 핵심은 “새 모델 서버 이미지 또는 새 설정”을 적용해 새 revision을 만든 뒤, 트래픽을 점진적으로 옮기는 것입니다.

1) 새 버전 배포(새 이미지로 교체)

아래처럼 image1.1.0으로 바꾸면 새 revision이 생성됩니다.

kubectl -n llm patch inferenceservice llm-infer --type='json' \
  -p='[{"op":"replace","path":"/spec/predictor/containers/0/image","value":"ghcr.io/your-org/llm-server:1.1.0"}]'

2) 트래픽을 90/10으로 분할

Knative Service 리소스에서 spec.traffic을 조정합니다. (KServe가 생성한 Knative Service 이름은 보통 InferenceService와 연계되며, 환경에 따라 다를 수 있으니 kubectl get ksvc -n llm로 확인하세요.)

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: llm-infer
  namespace: llm
spec:
  template:
    metadata:
      annotations:
        autoscaling.knative.dev/metric: concurrency
        autoscaling.knative.dev/target: "2"
  traffic:
    - latestRevision: false
      revisionName: llm-infer-predictor-00001
      percent: 90
    - latestRevision: false
      revisionName: llm-infer-predictor-00002
      percent: 10

환경에 따라 KServe가 직접 traffic을 관리하는 패턴도 있고, “KServe는 predictor를 만들고, 트래픽은 Knative Service에서 조정”하는 운영 패턴도 있습니다. 중요한 건 revision을 명시하고 비율을 고정해, 의도치 않게 latestRevision으로 쏠리지 않게 하는 것입니다.

3) 카나리 진행 중 관측해야 할 지표

  • HTTP: 5xx 비율, p95/p99, 응답 크기
  • LLM: TTFT, 토큰 생성 속도(token/s), 생성 길이 분포, OOM/재시작
  • GPU: 메모리 사용량, SM utilization, throttling
  • 큐/동시성: 요청 대기 시간, 활성 pod 수, 스케일 이벤트

카나리 판단을 “성공률만”으로 하면, 품질/지연 악화가 늦게 발견됩니다. 특히 LLM은 살아있지만 느린 상태가 흔합니다.

LLM 추론 오토스케일 튜닝 포인트

동시성 기반 vs RPS 기반

  • concurrency: 요청이 오래 걸리는 LLM에 잘 맞음(스트리밍 포함)
  • rps: 요청당 시간이 균일하고 짧을 때 유리

Knative annotation 예시는 다음과 같습니다.

metadata:
  annotations:
    autoscaling.knative.dev/metric: rps
    autoscaling.knative.dev/target: "1"   # pod당 초당 요청 목표

LLM은 요청 시간이 길고 분산이 크므로 보통 concurrency가 더 예측 가능합니다.

스케일 업/다운의 “너무 빠름”을 막기

  • 스케일 업이 늦으면 큐가 길어지고 지연이 폭증
  • 스케일 다운이 빠르면 워밍된 pod가 사라져 TTFT가 흔들림

대표적으로 scaleDownDelay를 늘리거나, minScale을 조절합니다.

metadata:
  annotations:
    autoscaling.knative.dev/scaleDownDelay: "300s"
    autoscaling.knative.dev/minScale: "1"

GPU 워크로드에서의 현실적인 제약

  • GPU는 빈번한 스케일 인/아웃이 비용/성능 모두에 불리할 수 있습니다.
  • 노드 오토스케일러(Cluster Autoscaler/Karpenter)와 결합 시, “pod 스케일은 됐는데 GPU 노드가 늦게 붙는” 구간이 생깁니다.

이 경우에는:

  • minScale로 최소 warm pod를 유지
  • 노드 그룹 최소 GPU 노드 수를 1 이상 유지
  • 모델 로딩 시간을 줄이기(이미지 레이어 최적화, 모델 캐시/프리로드)

카나리 배포 전략: 비율, 시간, 롤백

추천 카나리 스텝

  • 1% → 5% → 10% → 25% → 50% → 100%
  • 각 스텝마다 최소 10~30분(트래픽 패턴에 따라) 관측

자동 롤백의 기준 예시

  • 5xx 비율이 기준 대비 X% 이상 증가
  • p95가 기준 대비 Yms 이상 증가
  • OOM 재시작 발생
  • TTFT가 기준 대비 Z% 이상 악화

롤백은 트래픽을 이전 revision 100%로 되돌리면 즉시 효과가 있습니다.

spec:
  traffic:
    - revisionName: llm-infer-predictor-00001
      percent: 100

실전 운영 팁: “카나리 중 재시도 폭주” 방지

LLM 추론은 지연이 길어 타임아웃이 촉발되기 쉽고, 클라이언트가 공격적으로 리트라이하면 카나리 revision에 트래픽이 비정상적으로 몰릴 수 있습니다. 다음 원칙이 유효합니다.

  • 클라이언트/게이트웨이의 타임아웃을 “최대 생성 시간”에 맞춰 재설정
  • 리트라이는 지수 백오프 + 지터, 그리고 재시도 예산을 둠
  • 429/503을 받으면 즉시 재시도하지 말고 대기

구체적인 패턴은 gRPC MSA에서 데드라인·리트라이 폭주 막는 법에서 설명한 “데드라인 전파, 리트라이 상한, 벌크헤드”를 LLM에도 그대로 적용할 수 있습니다.

GitOps(Argo CD)로 카나리 선언을 관리할 때

카나리 트래픽 비율은 자주 바뀌므로 GitOps로 관리할 때 충돌이 생기기 쉽습니다.

  • “트래픽 분할 YAML”을 별도 오버레이로 분리
  • 자동화 도구가 비율을 조정한다면, Argo CD의 sync 정책/ignore 설정을 신중히 설계
  • RBAC 문제로 sync가 실패하면 AppProject 권한을 점검

관련 트러블슈팅은 Argo CD Sync 실패? AppProject RBAC로 해결하기가 도움이 됩니다.

디버깅 체크리스트

카나리로 올렸더니 새 revision만 느리다

  • 모델 파일 다운로드/캐시가 느린지 확인(초기화 로그)
  • GPU 메모리 단편화 또는 워밍업 차이 확인
  • readinessProbe가 너무 빨리 통과해 “준비 안 된 상태로 트래픽”을 받는지 확인

스케일이 안 늘어난다

  • Knative autoscaler 메트릭이 수집되는지 확인
  • maxScale이 너무 낮지 않은지 확인
  • 동시성 타깃이 너무 높아 스케일 트리거가 안 걸리는지 확인

스케일이 너무 자주 흔들린다

  • scaleDownDelay 증가
  • minScale 상향
  • 동시성 타깃을 낮춰 pod 수를 더 빨리 늘리도록 조정

마무리: KServe+Knative 조합의 핵심 가치

KServe와 Knative를 함께 쓰면, LLM 추론에서 가장 중요한 두 축을 비교적 간단히 달성할 수 있습니다.

  • 오토스케일: CPU 사용률이 아니라 요청 기반(동시성/RPS) 으로 스케일
  • 카나리: revision 기반 트래픽 분할로 안전한 모델 교체

다만 LLM은 콜드스타트와 GPU 제약 때문에 “0까지 스케일”이 항상 정답은 아닙니다. 비용과 사용자 경험(TTFT/p99)을 함께 보고 minScale, 동시성 타깃, 스케일 다운 지연을 조합해 최적점을 찾는 것이 운영의 핵심입니다.

다음 단계로는 (1) Prometheus/Grafana로 TTFT·token/s 대시보드 구성, (2) 카나리 자동 판정(예: SLO 기반)과 자동 롤백, (3) RAG를 붙였다면 벡터 검색 지연까지 포함한 엔드투엔드 SLO 설계를 추천합니다.