Cloudera AI Inference 엔드포인트가 "떠 있는데 응답이 없다" 거나 "첫 요청만 유독 느리다" 는 증상은 파드 상태와 로그만 봐서는 갈리지 않는다. 확인해야 할 것은 두 가지다 — 레플리카 하한이 0 인가, 그리고 헬스체크 임계값이 모델 로딩 시간보다 넉넉한가.
대형 모델을 여러 GPU 로 나눠 올리면 가중치 로딩과 CUDA graph 캡처까지 수 분에서 십수 분이 걸린다. 이 시간이 임계값을 넘으면 정상적으로 로딩 중인 컨테이너를 kubelet 이 계속 죽여 영원히 뜨지 않는다. 로그에는 오류 없이 로딩 진행만 반복해서 찍히므로 오히려 헷갈린다.
오토스케일 하한이 0 이면 레플리카 없이 엔드포인트만 존재한다. 자원을 쓰지 않는 대신 첫 요청이 들어오기 전까지 도달 불가 상태이고, 대형 모델이면 그 첫 요청은 거의 확실히 타임아웃된다. 사용자 눈에는 장애로 보인다.
NS=serving-default
ISVC=<inference-service>
kubectl -n $NS get isvc $ISVC \
-o jsonpath='min={.spec.predictor.minReplicas} max={.spec.predictor.maxReplicas}{"\n"}'
# 최종 권위는 Knative 서비스의 어노테이션이다
kubectl -n $NS get ksvc $ISVC -o jsonpath='{.spec.template.metadata.annotations}' | tr ',' '\n'
kubectl -n $NS get pods -l serving.kserve.io/inferenceservice=$ISVC
kubectl -n $NS get events --sort-by=.lastTimestamp | grep -iE "scale|revision"
어노테이션의 autoscaling.knative.dev/min-scale 이 실제 적용값이다. UI 의 Resource Profile 에 보이는 오토스케일 범위와 어긋날 수 있다.
세 프로브는 실패했을 때의 동작이 전혀 다르다.
| 프로브 | 실패 시 | 겉으로 보이는 증상 |
|---|---|---|
| Readiness | 트래픽 라우팅에서 제외. 파드는 살아 있다 | Running 인데 READY 0/2, 503 또는 no healthy upstream |
| Liveness | 컨테이너 강제 재시작 | RESTARTS 증가, CrashLoopBackOff |
| Startup | 초기 로딩 유예. 없으면 Liveness 가 로딩 중인 컨테이너를 죽인다 | 무한 재시작 루프 |
POD=$(kubectl -n $NS get pods -l serving.kserve.io/inferenceservice=$ISVC \
-o jsonpath='{.items[0].metadata.name}')
kubectl -n $NS get pod $POD -o json | jq -r '
.spec.containers[] |
"=== \(.name) ===",
" liveness : \(.livenessProbe // "none")",
" readiness: \(.readinessProbe // "none")",
" startup : \(.startupProbe // "none")"
'
kubectl -n $NS describe pod $POD | grep -E "^\s+(Liveness|Readiness|Startup):"
출력은 이런 형태다.
Liveness: http-get http://:8080/health delay=0s timeout=1s period=10s #success=1 #failure=3
허용 시간은 initialDelaySeconds + (failureThreshold × periodSeconds) 다. 위 예시는 30초다. 대형 모델을 여러 GPU 로 나눠 올리는 구성이라면 Startup 프로브 없이는 버티지 못한다.
Knative 는 사용자가 정의한 readiness 프로브를 queue-proxy 로 옮긴다. 그래서 서빙 컨테이너에는 readinessProbe 가 none 으로 보이는데도 정상 동작하는 경우가 있다. 원본 정의는 queue-proxy 의 환경변수에 JSON 으로 들어 있다.
kubectl -n $NS get pod $POD -o json | jq -r '
.spec.containers[] | select(.name=="queue-proxy") |
.env[] | select(.name|test("READINESS_PROBE")) | "\(.name)=\(.value)"
'
이 값이 있으면 정상이다. 서빙 컨테이너와 queue-proxy 양쪽 모두 비어 있으면 설정 누락이다.
kubectl -n $NS get isvc $ISVC -o json | jq '.spec.predictor'
REV=$(kubectl -n $NS get ksvc $ISVC -o jsonpath='{.status.latestReadyRevisionName}')
kubectl -n $NS get revision $REV -o json | jq '.spec.containers[].readinessProbe, .spec.containers[].livenessProbe'
대형 모델에서 실제로 발목을 잡는 것은 대개 이쪽이다.
| 항목 | 성격 |
|---|---|
serving.knative.dev/progress-deadline |
이 시간 안에 Revision 이 Ready 가 되지 않으면 Revision 자체가 실패 처리된다 |
Revision 의 timeoutSeconds |
긴 추론 요청이 게이트웨이에서 잘린다 |
kubectl -n $NS get ksvc $ISVC -o json | jq '{
annotations: .spec.template.metadata.annotations,
timeout: .spec.template.spec.timeoutSeconds,
containerConcurrency: .spec.template.spec.containerConcurrency
}'
TCP 연결과 TLS 핸드셰이크는 되는데 HTTP 응답이 오지 않고 타임아웃되는 증상 — curl 로 http=000 이 나오는 경우 — 은 Revision 이 Ready 가 되지 못한 구간과 겹칠 때 정확히 그렇게 보인다.
curl -sk -o /dev/null \
-w "connect=%{time_connect} tls=%{time_appconnect} http=%{http_code} total=%{time_total}\n" \
--max-time 15 "https://<endpoint>/v1/models"
설정값만으로는 실제로 죽었는지 알 수 없다.
kubectl -n $NS get pod $POD -o json | jq -r '
.status.containerStatuses[] |
"\(.name): restarts=\(.restartCount) ready=\(.ready) lastState=\(.lastState)"
'
kubectl -n $NS get events --field-selector involvedObject.name=$POD \
--sort-by=.lastTimestamp | grep -iE "probe|kill|unhealthy"
kubectl -n $NS get revision $REV -o json | jq '.status.conditions'
lastState 로 OOMKilled 인지 프로브에 의한 종료인지 갈린다. Readiness probe failed: HTTP probe failed with statuscode: 503 이 반복되면 로딩 중에 프로브가 계속 실패한 것이고, 그것이 간헐적 응답 불가의 직접 원인이다.
kubectl -n $NS logs $POD -c kserve-container --timestamps | \
grep -iE "Starting|Loading weights|capturing|startup complete|Route /" | head -20
첫 줄과 기동 완료 줄 사이의 시간 차가 콜드스타트 실소요다. 이 값이 프로브 허용 시간의 절반을 넘으면 부하 상황에서 간헐적으로 터진다. 이 숫자는 "프로브 임계값이 부족하다" 는 주장의 근거가 되므로 지원 티켓에 같이 넣는다.
NS=serving-default
ISVC=<inference-service>
echo "=== scale-to-zero ==="
kubectl -n $NS get isvc $ISVC -o jsonpath='min={.spec.predictor.minReplicas} max={.spec.predictor.maxReplicas}{"\n"}'
kubectl -n $NS get ksvc $ISVC -o jsonpath='{.spec.template.metadata.annotations}{"\n"}'
kubectl -n $NS get pods -l serving.kserve.io/inferenceservice=$ISVC
echo "=== probes ==="
for p in $(kubectl -n $NS get pods -l serving.kserve.io/inferenceservice=$ISVC -o name); do
kubectl -n $NS describe $p | grep -A3 -E "Liveness|Readiness|Startup|Conditions|State:|Last State"
done
echo "=== events ==="
kubectl -n $NS get events --sort-by=.lastTimestamp | tail -50
레이블 셀렉터가 걸리지 않으면 kubectl -n $NS get pods --show-labels 로 먼저 확인한다.
CAII 파드는 서빙 컨테이너와 queue-proxy 등 컨테이너가 여러 개다. READY 1/2 처럼 나오면 어느 쪽이 준비되지 않았는지 반드시 구분해 기록한다. queue-proxy 가 안 뜨는 것과 서빙 컨테이너가 안 뜨는 것은 원인이 완전히 다르다.