- Published on
Ray Serve 배포 후 503·헬스체크 실패 7가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
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
targetPort가80또는 다른 값 - 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나 연결 로그로 “패킷이 들어오는지”부터 확인
네트워크 문제는 애플리케이션 로그만 봐서는 티가 잘 안 납니다. curl을 LB와 동일한 경로로 재현하고, 실패 지점을 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의 위치”를 빠르게 좁힐 수 있습니다.
- 외부에서 실제로 호출하는 URL을 그대로
curl -i로 재현 - 연결 자체가 안 되면 바인딩/포트/방화벽(원인 1, 2, 6)
- 연결은 되는데
404/307/401이면 헬스체크 경로/라우팅(원인 3) - 배포 직후만 실패하면 초기화/리소스(원인 4, 5)
- 긴 요청에서만 실패하면 타임아웃/프록시(원인 7)
K8s에서 Pod가 계속 재시작하는 상황이라면 503은 결과일 뿐, 근본 원인은 CrashLoop일 수 있습니다. 이 경우에는 K8s CrashLoopBackOff 원인별 진단 체크리스트 흐름대로 이벤트와 프로브 설정부터 확인하는 것이 더 빠릅니다.
마무리: 503은 “Serve가 아니라 경계면”에서 난다
Ray Serve의 503은 종종 Ray Serve 버그라기보다, HTTP Proxy 바인딩, K8s Service/Ingress 포트, 헬스체크 경로, 초기화/리소스, 네트워크 정책, 타임아웃 같은 “경계면”에서 발생합니다.
운영에서 가장 효과적인 습관은 하나입니다.
- “어디까지는 되고, 어디부터 안 되는지”를
curl과 상태 명령으로 계층별로 분리한다
이 원칙만 지켜도 503의 80%는 30분 안에 원인에 도달합니다.