- Authors
- Name
- 들어가며
- Ray Serve 아키텍처와 핵심 개념
- Ray Serve vs vLLM vs TorchServe vs Triton 비교
- LLM 모델 서빙: vLLM + Ray Serve 통합
- 오토스케일링 설정과 리소스 관리
- 멀티모델 서빙 패턴 (Model Composition)
- 배치 추론 최적화
- Kubernetes 배포 (KubeRay Operator)
- 모니터링, 로깅, 메트릭 설정
- 트러블슈팅과 운영 주의사항
- 실패 사례와 복구 절차
- 프로덕션 배포 체크리스트
- 마무리
- 참고 자료

들어가며
ML 모델을 프로덕션에서 서빙하는 것은 학습과는 완전히 다른 엔지니어링 영역이다. 단일 모델 서빙은 Flask나 FastAPI로도 가능하지만, 여러 모델을 동시에 서빙하면서 트래픽에 따라 자동 스케일링하고, GPU 리소스를 효율적으로 관리하며, 장애 복구까지 처리해야 하는 프로덕션 환경에서는 전문적인 서빙 프레임워크가 필수적이다.
Ray Serve는 Ray 위에 구축된 확장 가능한 모델 서빙 라이브러리로, 프레임워크에 구애받지 않고 PyTorch, TensorFlow, Scikit-Learn, XGBoost 등 어떤 모델이든 서빙할 수 있다. 특히 멀티모델 컴포지션, 프로그래밍 가능한 API, 분산 서빙이라는 세 가지 핵심 강점을 통해, 복잡한 추론 파이프라인을 Python 코드로 자연스럽게 구성할 수 있다는 점에서 다른 서빙 프레임워크와 차별화된다.
이 글에서는 Ray Serve의 아키텍처부터 LLM 서빙, 오토스케일링, Kubernetes 배포, 운영 트러블슈팅까지 프로덕션 환경에서 필요한 모든 내용을 실전 코드와 함께 다룬다.
Ray Serve 아키텍처와 핵심 개념
아키텍처 개요
Ray Serve는 Ray 클러스터 위에서 동작하는 액터 기반 서빙 프레임워크다. 전체 구조는 다음과 같다.
Client (HTTP/gRPC)
|
v
+-------------------+
| HTTP Proxy | (Ingress, 라우팅, 로드밸런싱)
+-------------------+
|
v
+-------------------+ +-------------------+
| Deployment A | | Deployment B |
| - Replica 1 | | - Replica 1 |
| - Replica 2 | | - Replica 2 |
| - Replica 3 | | - Replica 3 |
+-------------------+ +-------------------+
| |
v v
+-------------------------------------------+
| Ray Cluster (분산 런타임) |
| - Head Node (Serve Controller) |
| - Worker Node 1 (GPU) |
| - Worker Node 2 (GPU) |
+-------------------------------------------+
핵심 개념 세 가지
Deployment: 모델이나 비즈니스 로직을 감싸는 서빙의 기본 단위다. 각 Deployment는 독립적으로 스케일링, 업데이트, 롤백할 수 있다. @serve.deployment 데코레이터로 정의한다.
Replica: Deployment의 실행 인스턴스로, Ray Actor로 구현된다. 하나의 Deployment에 여러 Replica를 두어 수평 확장하며, 각 Replica는 독립된 프로세스에서 동작한다. GPU 할당, CPU 코어 지정 등 리소스를 개별적으로 설정할 수 있다.
DeploymentHandle: Python 코드 내에서 다른 Deployment를 호출하기 위한 비동기 핸들이다. 일반 함수 호출처럼 사용할 수 있으며, Ray Serve가 내부적으로 로드밸런싱과 직렬화를 처리한다. 이것이 멀티모델 컴포지션의 핵심 메커니즘이다.
import ray
from ray import serve
from transformers import pipeline
@serve.deployment(
num_replicas=2,
ray_actor_options={"num_gpus": 1},
)
class SentimentModel:
def __init__(self):
self.model = pipeline(
"sentiment-analysis",
model="distilbert-base-uncased-finetuned-sst-2-english",
device=0,
)
async def __call__(self, request):
data = await request.json()
result = self.model(data["text"])
return {"prediction": result}
@serve.deployment(num_replicas=1)
class Router:
def __init__(self, sentiment_handle):
self.sentiment = sentiment_handle
async def __call__(self, request):
# DeploymentHandle을 통한 내부 호출
result = await self.sentiment.remote(request)
return result
sentiment_app = SentimentModel.bind()
router_app = Router.bind(sentiment_app)
serve.run(router_app, route_prefix="/predict")
Ray Serve vs vLLM vs TorchServe vs Triton 비교
모델 서빙 프레임워크를 선택할 때, 각 도구의 강점과 한계를 정확히 이해하는 것이 중요하다.
| 항목 | Ray Serve | vLLM | TorchServe | Triton Inference Server |
|---|---|---|---|---|
| 주요 용도 | 범용 모델 서빙, 멀티모델 파이프라인 | LLM 전용 고성능 추론 | PyTorch 모델 서빙 | 범용 GPU 추론 서버 |
| 프레임워크 지원 | 전체 (PyTorch, TF, ONNX 등) | LLM 전용 (HuggingFace 모델) | PyTorch 전용 | TF, PyTorch, ONNX, TensorRT |
| 구현 언어 | Python | Python/C++ | Java/Python | C++ |
| 멀티모델 컴포지션 | Python 네이티브 지원 | 미지원 | 제한적 | Model Ensemble (DAG 기반) |
| 오토스케일링 | 내장 (Replica 단위) | 외부 의존 | 외부 의존 | 외부 의존 |
| LLM 최적화 | vLLM 통합 지원 | Paged Attention, Continuous Batching | 제한적 | TensorRT-LLM 백엔드 |
| K8s 통합 | KubeRay Operator | 직접 배포 또는 Ray 통합 | KServe 통합 | Triton Operator |
| 배치 추론 | @serve.batch 데코레이터 | Continuous Batching | Dynamic Batching | Dynamic Batching |
| 스트리밍 응답 | 지원 | 지원 | 제한적 | 지원 |
| 러닝 커브 | 중간 | 낮음 (LLM 한정) | 중간 | 높음 |
선택 기준 요약:
- LLM만 서빙한다면: vLLM 단독 또는 Ray Serve + vLLM 통합
- 멀티모델 파이프라인이 필요하다면: Ray Serve가 가장 자연스러운 선택
- PyTorch 모델 위주의 단순 서빙이라면: TorchServe
- 극한의 GPU 성능이 필요하다면: Triton Inference Server
- 분산 멀티노드 GPU 서빙이 필요하다면: Ray Serve + vLLM 또는 Triton + Ray Serve 통합
LLM 모델 서빙: vLLM + Ray Serve 통합
Ray Serve LLM은 vLLM 엔진을 Ray Serve 위에서 구동하여, OpenAI 호환 API를 제공하면서도 분산 서빙과 오토스케일링을 지원하는 프레임워크다. 2025년 이후 Ray Serve LLM API가 정식 출시되면서, vLLM 통합이 더욱 간편해졌다.
from ray import serve
from ray.serve.llm import LLMServer, LLMConfig, build_openai_app
# LLM 설정 정의
llm_config = LLMConfig(
model_id="meta-llama/Llama-3.1-8B-Instruct",
engine_kwargs={
"tensor_parallel_size": 2,
"max_model_len": 4096,
"gpu_memory_utilization": 0.85,
"enforce_eager": False,
},
# 오토스케일링 설정
deployment_config={
"autoscaling_config": {
"min_replicas": 1,
"max_replicas": 4,
"target_ongoing_requests": 5,
}
},
accelerator_type="A100",
)
# OpenAI 호환 앱 빌드 및 배포
app = build_openai_app(llm_config)
serve.run(app, host="0.0.0.0", port=8000)
배포 후 OpenAI SDK로 직접 호출할 수 있다.
# 헬스체크
curl http://localhost:8000/v1/models
# Chat Completions API 호출
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "meta-llama/Llama-3.1-8B-Instruct",
"messages": [
{"role": "user", "content": "Ray Serve의 장점을 설명해주세요."}
],
"max_tokens": 512,
"temperature": 0.7,
"stream": true
}'
멀티 LLM 동시 서빙
하나의 Ray 클러스터에서 여러 LLM을 동시에 서빙할 수도 있다.
from ray.serve.llm import LLMConfig, build_openai_app
# 여러 모델 설정
configs = [
LLMConfig(
model_id="meta-llama/Llama-3.1-8B-Instruct",
engine_kwargs={
"tensor_parallel_size": 1,
"gpu_memory_utilization": 0.85,
},
accelerator_type="A100",
),
LLMConfig(
model_id="Qwen/Qwen2.5-7B-Instruct",
engine_kwargs={
"tensor_parallel_size": 1,
"gpu_memory_utilization": 0.85,
},
accelerator_type="A100",
),
]
# 모든 모델을 단일 앱으로 빌드
app = build_openai_app(configs)
오토스케일링 설정과 리소스 관리
오토스케일링 기본 설정
Ray Serve의 오토스케일러는 애플리케이션 레벨에서 동작하며, 진행 중인 요청 수(ongoing requests)를 기반으로 Replica 수를 조절한다. num_replicas="auto"를 설정하면 기본 오토스케일링이 활성화된다.
from ray import serve
@serve.deployment(
# 오토스케일링 상세 설정
autoscaling_config={
"min_replicas": 1, # 최소 Replica 수
"max_replicas": 10, # 최대 Replica 수 (피크 대비 20% 여유)
"target_ongoing_requests": 5, # Replica당 목표 진행중 요청 수
"upscale_delay_s": 30, # 스케일업 판단 대기 시간
"downscale_delay_s": 300, # 스케일다운 판단 대기 시간 (보수적)
"upscaling_factor": 1.0, # 스케일업 비율
"downscaling_factor": 0.5, # 스케일다운 비율
"smoothing_factor": 0.5, # 메트릭 스무딩 팩터
"metrics_interval_s": 10, # 메트릭 수집 주기
},
max_ongoing_requests=10, # Replica당 최대 동시 처리 요청 수
ray_actor_options={
"num_gpus": 1,
"num_cpus": 4,
"memory": 16 * 1024 * 1024 * 1024, # 16GB
},
health_check_period_s=10,
health_check_timeout_s=30,
graceful_shutdown_timeout_s=60,
)
class GPUModel:
def __init__(self):
# 모델 로드
pass
async def __call__(self, request):
# 추론 로직
pass
오토스케일링 동작 원리
오토스케일러의 스케일링 결정은 다음 공식을 따른다.
desired_replicas = ceil(total_ongoing_requests / target_ongoing_requests)
예를 들어, target_ongoing_requests=5이고 현재 총 진행 중인 요청이 23개라면, ceil(23/5) = 5개의 Replica가 필요하다. 이 결정은 upscale_delay_s와 downscale_delay_s에 의해 지연되어 급격한 스케일 변동을 방지한다.
리소스 관리 전략
Zero-replica 스케일다운: 트래픽이 장시간 없을 때 Replica를 0으로 줄여 리소스를 절약할 수 있다. 단, 콜드 스타트 레이턴시가 발생하므로 실시간 서비스에는 부적합하다.
@serve.deployment(
autoscaling_config={
"min_replicas": 0, # 트래픽 없으면 0으로 축소
"max_replicas": 5,
},
)
class BatchModel:
pass
Fractional GPU 할당: GPU를 분수 단위로 할당하여 하나의 GPU에서 여러 모델을 서빙할 수 있다.
@serve.deployment(
ray_actor_options={"num_gpus": 0.25} # GPU의 25%만 사용
)
class LightModel:
pass
멀티모델 서빙 패턴 (Model Composition)
Ray Serve의 가장 강력한 기능 중 하나는 여러 모델과 비즈니스 로직을 Python 코드로 자연스럽게 결합하는 모델 컴포지션이다. DeploymentHandle을 통해 Deployment 간 호출이 일반 함수 호출처럼 동작하면서도, 내부적으로는 분산 환경에서 최적의 로드밸런싱과 직렬화가 이루어진다.
순차 파이프라인 패턴
from ray import serve
@serve.deployment(num_replicas=2)
class Preprocessor:
def preprocess(self, text: str) -> dict:
# 텍스트 전처리: 토크나이징, 정규화
tokens = text.lower().split()
return {"tokens": tokens, "length": len(tokens)}
@serve.deployment(
num_replicas=3,
ray_actor_options={"num_gpus": 1},
)
class Classifier:
def __init__(self):
from transformers import pipeline
self.model = pipeline("text-classification", device=0)
def classify(self, preprocessed: dict) -> dict:
text = " ".join(preprocessed["tokens"])
result = self.model(text)
return {"label": result[0]["label"], "score": result[0]["score"]}
@serve.deployment(num_replicas=1)
class Postprocessor:
def format_response(self, classification: dict) -> dict:
return {
"result": classification["label"],
"confidence": round(classification["score"], 4),
"status": "success",
}
@serve.deployment(num_replicas=1)
class Pipeline:
def __init__(self, preprocessor, classifier, postprocessor):
self.preprocessor = preprocessor
self.classifier = classifier
self.postprocessor = postprocessor
async def __call__(self, request):
data = await request.json()
text = data["text"]
# 순차적으로 파이프라인 실행
preprocessed = await self.preprocessor.preprocess.remote(text)
classified = await self.classifier.classify.remote(preprocessed)
response = await self.postprocessor.format_response.remote(classified)
return response
# 배포 그래프 구성
preprocessor = Preprocessor.bind()
classifier = Classifier.bind()
postprocessor = Postprocessor.bind()
app = Pipeline.bind(preprocessor, classifier, postprocessor)
Model Multiplexing 패턴
하나의 Deployment에서 여러 모델을 동적으로 로드하여 서빙하는 패턴이다. 수백 개의 고객별 모델을 서빙할 때 유용하다.
from ray import serve
@serve.deployment(
num_replicas=2,
max_ongoing_requests=10,
)
class MultiplexedModel:
def __init__(self):
self.models = {}
def _load_model(self, model_id: str):
if model_id not in self.models:
# 모델 동적 로드 (캐시 관리)
import joblib
self.models[model_id] = joblib.load(f"/models/{model_id}.pkl")
# 메모리 관리: LRU 캐시로 최대 10개 모델 유지
if len(self.models) > 10:
oldest = next(iter(self.models))
del self.models[oldest]
return self.models[model_id]
async def __call__(self, request):
data = await request.json()
model_id = data["model_id"]
features = data["features"]
model = self._load_model(model_id)
prediction = model.predict([features])
return {"model_id": model_id, "prediction": prediction.tolist()}
배치 추론 최적화
@serve.batch 데코레이터를 사용하면 개별 요청을 자동으로 묶어 배치로 처리할 수 있다. GPU 활용률을 극대화하는 핵심 기법이다.
import numpy as np
from ray import serve
@serve.deployment(
num_replicas=2,
ray_actor_options={"num_gpus": 1},
)
class BatchClassifier:
def __init__(self):
from transformers import AutoTokenizer, AutoModel
import torch
self.tokenizer = AutoTokenizer.from_pretrained(
"bert-base-uncased"
)
self.model = AutoModel.from_pretrained(
"bert-base-uncased"
).cuda().eval()
@serve.batch(max_batch_size=32, batch_wait_timeout_s=0.1)
async def handle_batch(self, texts: list[str]) -> list[dict]:
import torch
# 배치 토크나이징
inputs = self.tokenizer(
texts,
padding=True,
truncation=True,
max_length=512,
return_tensors="pt",
).to("cuda")
# 배치 추론
with torch.no_grad():
outputs = self.model(**inputs)
embeddings = outputs.last_hidden_state[:, 0, :].cpu().numpy()
# 결과를 개별 응답으로 분리
results = []
for i, emb in enumerate(embeddings):
results.append({
"embedding": emb.tolist()[:10], # 일부만 반환
"batch_size": len(texts),
})
return results
async def __call__(self, request):
data = await request.json()
return await self.handle_batch(data["text"])
배치 추론 튜닝 포인트:
max_batch_size: GPU 메모리에 맞춰 조절한다. 너무 크면 OOM, 너무 작으면 GPU 활용률 저하batch_wait_timeout_s: 배치가 다 차지 않아도 이 시간이 지나면 처리한다. 레이턴시와 처리량의 트레이드오프max_ongoing_requests를max_batch_size보다 크게 설정하여 배치가 잘 형성되도록 한다
Kubernetes 배포 (KubeRay Operator)
KubeRay 아키텍처
KubeRay는 Kubernetes에서 Ray 워크로드를 관리하기 위한 오퍼레이터로, 세 가지 CRD를 제공한다.
- RayCluster: Ray 클러스터 생명주기 관리
- RayJob: 일회성 배치 작업 실행
- RayService: Ray Serve 애플리케이션의 서빙 관리 (무중단 업그레이드 지원)
RayService 배포 YAML
apiVersion: ray.io/v1
kind: RayService
metadata:
name: llm-serving
namespace: ml-serving
spec:
# Serve 애플리케이션 설정
serveConfigV2: |
applications:
- name: llm-app
route_prefix: /
import_path: serve_app:app
runtime_env:
working_dir: "https://github.com/my-org/serve-apps/archive/main.zip"
pip:
- transformers>=4.40.0
- torch>=2.2.0
- vllm>=0.6.0
- ray[serve]>=2.40.0
deployments:
- name: LLMDeployment
num_replicas: auto
autoscaling_config:
min_replicas: 1
max_replicas: 4
target_ongoing_requests: 5
upscale_delay_s: 30
downscale_delay_s: 300
max_ongoing_requests: 10
ray_actor_options:
num_gpus: 1
num_cpus: 4
# Ray 클러스터 설정
rayClusterConfig:
rayVersion: '2.40.0'
headGroupSpec:
rayStartParams:
dashboard-host: '0.0.0.0'
num-gpus: '0'
template:
spec:
containers:
- name: ray-head
image: rayproject/ray-ml:2.40.0-py310-gpu
ports:
- containerPort: 6379
name: gcs-server
- containerPort: 8265
name: dashboard
- containerPort: 8000
name: serve
resources:
limits:
cpu: '4'
memory: '16Gi'
requests:
cpu: '2'
memory: '8Gi'
workerGroupSpecs:
- replicas: 2
minReplicas: 1
maxReplicas: 8
groupName: gpu-workers
rayStartParams:
num-gpus: '1'
template:
spec:
containers:
- name: ray-worker
image: rayproject/ray-ml:2.40.0-py310-gpu
resources:
limits:
cpu: '8'
memory: '32Gi'
nvidia.com/gpu: '1'
requests:
cpu: '4'
memory: '16Gi'
nvidia.com/gpu: '1'
tolerations:
- key: 'nvidia.com/gpu'
operator: 'Exists'
effect: 'NoSchedule'
nodeSelector:
cloud.google.com/gke-accelerator: nvidia-tesla-a100
KubeRay 배포 명령
# KubeRay Operator 설치
helm repo add kuberay https://ray-project.github.io/kuberay-helm/
helm repo update
helm install kuberay-operator kuberay/kuberay-operator \
--namespace kuberay-system \
--create-namespace \
--version 1.2.2
# RayService 배포
kubectl apply -f ray-service-llm.yaml
# 상태 확인
kubectl get rayservice llm-serving -n ml-serving
kubectl describe rayservice llm-serving -n ml-serving
# 서비스 로그 확인
kubectl logs -l ray.io/serve=llm-serving -n ml-serving --tail=100
# 포트 포워딩으로 로컬 접근
kubectl port-forward svc/llm-serving-serve-svc 8000:8000 -n ml-serving
모니터링, 로깅, 메트릭 설정
Prometheus 메트릭 수집
Ray Serve는 각 노드에서 Prometheus 포맷의 메트릭을 자동으로 노출한다. 핵심 메트릭은 다음과 같다.
| 메트릭 | 설명 | 활용 |
|---|---|---|
ray_serve_num_ongoing_requests | 현재 진행 중인 요청 수 | 오토스케일링 판단 기준 |
ray_serve_request_latency_ms | 요청 레이턴시 히스토그램 | p50/p95/p99 레이턴시 모니터링 |
ray_serve_num_replicas | 현재 활성 Replica 수 | 스케일링 상태 확인 |
ray_serve_request_counter | 총 요청 수 (성공/실패) | 에러율 알림 |
ray_component_rss_mb | 프로세스 RSS 메모리 | 메모리 누수 감지 |
Kubernetes에서 Prometheus 연동
# ServiceMonitor로 Ray 메트릭 수집
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: ray-serve-monitor
namespace: ml-serving
spec:
selector:
matchLabels:
ray.io/serve: llm-serving
endpoints:
- port: metrics
interval: 15s
path: /metrics
namespaceSelector:
matchNames:
- ml-serving
로깅 설정
Ray Serve의 로그는 기본적으로 /tmp/ray/session_latest/logs/serve/ 디렉토리에 저장된다. Kubernetes 환경에서는 Loki + Promtail 조합으로 중앙 집중식 로그 관리를 구성하는 것이 권장된다.
import logging
from ray import serve
logger = logging.getLogger("ray.serve")
@serve.deployment
class MonitoredModel:
def __init__(self):
self.request_count = 0
async def __call__(self, request):
self.request_count += 1
data = await request.json()
logger.info(
f"Request #{self.request_count} received",
extra={
"request_id": request.headers.get("X-Request-ID", "unknown"),
"model": "monitored-model",
},
)
try:
result = self._predict(data)
logger.info(f"Request #{self.request_count} completed successfully")
return result
except Exception as e:
logger.error(
f"Request #{self.request_count} failed: {str(e)}",
exc_info=True,
)
raise
def _predict(self, data):
# 추론 로직
return {"status": "ok"}
메모리 프로파일링
메모리 누수가 의심될 때 memray를 활용할 수 있다.
# 메모리 프로파일링 활성화
export RAY_SERVE_ENABLE_MEMORY_PROFILING=1
# 프로파일링 결과 확인
ls /tmp/ray/session_latest/logs/serve/*.bin
# 레이턴시 히스토그램 버킷 커스터마이징
export RAY_SERVE_REQUEST_LATENCY_BUCKETS_MS="5,10,25,50,100,250,500,1000,2500,5000"
트러블슈팅과 운영 주의사항
자주 발생하는 문제와 해결책
1. OOM(Out of Memory) 에러로 인한 연쇄 장애
24/7 서빙 환경에서 가장 치명적인 문제다. 메모리가 부족해지면 Ray가 Deployment를 재시작하지만, 재시작 간격이 점점 짧아지면서 대부분의 요청이 실패하는 장애 연쇄(failure spiral)가 발생할 수 있다.
# 예방 전략: 리소스 제한 명시
@serve.deployment(
ray_actor_options={
"num_gpus": 1,
"memory": 16 * 1024 * 1024 * 1024, # 16GB 명시
},
# 헬스체크로 비정상 상태 조기 감지
health_check_period_s=10,
health_check_timeout_s=30,
)
class SafeModel:
def __init__(self):
import torch
# GPU 메모리 사용량 로깅
if torch.cuda.is_available():
allocated = torch.cuda.memory_allocated() / 1e9
reserved = torch.cuda.memory_reserved() / 1e9
logger.info(
f"GPU memory - allocated: {allocated:.2f}GB, "
f"reserved: {reserved:.2f}GB"
)
2. GPU 메모리 단편화
vLLM 통합 시 기본적으로 GPU VRAM의 90%를 예약한다. 시스템 레벨 예약 메모리가 10%를 초과하면 gpu_memory_utilization 값을 낮춰야 한다.
# gpu_memory_utilization 조절
llm_config = LLMConfig(
model_id="meta-llama/Llama-3.1-8B-Instruct",
engine_kwargs={
"gpu_memory_utilization": 0.80, # 90%에서 80%로 낮춤
"max_model_len": 2048, # 컨텍스트 길이 제한
},
)
3. 멀티노드 GPU 서빙 시 통신 오류
멀티노드 텐서 병렬 처리 시 노드 간 네트워크 설정 문제가 자주 발생한다.
# NCCL 환경 변수 확인
export NCCL_DEBUG=INFO
export NCCL_SOCKET_IFNAME=eth0
export NCCL_IB_DISABLE=1 # InfiniBand 없는 환경에서
4. RayService 터미널 실패 상태
RayService가 터미널 실패 상태(terminal failed state)에 빠지면, spec을 업데이트해도 재시도가 트리거되지 않는다. 이 경우 RayService를 삭제하고 재생성해야 한다.
# 실패 상태 확인
kubectl get rayservice llm-serving -n ml-serving -o jsonpath='{.status.serviceStatus}'
# 복구: 삭제 후 재생성
kubectl delete rayservice llm-serving -n ml-serving
kubectl apply -f ray-service-llm.yaml
5. 오토스케일러 진동(Oscillation)
트래픽이 급변하면 스케일업/다운이 반복적으로 발생하여 성능이 불안정해질 수 있다.
# 진동 방지 설정
autoscaling_config={
"upscale_delay_s": 60, # 스케일업 대기 시간 증가
"downscale_delay_s": 600, # 스케일다운 대기 시간 크게 증가
"smoothing_factor": 0.3, # 메트릭 변화에 둔감하게
"downscaling_factor": 0.3, # 한 번에 30%만 축소
}
실패 사례와 복구 절차
사례 1: 모델 업데이트 중 서비스 중단
상황: RayService의 serveConfigV2를 업데이트했는데, 새 모델이 GPU 메모리를 초과하여 모든 Replica가 CrashLoopBackOff에 빠짐.
복구 절차:
- 즉시 이전 설정으로 롤백:
kubectl apply -f ray-service-llm-previous.yaml - 모든 Replica 정상화 확인:
kubectl get pods -l ray.io/serve=llm-serving - 새 모델을 별도 RayCluster에서 메모리 프로파일링 수행
gpu_memory_utilization과max_model_len조정 후 재배포
사례 2: OOM으로 인한 연쇄 장애
상황: 트래픽 급증으로 Worker 노드 메모리 초과. Ray 메모리 모니터가 프로세스를 반복적으로 kill하면서 서비스 전체 장애.
복구 절차:
min_replicas를 낮춰 새 요청 유입 차단kubectl scale또는 Cluster Autoscaler로 Worker 노드 추가- 메모리 사용 원인 분석:
ray_component_rss_mb메트릭 확인 max_ongoing_requests값을 낮춰 Replica당 부하 감소- 정상화 후 점진적으로 트래픽 복구
사례 3: 네트워크 파티션으로 Head 노드 단절
상황: Head 노드와 Worker 노드 간 네트워크 단절로 GCS(Global Control Store) 접근 불가. 모든 Deployment가 응답 불가.
복구 절차:
- Head 노드 상태 확인:
kubectl exec -it ray-head-0 -- ray status - 네트워크 연결 복구 확인 후, Worker 노드가 자동으로 재연결되는지 대기 (기본 타임아웃: 300초)
- 재연결 실패 시 Worker Pod 재시작:
kubectl delete pod <worker-pod> - GCS 데이터 손실 시 RayCluster 전체 재생성 필요
프로덕션 배포 체크리스트
배포 전 반드시 확인해야 할 항목들이다.
리소스 설정
- GPU 메모리 사용량을 사전에 프로파일링했는가
ray_actor_options에num_gpus,num_cpus,memory를 명시했는가gpu_memory_utilization을 적절하게 설정했는가 (권장: 0.80~0.85)- Fractional GPU 사용 시 모델 간 메모리 충돌 가능성을 검증했는가
오토스케일링
min_replicas를 실제 최소 트래픽에 맞게 설정했는가max_replicas를 피크 트래픽 대비 20% 여유분으로 설정했는가downscale_delay_s를 충분히 보수적으로 설정했는가 (최소 300초 권장)- Worker 노드 오토스케일러(KubeRay/Cluster Autoscaler)와 Serve 오토스케일러의 상호작용을 테스트했는가
안정성
health_check_period_s와health_check_timeout_s를 설정했는가graceful_shutdown_timeout_s를 모델 언로드 시간보다 크게 설정했는가- OOM 발생 시 복구 절차를 문서화했는가
- RayService 터미널 실패 상태 시 대응 런북이 있는가
모니터링
- Prometheus + Grafana 대시보드를 구성했는가
ray_serve_request_latency_msp99 레이턴시 알림을 설정했는가ray_serve_request_counter에러율 알림을 설정했는가- 메모리 사용량(
ray_component_rss_mb) 임계치 알림을 설정했는가
배포 프로세스
- 새 모델을 스테이징 환경에서 로드 테스트했는가
- RayService의 무중단 업그레이드(rolling update)를 테스트했는가
- 롤백 절차와 이전 YAML 설정을 보관하고 있는가
- 멀티노드 배포 시 NCCL 통신 설정을 검증했는가
마무리
Ray Serve는 Python 네이티브 프로그래밍 모델, 강력한 멀티모델 컴포지션, 내장 오토스케일링이라는 세 가지 핵심 강점으로 프로덕션 모델 서빙 플랫폼의 유력한 선택지가 되었다. 특히 vLLM 통합을 통한 LLM 서빙, KubeRay를 통한 Kubernetes 네이티브 배포는 MLOps 팀이 복잡한 추론 인프라를 효율적으로 운영할 수 있게 해준다.
다만 운영 시 OOM 연쇄 장애, 오토스케일러 진동, RayService 터미널 실패 상태 등 특유의 함정이 있으므로, 사전에 충분한 부하 테스트와 장애 시뮬레이션을 수행하고, 명확한 복구 절차를 문서화해 두는 것이 필수적이다. 이 글에서 다룬 설정과 패턴이 프로덕션 Ray Serve 운영의 출발점이 되기를 바란다.
참고 자료
- Ray Serve 공식 문서 - 아키텍처, API 레퍼런스, 튜토리얼
- Ray Serve LLM 배포 가이드 - vLLM 통합, OpenAI 호환 API 설정
- Ray Serve 오토스케일링 가이드 - 오토스케일링 설정 및 튜닝
- KubeRay RayService 배포 - Kubernetes 배포 YAML, 무중단 업그레이드
- Ray Serve 모니터링 - Prometheus 메트릭, Grafana 대시보드
- RayService 트러블슈팅 - 장애 진단 및 복구
- KubeRay GitHub 저장소 - Helm 차트, CRD 정의, 예제
- Ray Serve 멀티노드 GPU 트러블슈팅 - NCCL 설정, 네트워크 진단