[ray] Ray Serve LLM 인그레스 최적화: 로컬 라우팅을 통한 병목 현상 제거
PR 링크: ray-project/ray#64489 상태: Merged | 변경: +129 / -30
들어가며
대규모 LLM(Large Language Model) 서비스를 운영할 때, 모든 요청이 단일 인그레스 라우터(Ingress Router)를 거쳐야 한다면 이는 곧 시스템의 단일 장애점(SPOF)이자 처리량의 병목(Throughput Bottleneck)이 됩니다. Ray Serve의 기존 구조에서는 HAProxy가 글로벌하게 설정된 단일 라우터 엔드포인트로 요청을 전달하는 방식이었습니다.
이번 PR([serve][llm] Route direct-streaming ingress to the co-located router)은 이러한 구조적 한계를 극복하기 위해, 각 프록시 노드에 라우터 복제본을 배치하고 HAProxy가 동일한 노드에 있는(Co-located) 라우터에게 우선적으로 요청을 전달하도록 개선했습니다. 이를 통해 네트워크 홉(Hop)을 줄이고, 클러스터 규모에 따라 라우팅 성능이 선형적으로 확장될 수 있는 기반을 마련했습니다.
코드 분석: 핵심 변경 사항
1. 라우터 배치 및 리소스 최적화 (builder.py)
먼저 LLM 라우터가 프록시 노드와 함께 유연하게 배치될 수 있도록 리소스 요구 사항을 조정했습니다.
Before:
deployment = serve.deployment(
LLMRouter,
num_replicas=1,
max_ongoing_requests=1000,
)
After:
deployment = serve.deployment(
LLMRouter,
max_ongoing_requests=1000,
ray_actor_options={"num_cpus": 0},
)
num_cpus=0설정: 라우터는 주로 I/O 바운드 작업을 수행하므로, CPU 리소스를 명시적으로 점유하지 않도록 설정했습니다. 이를 통해 리소스가 부족한 헤드 노드(Head Node)나 프록시 노드에도 제약 없이 라우터 복제본을 함께 띄울 수 있습니다.max_ongoing_requests유지: 라우터는 인그레스의 핵심 경로(Hot path)에 있으므로, Serve의 기본값보다 높은 동시 요청 처리량을 허용하여 스로틀링을 방지합니다.
2. 로컬 라우터 우선 선택 로직 (haproxy.py)
HAProxy 설정 생성 시, 현재 노드의 IP를 확인하여 동일 노드에 있는 라우터들을 우선적으로 그룹화(Pool)합니다.
Before:
# 단순히 호스트/포트 기준으로 가장 작은 라우터 하나만 선택
routers[backend.name] = min(
backend.ingress_request_router_servers, key=lambda s: (s.host, s.port)
)
After:
# 동일 노드(local_host)에 있는 라우터들을 우선적으로 풀(Pool)로 구성
candidates = backend.ingress_request_router_servers
colocated = [s for s in candidates if s.host == local_host]
if colocated:
pool = sorted(colocated, key=lambda s: (s.host, s.port))
else:
# 로컬 라우터가 없으면 기존처럼 결정론적인 최소값 하나를 선택 (Fallback)
pool = [min(candidates, key=lambda s: (s.host, s.port))]
routers[backend.name] = pool
이 변경을 통해 HAProxy는 이제 단일 엔드포인트가 아닌 라우터 풀(Router Pool)을 관리하게 됩니다. local_host와 일치하는 서버가 있다면 이를 우선적으로 사용함으로써 불필요한 노드 간 통신(Cross-node traffic)을 제거합니다.
3. Lua를 이용한 고성능 라운드 로빈 및 페일오버 (ingress_request_router.lua.tmpl)
HAProxy 내부에서 동작하는 Lua 스크립트는 이제 전달받은 라우터 풀을 대상으로 라운드 로빈(Round-Robin)을 수행합니다.
After (Lua logic):
local _rr_cursor = {}
-- ... 중략 ...
local n = #pool
local start = _rr_cursor[app] or 0
_rr_cursor[app] = start + 1
local response = nil
for i = 0, n - 1 do
local router = pool[(start + i) % n + 1]
response = call_router(router, body, truncated, session_id)
if response then
break
end
end
_rr_cursor: 각 앱별로 현재 라운드 로빈 위치를 추적합니다. 중요한 점은 이 커서가 Lua 워커 스레드별로 독립적이라는 것입니다. 덕분에 스레드 간 동기화(Lock) 없이도 부하를 효과적으로 분산할 수 있습니다.- 자동 페일오버:
for루프를 통해 현재 선택된 라우터와 연결에 실패할 경우, 즉시 풀 내의 다음 라우터로 재시도합니다. 이는 시스템의 가용성을 크게 향상시킵니다.
왜 이게 좋은가?
1. 성능 최적화: 로컬리티(Locality) 극대화
기존에는 HAProxy가 다른 노드에 있는 라우터로 요청을 보낼 때 네트워크 레이턴시가 발생했습니다. 이제는 동일 노드 내의 라우터와 통신하므로 Internal Routing Hop이 On-node로 유지됩니다. 이는 특히 스트리밍 응답이 중요한 LLM 서비스에서 응답 시작 시간(TTFT)을 줄이는 데 기여합니다.
2. 확장성: 병목 현상 해소
클러스터가 커짐에 따라 프록시 노드와 라우터 복제본을 함께 늘릴 수 있습니다. 라우팅 부하가 특정 글로벌 라우터에 집중되지 않고 각 노드별로 분산되므로, 전체 시스템의 처리량이 선형적으로 증가합니다.
3. 가용성: 투명한 페일오버
특정 라우터 인스턴스에 문제가 생기더라도 HAProxy 수준에서 즉시 다른 로컬 라우터로 재시도합니다. 사용자 입장에서는 에러율이 낮아지고 서비스 안정성이 높아집니다.
4. 기술적 교훈: 테스트의 순수성(Purity)
리뷰 과정에서 유의미한 논의가 있었습니다. 테스트 코드에서 get_localhost_ip()를 사용하여 실제 머신의 IP에 의존하던 방식을 버리고, 10.0.0.1과 같은 가상의 리터럴 IP를 사용하도록 수정되었습니다.
"
_routers_and_targets_by_backend는 순수 함수(Pure function)이므로 실제 머신의 IP가 필요하지 않습니다. 리터럴을 사용함으로써 머신 환경에 독립적인 테스트를 보장할 수 있습니다."
이는 분산 시스템의 로직을 테스트할 때 환경 의존성을 제거하고 결정론적인 결과를 얻기 위한 훌륭한 엔지니어링 사례입니다.
마치며
이번 최적화는 Ray Serve가 단순한 모델 서빙 프레임워크를 넘어, 대규모 트래픽을 견고하게 처리해야 하는 엔터프라이즈급 LLM 인프라로 진화하고 있음을 보여줍니다. 로컬리티를 활용한 라우팅 전략은 분산 시스템 설계에서 언제나 유효한 강력한 패턴입니다.
References
- Ray Serve Deployment Options —
ray_actor_options및 리소스 설정 관련 공식 문서 - HAProxy Lua API — HAProxy 내 Lua 스크립트 작성 가이드
- Ray Serve Architecture — Ray Serve의 내부 구조 및 프록시/라우터 동작 원리
참고 자료
- https://docs.ray.io/en/latest/serve/production-guide/config.html
- https://www.haproxy.org/download/2.4/doc/lua.txt
- https://docs.ray.io/en/latest/serve/architecture.html
⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.
관련 포스트
- [ray] Ray Object Manager의 Pull RPC 배치 처리 최적화 분석
- [Ray Serve] Direct Ingress 최적화: 상수 순서 정리 및 빈 프록시 조기 반환
- [Ray Serve] Pack 스케줄링 최적화: O(replicas x total_replicas)에서 O(replicas x nodes)로
- [vllm] vLLM에 Dots3 NOTE 모델 네이티브 지원 추가: 멀티모달 및 하이브리드 MLA 최적화
- [flashinfer] [FlashInfer] CUTLASS MoE 커널 최적화: 벡터화와 동적 스레드 할당으로 성능 한계 돌파하기
PR Analysis 의 다른글
- 이전글 [hermes-agent] SQLite FTS5 최적화: CJK Bigram 인덱싱으로 검색 성능 개선하기
- 현재글 : [ray] Ray Serve LLM 인그레스 최적화: 로컬 라우팅을 통한 병목 현상 제거
- 다음글 [llm-compressor] vLLM LLM-Compressor: CUDA OOM 문제 해결을 위한 메모리 핀닝 최적화
댓글