이 글에서 다루는 것
MLflow 2.x의 alias 기반 모델 버전 관리로 Champion/Challenger 패턴을 구현하고, FastAPI와 연동한 zero-downtime hot swap 구조를 설계하는 과정
선수지식
이 단계에서 해결하려는 문제
MLflow 2.x부터 Staging/Production Stage가 deprecated됩니다. 대신 Registered Model Alias 방식이 공식 권장 패턴이 되었습니다.
이 변화는 단순한 API 교체가 아닙니다. 모델 버전 관리의 책임 구조가 바뀌는 것입니다.
이 글은 Stage 방식의 한계를 분석하고, alias 기반 Champion/Challenger 패턴과 FastAPI와 연동한 zero-downtime hot swap 구조를 설명합니다.
🎯 핵심 요약
- Stage 방식 한계: 고정된 Stage 이름(Staging/Production)은 운영 유연성 부족
- alias 방식 전환:
@champion,@challenger등 의미 있는 이름으로 버전 지정 - Champion/Challenger 패턴: 운영 모델(A)과 검증 모델(B)을 동시에 서빙
- hot swap 구조: FastAPI
/variant/{alias}/reload-> MLflow alias 조회 -> 모델 교체 - zero-downtime: 모델 교체 시 서비스 중단 없음 (메모리 내 교체)
- MLflow FQDN
- dev:
http://mlflow-dev-service.mlflow-dev.svc.cluster.local:5000 - prod:
http://mlflow-prod-service.mlflow-prod.svc.cluster.local:5000
- dev:
1️⃣ Stage 방식의 한계
MLflow 1.x 시대에는 모델 버전에 Stage를 부여했습니다.
# 구방식 (deprecated)
client.transition_model_version_stage(
name="best_model",
version=3,
stage="Production", # None / Staging / Production / Archived
)
이 방식에는 다음 문제가 있습니다.
고정된 Stage 이름
Stage는 None, Staging, Production, Archived 네 가지만 존재합니다.
운영에서 필요한 champion, challenger, shadow, rollback 같은 개념을 표현할 수 없습니다.
동시 서빙 불편
Production Stage에는 하나의 버전만 있는 것이 일반적입니다.
Champion/Challenger 패턴에서 두 모델을 동시에 서빙하려면 Stage 이름만으로는 구분이 어렵습니다.
2️⃣ alias 방식으로 전환
alias는 특정 모델 버전에 이름을 부여하는 방식입니다.
from mlflow.tracking import MlflowClient
client = MlflowClient()
# alias 설정: version 3에 champion alias 부여
client.set_registered_model_alias(
name="best_model",
alias="champion",
version=3,
)
# alias 설정: version 4에 challenger alias 부여
client.set_registered_model_alias(
name="best_model",
alias="challenger",
version=4,
)
alias로 모델을 로드할 때는 다음 URI를 사용합니다.
import mlflow.pyfunc
# champion 모델 로드
model = mlflow.pyfunc.load_model("models:/best_model@champion")
# challenger 모델 로드
model = mlflow.pyfunc.load_model("models:/best_model@challenger")
alias의 장점:
models:/best_model@champion → 현재 운영 모델
models:/best_model@challenger → 검증 중인 모델
models:/best_model@shadow → Shadow 테스트 모델
models:/best_model@rollback → 롤백 대상 모델
이름이 명시적이므로 운영 의도가 코드에 직접 드러납니다.
3️⃣ Champion/Challenger 패턴
Champion은 현재 운영 중인 모델, Challenger는 검증 중인 새 모델입니다.
트래픽 분배 예시 (A/B 테스트)
요청 → FastAPI
↓
client_id 해시 기반 분기
↓
90% → champion (alias=A)
10% → challenger (alias=B)
이 구조의 핵심:
- Champion을 교체하지 않고 Challenger와 동시에 운영
- Challenger 성능이 충분하면 alias를 재지정해서 Champion으로 승격
- 서비스 중단 없이 모델 교체
alias 재지정으로 승격
# Challenger(version 4)를 Champion으로 승격
client.set_registered_model_alias(
name="best_model",
alias="champion",
version=4,
)
# 기존 Champion(version 3)은 rollback alias로 유지
client.set_registered_model_alias(
name="best_model",
alias="rollback",
version=3,
)
alias만 재지정하면 됩니다. 모델 파일은 그대로입니다.
4️⃣ FastAPI와의 hot swap 구조
전체 흐름
[alias 재지정]
MLflow Registry: best_model@champion → version 4
↓
[hot swap 요청]
POST /variant/A/reload
Header: x-token: <RELOAD_SECRET_TOKEN>
↓
[FastAPI 처리]
1. x-token 검증
2. MLflow에서 alias=A 조회
3. mlflow.pyfunc.load_model(model_uri)
4. app.state.models["A"] 교체
5. Slack 알림 전송
↓
[결과]
서비스 중단 없이 version 4 모델로 교체 완료
model_loader.py (핵심)
# services/model_loader.py
def load_model_by_alias(alias: str):
mlflow.set_tracking_uri(settings.mlflow_tracking_uri)
client = MlflowClient()
model_uri = f"models:/{settings.model_name}@{alias}"
model = mlflow.pyfunc.load_model(model_uri)
version_info = client.get_model_version_by_alias(settings.model_name, alias)
return {
"model": model,
"info": {
"model_name": settings.model_name,
"alias": alias,
"version": version_info.version,
"run_id": version_info.run_id,
"model_uri": model_uri,
},
}
# ... (에러 처리 생략)
reload.py (핵심)
# routes/reload.py
@router.post("/variant/{alias}/reload")
def reload_model(alias: str, request: Request, x_token: str | None = Header(default=None)):
if x_token != settings.reload_secret_token:
raise HTTPException(status_code=403, detail="Invalid token")
new_entry = load_model_by_alias(alias)
if new_entry is None:
raise HTTPException(status_code=500, detail=f"모델 로딩 실패: alias={alias}")
# 메모리 내 교체 (zero-downtime)
request.app.state.models[alias] = new_entry
# ... (로깅, Slack 알림 생략)
return {"status": "reloaded", "alias": alias, "version": new_entry["info"]["version"]}
5️⃣ alias 정보 조회: mlflow_meta.py
현재 어떤 버전이 어떤 alias를 가지고 있는지 확인하는 유틸리티입니다.
# utils/mlflow_meta.py
def get_alias_info(model_name: str, alias: str, tracking_uri: str) -> dict:
mlflow.set_tracking_uri(tracking_uri)
client = MlflowClient()
version_info = client.get_model_version_by_alias(model_name, alias)
return {
"model_name": model_name,
"alias": alias,
"version": version_info.version,
"run_id": version_info.run_id,
"model_uri": f"models:/{model_name}@{alias}",
"status": version_info.status,
}
# ... (list_aliases 함수 생략)
6️⃣ Airflow DAG에서 alias 자동 관리
E2E 파이프라인에서 모델 학습 후 alias를 자동으로 관리합니다.
# dags/mlops_lib/core/registry.py (핵심 부분)
def promote_to_champion(model_name: str, version: str, tracking_uri: str):
mlflow.set_tracking_uri(tracking_uri)
client = MlflowClient()
# 기존 champion을 rollback으로 보존
try:
old_champion = client.get_model_version_by_alias(model_name, "champion")
client.set_registered_model_alias(name=model_name, alias="rollback", version=old_champion.version)
except Exception:
pass
# 새 버전을 champion으로 승격
client.set_registered_model_alias(name=model_name, alias="champion", version=version)
DAG에서 Promotion/Shadow 분기 후 호출합니다.
if should_promote:
promote_to_champion(model_name, version, tracking_uri)
requests.post(f"{fastapi_url}/variant/A/reload", headers={"x-token": reload_token})
else:
assign_as_challenger(model_name, version, tracking_uri)
7️⃣ 운영 표준 확인 루틴
# 현재 champion alias 버전 확인
curl -s http://mlflow-dev-service.mlflow-dev.svc.cluster.local:5000/api/2.0/mlflow/registered-models/alias \
-G -d name=best_model -d alias=champion \
| jq '.model_version | {version, run_id, status}'
# FastAPI 현재 로딩 모델 확인
curl -sk https://fastapi.local/models | jq .
# hot swap 테스트 (alias A 교체)
curl -sk -X POST https://fastapi.local/variant/A/reload \
-H "x-token: <RELOAD_SECRET_TOKEN>" | jq .
🧩 트러블슈팅
(1) MlflowException: Registered model alias ... not found
alias가 설정되지 않은 상태에서 로드를 시도한 경우입니다.
set_registered_model_alias로 먼저 등록합니다.
(2) hot swap 후에도 이전 버전이 계속 사용됨
app.state.models 교체는 정상이지만 예측에서 계속 이전 모델을 쓰는 경우입니다.
predict.py에서 모듈 레벨 전역 변수에 모델을 캐싱하고 있지 않은지 확인합니다.
매 요청마다 request.app.state.models에서 조회해야 합니다.
(3) MLflow Tracking Server 연결 실패
K8s 내부에서 FQDN을 정확히 써야 합니다: <service>.<namespace>.svc.cluster.local:<port>
설계 판단 (Why This Way?)
Stage의 고정된 4가지 상태 대신 alias를 사용하여 champion/challenger/shadow/rollback 같은 운영 의도를 자유롭게 표현하고, FastAPI 메모리 내 모델 교체(hot swap)로 Pod 재시작 없이 zero-downtime 모델 전환을 구현합니다.
다음에 읽을 글
→ GitOps 기반 E2E ML Platform - 설계 의도 — Level 5 시작: 플랫폼 전체 설계 철학과 목표