- Published on
KServe LLM 503 해결 - autoscaling·readiness 진단
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서빙이 잘 되다가도 트래픽이 조금만 튀면 KServe 추론 엔드포인트가 503 Service Unavailable 를 뱉는 경우가 있습니다. 특히 LLM은 콜드스타트(모델 로딩, GPU 초기화, KV cache 준비)가 길고, 동시성 제어가 까다로워서 autoscaling 과 readiness 가 조금만 어긋나도 503이 급증합니다.
이 글은 KServe(대개 Knative Serving 기반)에서 LLM 추론 503을 만났을 때, 어디부터 어떤 순서로 확인해야 가장 빨리 원인을 좁힐 수 있는지에 초점을 맞춥니다. 결론부터 말하면, 503은 “애플리케이션이 죽어서”가 아니라 “라우팅 계층이 지금은 보낼 곳이 없다고 판단해서” 나는 경우가 훨씬 많습니다.
관련해서 추론 서버 자체 튜닝이 필요하다면 TorchServe 관점의 503/워커/메모리 이슈도 함께 참고할 만합니다: TorchServe 503·OOM·워커 튜닝 실전 가이드
503이 나는 지점부터 구분하기
KServe에서 외부 요청이 들어와 503이 되기까지의 경로는 보통 다음 중 하나입니다.
- Ingress 또는 Gateway 레벨에서 업스트림이 없어서 503
- Knative queue-proxy가 백엔드 컨테이너로 전달하지 못해 503
- 백엔드 컨테이너는 떠 있지만 readiness가
False라서 라우팅에서 제외되어 503 - 백엔드가 과부하로 타임아웃 또는 연결 리셋이 나고, 그 결과가 503으로 표면화
핵심은 “누가 503을 응답했는지”를 로그로 확인하는 것입니다.
가장 먼저 보는 명령어
kubectl -n ${NS} get inferenceservice
kubectl -n ${NS} describe inferenceservice ${ISVC}
# KServe가 만든 Knative Service 확인
kubectl -n ${NS} get ksvc
kubectl -n ${NS} describe ksvc ${KSVC}
# Revision / Pod 상태
kubectl -n ${NS} get revision
kubectl -n ${NS} get pod -l serving.knative.dev/service=${KSVC} -o wide
kubectl -n ${NS} describe pod ${POD}
describe 출력에서 특히 아래를 봅니다.
Conditions에서Ready가False인 이유- 최근 이벤트에
Readiness probe failed,ContainerCreating,FailedScheduling,Back-off restarting failed container같은 힌트 Autoscaler관련 이벤트(스케일 업/다운)
CrashLoop이 섞여 있다면 readiness 이전에 컨테이너 생존성부터 해결해야 합니다. 이 경우는 아래 글의 체크리스트가 그대로 적용됩니다: Kubernetes CrashLoopBackOff 원인 7가지·즉시복구
KServe/Knative에서 503이 자주 생기는 구조적 이유
1) 스케일이 0 으로 내려갔다가 콜드스타트가 길어지는 경우
Knative는 기본적으로 유휴 시 스케일을 0 까지 내릴 수 있습니다. LLM은 모델 로딩이 수십 초에서 수분까지 갈 수 있어, 이 동안 요청이 들어오면 다음 문제가 생깁니다.
- 스케일 업 트리거는 됐지만 Pod가 아직
Ready가 아님 - queue-proxy가 버퍼링할 수 있는 한계를 넘으면 503
- Ingress의 업스트림이 아직 준비되지 않아 503
해결 방향은 두 갈래입니다.
- 스케일
0을 막고 최소 레플리카를 유지 - 콜드스타트 동안 요청을 충분히 버퍼링할 수 있게 timeouts/queue 설정 조정
2) readiness가 “프로세스 up”이 아니라 “모델 준비 완료”를 반영하지 못하는 경우
LLM 컨테이너가 HTTP 서버는 먼저 뜨고, 실제 모델 로딩은 백그라운드로 진행하는 패턴이 흔합니다. 이때 readiness probe가 단순히 GET /healthz 로 200을 반환하면, 라우터는 “보낼 준비 완료”로 오판합니다.
그 결과:
- 처음 몇 요청이 모델 로딩 중에 들어와 타임아웃
- 추론 엔진이 초기화 중이라 5xx
- 라우터/클라이언트 레벨에서는 503으로 관측
readiness는 반드시 “모델이 메모리에 올라가고, 첫 토큰 생성까지 가능한 상태”를 의미해야 합니다.
3) 동시성 설정 불일치로 큐가 터지는 경우
Knative는 containerConcurrency 를 기준으로 동시 요청을 제어합니다. LLM 서버는 내부적으로도 max_concurrency 나 num_workers 같은 제한이 있습니다.
- Knative는 동시
N개를 허용 - 실제 엔진은 동시에
M개만 처리 가능 (M이 더 작음)
이 불일치가 있으면 큐가 급격히 쌓이고, 지연이 늘다가 503이 발생합니다.
진단 1: 실제로 스케일링이 따라가고 있는가
현재 스케일 전략이 KPA인지 HPA인지 확인
Knative의 기본은 KPA(Knative Pod Autoscaler)입니다. 환경에 따라 HPA를 쓰기도 합니다.
kubectl -n ${NS} get podautoscaler
kubectl -n ${NS} describe podautoscaler ${PA}
kubectl -n ${NS} get hpa
kubectl -n ${NS} describe hpa ${HPA}
- KPA면
panic window,stable window,target concurrency같은 개념이 중요합니다. - HPA면 CPU/GPU/커스텀 메트릭 지연이 중요합니다.
스케일 업이 느린 흔한 원인
- GPU 노드가 부족해서
FailedScheduling - 이미지 pull이 느림(대형 이미지)
- 모델 다운로드를 런타임에 수행(시작마다 네트워크 의존)
- 초기화 시간이 길어 readiness가 늦게
True
특히 3번은 NAT 비용과도 연결됩니다. 모델을 매번 외부에서 당겨오면 지연뿐 아니라 egress 비용이 튈 수 있습니다. 비용 관점은 아래 글이 진단 프레임을 제공해줍니다: VPC NAT Gateway 비용 폭증 10분 진단·절감
진단 2: readiness probe가 LLM 현실을 반영하는가
권장 패턴: “모델 준비 완료” 엔드포인트 분리
애플리케이션 내부에 다음 상태를 분리하는 것을 권장합니다.
live: 프로세스가 살아있음ready: 모델 로딩 완료, 추론 가능
예시(파이썬 FastAPI):
from fastapi import FastAPI
import threading
app = FastAPI()
model_ready = False
def load_model():
global model_ready
# 무거운 모델 로딩
# tokenizer = ...
# model = ...
model_ready = True
@app.on_event("startup")
def startup():
t = threading.Thread(target=load_model, daemon=True)
t.start()
@app.get("/health/live")
def live():
return {"ok": True}
@app.get("/health/ready")
def ready():
if not model_ready:
# readiness는 200을 주면 안 됨
return ("not ready", 503)
return {"ready": True}
이때 KServe/Knative가 사용하는 readiness probe가 /health/ready 를 보도록 맞춰야 합니다.
초기화가 긴 LLM에서 probe 파라미터가 너무 공격적인 경우
기본 probe 설정이 짧으면, 로딩 중에 실패가 누적되어 재시작 루프로 들어가거나, 준비가 되기 전에 트래픽이 붙었다가 실패합니다.
KServe InferenceService 예시(개념적으로 이해하기 위한 YAML이며, 실제 필드는 런타임에 따라 다를 수 있습니다):
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: llm
spec:
predictor:
containers:
- name: user-container
image: myrepo/llm-server:latest
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /health/ready
port: 8080
# 모델 로딩이 120초 걸리면 그 이상으로 잡아야 함
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 60
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 6
포인트는 readiness 를 쉽게 True 로 만들지 말고, 대신 failureThreshold 를 충분히 크게 잡아 “정상적인 로딩 시간”을 허용하는 것입니다.
진단 3: queue-proxy/동시성/타임아웃이 병목인가
LLM은 “요청 1개가 오래 걸리는” 워크로드라, 동시성 설정이 503에 직결됩니다.
확인할 설정 축
containerConcurrency: Pod 하나가 동시에 처리할 요청 수- 요청 타임아웃(클라이언트, Ingress, Knative Revision)
- 스트리밍 응답 여부(스트리밍이면 프록시 타임아웃과 궁합)
containerConcurrency 를 너무 크게 잡으면 GPU 메모리와 KV cache가 터지거나 지연이 폭증합니다. 너무 작게 잡으면 스케일 아웃이 잦아지고, 스케일링 지연 동안 503이 늘 수 있습니다.
실전 권장 접근
- 먼저 엔진이 안정적으로 처리 가능한 동시성을 측정
- 그 값에 맞춰
containerConcurrency를 설정 - 오토스케일 타깃을 동시성 기반으로 조정
엔진 최적화(4bit, FlashAttention 등)로 “요청당 시간”을 줄이면 503도 같이 줄어듭니다. 로컬/단일 노드 기준이지만 병목 이해에 도움이 됩니다: Transformers 로컬 LLM 느림·OOM, 4bit+FlashAttn2
자주 나오는 원인별 처방전
1) 스케일 0 콜드스타트로 503
증상
- 유휴 후 첫 요청이 자주 503
- Pod 생성 이벤트가 503 시점과 겹침
처방
- 최소 레플리카 유지(스케일
0방지) - 모델을 이미지에 포함하거나, 노드 로컬 캐시/퍼시스턴트 볼륨으로 다운로드 비용 제거
- 초기화 시간을 반영해 readiness를 보수적으로
2) readiness가 너무 빨리 True
증상
- Pod는
Ready로 보이는데 첫 요청들이 실패 - 서버 로그에
model loading중 에러 또는 타임아웃
처방
- readiness 엔드포인트를 “모델 로딩 완료”로 정의
- 워밍업 요청을 startup 단계에서 수행(가능하면)
3) 동시성 과다로 큐 적체 후 503
증상
- 트래픽 증가 시 p95/p99 지연이 먼저 튄 뒤 503 발생
- GPU 메모리 사용량이 급상승
처방
containerConcurrency를 엔진 한계에 맞춤- 배치/동적 배치가 있다면 설정하되, 지연과의 트레이드오프 측정
- 요청 크기 제한(최대 토큰, 최대 입력 길이) 강제
4) 스케줄링 지연(특히 GPU)
증상
- 이벤트에
FailedScheduling이 보임 - GPU 노드 오토스케일이 늦음
처방
- GPU 노드풀 최소 수량 확보
- Pod priority, node affinity, taints/tolerations 재점검
- 이미지 크기 최적화 및 registry pull 가속
503을 줄이는 운영 체크리스트
아래 순서로 보면 대부분의 케이스가 빠르게 정리됩니다.
- 503 응답 주체 확인: Ingress 로그, queue-proxy 로그, 앱 로그
ksvc와revision의Conditions에서Ready가 왜False인지 확인- Pod 이벤트에서
FailedScheduling또는 probe 실패 여부 확인 - readiness가 “모델 준비 완료”를 보장하는지 점검
containerConcurrency와 엔진 내부 동시성 제한이 일치하는지 점검- 스케일
0을 쓸지 말지 결정(LLM은 대체로 최소 레플리카 권장) - 모델 다운로드/초기화를 런타임에서 제거해 콜드스타트 단축
마무리
KServe에서 LLM 503은 단순 장애라기보다 “스케일링과 준비 상태의 계약이 깨졌을 때” 나타나는 신호인 경우가 많습니다. 특히 LLM은 콜드스타트가 길고 요청이 무거워서, 웹 API에서 흔히 쓰는 readiness/오토스케일 기본값이 그대로는 잘 맞지 않습니다.
우선 Ready 조건과 Pod 이벤트로 “라우팅이 왜 제외됐는지”를 확인하고, readiness를 모델 준비 완료 기준으로 재정의한 뒤, 동시성과 스케일 정책을 맞추면 503의 상당수를 구조적으로 없앨 수 있습니다.