Published on

Ray Serve 배포 후 503·헬스체크 실패 7가지

Authors

Ray Serve는 배포가 성공했다고 보이는데도 외부에서는 503이 떨어지거나, 로드밸런서 헬스체크가 계속 실패하는 경우가 자주 발생합니다. 특히 K8s, VM, 혹은 클라우드 L7 로드밸런서 앞에 Ray Serve를 두면 “Serve는 뜬 것 같은데 트래픽이 못 들어오는” 상태가 생깁니다.

이 글은 Ray Serve 배포 후 503 또는 헬스체크 실패를 만드는 원인 7가지를, 실제 운영에서 많이 마주치는 순서대로 정리한 진단 가이드입니다. 원인별로 **관측 포인트(로그, 엔드포인트, 상태 확인)**와 수정 방법, 그리고 재현 가능한 설정/코드를 포함합니다.

비슷한 유형의 장애 패턴은 컨테이너/오케스트레이션에서도 반복됩니다. CrashLoop 관점의 진단은 Kubernetes CrashLoopBackOff 원인 7가지와 실전 디버깅도 함께 참고하면 좋습니다.


0. 먼저 확인할 것: Ray Serve의 “정상” 기준

Ray Serve는 크게 아래 흐름으로 요청을 처리합니다.

  • 외부 요청이 Serve HTTP Proxy로 들어옴
  • Proxy가 **Deployment Replica(Actor)**로 라우팅
  • Replica가 사용자 코드를 실행해 응답

따라서 503은 대체로 다음 중 하나입니다.

  • HTTP Proxy가 안 떠 있거나, 외부에서 접근 불가
  • Proxy는 떠 있는데 라우팅 대상 Replica가 준비되지 않음
  • Replica는 떠 있는데 사용자 코드 예외/타임아웃/리소스 부족

상태 확인 명령(최소 체크)

아래 명령들은 “지금 어디에서 막히는지”를 빠르게 가릅니다.

# Ray 클러스터 상태
ray status

# Serve 앱 상태(가능한 환경에서)
serve status

# K8s라면 Pod/서비스/엔드포인트
kubectl get pod -A | grep -i ray
kubectl get svc -A | grep -i ray
kubectl get endpoints -A | grep -i ray

또한 외부에서 실제로 헬스체크가 찍는 URL을 그대로 curl로 재현해보는 게 핵심입니다.

curl -i http://YOUR_HOST:YOUR_PORT/
curl -i http://YOUR_HOST:YOUR_PORT/-/healthz
curl -i http://YOUR_HOST:YOUR_PORT/-/routes

Serve 버전/설정에 따라 기본 라우트나 헬스 경로가 다를 수 있으니, 실제 환경에서 노출된 경로를 기준으로 확인하세요.


1) HTTP Proxy 바인딩 주소/포트 문제: 127.0.0.1에만 떠 있음

증상

  • Pod/노드 내부에서만 curl localhost는 되는데, 외부에서는 503 또는 연결 실패
  • 로드밸런서 헬스체크가 계속 실패

원인

Serve HTTP Proxy가 127.0.0.1 또는 특정 인터페이스에만 바인딩되어 외부에서 접근이 안 되는 경우입니다. 특히 컨테이너 환경에서 “내부에서는 되는데 서비스로는 안 됨”의 전형적인 패턴입니다.

해결

HTTP Proxy가 0.0.0.0에 바인딩되도록 설정합니다. Ray Serve 설정 방식은 버전/배포 방식에 따라 다르지만, 핵심은 외부에서 접근 가능한 인터페이스에 붙이는 것입니다.

예시(개념 코드):

from ray import serve

serve.start(http_options={
    "host": "0.0.0.0",
    "port": 8000,
})

K8s/클라우드 환경에서 이 문제가 반복된다면, 컨테이너 기동/포트 노출 문제의 일반론도 함께 점검하세요. 유사한 패턴은 Cloud Run 503·컨테이너 미기동 원인 7가지에서도 자주 등장합니다.


2) 서비스/Ingress 포트 미스매치: Service는 80인데 컨테이너는 8000

증상

  • Pod는 Running인데 Service 뒤로 붙으면 503
  • Ingress/ALB에서는 타겟 헬시가 안 됨
  • kubectl port-forward로는 되는데, 외부 라우팅만 실패

원인

K8s에서 흔한 실수는 다음 조합입니다.

  • 컨테이너는 8000에서 Listen
  • Service targetPort80 또는 다른 값
  • Ingress가 Service port만 보고 라우팅

결과적으로 L7은 “정상 라우트”라고 생각하지만, 실제 백엔드 포트로 연결이 안 되어 5xx/헬스체크 실패가 납니다.

해결

Service의 targetPort를 컨테이너 Listen 포트와 맞추고, Ingress/ALB의 헬스체크 포트도 일치시키세요.

apiVersion: v1
kind: Service
metadata:
  name: ray-serve
spec:
  selector:
    app: ray-serve
  ports:
    - name: http
      port: 80
      targetPort: 8000

Ingress/ALB를 쓴다면 타겟 그룹 포트/헬스체크 경로도 함께 점검해야 합니다. 특히 “규칙이 반영되기까지 지연”이나 TG 상태 진단은 EKS ALB Ingress 404 고정 - 10분 규칙·TG 진단의 체크리스트가 그대로 도움이 됩니다.


3) 헬스체크 경로가 앱 라우팅과 충돌: 200이 아닌 응답

증상

  • 앱 엔드포인트는 정상인데 LB 헬스체크만 실패
  • 헬스체크가 404, 405, 307 등으로 떨어짐
  • /가 앱에서 리다이렉트/인증 요구로 막혀 있음

원인

로드밸런서 헬스체크는 보통 “간단히 200 OK를 기대”합니다. 그런데 Ray Serve 앱이 /를 다른 용도로 쓰거나, 미들웨어/인증/리다이렉트가 걸려 있으면 헬스체크가 실패합니다.

또한 프록시/Ingress 설정에서 경로 프리픽스가 붙거나, rewrite가 적용되면서 기대 경로와 실제 경로가 어긋나는 경우도 많습니다.

해결

  • 헬스체크 전용 경로를 만들고 무조건 200을 반환
  • LB 헬스체크 경로를 그 경로로 변경

FastAPI 기반 Serve 예시:

from fastapi import FastAPI
from ray import serve

app = FastAPI()

@app.get("/-/healthz")
def healthz():
    return {"ok": True}

@serve.deployment
@serve.ingress(app)
class Api:
    @app.get("/v1/predict")
    def predict(self):
        return {"result": "ok"}

Api.deploy()

헬스체크는 “가벼워야” 합니다. 모델 로딩 여부까지 확인하고 싶다면 별도 readiness 경로를 두고, 타임아웃을 넉넉히 잡으세요.


4) Replica 준비 전 트래픽 유입: 초기 모델 로딩이 길어 readiness 실패

증상

  • 배포 직후 수 분간 503, 이후 정상화
  • 오토스케일로 새 replica가 붙을 때만 간헐적 503
  • 로그에 모델 다운로드/가중치 로딩이 오래 걸림

원인

Ray Serve replica는 actor로 뜬 뒤 사용자 코드 초기화(예: 모델 로드)를 수행합니다. 이 초기화가 길면, Proxy 관점에서 “라우팅할 준비가 안 된 replica”가 생기고, 그 동안 503이 발생할 수 있습니다.

K8s에서는 liveness/readiness probe 타임아웃이 짧으면 더 쉽게 장애로 보입니다.

해결

  • 모델/아티팩트는 가능하면 이미지 빌드 단계에 포함하거나, 노드 캐시를 활용
  • readiness 타임아웃을 현실적으로 조정
  • 초기화 중에도 헬스체크는 통과시키고, 실제 요청만 대기시키는 전략을 고려

초기화 비용을 줄이는 간단한 패턴(개념 코드):

import time
from ray import serve

@serve.deployment
class Model:
    def __init__(self):
        # 무거운 초기화
        time.sleep(30)
        self.ready = True

    async def __call__(self, request):
        return {"ready": self.ready}

운영에서는 이 sleep 자리에 모델 로딩이 들어가며, 이 시간을 기준으로 readiness 설계를 해야 합니다.


5) 리소스 부족으로 스케줄링 실패: replica가 뜨지 않아 503

증상

  • serve status에서 deployment가 UNHEALTHY 또는 replica 수가 0
  • Ray 대시보드/로그에 “insufficient resources”류 메시지
  • CPU/GPU/메모리 요청이 커서 스케줄링이 안 됨

원인

Ray Serve는 replica를 Ray 스케줄러가 자원에 맞춰 배치합니다. 아래 케이스가 흔합니다.

  • num_gpus=1로 설정했는데 실제 노드에 GPU가 없음
  • CPU 요청이 과도하거나, 다른 작업이 자원을 점유
  • 오토스케일이 따라오지 못해 순간적으로 replica가 0

replica가 0이면 Proxy는 라우팅할 대상이 없어 503을 반환할 수 있습니다.

해결

  • deployment의 리소스 옵션을 현실적으로 조정
  • Ray 클러스터 노드 타입/오토스케일 정책 점검
  • 메모리 사용량이 큰 경우 로딩 방식을 최적화

리소스 지정 예시(개념 코드):

from ray import serve

@serve.deployment(ray_actor_options={"num_cpus": 2, "num_gpus": 0})
class CpuOnly:
    def __call__(self, request):
        return "ok"

메모리 병목이 원인이라면 데이터 로딩/전처리 방식을 바꾸는 것도 효과가 큽니다. 대용량 데이터 처리 관점에서는 Python Polars로 100GB CSV 메모리 오류 해결처럼 “메모리 친화적 파이프라인”으로 바꾸는 접근이 그대로 통합니다.


6) 네트워크 정책/보안그룹/방화벽: 노드 내부는 되는데 LB에서만 실패

증상

  • Pod IP로 직접 접근하면 되는데, Service/LB로만 실패
  • 특정 AZ/노드에서만 실패(간헐적)
  • 헬스체크 소스 IP 대역이 차단됨

원인

Ray Serve 자체 문제가 아니라, 인프라 레이어에서 트래픽이 막히는 경우입니다.

  • K8s NetworkPolicy가 LB에서 오는 트래픽을 차단
  • 보안그룹에서 노드 포트/타겟 포트를 허용하지 않음
  • 방화벽에서 헬스체크 경로는 되지만 응답 포트가 막힘

해결

  • 헬스체크 소스(로드밸런서)에서 대상 포트로의 인바운드 허용
  • NetworkPolicy가 있다면 네임스페이스/셀렉터 기준으로 예외 규칙 추가
  • 노드/파드 레벨에서 tcpdump나 연결 로그로 “패킷이 들어오는지”부터 확인

네트워크 문제는 애플리케이션 로그만 봐서는 티가 잘 안 납니다. curlLB와 동일한 경로로 재현하고, 실패 지점을 OSI 레벨로 분리하는 게 빠릅니다.


7) 프록시/타임아웃 설정 문제: 요청은 들어오지만 처리 중 끊겨 503

증상

  • 짧은 요청은 성공, 긴 요청만 503 또는 504
  • 헬스체크는 성공하지만 실제 트래픽에서 오류
  • 로그에 요청 처리 중 연결 종료, 타임아웃

원인

L7 프록시(Envoy, Nginx Ingress, ALB 등)와 Ray Serve 사이에 타임아웃 불일치가 있으면, 백엔드는 처리 중인데 프록시가 먼저 연결을 끊고 503으로 반환할 수 있습니다.

특히 모델 추론이 길거나, 콜드 스타트로 첫 요청이 느리면 더 잘 재현됩니다.

해결

  • Ingress/LB의 idle timeout, request timeout을 작업 특성에 맞게 조정
  • Ray Serve 쪽에서도 concurrency, batching, replica 수를 조정해 지연을 낮춤
  • 긴 작업은 비동기로 전환하고 폴링/콜백 패턴 고려

간단한 부하 재현(긴 요청):

import asyncio
from ray import serve

@serve.deployment
class Slow:
    async def __call__(self, request):
        await asyncio.sleep(20)
        return "done"

이런 엔드포인트를 두고 Ingress 타임아웃이 15s라면, 백엔드는 정상이어도 클라이언트는 503을 볼 수 있습니다.


진단을 빠르게 만드는 체크리스트(운영 순서)

아래 순서대로 보면 “503의 위치”를 빠르게 좁힐 수 있습니다.

  1. 외부에서 실제로 호출하는 URL을 그대로 curl -i로 재현
  2. 연결 자체가 안 되면 바인딩/포트/방화벽(원인 1, 2, 6)
  3. 연결은 되는데 404/307/401이면 헬스체크 경로/라우팅(원인 3)
  4. 배포 직후만 실패하면 초기화/리소스(원인 4, 5)
  5. 긴 요청에서만 실패하면 타임아웃/프록시(원인 7)

K8s에서 Pod가 계속 재시작하는 상황이라면 503은 결과일 뿐, 근본 원인은 CrashLoop일 수 있습니다. 이 경우에는 K8s CrashLoopBackOff 원인별 진단 체크리스트 흐름대로 이벤트와 프로브 설정부터 확인하는 것이 더 빠릅니다.


마무리: 503은 “Serve가 아니라 경계면”에서 난다

Ray Serve의 503은 종종 Ray Serve 버그라기보다, HTTP Proxy 바인딩, K8s Service/Ingress 포트, 헬스체크 경로, 초기화/리소스, 네트워크 정책, 타임아웃 같은 “경계면”에서 발생합니다.

운영에서 가장 효과적인 습관은 하나입니다.

  • “어디까지는 되고, 어디부터 안 되는지”를 curl과 상태 명령으로 계층별로 분리한다

이 원칙만 지켜도 503의 80%는 30분 안에 원인에 도달합니다.