Skip to content
Published on

Ray Serve 모델 서빙 플랫폼 구축 가이드 — 오토스케일링, 멀티모델, 프로덕션 배포

Authors
  • Name
    Twitter
Ray Serve 모델 서빙 플랫폼

들어가며

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 ServevLLMTorchServeTriton Inference Server
주요 용도범용 모델 서빙, 멀티모델 파이프라인LLM 전용 고성능 추론PyTorch 모델 서빙범용 GPU 추론 서버
프레임워크 지원전체 (PyTorch, TF, ONNX 등)LLM 전용 (HuggingFace 모델)PyTorch 전용TF, PyTorch, ONNX, TensorRT
구현 언어PythonPython/C++Java/PythonC++
멀티모델 컴포지션Python 네이티브 지원미지원제한적Model Ensemble (DAG 기반)
오토스케일링내장 (Replica 단위)외부 의존외부 의존외부 의존
LLM 최적화vLLM 통합 지원Paged Attention, Continuous Batching제한적TensorRT-LLM 백엔드
K8s 통합KubeRay Operator직접 배포 또는 Ray 통합KServe 통합Triton Operator
배치 추론@serve.batch 데코레이터Continuous BatchingDynamic BatchingDynamic 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_sdownscale_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_requestsmax_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에 빠짐.

복구 절차:

  1. 즉시 이전 설정으로 롤백: kubectl apply -f ray-service-llm-previous.yaml
  2. 모든 Replica 정상화 확인: kubectl get pods -l ray.io/serve=llm-serving
  3. 새 모델을 별도 RayCluster에서 메모리 프로파일링 수행
  4. gpu_memory_utilizationmax_model_len 조정 후 재배포

사례 2: OOM으로 인한 연쇄 장애

상황: 트래픽 급증으로 Worker 노드 메모리 초과. Ray 메모리 모니터가 프로세스를 반복적으로 kill하면서 서비스 전체 장애.

복구 절차:

  1. min_replicas를 낮춰 새 요청 유입 차단
  2. kubectl scale 또는 Cluster Autoscaler로 Worker 노드 추가
  3. 메모리 사용 원인 분석: ray_component_rss_mb 메트릭 확인
  4. max_ongoing_requests 값을 낮춰 Replica당 부하 감소
  5. 정상화 후 점진적으로 트래픽 복구

사례 3: 네트워크 파티션으로 Head 노드 단절

상황: Head 노드와 Worker 노드 간 네트워크 단절로 GCS(Global Control Store) 접근 불가. 모든 Deployment가 응답 불가.

복구 절차:

  1. Head 노드 상태 확인: kubectl exec -it ray-head-0 -- ray status
  2. 네트워크 연결 복구 확인 후, Worker 노드가 자동으로 재연결되는지 대기 (기본 타임아웃: 300초)
  3. 재연결 실패 시 Worker Pod 재시작: kubectl delete pod <worker-pod>
  4. GCS 데이터 손실 시 RayCluster 전체 재생성 필요

프로덕션 배포 체크리스트

배포 전 반드시 확인해야 할 항목들이다.

리소스 설정

  • GPU 메모리 사용량을 사전에 프로파일링했는가
  • ray_actor_optionsnum_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_shealth_check_timeout_s를 설정했는가
  • graceful_shutdown_timeout_s를 모델 언로드 시간보다 크게 설정했는가
  • OOM 발생 시 복구 절차를 문서화했는가
  • RayService 터미널 실패 상태 시 대응 런북이 있는가

모니터링

  • Prometheus + Grafana 대시보드를 구성했는가
  • ray_serve_request_latency_ms p99 레이턴시 알림을 설정했는가
  • 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 운영의 출발점이 되기를 바란다.

참고 자료