- Published on
KServe+Knative 콜드스타트 10배 줄이는 실전 처방
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
KServe를 Knative 위에 올려 서빙하면, 트래픽이 없을 때 scale-to-zero로 비용을 아끼는 대신 첫 요청이 느려지는 콜드스타트가 따라옵니다. 문제는 “느리다”로 끝나지 않습니다. L7 라우팅(Istio), 큐 프록시(queue-proxy), 파드 스케줄링, 이미지 풀, 모델 다운로드, 런타임 워밍업이 직렬로 이어지면서 P95가 수 초에서 수십 초까지 튀는 경우가 흔합니다.
이 글은 콜드스타트를 어디서 얼마나 쓰는지를 분해하고, 실제로 체감 성능을 10배 수준으로 줄이는 데 자주 쓰는 처방(설정/아키텍처/운영 팁)을 한 번에 정리합니다.
문제 상황 중 503이나 라우팅/리비전 전환 이슈가 섞여 있다면 먼저 아래 글의 체크리스트로 “정상 동작”을 확보하는 게 좋습니다.
콜드스타트는 5단계로 쪼개서 봐야 줄어든다
KServe+Knative에서 첫 요청 지연은 대개 아래 5단계 합입니다.
- Activator 경유 및 라우팅 준비: 트래픽이 0이면 Activator가 받았다가 새 파드가 뜰 때까지 버퍼링
- 스케줄링 대기: 노드 여유/오토스케일(Karpenter, Cluster Autoscaler)로 노드가 늦게 붙는 경우 포함
- 이미지 Pull: 대형 런타임/베이스 이미지면 여기서 수 초~수십 초
- 컨테이너 기동 및 런타임 초기화: Python import, CUDA 초기화, TorchScript 로딩 등
- 모델 로딩/다운로드: PV/S3/GCS에서 weight를 가져오거나, 로컬 캐시 미스
콜드스타트를 10배 줄이는 핵심은 “각 단계의 상한을 깎고, 병렬화/캐시로 직렬 구간을 끊는 것”입니다.
1) scale-to-zero를 포기하지 말고 minScale부터 조절한다
가장 확실한 방법은 0으로 내려가지 않게 만드는 겁니다. 하지만 비용이 문제라면, 전 서비스에 적용하지 말고 핵심 엔드포인트만 minScale을 1로 두는 식으로 타협합니다.
KServe InferenceService에 Knative autoscaling annotation을 걸어 minScale을 설정할 수 있습니다.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: sentiment
namespace: ml
annotations:
autoscaling.knative.dev/minScale: "1"
autoscaling.knative.dev/maxScale: "10"
spec:
predictor:
containers:
- name: kserve-container
image: ghcr.io/acme/sentiment:1.0.0
ports:
- containerPort: 8080
운영 팁
minScale: 1만으로도 “첫 요청 20초”가 “항상 200ms”로 떨어지는 케이스가 많습니다.- 비용이 부담이면 업무 시간대만
minScale을 올리고 야간에는 0으로 내리는 스케줄도 실전에서 자주 씁니다(예: CronJob으로 annotation 패치).
2) scaleDownDelay로 “자주 식는 서비스”를 막는다
트래픽이 간헐적으로 들어오는 서비스는 scale-to-zero로 내려갔다가 다시 올라오는 사이클이 반복되며 체감이 최악이 됩니다. 이때는 내려가는 시간을 늦추는 것만으로도 콜드스타트 빈도를 크게 줄일 수 있습니다.
metadata:
annotations:
autoscaling.knative.dev/scaleDownDelay: "10m"
- 10분 동안 요청이 없더라도 바로 0으로 내리지 않게 하여, “간헐 트래픽”의 첫 요청을 대부분 웜 상태에서 처리합니다.
3) 컨테이너 동시성(containerConcurrency)과 타깃(target)을 재설계한다
Knative는 containerConcurrency와 autoscaling target(동시 요청/초 기반)을 기준으로 파드를 늘립니다. 여기 설정이 모델 특성과 안 맞으면 “파드 1개가 과부하로 느려짐” 또는 “불필요하게 파드가 늘어 콜드스타트가 더 자주 발생”합니다.
예시로, GPU 1장당 동시성 1~2가 적절한 모델인데 동시성을 10으로 두면 첫 파드가 뜬 뒤에도 응답이 길어져 “느린데 스케일도 늦는” 상황이 생깁니다.
metadata:
annotations:
autoscaling.knative.dev/target: "1"
autoscaling.knative.dev/metric: "concurrency"
spec:
predictor:
containerConcurrency: 1
containers:
- name: kserve-container
image: ghcr.io/acme/gpu-model:2.1.0
기준 잡는 법
- GPU 추론이 무거우면
containerConcurrency: 1부터 시작해 측정으로 올립니다. - CPU 경량 모델이면
containerConcurrency를 올려 파드 수를 줄이면 콜드스타트 빈도가 감소합니다.
4) 이미지 Pull 시간을 없애려면 “작게”보다 “가까이”가 먼저다
이미지 최적화(멀티스테이지, slim)는 중요하지만, 실전에서 더 큰 효과는 다음 2가지가 자주 냅니다.
4-1) 노드에 이미지 프리풀(Pre-pull)하기
트래픽이 올 때마다 새 노드가 생기거나(오토스케일), 노드가 자주 교체되면 이미지 pull이 반복됩니다. DaemonSet으로 주요 서빙 이미지를 미리 당겨두면 콜드스타트가 크게 줄어듭니다.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: prepull-kserve-images
namespace: kube-system
spec:
selector:
matchLabels:
app: prepull-kserve-images
template:
metadata:
labels:
app: prepull-kserve-images
spec:
containers:
- name: prepull
image: ghcr.io/acme/sentiment:1.0.0
command: ["/bin/sh", "-c", "sleep 360000"]
tolerations:
- operator: "Exists"
- 실제 서비스 파드가 아니라 “이미지 캐시를 채우는 용도”입니다.
- 이미지가 여러 개면 컨테이너를 여러 개 두거나, 태그를 환경별로 분리합니다.
4-2) 레지스트리를 클러스터 근처로
- EKS라면 ECR, GKE라면 Artifact Registry처럼 동일 리전 레지스트리를 쓰는 것만으로 pull 편차가 줄어듭니다.
5) 모델 다운로드를 “요청 경로”에서 빼라: PV 캐시 또는 웨이트 프리패치
KServe에서 가장 큰 병목은 모델 웨이트를 원격 스토리지에서 가져오는 단계입니다. 이를 첫 요청에 수행하면 콜드스타트는 구조적으로 길어집니다.
선택지 A: PV에 모델 캐시(노드/파드 재시작에도 유지)
- 모델을 PVC에 두고, 컨테이너가 로컬 파일로 로딩하게 만듭니다.
- 장점: 네트워크/스토리지 지연이 줄고, 재기동에도 캐시가 남습니다.
선택지 B: initContainer로 프리패치(요청 전에 받기)
initContainer는 본 컨테이너가 뜨기 전에 실행되므로, 최소한 “서빙 컨테이너가 뜬 뒤 다운로드”는 막을 수 있습니다.
spec:
predictor:
containers:
- name: kserve-container
image: ghcr.io/acme/sentiment:1.0.0
env:
- name: MODEL_PATH
value: /models/model.pt
volumeMounts:
- name: model-vol
mountPath: /models
initContainers:
- name: fetch-model
image: curlimages/curl:8.5.0
command:
- /bin/sh
- -c
- |
set -e
test -f /models/model.pt || \
curl -L -o /models/model.pt "https://storage.example.com/models/model.pt"
volumeMounts:
- name: model-vol
mountPath: /models
volumes:
- name: model-vol
persistentVolumeClaim:
claimName: sentiment-model-pvc
test -f로 캐시 히트 시 다운로드를 건너뜁니다.
6) 런타임 워밍업: “첫 추론”을 부팅 시점에 끝내기
PyTorch/TF는 첫 추론에서 커널 컴파일, 메모리 할당, 그래프 최적화가 터지며 지연이 큽니다. 이를 첫 사용자 요청에 떠넘기지 말고, 컨테이너 시작 시점에 끝내야 합니다.
가장 단순한 방법은 애플리케이션 엔트리포인트에서 더미 입력으로 1회 워밍업을 수행하는 겁니다.
# app.py
import os
import time
import torch
MODEL_PATH = os.getenv("MODEL_PATH", "/models/model.pt")
def load_model():
model = torch.jit.load(MODEL_PATH, map_location="cpu")
model.eval()
return model
model = load_model()
# warmup
with torch.no_grad():
x = torch.zeros((1, 768))
_ = model(x)
STARTED_AT = time.time()
def predict(vec):
with torch.no_grad():
return model(vec)
추가로 모델 자체를 경량화하면 워밍업/로딩 시간이 줄어 콜드스타트가 더 내려갑니다. 특히 CPU 서빙이면 PTQ 기반 int8 양자화가 체감이 큽니다.
7) Readiness Probe를 “진짜 준비됨”으로 바꿔라
콜드스타트가 길어 보이는 이유가 “파드는 떴는데 준비가 안 됨”인 경우가 많습니다. 이때 readiness가 너무 이르게 통과하면, 실제로는 모델이 준비되지 않았는데 트래픽이 들어와 타임아웃/재시도 폭탄이 생깁니다.
권장 패턴은 /readyz가 모델 로딩 완료 + 워밍업 완료를 확인하도록 만들고, K8s readinessProbe가 그 엔드포인트만 보게 하는 겁니다.
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 1
periodSeconds: 2
timeoutSeconds: 1
failureThreshold: 30
failureThreshold를 늘려 “초기 준비 시간”을 충분히 허용합니다.- 이 설정은 콜드스타트 시간을 줄이진 않지만, 첫 요청 실패/지연 폭발을 줄여 체감 품질을 크게 올립니다.
8) 노드 스케줄링이 병목이면: 리소스/어피니티/프로비저닝을 먼저 손본다
파드가 뜨기까지 오래 걸리는 경우, 원인은 애플리케이션이 아니라 노드가 없어서입니다. 특히 GPU는 더 심합니다.
체크 포인트:
- GPU 노드가
0으로 내려가면, 첫 요청은 “노드 생성 + 드라이버 준비 + 이미지 pull”까지 포함됩니다. - 우선순위가 낮아 스케줄링이 밀리는 경우가 있습니다.
실전 처방:
- GPU 풀은
min을 1로 유지(최소 1대 상시) nodeSelector/tolerations/affinity로 정확한 노드에만 올라가게 고정- 가능하면 모델별로 노드풀을 분리해 간섭을 줄임
노드 오토스케일이 기대대로 안 움직이는 케이스는 아래 글의 점검 항목이 도움이 됩니다.
9) Knative/KServe에서 “측정” 없이는 최적화가 안 된다
콜드스타트 최적화는 감으로 하면 실패합니다. 최소한 아래 3가지는 시간으로 찍어야 합니다.
Request가 들어온 시점부터Pod Ready까지Pod Ready부터 첫200응답까지- 첫
200응답의 서버 내부 구간(모델 로딩, 워밍업, 전처리)
빠른 측정 커맨드
# 리비전/파드 이벤트로 스케줄링 지연 확인
kubectl -n ml describe pod -l serving.kserve.io/inferenceservice=sentiment
# Knative 서비스/리비전 상태 확인
kubectl -n ml get ksvc
kubectl -n ml get revision
# 실측: 첫 요청과 두 번째 요청 비교
curl -s -o /dev/null -w "time_total=%{time_total}\n" https://your-domain/v1/models/sentiment:predict -d '{"inputs":[0]}'
curl -s -o /dev/null -w "time_total=%{time_total}\n" https://your-domain/v1/models/sentiment:predict -d '{"inputs":[0]}'
- 첫 번째와 두 번째 요청 차이가 크면 콜드스타트가 맞고, 두 번째도 느리면 모델/서버 자체 병목일 가능성이 큽니다.
10) “10배”를 만드는 현실적인 조합(추천 레시피)
환경마다 다르지만, 운영에서 성공률이 높은 조합을 정리하면 아래와 같습니다.
레시피 A: 비용보다 SLA가 중요한 핵심 모델
autoscaling.knative.dev/minScale: 1containerConcurrency를 모델 특성에 맞게 낮춤(GPU면 1부터)- readiness를 모델 준비 완료 기준으로 강화
- 이미지 프리풀 + 동일 리전 레지스트리
체감: 첫 요청이 사실상 사라져 “항상 웜”에 가까워집니다.
레시피 B: 비용 민감 + 간헐 트래픽
scaleDownDelay: 10m로 빈번한 냉각 방지- PV 캐시 또는 initContainer 프리패치
- 이미지 프리풀(특히 노드 교체가 잦을 때)
체감: 콜드스타트 발생 빈도가 크게 줄고, 발생하더라도 상한이 내려갑니다.
레시피 C: GPU + 노드 오토스케일이 섞인 최악의 케이스
- GPU 노드풀
min을 1로 유지(또는 업무 시간대만) - 모델 weight를 노드 로컬/PV로 캐시
- 프리풀로 이미지 pull 제거
- 워밍업을 부팅 시점에 수행
체감: “노드 생성이 포함된 60초”를 “수 초” 수준으로 줄이는 게 목표입니다.
마무리: 콜드스타트는 설정 하나가 아니라 파이프라인 최적화다
KServe+Knative 콜드스타트는 단일 원인이 아니라, 라우팅부터 모델 로딩까지 여러 구간이 직렬로 이어진 결과입니다. 따라서 10배 개선도 “한 방”보다는 다음 순서로 접근할 때 성공 확률이 높습니다.
minScale또는scaleDownDelay로 콜드스타트 빈도부터 줄이고- 이미지 프리풀/레지스트리 최적화로 pull 지연을 없애고
- PV 캐시/프리패치로 모델 다운로드를 요청 경로에서 제거하고
- 워밍업+readiness로 첫 요청 실패/폭발을 막습니다.
이 과정을 거치면, 대부분의 팀이 목표하는 “첫 요청도 실서비스 수준”에 가까운 응답 시간을 만들 수 있습니다.