이 글에서 다루는 것

모델 등록(MLflow)과 서빙 반영(Triton + FastAPI)을 분리한 구조, explicit 모드, FastAPI Reload API, 확장 전략(replica/batching), Feature Store 연동 패턴

선수지식


MLflow + Triton + FastAPI

  • 등록과 서빙 반영을 분리한 이유

들어가며

ML 시스템을 처음 구축할 때 흔히 다음과 같은 구조를 사용합니다.

Train
  ↓
MLflow Register
  ↓
Deploy

즉, 모델 등록과 서빙 반영이 거의 같은 단계로 취급됩니다.

이 방식은 간단하고 구현하기 쉽습니다.

하지만 운영 환경에서는 다음과 같은 문제가 발생할 수 있습니다.

  • 등록된 모델이 바로 운영 모델이 되어버림
  • 서빙 환경 상태를 고려하지 않고 배포 진행
  • 모델 로딩 실패 시 서비스 장애 발생
  • 모델 전환 시점 제어가 어려움

이 프로젝트에서는 이러한 문제를 피하기 위해

모델 등록과 실제 서빙 반영을 명확히 분리했습니다.

핵심 구조는 다음과 같습니다.

MLflow
   ↓
Model Registry
   ↓
Triton Serving Runtime
   ↓
FastAPI Serving Control Layer

이 글에서는 이 구조를 설명합니다.


모델 생명주기 구조

이 플랫폼에서 모델은 다음 단계를 거칩니다.

Train
   ↓
MLflow Register
   ↓
Triton Model Repository
   ↓
FastAPI Reload
   ↓
Serving Runtime

여기서 중요한 점은 다음입니다.

MLflow에 등록된 모델이 곧 운영 모델은 아니다.

실제로 운영 모델은

Triton에서 READY 상태로 로드된 모델입니다.


MLflow: 모델 기록 시스템

MLflow는 이 플랫폼에서 다음 역할을 합니다.

Experiment Tracking
Model Registry
Training Metadata

예를 들어 MLflow에는 다음 정보가 저장됩니다.

  • model version
  • run_id
  • training parameters
  • metrics

즉 MLflow는

모델 학습 결과를 기록하는 시스템

입니다.

하지만 MLflow 자체는

서빙 상태를 관리하지 않습니다.


Triton: Serving Runtime

실제 모델 서빙은 Triton이 담당합니다.

관련 설정:

model-control-mode=explicit

이 설정의 의미는 다음과 같습니다.

모델 자동 로드 금지
명시적으로 load/unload 수행

즉 Triton은 다음 구조로 동작합니다.

Model Repository
      ↓
Explicit Load
      ↓
READY 상태

관련 설정 파일:

charts/triton/templates/deployment.yaml

Triton 실행 옵션 일부:

--model-repository=/models
--model-control-mode=explicit

또한 throughput 최적화를 위해 dynamic_batchinginstance_group 설정을 Airflow Variable로 제어할 수 있도록 구성했습니다. 기본값은 비활성이며, Variable을 통해 활성화하면 다음 설정이 config.pbtxt에 반영됩니다.

dynamic_batching {
  preferred_batch_size: [ 8, 16 ]
}
instance_group [
  {
    kind: KIND_CPU
    count: 1
  }
]

count 값은 Airflow Variable(triton_instance_group_count)로 런타임에 변경할 수 있으며, 기본값은 1입니다.

이 설정의 효과는 다음과 같습니다.

preferred_batch_size: [8, 16] → 배치 단위 추론으로 처리량 향상
instance_group KIND_CPU count:1 → CPU 인스턴스 1개 (런타임 오버라이드 가능)

이 설정 덕분에 모델 로딩을

파이프라인에서 명시적으로 제어할 수 있습니다.


실제 Triton READY 상태

실제로 Triton이 모델을 정상적으로 로드했는지는

repository index API로 확인할 수 있습니다.

Proof:

docs/proof/latest/e2e_success/triton_dev_ready_and_repo_index.txt

예시 결과:

HTTP/1.1 200 OK

그리고 repository index:

[
 {"name":"best_model","version":"46","state":"READY"}
]

즉 Triton에서 다음이 확인됩니다.

model = best_model
version = 46
state = READY

이 상태가 되어야

모델이 실제 서빙 가능한 상태입니다.


FastAPI: Serving Control Layer

Triton 위에는 FastAPI gateway가 존재합니다.

FastAPI는 다음 역할을 합니다.

Inference Gateway
Serving Control
Traffic Routing
Reload Interface

FastAPI는 단순한 inference API가 아니라

서빙 제어 계층입니다.

관련 코드:

charts/fastapi/app/main.py

FastAPI는 다음 라우터를 제공합니다.

/health
/ready
/models
/predict
/variant/{alias}/reload

/models API

/models API는 현재 서빙 상태를 확인하는 API입니다.

관련 코드:

charts/fastapi/app/routes/models.py

이 API는 다음 정보를 반환합니다.

{
 "ssot": {
   "prod_served_version": 46,
   "shadow_served_version": null,
   "prod_triton_url": "http://triton.triton-dev.svc.cluster.local:8000",
   "model_name": "best_model"
 },
 "effective": {
   "A": {"mode": "ssot", "version": 46, "run_id": "..."}
 }
}

여기서 중요한 개념이 등장합니다.

SSOT = Single Source of Truth

이 플랫폼에서는

Triton served version이 SSOT입니다.

즉 운영 진실은 다음입니다.

Triton served model

MLflow registry는

운영 진실이 아닙니다.


FastAPI Reload API

모델 전환은 FastAPI reload API로 수행됩니다.

API:

POST /variant/{alias}/reload

관련 코드:

charts/fastapi/app/routes/reload.py

이 API는 다음 기능을 수행합니다.

Triton served version 확인
      ↓
MLflow metadata 조회
      ↓
서빙 상태 업데이트

예시 응답:

{
 "status":"success",
 "variant":"A",
 "version":46,
 "run_id":"e57473991dc14de6a57dc7e5dfa1aa70"
}

Proof:

docs/proof/latest/e2e_success/reload_dev_variant_A.json

FastAPI Health 상태

FastAPI의 상태는 다음 API로 확인할 수 있습니다.

GET /health

Proof:

docs/proof/latest/e2e_success/fastapi_dev_health_models_metrics.txt

예시:

{
 "status":"ok",
 "loaded_aliases": ["A","B"]
}

  • FastAPI 정상
  • 모델 alias 정상

입니다.


확장 전략: FastAPI vs Triton

FastAPI와 Triton은 역할이 다르기 때문에

확장 전략도 다르게 설계했습니다.

FastAPI: replicaCount 2 고정 + RollingUpdate

FastAPI는 stateless gateway이므로 수평 확장이 가능한 구조입니다.

현재 배포 설정:

# charts/fastapi/values.yaml
replicaCount: 2

updateStrategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 0

resources:
  requests:
    cpu: 100m
    memory: 256Mi
  limits:
    cpu: 500m
    memory: 512Mi

현재는 replicaCount: 2 고정으로 운영하며,

maxUnavailable: 0으로 무중단 롤아웃을 보장합니다.

FastAPI replica 2 (RollingUpdate 기반 무중단 배포)
      ↓ 부하 증가 시
values.yaml replicaCount 조정 → ArgoCD sync → 반영

Triton: 1 replica 고정 + RollingUpdate 전략

Triton은 다음 이유로 1 replica를 고정합니다.

모델 파일을 NFS로 마운트
쓰기는 Airflow DAG만 수행
Triton은 읽기만 수행 (explicit 모드)

관련 설정:

# charts/triton/templates/deployment.yaml
strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 0
replicas: 1

--model-control-mode=explicit에서는 Triton이 NFS 디렉토리를 자동 폴링하지 않으므로

Pod restart 중 NFS partial-read 위험이 없습니다.

모델 로딩은 Airflow DAG가 명시적으로 Triton API를 호출할 때만 발생합니다.

maxSurge=1        → 새 Pod이 먼저 뜬 후 readinessProbe 통과 후 트래픽 전환
maxUnavailable=0  → 기존 Pod은 새 Pod 준비 완료까지 유지

즉, explicit 모드 덕분에 RollingUpdate로 무중단 배포가 가능합니다.

두 전략 비교

컴포넌트전략이유
FastAPIreplicaCount: 2 + RollingUpdatestateless, 무중단 롤아웃 가능
TritonRollingUpdate + 1 replicaexplicit 모드로 NFS 충돌 없음, 무중단 배포

처리량 증가가 필요할 때는

FastAPI replica를 늘려서 수용하고

Triton은 dynamic_batching + instance_group으로 단일 Pod 내 처리량을 높입니다.


Traffic Routing

FastAPI는 트래픽 분기도 담당합니다.

관련 코드:

charts/fastapi/app/services/alias_selector.py

이 시스템은 다음 전략을 지원합니다.

mirror traffic
split traffic

그리고 다음 방식으로 사용자 트래픽을 분기합니다.

client_id hash routing

내부 동작 원리 (pseudocode):

sha256(client_id) % 100

실제 구현 함수:

pct = _stable_pct_0_99(client_id)

_stable_pct_0_99는 client_id를 받아 0~99 범위의 정수를 반환합니다.

같은 client_id는 항상 같은 값을 반환하므로 routing이 결정적(deterministic)입니다.

이 방식은 다음 특징을 가집니다.

  • deterministic routing
  • sticky traffic

즉 같은 사용자는 항상 같은 모델로 이동합니다.


실제 서빙 상태 Proof

FastAPI는 다음 정보를 제공합니다.

Proof:

docs/proof/latest/e2e_success/fastapi_dev_health_models_metrics.txt

예시 결과:

{
 "active": {
   "A": {
     "model_name":"best_model",
     "version":46,
     "run_id":"e5747399..."
   }
 }
}

또한 metrics endpoint도 존재합니다.

GET /metrics

예시:

http_requests_total
process_cpu_seconds_total
python_gc_objects_collected_total

즉 FastAPI는

Prometheus metrics exporter 역할도 합니다.


Feature Store와 서빙 구조의 연결

이 플랫폼에서 Feature Store는 Optional 레이어에 위치하지만

FastAPI 서빙 구조와 실제로 연결됩니다.

build_features → store_features → FastAPI 조회 흐름

E2E DAG에서 Feature Pipeline은 다음 순서로 동작합니다.

build_features
      ↓
store_features (Feast online store에 저장)
      ↓
Train (저장된 feature로 학습)

그리고 추론 시점에 다음 흐름이 일어납니다.

클라이언트 요청 (entity_id 포함)
      ↓
FastAPI /predict
      ↓
Feast online store에서 feature 조회
      ↓
Triton /v2/models/best_model/infer (feature 전달)
      ↓
응답 반환

Feast online store 조회 패턴

현재 상태: FastAPI /predict 핸들러는 Feast와 연동되어 있지 않습니다. 요청 payload에서 data: list[list[float]] 형태로 feature를 직접 받아 Triton으로 전달합니다. Optional 레이어(Feast + Redis)를 활성화하면 아래와 같은 패턴으로 확장할 수 있습니다.

# 향후 Optional ON 시 예상 연동 패턴
from feast import FeatureStore

store = FeatureStore(repo_path="/feast/feature_repo")

def get_features(entity_id: str) -> list:
    feature_vector = store.get_online_features(
        features=["iris_features:sepal_length",
                  "iris_features:sepal_width",
                  "iris_features:petal_length",
                  "iris_features:petal_width"],
        entity_rows=[{"entity_id": entity_id}],
    ).to_dict()
    return [
        feature_vector["sepal_length"][0],
        feature_vector["sepal_width"][0],
        feature_vector["petal_length"][0],
        feature_vector["petal_width"][0],
    ]

이 조회는 Redis backend(online store)에서 수행되므로

ms 단위 latency로 동작합니다.

Optional ON/OFF 시 동작 방식

상태동작
Optional ON (Feature Store 활성화)Feast에서 feature 조회 → Triton 추론
Optional OFF (Feature Store 없음, 현재 기본 상태)요청 payload의 feature를 직접 사용 → Triton 추론

FastAPI는 Feast 연결 실패 시 fallback으로 요청 payload를 사용합니다.

이 구조 덕분에 Feature Store 없이도 서빙이 중단되지 않습니다.

Feast 연결 가능   → Feast feature 사용
Feast 연결 불가   → request payload feature 사용 (fallback)

즉 Optional 레이어가 비활성화되어도

Core 서빙 경로는 정상 동작합니다.


왜 이 구조가 중요한가

이 플랫폼에서 모델 서빙 구조는 다음과 같이 분리되어 있습니다.

MLflow
   ↓
Model Registry
   ↓
Triton
   ↓
Serving Runtime
   ↓
FastAPI
   ↓
Serving Control Layer

이 구조 덕분에 다음이 가능합니다.

모델 기록

MLflow


실제 서빙

Triton


서빙 제어

FastAPI


모델 기록
모델 서빙
서빙 전환

세 가지 역할이 분리됩니다.


핵심 메시지

이 플랫폼의 중요한 설계 원칙은 다음입니다.

등록된 모델 ≠ 운영 모델

운영 모델은 다음 조건을 만족해야 합니다.

Triton READY 상태
AND
FastAPI reload 완료

이 구조 덕분에

  • 모델 등록
  • 서빙 준비
  • 서비스 반영

단계적으로 통제할 수 있습니다.


다음 글

지금까지는 서빙 아키텍처 구조를 살펴보았습니다.

다음 글에서는 실제 런타임에서

이 시스템이 정상 동작하는지 확인합니다.

다음 내용이 포함됩니다.

  • Triton READY 상태 확인
  • repository index 확인
  • FastAPI health 확인
  • reload API 동작 확인

관련 Proof:

docs/proof/latest/e2e_success/*

즉 다음 글에서는

이 플랫폼이 실제로 동작하는지

Serving Runtime 증거를 통해 확인합니다.


설계 판단 (Why This Way?)

MLflow 등록 모델이 곧 운영 모델이 되지 않도록 Triton READY + FastAPI reload 완료를 운영 모델의 조건으로 정의하고, Triton explicit 모드로 파이프라인이 모델 로딩 시점을 정확히 제어합니다. FastAPI는 stateless RollingUpdate, Triton은 explicit 모드 덕분에 NFS partial-read 위험 없이 1 replica RollingUpdate(maxSurge=1, maxUnavailable=0) 전략을 적용하여 워크로드 특성에 맞는 확장 전략을 분리했습니다.


다음에 읽을 글

GitOps 기반 E2E ML Platform - 실제 동작 확인 — Serving Runtime Proof