Published on

KServe InferenceService 503 - Knative 스케일링 해결

Authors

서빙 중인 KServe InferenceService가 갑자기 503 Service Unavailable을 뱉는 순간은 대개 “모델이 죽었다”기보다 요청이 라우팅될 시점에 트래픽을 받을 준비가 안 된 상태에서 발생합니다. KServe는 내부적으로 Knative Serving(및 Istio/Ingress)을 통해 트래픽을 라우팅하고, Knative는 오토스케일러가 Revision을 늘리고 줄이며(심지어 0까지), 그 과정에서 콜드스타트프로브/타임아웃 이슈가 겹치면 503이 쉽게 만들어집니다.

이 글은 KServe InferenceService 503을 “Knative 스케일링 관점”에서 재현 가능한 형태로 진단하고, 운영에서 많이 쓰는 해결책을 설정 예시와 함께 정리합니다.

참고: Pod가 실제로 재시작/크래시 중이라면 503이 아니라 근본 원인이 CrashLoopBackOff, OOMKilled, Probe 실패일 수 있습니다. 해당 관점의 진단은 K8s CrashLoopBackOff - OOMKilled·Probe 실패 진단도 함께 보면 좋습니다.

503이 나는 지점: KServe- Knative- Queue Proxy

KServe의 기본 경로(대부분의 설치에서)는 대략 다음 흐름입니다.

  • 외부 요청이 Ingress(Gateway)로 진입
  • Knative Route가 트래픽을 특정 Revision으로 전달
  • Revision Pod 내부의 queue-proxy가 요청을 받아 사용자 컨테이너(모델 서버)로 전달

여기서 503은 주로 다음 상황에서 발생합니다.

  1. 스케일 투 제로 이후 첫 요청: Pod가 0개라 즉시 처리 불가, 혹은 준비 완료 전 라우팅
  2. 컨테이너는 떴지만 준비가 늦음: 모델 로딩이 느려 readiness가 늦게 성공
  3. 오토스케일이 트래픽을 과소평가: 동시성/타깃 설정이 안 맞아 순간 부하에 Pod가 부족
  4. 요청 타임아웃이 짧음: 콜드스타트 시간보다 라우팅/프록시 타임아웃이 짧아 503/504
  5. 프로브 설정이 부정확: readiness가 너무 엄격하거나, 반대로 너무 느슨해 라우팅 시점 불일치

1단계: 503의 “발생 주체”를 로그로 구분하기

먼저 503이 어디서 만들어지는지(ingress, activator, queue-proxy, 사용자 컨테이너)를 나눠야 합니다.

이벤트와 상태로 빠르게 감 잡기

kubectl get inferenceservice -A
kubectl describe inferenceservice -n <namespace> <name>

kubectl get ksvc -n <namespace>
kubectl describe ksvc -n <namespace> <ksvc-name>

kubectl get revision -n <namespace>
kubectl describe revision -n <namespace> <rev-name>

kubectl get pod -n <namespace> -l serving.knative.dev/service=<ksvc-name>
kubectl get events -n <namespace> --sort-by=.lastTimestamp | tail -n 50
  • Revision이 계속 바뀌거나 Ready가 흔들리면, 배포/설정 또는 프로브 문제 가능성이 큽니다.
  • Pod가 0이었다가 트래픽 시점에 급히 생성되면, 스케일 투 제로 콜드스타트 가능성이 큽니다.

queue-proxy 로그 확인(503의 흔한 생성 지점)

POD=$(kubectl get pod -n <namespace> -l serving.knative.dev/service=<ksvc-name> -o jsonpath='{.items[0].metadata.name}')

kubectl logs -n <namespace> $POD -c queue-proxy --tail=200
kubectl logs -n <namespace> $POD -c user-container --tail=200
  • queue-proxyno ready endpoints, context deadline exceeded 류가 보이면 준비/타임아웃/스케일링 이슈일 확률이 큽니다.
  • 사용자 컨테이너에서 5xx가 나오면 모델 서버 자체 오류(메모리, 디스크, 모델 파일, 의존성 등)도 의심해야 합니다.

2단계: 가장 흔한 원인 3가지와 해결

원인 A: 스케일 투 제로로 인한 콜드스타트 503

Knative는 트래픽이 없으면 비용 절감을 위해 Pod를 0으로 줄일 수 있습니다. 그런데 모델 로딩이 20초, 60초 걸리는 워크로드에서 첫 요청이 들어오면, 다음 중 하나가 발생합니다.

  • 첫 요청이 activator를 거쳐 대기하다가 타임아웃
  • 준비되기 전 라우팅되며 503

해결 1: 최소 스케일을 1로 고정(가장 확실)

KServe InferenceService에 Knative 어노테이션을 넣어 최소 Pod를 1로 유지합니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: my-model
  namespace: ml
  annotations:
    autoscaling.knative.dev/minScale: "1"
spec:
  predictor:
    containers:
      - name: user-container
        image: myrepo/my-model-server:1.0.0
        ports:
          - containerPort: 8080
  • 장점: 콜드스타트로 인한 503이 사실상 사라집니다.
  • 단점: 유휴 시간에도 최소 1 Pod 비용이 듭니다.

해결 2: scale to zero는 유지하되, 타임아웃과 윈도우 조정

비용 때문에 minScale을 올릴 수 없다면, “첫 요청이 기다릴 수 있게” 타임아웃을 늘리는 쪽이 현실적입니다.

metadata:
  annotations:
    autoscaling.knative.dev/scaleToZeroPodRetentionPeriod: "5m"
    autoscaling.knative.dev/window: "60s"
    autoscaling.knative.dev/target: "1"
  • scaleToZeroPodRetentionPeriod: 0으로 내리기 전에 Pod를 조금 더 유지
  • window, target: 오토스케일러의 민감도를 조절(급격한 트래픽에서 과소 스케일 방지)

환경마다 의미가 달라질 수 있으니, 적용 전후로 Revision의 Pod 수 변화와 503 빈도를 함께 관찰하는 것이 좋습니다.

원인 B: readiness 프로브/모델 로딩 타이밍 불일치

모델 서버가 “프로세스는 살아있지만 모델이 아직 로딩 중”인 상태에서 readiness가 Ready로 바뀌면, 라우팅은 시작되는데 inference는 실패하여 503(혹은 5xx)이 발생할 수 있습니다.

해결: readiness를 “진짜 준비 완료”로 맞추기

가능하면 모델 로딩 완료를 확인하는 /readyz 같은 엔드포인트를 구현하고 readiness가 그 엔드포인트를 보도록 합니다.

spec:
  predictor:
    containers:
      - name: user-container
        image: myrepo/my-model-server:1.0.0
        readinessProbe:
          httpGet:
            path: /readyz
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 2
          timeoutSeconds: 1
          failureThreshold: 15
  • initialDelaySeconds를 크게 주는 방식은 “느리게 시작”에는 도움이 되지만, 모델 로딩 시간이 환경에 따라 달라지면 여전히 흔들릴 수 있습니다.
  • /readyz가 모델 로딩 완료를 기준으로 200을 반환하도록 만드는 게 가장 안정적입니다.

추가로, readiness가 실패하는 동안 Knative가 트래픽을 어떻게 대기/리트라이하는지에 따라 503로 관측될 수 있으니, queue-proxy 로그에서 준비 상태 전환 시점을 꼭 확인하세요.

원인 C: 동시성/스케일링 목표치가 워크로드와 불일치

Knative는 기본적으로 “동시 요청 수(Concurrency)”를 기준으로 스케일링합니다. 모델이 CPU 바운드거나 GPU에서 배치 처리로 동작할 때, 동시성 목표치가 너무 높으면 Pod 하나에 과도한 요청이 몰려 지연이 커지고, 타임아웃으로 503/504가 발생할 수 있습니다.

해결 1: 컨테이너 동시성 제한

metadata:
  annotations:
    autoscaling.knative.dev/target: "1"
    autoscaling.knative.dev/metric: "concurrency"
spec:
  predictor:
    containerConcurrency: 1
    containers:
      - name: user-container
        image: myrepo/my-model-server:1.0.0
  • containerConcurrency: 1은 “Pod 당 1요청”에 가깝게 동작하게 만들어 지연/타임아웃을 줄이는 데 도움이 됩니다.
  • 대신 스케일 아웃이 더 자주 필요하므로, 클러스터 리소스 여유와 함께 봐야 합니다.

해결 2: 순간 트래픽 대비 초기 스케일 가속

갑자기 트래픽이 튀는 서비스라면, 최소 스케일을 1로 두고 최대 스케일을 제한/확대하거나, 오토스케일 정책을 더 공격적으로 가져가야 합니다.

metadata:
  annotations:
    autoscaling.knative.dev/minScale: "1"
    autoscaling.knative.dev/maxScale: "10"
    autoscaling.knative.dev/target: "2"

3단계: 타임아웃(요청-라우팅-서버) 정합성 맞추기

503은 종종 “서버는 느리지만 결국 응답은 가능한데, 중간 계층이 먼저 포기”할 때 발생합니다. Knative, Ingress, 클라이언트 각각의 타임아웃이 따로 놀면 재현이 어려운 503이 생깁니다.

  • 모델 콜드스타트: 예를 들어 40초
  • 첫 요청이 기다려주는 시간: 10초

이런 조합이면 첫 요청은 계속 503이 납니다.

Knative/KServe 환경에서 타임아웃은 설치 구성(ingress 종류, mesh 유무)에 따라 설정 위치가 달라질 수 있습니다. 공통적으로는 다음을 점검하세요.

  • 클라이언트 타임아웃
  • Ingress(Gateway) 타임아웃
  • Knative Route/Revision 타임아웃(설치에 따라 어노테이션/ConfigMap)

Cloud Run에서도 비슷한 증상(콜드스타트, 동시성, 타임아웃 불일치)으로 503/504가 발생합니다. 개념 정리는 GCP Cloud Run 503/504 원인별 해결 - 타임아웃·동시성 글이 도움이 됩니다.

4단계: 운영에서 자주 쓰는 “안전한” 권장 조합

모델 로딩이 길고(수십 초), 트래픽이 간헐적이며, 503이 비즈니스에 치명적이라면 다음 조합이 가장 무난합니다.

  1. minScale: 1로 콜드스타트 제거
  2. containerConcurrency: 1 또는 모델 특성에 맞는 낮은 값
  3. readiness는 /readyz로 “모델 로딩 완료”를 기준으로
  4. 리소스(특히 메모리) 넉넉히: 로딩 중 메모리 피크로 OOM이 나면 503이 아니라 장애가 반복됩니다.

OOM/재시작이 의심되면 이벤트에 OOMKilled가 찍히는지 먼저 보세요. 관련 진단은 Kubernetes CrashLoopBackOff와 OOMKilled(ExitCode 137) 해결도 참고할 만합니다.

5단계: 재현과 검증 방법(부하-스케일-관측)

설정을 바꿨다면 “503이 줄었는지”를 체계적으로 확인해야 합니다.

스케일 투 제로 재현

  • 트래픽을 멈추고(수 분) Pod가 0으로 내려가는지 확인
  • 이후 첫 요청을 보내고 응답/지연/503 여부 확인
kubectl get pod -n ml -l serving.knative.dev/service=<ksvc-name> -w

간단한 부하로 스케일 아웃 검증

아래는 단순 예시입니다. 실제 운영에서는 hey, wrk 등을 쓰되, 클러스터/Ingress 정책에 맞게 조정하세요.

URL=$(kubectl get inferenceservice -n ml my-model -o jsonpath='{.status.url}')

# 동시 요청을 순간적으로 늘려 503가 나는지 확인
for i in $(seq 1 30); do
  curl -sS -m 10 -X POST "$URL/v1/models/my-model:predict" \
    -H 'Content-Type: application/json' \
    -d '{"instances":[[1,2,3,4]]}' > /dev/null &
done
wait
  • -m 10은 클라이언트 타임아웃이므로, 콜드스타트가 있을 때는 일부러 짧게 두면 503을 더 잘 드러낼 수 있습니다.
  • 같은 테스트를 minScale: 1 적용 전후로 비교하면 효과가 명확합니다.

체크리스트: 503을 줄이는 순서

  1. Pod가 0으로 내려갔는지 확인(스케일 투 제로 여부)
  2. queue-proxy 로그에서 no ready endpoints 또는 타임아웃 흔적 확인
  3. readiness가 “모델 로딩 완료”를 의미하는지 점검
  4. minScale, containerConcurrency, autoscaling target/window 조정
  5. Ingress/Knative/클라이언트 타임아웃 정합성 맞추기
  6. 리소스 부족(OOM, CPU 스로틀링) 여부 확인

마무리

KServe InferenceService의 503은 “KServe 자체 버그”라기보다, Knative가 제공하는 강력한 스케일링(특히 0까지 축소)과 모델 서빙의 특성(긴 로딩, 무거운 초기화, 제한된 동시성)이 충돌하면서 발생하는 경우가 많습니다.

가장 빠른 해결은 autoscaling.knative.dev/minScale: "1"로 콜드스타트를 제거하는 것이고, 비용/트래픽 패턴상 scale to zero가 필요하다면 readiness를 정확히 만들고 타임아웃과 오토스케일 파라미터를 “모델의 현실”에 맞춰 조정해야 503을 안정적으로 줄일 수 있습니다.