MLflow 与血缘
这一课你将能做到
- 启动集中式 MLflow Server(元数据 + artifact 分离),把模型注册进 Model Registry
- 用别名(alias)实现模型晋升 challenger → champion,并让服务按别名动态加载
- 打通「模型版本 → run → 数据版本/代码版本」的血缘链,做到任何线上模型可回溯
为什么要学
上一课的 Tracking 解决了「实验记下来」,但还有三个生产问题没解决:哪个 run 有资格上线(需要注册与门禁)、线上跑的是哪一版(需要版本指针)、出事后怎么查(需要血缘)。MLflow Model Registry 就是模型界的「发布仓库」:run 是草稿,Model Version 是待审品,alias(champion/challenger)是线上指针,门禁是审核规则。
人话:把「把最新模型文件 scp 到服务器覆盖旧的」这种裸奔操作,升级成「注册 → 评估 → 晋升 → 回滚都动指针」的正规军流程。本站判题超时模型将获得一个真正的生产身份:timeout_classifier@champion。
先建立图像
训练 run (src/train.py)
│ mlflow.register_model
▼
Model Registry: timeout_classifier
├─ Version 5 alias: challenger ← 新模型先挂挑战者
├─ Version 4 alias: champion ← 线上正在服务
└─ Version 3 (无 alias,可随时指回)
线上服务启动时读 models:/timeout_classifier@champion
晋升 = 把 champion 从 v4 挪到 v5(一次 API 调用),服务重启即生效
回滚 = 把 champion 指回 v4,秒级完成,不需要重新部署
血缘:Version 5 ──▶ run abc123 ──▶ params/tags ──▶ git_sha + 数据文件哈希
跟着做
步骤 1:启动集中式 MLflow Server
mlflow server \
--backend-store-uri sqlite:///mlflow.db \
--artifacts-destination ./mlartifacts \
--host 127.0.0.1 --port 5000
元数据(实验/run/注册信息)进 SQLite,模型文件进本地目录。生产把两者换成 PostgreSQL + S3/OSS 即可,客户端代码不变。
步骤 2:训练时直连 Server 并注册
给上一课的 src/train.py 增加两处:脚本开头设置远端 URI,run 内注册模型。
# src/train.py 顶部新增
import os
mlflow.set_tracking_uri(os.getenv("MLFLOW_TRACKING_URI", "http://127.0.0.1:5000"))
# main() 里 log_model 之后新增
mlflow.register_model(
model_uri=f"runs:/{run.info.run_id}/model",
name="timeout_classifier",
tags={"git_sha": git_sha()},
)
export MLFLOW_TRACKING_URI=http://127.0.0.1:5000
python src/train.py
# UI 的 Models 页面出现 timeout_classifier 的最新 Version
步骤 3:用别名管理晋升与回滚
新版 MLflow 推荐 alias 替代旧的 stage(staging/production)机制——后者已标记弃用,但面试仍会问到,概念一致。
# src/promote.py
import sys
from mlflow.tracking import MlflowClient
MODEL = "timeout_classifier"
client = MlflowClient()
def promote(version: int):
v = client.get_model_version(MODEL, str(version))
assert v.run_id, "必须来自真实 run,拒绝裸文件注册"
client.set_registered_model_alias(MODEL, "challenger", version)
print(f"v{version} → challenger")
def gate_and_release(version: int, min_auc: float = 0.80):
run = client.get_run(client.get_model_version(MODEL, str(version)).run_id)
auc = run.data.metrics["roc_auc"]
if auc < min_auc:
sys.exit(f"门禁不通过:auc={auc:.3f} < {min_auc}") # 指标门禁
client.set_registered_model_alias(MODEL, "champion", version)
client.update_model_version(
name=MODEL, version=str(version),
description=f"released, auc={auc:.3f}") # 留痕
print(f"v{version} → champion")
if __name__ == "__main__":
cmd, version = sys.argv[1], int(sys.argv[2])
if cmd == "stage":
promote(version)
elif cmd == "release":
gate_and_release(version)
else:
sys.exit("用法: python src/promote.py stage|release <version>")
python src/promote.py stage 5 # 先挂挑战者
python src/promote.py release 5 # 过指标门禁后晋升冠军
步骤 4:服务端按别名加载
# src/serve.py(替换原来的 joblib.load)
import mlflow
mlflow.set_tracking_uri("http://127.0.0.1:5000")
model = mlflow.pyfunc.load_model("models:/timeout_classifier@champion")
# predict 函数改为:
# proba = model.predict(X)[0] → pyfunc 统一入口,换模型类型不改服务代码
champion 指向新版本后重启服务(或热重载),就完成了一次「发布」。回滚同理:把 champion 指回旧 version。
步骤 5:血缘回溯
from mlflow.tracking import MlflowClient
client = MlflowClient()
mv = client.get_model_version_by_alias("timeout_classifier", "champion")
run = client.get_run(mv.run_id)
print("模型版本:", mv.version)
print("代码版本:", run.data.tags.get("git_sha"))
print("数据来源:", run.data.tags.get("data"))
print("参数:", run.data.params)
print("指标:", run.data.metrics)
线上出问题时,这段脚本 30 秒给出答案:线上模型是哪个 run、哪份代码、哪份数据训的。
讲清楚要点
- Registry 是状态机,不是文件夹:Version 从注册到发布经过「注册 → 挑战者 → 门禁 → 冠军 → 可回滚」,每个动作都是对指针的原子操作,全程留痕。
- alias 与 stage 的关系:stage(Staging/Production/Archived)是旧机制,多套环境时容易和真实环境脱节;alias 是自由标签,
champion/challenger/shadow随你定义,一个 version 可挂多个 alias,语义更干净。 - 门禁必须是代码而不是默契:
gate_and_release里的min_auc判断就是最简门禁。进阶做法是接 Evidently 评估报告(第 10 课)作为第二道门禁。 - 血缘三件套:run 打
git_shatag、data数据标识 tag、模型 version 关联 run。DVC 的数据哈希、Git 的提交号、MLflow 的 run_id 三者互相引用,构成完整证据链。 - pyfunc 统一推理入口:服务端不关心模型是 sklearn 还是 PyTorch,
model.predict(X)一个签名走天下,切换模型框架不改服务代码。
你可能会懵的地方
- 新版本 MLflow 找不到 transition_model_stage 了?
该 API 已弃用,改用
set_registered_model_alias。迁移思路:@champion对应旧 Production,@staging对应旧 Staging。老教程里的 stage 代码照抄会报错,面试回答时先讲 alias 再补一句 stage 的历史即可。 - 灰度发布和 Registry 怎么配合?
Registry 管版本指针,灰度管流量切分。常见模式:起两个服务实例,一个读
@champion(95% 流量)一个读@challenger(5% 流量),网关按权重转发;观察无异常后晋升 challenger。 - 注册模型需要审批流吗? 小团队用「门禁脚本 + PR 审核 promote.py 的改动」即可;平台化团队把 alias 变更接入工单或 GitOps:alias 配置进 Git,CI 审核后调 API,天然带审计日志。
- run 和 Model Version 生命周期有何区别? run 是一次训练的完整档案,实验期可随意产生与清理;Model Version 是通过注册动作「转正」的产物,只应从有价值的 run 创建,且有指针引用、不可变。清理实验时永远先查有无 version 依赖。
- 血缘只靠 tag 够吗? 小规模够用。规模化后可升级为 OpenLineage 等标准协议自动采集事件,生成 DAG 式血缘图(数据表 → 作业 → 模型 → 服务)。先用 tag 养成习惯,再谈工具。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 新模型直接把 champion 覆盖 | 无门禁、无法回滚 | 先挂 challenger,过门禁再晋升 |
| 从本地 joblib 文件手动注册 | run_id 缺失,血缘断裂 | 必须从 runs:/ URI 注册 |
| version 只换不写描述 | 事后没人知道 v5 改了什么 | 晋升时 update_model_version 写变更摘要 |
| 服务端硬编码模型版本号 | 回滚要改代码重新部署 | 服务只认 @champion,回滚动指针 |
| 删除旧 version 腾空间 | 血缘链断裂,事故无法归因 | 旧版本归档保留,artifact 用生命周期策略管理 |
卡点急救
- register_model 报 not found:客户端
MLFLOW_TRACKING_URI与 Server 不一致;mlflow.set_tracking_uri或环境变量二选一,训练与服务保持同一地址。 - load_model 报 alias 不存在:
client.search_model_versions或 UI 确认 version 与 alias 拼写;alias 区分大小写。 - Server 重启后实验消失:每次启动
--backend-store-uri指向不同位置会各建一个库;固定用同一个 SQLite 文件路径(或生产换 PostgreSQL)。
面试常问
- 模型注册中心解决什么问题? 模型的版本化登记、发布状态管理(晋升/回滚)、门禁集成与血缘追溯。它让「上线哪个模型」从口头约定变成可审计的指针操作,是 CI/CD 与服务之间的枢纽。
- 描述一次完整的模型发布与回滚流程。 训练 run 注册为 Version → 挂 challenger → 自动门禁(指标 + 数据漂移检查)通过 → champion 指针切换 → 服务滚动重启读新指针 → 监控观察;异常时把 champion 指回上一版本,服务重启即回滚,全程不重新训练、不重新构建镜像。
- 数据血缘在你项目里如何落地? 三层:数据层 DVC 哈希标识数据集版本;训练层 run 打 git_sha 与数据哈希 tag;发布层 Model Version 关联 run。任何线上模型可沿「version → run → tag」30 秒内定位代码与数据来源,满足审计与事故归因。
自测三问
- alias 机制相比 stage 机制好在哪?
- 「门禁」至少应包含哪些检查?用代码怎么表达?
- 从线上模型出发回溯训练数据,需要哪三层血缘引用?
小结一句话
Model Registry 用「版本 + 别名 + 门禁 + 血缘」把模型发布变成可审计的指针操作,champion/challenger 双指针覆盖发布、灰度与回滚。
延伸
- MLflow 官方文档:Model Registry——alias、版本管理与部署集成的权威说明。
- 下一课:编排与调度——用 Prefect/Airflow 把特征、训练、注册串成每天自动跑的流水线。