Published on

KServe+Istio에서 모델 503 해결 7단계

Authors

KServe를 Istio 위에 올려 운영하다 보면, 모델이 분명 배포됐는데도 요청이 503 Service Unavailable 로 떨어지는 순간이 자주 옵니다. 특히 트래픽이 들어오는 경로가 Gateway -> VirtualService -> (KServe가 만든) Knative Service/Revision -> Pod 로 길어지면서, 503이 “어디에서” 발생했는지부터 헷갈리기 쉽습니다.

이 글은 503을 7단계로 분해해서, 각 단계에서 무엇을 확인하고 어떤 커맨드로 증거를 모을지 정리한 실전 가이드입니다. 목표는 감으로 재배포하는 게 아니라, 증상-원인-조치를 빠르게 연결하는 것입니다.

참고: 네트워크 타임아웃/라우팅 문제를 체계적으로 좁혀가는 접근은 EKS Pod→S3 504 타임아웃 - VPC 엔드포인트·NAT·DNS 진단 글의 “경로 분해” 방식과 유사합니다.

503의 “발생 지점”부터 구분하기

Istio 환경에서 503은 대체로 아래 중 하나입니다.

  • Envoy가 업스트림을 못 찾음: 라우팅 대상 엔드포인트가 0개, 혹은 subset/port mismatch
  • 업스트림 연결 실패: connection refused, TLS handshake error
  • 업스트림이 과부하/준비 안 됨: readiness 실패, scale-to-zero에서 콜드스타트, 큐 프록시 문제
  • 권한/정책으로 차단: AuthorizationPolicy, PeerAuthentication, mTLS 모드 불일치

먼저 클라이언트에서 받은 응답 헤더/바디에 단서가 많습니다.

# 예: 외부에서 호출
curl -sv https://YOUR_HOST/v1/models/MODEL:predict \
  -H 'Content-Type: application/json' \
  -d '{"instances":[1,2,3]}'

확인 포인트:

  • 응답 헤더에 server: istio-envoy 가 보이면, 최소한 게이트웨이/사이드카 레벨에서 생성된 응답일 가능성이 큽니다.
  • 바디에 no healthy upstream 같은 문자열이 있으면 “엔드포인트 0개” 계열로 거의 확정입니다.

이제부터는 경로를 따라가며 7단계로 좁힙니다.

1단계: InferenceService 상태부터 확정하기

KServe 리소스가 Ready가 아니면, 뒤에서 아무리 Istio를 만져도 503이 반복됩니다.

kubectl get inferenceservice -A
kubectl get inferenceservice YOUR_ISVC -n YOUR_NS -o yaml

체크리스트:

  • status.conditions 에서 Ready=True 인지
  • PredictorReady 가 False면 predictor 배포가 안 된 상태
  • status.url 이 비어 있거나, 예상과 다른 호스트라면 라우팅 구성부터 재점검

자주 나오는 원인:

  • 이미지 풀 실패, 모델 다운로드 실패, 스토리지 접근 실패
  • 리소스 부족으로 스케줄링 실패

이 단계에서 이미 kubectl describe 로 이벤트가 쏟아지는 경우가 많습니다.

kubectl describe inferenceservice YOUR_ISVC -n YOUR_NS

2단계: 실제 트래픽 엔드포인트(Revision/Pod)가 존재하는지

KServe는 내부적으로 Knative Serving 리소스를 생성합니다(설치 형태에 따라 다르지만, 많은 배포에서 그렇습니다). 503의 흔한 원인은 “라우팅은 됐는데 실제 엔드포인트가 없다” 입니다.

kubectl get ksvc -n YOUR_NS
kubectl get revision -n YOUR_NS
kubectl get pod -n YOUR_NS -owide

체크리스트:

  • predictor pod가 Running인지
  • Ready가 0/1로 남아 있다면 readiness probe 실패
  • scale-to-zero 상태에서 트래픽이 들어오는데도 스케일이 안 올라오면 오토스케일/큐 프록시 문제

Pod 로그도 바로 봅니다.

# predictor 컨테이너 로그
kubectl logs -n YOUR_NS deploy/YOUR_ISVC-predictor -c kserve-container --tail=200

# 사이드카/큐프록시가 있는 경우
kubectl logs -n YOUR_NS pod/YOUR_POD -c queue-proxy --tail=200
kubectl logs -n YOUR_NS pod/YOUR_POD -c istio-proxy --tail=200

여기서 모델 로딩이 오래 걸리면, 콜드스타트 동안 503이 발생할 수 있습니다. 이 경우는 뒤 단계(타임아웃/프로브)에서 더 구체적으로 잡습니다.

3단계: Service/Endpoints가 0인지 확인 (가장 흔한 503)

Envoy가 no healthy upstream 을 내는 전형적인 케이스가 “Kubernetes Endpoints가 비어 있음” 입니다.

kubectl get svc -n YOUR_NS
kubectl get endpoints -n YOUR_NS

# 특정 서비스의 엔드포인트 확인
kubectl get endpoints YOUR_SERVICE -n YOUR_NS -o yaml

원인 패턴:

  • Service selector가 Pod label과 불일치
  • Pod는 떠 있는데 readiness가 false라 endpoints에 안 붙음
  • 포트 이름/포트 번호 불일치 (Istio 라우팅에서 특히 문제)

조치 방향:

  • kubectl describe svc 로 selector 확인
  • kubectl get pod --show-labels 로 라벨 비교
  • readiness probe 실패면 5단계에서 해결

4단계: Istio Gateway/VirtualService 라우팅 정합성

외부에서 호출하는 경우, 대개 GatewayVirtualService 를 거칩니다. 호스트/경로/포트가 조금만 어긋나도 503이 납니다.

kubectl get gateway,virtualservice -A
kubectl get virtualservice YOUR_VS -n YOUR_NS -o yaml
kubectl get gateway YOUR_GW -n istio-system -o yaml

체크리스트:

  • hosts 가 실제 요청 Host 헤더와 일치하는지
  • gateways 참조가 맞는지
  • http.match 의 prefix/path가 실제 요청 경로와 맞는지
  • route.destination.host 가 올바른 서비스 DNS인지
  • destination.port.number 가 서비스 포트와 동일한지

실전 팁: 라우팅이 애매할 때는 게이트웨이 Envoy 로그나 접근 로그를 켜서 “어떤 라우트가 매칭되었는지”를 보는 게 빠릅니다.

# istio ingressgateway 프록시 로그
kubectl logs -n istio-system deploy/istio-ingressgateway -c istio-proxy --tail=200

5단계: readiness/liveness, 모델 로딩 시간, 프로브 설계

모델 서버는 시작 시 모델 로딩으로 수십 초에서 수 분까지 걸릴 수 있습니다. 이때 readiness가 너무 공격적이면 endpoints가 붙지 않아 503이 납니다.

확인:

kubectl describe pod YOUR_POD -n YOUR_NS
kubectl get pod YOUR_POD -n YOUR_NS -o jsonpath='{.status.containerStatuses[*].ready}'

자주 보이는 이벤트:

  • Readiness probe failed
  • Back-off restarting failed container

조치 예시(개념):

  • 초기 로딩이 긴 경우 startupProbe 를 사용하거나 readiness의 initialDelaySeconds 를 늘립니다.
  • 모델 로딩 완료 후에만 readiness가 true가 되도록 엔드포인트(/health, /ready)를 분리합니다.

KServe/Knative 조합에서는 큐 프록시가 앞단에 있어 “애플리케이션이 뜨는 시간”과 “트래픽을 받을 준비가 된 시간”이 엇갈릴 수 있습니다. 이때는 다음도 같이 봅니다.

  • queue-proxy 로그에서 요청이 들어왔는지
  • 오토스케일이 실제로 0에서 1로 올라갔는지

6단계: Istio mTLS 및 정책(AuthorizationPolicy)로 인한 차단

503이 “권한 문제”처럼 보이지 않게 나타나는 경우가 있습니다. 특히 mTLS 모드가 STRICT 인데, 대상 워크로드가 기대한 방식으로 통신하지 못하면 연결 실패로 503이 터질 수 있습니다.

확인할 리소스:

kubectl get peerauthentication -A
kubectl get authorizationpolicy -A
kubectl get destinationrule -A

체크리스트:

  • 네임스페이스에 PeerAuthenticationSTRICT 인지
  • 특정 워크로드에 AuthorizationPolicy 가 deny로 걸려 있지 않은지
  • DestinationRule 의 TLS 모드가 서비스 호출 방식과 일치하는지

트러블슈팅 팁:

  • istio-proxy 로그에 rbac_access_denied 가 보이면 AuthorizationPolicy 가능성이 큼
  • TLS 관련 에러(핸드셰이크 실패)가 보이면 PeerAuthentication/DestinationRule 조합을 의심

7단계: 타임아웃, 리트라이, 커넥션 풀, 그리고 “간헐적 503”

모든 설정이 맞는데도 503이 간헐적으로 뜬다면, 대개는 아래 범주입니다.

  • 업스트림이 순간적으로 과부하로 응답 불가
  • Envoy 타임아웃이 모델 추론 시간보다 짧음
  • 리트라이가 오히려 폭주를 키움
  • 커넥션 재사용/HTTP2 설정 문제로 특정 상황에서만 실패

VirtualService에서 타임아웃/리트라이를 명시해 “기본값”에 끌려다니지 않게 만드는 게 좋습니다.

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: model-vs
  namespace: YOUR_NS
spec:
  hosts:
    - your.model.host
  gateways:
    - istio-system/your-gateway
  http:
    - route:
        - destination:
            host: your-service.YOUR_NS.svc.cluster.local
            port:
              number: 80
      timeout: 30s
      retries:
        attempts: 2
        perTryTimeout: 10s
        retryOn: gateway-error,connect-failure,refused-stream,reset

추론 시간이 길거나 배치 요청이 큰 경우, timeout 을 늘리는 것만으로 해결되는 케이스가 많습니다. 반대로 모델이 무거운데 retries를 과하게 두면, 실패 순간에 트래픽이 증폭되어 더 많은 503을 만들 수 있습니다.

간헐적 네트워크/경로 문제를 추적하는 관점에서는 “어느 홉에서 지연이 커지는지”를 나눠 보는 게 핵심인데, 이 접근은 EKS Pod→S3 504 타임아웃 - VPC 엔드포인트·NAT·DNS 진단 에서도 동일하게 적용됩니다.

빠른 결론: 503을 7개의 질문으로 바꾸기

503을 보면 바로 아래 7개 질문으로 변환해 보세요.

  1. InferenceService가 Ready인가
  2. 실제 predictor pod가 존재하고 Running인가
  3. Service의 endpoints가 0인가
  4. Gateway/VirtualService의 host, path, port, destination이 맞는가
  5. readiness/liveness가 모델 로딩 특성과 맞는가
  6. mTLS/AuthorizationPolicy/DestinationRule이 통신을 막고 있지 않은가
  7. timeout, retry, 부하 패턴 때문에 간헐적 503이 생기지 않는가

이 순서대로 보면 “재배포로 운 좋게 해결” 같은 흐름에서 벗어나, 503을 재현 가능하고 설명 가능한 문제로 바꿀 수 있습니다.

부록: 현장에서 바로 쓰는 커맨드 묶음

# 1) KServe 상태
kubectl get isvc -A
kubectl describe isvc YOUR_ISVC -n YOUR_NS

# 2) Pod/로그
kubectl get pod -n YOUR_NS -owide
kubectl describe pod YOUR_POD -n YOUR_NS
kubectl logs -n YOUR_NS pod/YOUR_POD -c kserve-container --tail=200
kubectl logs -n YOUR_NS pod/YOUR_POD -c istio-proxy --tail=200

# 3) Service/Endpoints
kubectl get svc,endpoints -n YOUR_NS
kubectl get endpoints YOUR_SERVICE -n YOUR_NS -o yaml

# 4) Istio 라우팅
kubectl get gw,vs -A
kubectl get vs YOUR_VS -n YOUR_NS -o yaml
kubectl logs -n istio-system deploy/istio-ingressgateway -c istio-proxy --tail=200

# 5) 정책/mTLS
kubectl get peerauthentication,authorizationpolicy,destinationrule -A

원하시면, 사용 중인 설치 조합(예: KServe RawDeployment 여부, Knative 사용 여부, Ingress가 istio-ingressgateway 인지, 네임스페이스 mTLS 모드)을 알려주시면 위 7단계를 당신 환경에 맞춘 “우선순위”로 재정렬해서 체크리스트를 더 공격적으로 최적화해 드릴 수 있습니다.