- Published on
KServe+Knative로 LLM 서버리스 배포·오토스케일
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스 LLM 서빙을 쿠버네티스 위에서 “운영 가능”한 수준으로 만들려면, 단순히 모델을 띄우는 것보다 배포 단순화, 오토스케일, 콜드스타트 관리, 관측(로그·메트릭·트레이싱), 롤아웃/롤백까지 한 번에 설계해야 합니다. 이때 조합이 좋은 스택이 KServe + Knative 입니다.
- KServe: 모델 서빙을 위한 CRD(예:
InferenceService)와 트래픽 라우팅, 스토리지 연동, 추론 런타임(예: vLLM, Triton, TorchServe 등) 배포를 표준화합니다. - Knative Serving: 요청 기반 오토스케일(필요하면 0까지), 리비전 기반 롤아웃, 트래픽 분할, 컨커런시 기반 스케일링을 제공합니다.
이 글에서는 LLM(예: vLLM 기반 OpenAI 호환 API)을 KServe로 선언적으로 배포하고, Knative로 오토스케일하며, 운영에서 자주 부딪히는 함정(콜드스타트, 타임아웃, GPU 리소스, 스토리지, 롤링 업데이트)을 실전 관점에서 정리합니다.
전체 아키텍처 한 장으로 보기
구성 요소를 요청 흐름으로 풀면 다음과 같습니다.
- 클라이언트가 HTTP 요청을 보냅니다.
- (선택) Ingress/Gateway가 외부 트래픽을 받아 Knative로 전달합니다.
- Knative가
Revision(실제 파드 템플릿)으로 라우팅합니다. - 오토스케일러(KPA/HPA)가 RPS/컨커런시/메트릭을 기반으로 파드를 늘리거나 줄입니다.
- KServe
InferenceService가 모델 서버 런타임(예: vLLM)을 관리하고, 모델 아티팩트를 스토리지에서 가져옵니다.
핵심은 “모델 배포는 KServe가 표준화하고, 트래픽과 스케일링은 Knative가 담당한다”는 역할 분리입니다.
언제 KServe+Knative가 특히 유리한가
다음 조건에서 체감 이점이 큽니다.
- 트래픽이 불규칙해서 상시 GPU를 붙잡아두기 아까운 경우(스케일 투 제로)
- 여러 모델/버전을 운영하며, 트래픽 분할(카나리)과 롤백이 잦은 경우
- 플랫폼 팀이 표준 배포 방식(CRD)과 가드레일(리소스, 네트워크, 보안)을 제공해야 하는 경우
반대로, 초저지연(수 ms 단위)만 절대적으로 요구되거나, 콜드스타트를 절대 허용하지 않는 경우에는 “서버리스”보다는 상시 프로비저닝(고정 replica) + 별도 오토스케일 전략이 더 맞을 수 있습니다.
설치/전제 조건 체크리스트
환경마다 디테일이 다르지만, 최소 전제는 다음입니다.
- Kubernetes 클러스터
- Knative Serving 설치(및 Ingress 설정)
- KServe 설치(Controller, CRD)
- GPU 노드(LLM이면 사실상 필수)와 NVIDIA Device Plugin
- 이미지 레지스트리 접근, 모델 스토리지(S3/MinIO/PVC 등)
설치 자체는 배포판(예: Istio/Contour/Kourier) 선택과 클러스터 정책에 따라 달라져서 여기서는 생략하고, 리소스 선언과 운영 포인트에 집중합니다.
예제 1: KServe InferenceService로 vLLM(OpenAI 호환) 배포
아래 예시는 “컨테이너 하나에 vLLM 서버를 띄우고, 모델은 PVC에 미리 준비되어 있다”는 가정입니다. 운영에서는 S3/MinIO에서 동적으로 당겨오기도 하지만, LLM은 아티팩트가 크고 콜드스타트가 민감하므로 PVC 프리로드가 더 예측 가능합니다.
주의: 본문에서
<>문자는 MDX 빌드 에러를 유발할 수 있어 코드 블록에만 사용합니다.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: llm-vllm
namespace: llm
spec:
predictor:
containers:
- name: vllm
image: ghcr.io/vllm-project/vllm-openai:latest
args:
- --model=/models/llama
- --host=0.0.0.0
- --port=8000
- --max-model-len=4096
- --gpu-memory-utilization=0.90
ports:
- containerPort: 8000
resources:
limits:
nvidia.com/gpu: "1"
cpu: "4"
memory: 16Gi
requests:
nvidia.com/gpu: "1"
cpu: "2"
memory: 12Gi
volumeMounts:
- name: model-pvc
mountPath: /models
volumes:
- name: model-pvc
persistentVolumeClaim:
claimName: llama-model-pvc
이 리소스를 적용하면 KServe가 내부적으로 Knative Service(리비전 포함)를 생성해 서빙합니다. 즉, KServe 선언 하나로 Knative의 배포/스케일링 메커니즘을 간접적으로 사용하게 됩니다.
모델 메모리(OOM)부터 잡아야 서버리스가 된다
스케일 투 제로가 가능해도, “뜨자마자 OOM”이면 아무 의미가 없습니다. LLM은 특히 다음을 먼저 점검하세요.
- 컨테이너 메모리/VRAM 상한과 모델 크기의 정합
- KV 캐시가 폭증하는 요청 패턴(긴 컨텍스트)
- 배치/컨커런시 증가로 인한 VRAM 피크
4bit 양자화나 메모리 최적화는 서버리스 비용에도 직결됩니다. 관련해서는 다음 글을 같이 읽어두면 좋습니다.
예제 2: Knative 오토스케일 설정(컨커런시 기반)
Knative는 기본적으로 “요청 수/동시성”을 기준으로 스케일링합니다. LLM은 CPU 웹앱과 달리 1 파드가 감당 가능한 동시 요청 수가 매우 낮을 수 있습니다(특히 긴 생성, 스트리밍, 큰 컨텍스트).
아래는 KServe가 생성하는 Knative Service/Revision에 주입할 수 있는 대표적인 어노테이션 예시입니다. (클러스터 정책에 따라 KServe에서 직접 패치하거나, InferenceService 템플릿/웹훅으로 주입합니다.)
metadata:
annotations:
autoscaling.knative.dev/minScale: "0"
autoscaling.knative.dev/maxScale: "10"
autoscaling.knative.dev/target: "1"
autoscaling.knative.dev/metric: "concurrency"
autoscaling.knative.dev/window: "60s"
minScale: 0이면 스케일 투 제로가 가능해집니다.target: 1은 “파드 하나당 동시 1 요청”을 목표로 합니다. LLM은 이 값이 생각보다 중요합니다.window는 스케일 결정을 위한 관찰 창입니다. 너무 짧으면 출렁이고, 너무 길면 반응이 느립니다.
동시성(target)을 잘못 잡으면 생기는 문제
- 너무 높게 잡으면: 한 파드에 요청이 몰려 토큰 생성 속도 급락, tail latency 폭발, 타임아웃 증가
- 너무 낮게 잡으면: 파드가 과도하게 늘어 GPU 비용 급증, 스케줄링 지연
LLM은 “평균”이 아니라 “최악 구간”이 사용자 경험을 망치므로, 실제 트래픽으로 부하 테스트를 돌려 p95/p99 기준으로 target을 찾는 것이 안전합니다.
콜드스타트: LLM 서버리스의 가장 큰 현실 문제
스케일 투 제로는 비용을 줄이지만, LLM은 콜드스타트가 큽니다.
- 컨테이너 이미지 pull
- 모델 로딩(수 GB)
- GPU 초기화
- 토크나이저/엔진 워밍업
여기서 운영 선택지는 크게 3가지입니다.
- 완전 서버리스:
minScale: 0유지, 대신 첫 요청 지연을 수용 - 웜 유지:
minScale: 1로 최소 1개 파드를 상시 유지 - 하이브리드: 업무 시간에는
minScale: 1, 야간에는minScale: 0같은 스케줄링
실무에서는 2 또는 3이 가장 흔합니다. “LLM API는 항상 느리다”는 인상을 주면 제품이 망가집니다.
콜드스타트 줄이는 실전 팁
- 이미지 크기 최소화 및 레이어 캐시 최적화
- 모델 아티팩트는 가능한 노드 로컬 캐시/PVC로 프리로드
- 워밍업 요청(Job/CronJob)로 엔진 준비
컨테이너 빌드 최적화는 배포 리드타임까지 줄여주므로, 다음 글도 같이 참고하면 좋습니다.
타임아웃 설계: 스트리밍과 긴 생성에서 반드시 터진다
Knative, Ingress, 프록시, 클라이언트 SDK까지 여러 계층에 타임아웃이 존재합니다. LLM은 특히 다음 케이스에서 타임아웃이 빈번합니다.
- 첫 토큰까지 시간이 긴 요청(대형 프롬프트, 콜드스타트)
- 스트리밍 중 네트워크 지연
- 큐잉(스케일 아웃 중 대기)
따라서 “어느 계층의 타임아웃이 먼저 만료되는지”를 설계해야 합니다. gRPC를 쓰거나, 내부 호출이 gRPC인 경우 데드라인 전파 개념이 그대로 적용됩니다.
Knative 요청 타임아웃 예시
metadata:
annotations:
serving.knative.dev/timeoutSeconds: "600"
요청이 길어질 수 있는 LLM에서는 기본값(환경에 따라 다름)을 그대로 두면 운영 중에 “가끔만 터지는” 난감한 장애가 생깁니다. 단, 무작정 늘리면 커넥션이 오래 붙어 있어 리소스를 잡아먹으니, 스트리밍이면 서버/클라이언트 keep-alive, 프록시 idle timeout도 같이 봐야 합니다.
트래픽 분할로 카나리 배포하기(모델 버전 교체)
KServe/Knative 조합의 강점은 “리비전 단위”로 트래픽을 나눌 수 있다는 점입니다. 예를 들어 새 모델(또는 새 양자화/새 텐서 병렬 설정)을 10%만 흘려보고 문제 없으면 100%로 올리는 방식입니다.
개념적으로는 다음과 같습니다.
- Revision A: 기존 모델
- Revision B: 신규 모델
- 90:10 으로 트래픽 분할
구체 YAML은 환경/버전에 따라 달라질 수 있지만, 운영 포인트는 동일합니다.
- 신규 리비전의 첫 토큰 지연, 토큰/초, 에러율, GPU 메모리 피크를 기존과 비교
- 장애 시 즉시 100%를 기존으로 되돌릴 수 있게 라우팅을 준비
관측(Observability): “느리다”를 숫자로 쪼개기
LLM 서빙의 지표는 일반 웹 API와 다릅니다. 최소한 아래를 분리해서 봐야 합니다.
- 요청 지연:
time_to_first_token,time_per_output_token,total_latency - 처리량: tokens/sec, requests/sec
- 큐잉: pending requests, scale-out 대기 시간
- 자원: GPU utilization, GPU memory, CPU throttling
- 모델 레벨: 컨텍스트 길이 분포, 출력 토큰 분포
가능하면 애플리케이션 레벨에서 구조화 로그(JSON)로 남기고, 요청 ID로 추적 가능하게 만드세요.
운영에서 자주 터지는 이슈와 대응
1) 스케일 아웃이 느리다
- GPU 노드가 부족하거나, 스케줄링이 밀리는 경우가 많습니다.
- 해결: 노드 오토스케일러(Cluster Autoscaler)와 GPU 노드 풀 정책을 함께 설계하고,
maxScale을 현실적으로 잡습니다.
2) 스케일 투 제로 후 첫 요청이 너무 느리다
- 해결:
minScale: 1또는 워밍업, 모델 프리로드(PVC), 이미지 최적화
3) 동시 요청에서 품질/속도가 급락한다
- 해결:
target(컨커런시) 낮추기, 요청당max_tokens제한, 컨텍스트 제한, 큐잉/레이트리밋 도입
4) OOM이 간헐적으로 난다
- 해결: 컨텍스트 길이 상한, 배치 전략 조정, 양자화(4bit 등), 모델별 리소스 프로파일링
로컬에서 빠르게 검증하는 최소 호출 예시
배포 후에는 서비스 URL(클러스터 설정에 따라 다름)을 확인하고 OpenAI 호환 엔드포인트를 호출합니다.
curl -s http://YOUR_LLM_HOST/v1/models | jq
curl -s http://YOUR_LLM_HOST/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "llama",
"messages": [{"role": "user", "content": "KServe와 Knative 차이를 3줄로 요약해줘"}],
"temperature": 0.2,
"max_tokens": 128
}' | jq
스트리밍을 쓴다면 프록시/Ingress의 버퍼링 설정, idle timeout을 반드시 같이 보세요. “서버는 스트리밍을 보내는데 중간에서 끊기는” 현상이 가장 흔합니다.
결론: KServe는 표준화, Knative는 비용 최적화
KServe + Knative 조합은 LLM 서빙을 “쿠버네티스 네이티브”하게 만들고, 모델 배포를 선언적으로 표준화하며, 트래픽 기반으로 자동 확장·축소까지 연결해줍니다. 다만 LLM의 특성상 서버리스의 달콤함(스케일 투 제로)만 보고 들어가면 콜드스타트·타임아웃·OOM에서 바로 막힙니다.
실무적으로는 다음 순서로 접근하는 것을 권합니다.
- 단일 파드에서 OOM 없이 안정적으로 뜨는지(모델/VRAM/컨텍스트 상한)
- target 컨커런시를 보수적으로 잡고, p95/p99로 튜닝
- 타임아웃을 계층별로 정리하고, 스트리밍 경로를 검증
- 비용과 UX 사이에서
minScale전략을 결정(0 vs 1 vs 스케줄) - 카나리/롤백과 관측 지표를 붙여 운영 루프를 완성
이 루프를 만들면 “LLM을 띄웠다”가 아니라 “LLM을 운영한다”로 넘어갈 수 있습니다.