이 글에서 다루는 것
모델 등록(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_batching과 instance_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로 무중단 배포가 가능합니다.
두 전략 비교
| 컴포넌트 | 전략 | 이유 |
|---|---|---|
| FastAPI | replicaCount: 2 + RollingUpdate | stateless, 무중단 롤아웃 가능 |
| Triton | RollingUpdate + 1 replica | explicit 모드로 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