1. 项目概览:为什么偏偏是MLRun
先聊两句废话之外的实话。MLOps这个赛道,工具多到让人选择困难,Kubeflow、Airflow、MLflow、Metaflow……每一个都有自己的拥趸。但如果你和我一样,既想要Notebook里那种随手就跑的爽快感,又不想在上生产时被基础设施细节折磨到崩溃,MLRun是一个非常值得花时间研究的选择。
一句话概括MLRun:它是一个开源的统一编排平台,把数据获取、训练、调参、模型注册、部署、监控整个ML生命周期串成一条流水线,并且让你用近乎写普通Python函数的体验去定义这些流程。对于刚刚接触MLOps的个人开发者,或者一个正在从手工训练模型向自动化流水线转型的小团队来说,MLRun能帮你把90%的工程化脏活先扛下来。
它的核心优势在于“本地开发、云端运行”的无缝切换。本地用代码跑通一个小实验,不加修改,直接调度到Kubernetes集群上跑大规模数据并行。另一个让我觉得惊艳的点在于,它把“任务”、“函数”、“工作流”这几个抽象概念做到了极简,学习曲线比Kubeflow平缓得多,却又比Airflow这种单纯编排工具多了很多ML场景的原生支持。
这篇教程我会从零开始,带你过一遍MLRun的安装、核心概念、第一个完整Pipeline,以及我在实际项目中踩过的坑。无论你是刚入门的算法工程师,还是已经在用别的MLOps工具想横向对比的老手,这篇文章都能省下你不少折腾时间。
2. 核心理念与核心概念拆解
2.1 四个必须搞懂的关键名词
MLRun所有文档里翻来覆去就四个词:Project、Function、Task、Run。这四个概念搞明白,整个平台就算入门了一半。
Project是容器,也就是逻辑上的“项目边界”。它可以对应一个业务项目、一个团队的工作目录,或者一条产品线。Project内部统一管理数据集、模型、配置项、秘钥和运行环境信息。注意,Project不是简单的文件夹,它还承担了“环境隔离”的作用。
Function代表一个可执行的代码单元,可以是一个Python函数、一个容器镜像、或者一个远程Spark作业。它包含了代码、运行环境依赖、硬件资源需求(CPU/GPU)和执行模式。你可以把一个Function想象成一个“带打包和部署能力的函数”,写完了可以本地立即调,也可以发给集群执行。
Task是执行Function时传入的具体任务定义,包括输入参数、数据路径、调度标签等。一个Function可以被不同Task反复调用,就像同一个函数用不同的参数跑多次。
Run就是Task的实际执行实例。每一次运行都会产生一个唯一的Run记录,里面包含日志、指标、产出物(Artifacts)的引用、资源占用情况。这相当于是MLRun的“黑匣子”,出了问题随时可以回溯。
这四者的关系可以类比为:Project是公司,Function是岗位,Task是任务单,Run是任务完成后的归档记录。公司里有很多岗位,每个岗位接不同的任务单,每张任务单执行完都要写一份归档。
2.2 MLOps中被大多数人忽略的“数据版本”问题
很多刚开始用MLRun的人,把它当成了单纯的“任务调度工具”,然后觉得不如Airflow好用。这其实大错特错。MLRun真正区别于普通编排工具的地方,是它对Artifact(产物)和Dataset的版本化处理。
在我接触过的ML项目中,最痛苦的事情之一就是“复现”。上个月跑出一个AUC 0.87的模型,这个月想复现,发现数据被改了、特征工程脚本被覆盖了、超参数记不清了。在普通工作流里,这基本靠人品。MLRun对每一次Run自动记录输入数据的版本信息、代码哈希、参数快照,以及产出的模型文件、图表、DataFrame快照。你不需要自己额外维护一套“实验记录”系统,它已经替你做完了。
因此,我建议所有想在团队里推行MLOps的人,不要着急搭建那些看起来很酷炫的大平台,先用MLRun把“一次运行”的记录做到位,这是整个体系的地基。
3. 环境安装与快速开始
3.1 本地安装的两种方式与踩坑记录
安装这一步,网上的教程很多,但多数都默认你已经具备了一套复杂的Kubernetes环境。实际上,个人学习和初期开发,本地单机模式完全够用。MLRun支持两种安装路径:
第一种是完整分布式模式,需要一套Kubernetes集群,然后通过Helm安装。这种模式适合生产环境,我后面会单独讲。第二种是本地单机模式,用Docker启动一个内置的MLRun服务端,然后用pip安装客户端即可。
我先说本地单机的安装,因为它最简单,也最不容易出问题:
# 启动本地服务端(Jupyter和MLRun一体镜像) docker run -p 8080:8080 -v $(pwd):/home/jovendir --name mlrun-local mlrun/mlrun:latest # 在Python虚拟环境中安装客户端 pip install mlrun这里有一个非常关键的坑要提醒:MLRun客户端和服务端版本必须保持一致,否则会出现“客户端版本高于服务端”导致的接口不兼容问题。我第一次装的时候,服务端用了镜像的最新标签,客户端用了PyPI上的一个旧版本,结果跑任何任务都报mlrun.common.errors.MLRunRuntimeError,排错排了半天。后来把两边版本统一到完全一致的版本号,立刻恢复正常。
3.2 快速完成第一次任务提交
安装完成后,推荐直接在你的本地Python交互环境里跑下面这段代码,验证整个链路是否通畅:
import mlrun # 获取默认项目(首次使用会自动创建) project = mlrun.get_or_create_project("beginner-proj", context="./") # 定义第一个函数:直接用Python装饰器 @mlrun.handler() def hello(name: str) -> str: return f"Hello, {name}, welcome to MLRun!" # 把函数加入到项目 project.set_function("hello_func.py", name="hello_func", kind="job", image="mlrun/mlrun") # 运行任务 run = project.run_function("hello_func", params={"name": "MLOps Learner"}) # 查看输出 print(run.output("result"))这里解释一下上面做了什么:set_function的时候,指定了kind="job",意思就是这个函数会以“Pod任务”的方式运行。即使是在本地单机模式下,MLRun也会通过容器技术把任务隔离起来运行。如果不指定镜像,则使用默认镜像。
运行成功的话,你会看到类似下面的输出:
> 2025-xx-xx 12:00:00 reading and parsing the function > 2025-xx-xx 12:00:01 running on local > 2025-xx-xx 12:00:02 got result: Hello, MLOps Learner, welcome to MLRun!3.3 关于镜像加速的个人经验
这里要额外插一句:在中国大陆的网络环境下,拉取镜像是个大痛点。MLRun默认会从公开仓库拉取Python镜像,如果需要自定义依赖,构建新镜像时经常卡住。我的经验是给Docker配置好国内镜像加速源,并且尽量使用已经包含了常用依赖的镜像作为基底,这样可以少踩很多坑。另外,如果你使用的是公司内部的集群,建议提前准备好私有镜像仓库,在MLRun的Function定义中通过image参数直接指向私有仓库地址。
4. 第一个完整Pipeline从数据处理到模型服务
4.1 完全基于Python定义的Pipeline思路
安装和环境验证通过之后,下面就要进入正题了:用MLRun搭建一条完整可用的ML流水线。我会结合一个实际的Demo场景,来演示如何将数据处理、模型训练和模型部署三个步骤串联起来。
场景选择来做一个最经典的任务:鸢尾花分类模型。场景不用很复杂,因为核心目的是演示MLRun的编排能力。我们先在Python文件的顶层设计整个流程:
@mlrun.handler() def load_and_preprocess(context: mlrun.MLClientCtx, source_path: str): import pandas as pd from sklearn.datasets import load_iris iris = load_iris() df = pd.DataFrame(iris.data, columns=iris.feature_names) df["label"] = iris.target df.to_csv(source_path, index=False) context.log_dataset("iris_data", df, format="csv")这个阶段做的事情很简单:生成数据、写入Csv,并且把DataFrame作为Artifact登记到MLRun中。注意context.log_dataset这一步,它会自动记录数据集的摘要信息、列名、大小和版本。这一步是我特别推崇MLRun的原因:数据集的“登记”是整个流水线复现性的基石,而很多团队往往忽略这一步。
4.2 训练阶段与超参数记录的细节处理
接下来定义训练阶段:
@mlrun.handler() def train_model(context: mlrun.MLClientCtx, dataset_key: str, n_estimators: int = 100, max_depth: int = 3): import mlrun from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score df = context.get_dataset(dataset_key).as_df() X = df.drop("label", axis=1) y = df["label"] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) model = RandomForestClassifier(n_estimators=n_estimators, max_depth=max_depth) model.fit(X_train, y_train) acc = accuracy_score(y_test, model.predict(X_test)) print(f"accuracy: {acc:.4f}") context.log_model("iris_model", body=model, model_format="joblib", metrics={"accuracy": acc})写到这里,有一个细节要重点标注:训练时通过函数入参定义的超参数(n_estimators,max_depth),会被MLRun自动记录到Run元数据中。这意味着你不需要再用任何额外的工具去记录超参数,只要从MLRun的UI里点击那次Run,就能看到当时的超参数配置。如果后续需要并行执行多个超参数组合的实验,也可以用mlrun.run_function的params参数循环提交多次,MLRun会自动帮你生成多个Run实例,并且每个实例都独立记录,大大方便了调参实验的管理。
4.3 将Pipeline串起来并调度执行
有了前两步的函数定义,接下来就是用project.run_function把它们串起来执行。强烈建议用mlrun.run或者Project的run_function方法按顺序调用,而不是手动在外部一个个跑:
base_funcs = project.set_function("pipeline.py") # 执行数据准备 data_run = project.run_function("load_and_preprocess", params={"source_path": "./data/iris.csv"}) # 执行训练 train_run = project.run_function( "train_model", params={"dataset_key": "iris_data", "n_estimators": 200, "max_depth": 4}, inputs={"dataset": train_run.outputs["iris_data"]} )这里有一个很实用的套路:通过上一次Run的outputs去连接下一次Run的输入。当流水线变得很长时,你不需要手动记忆哪个数据产物在哪个路径下,只需要引用对应Run的outputs即可。MLRun会在后台自动做好物理解析。
如果你需要周期调度或者事件驱动,MLRun也提供了调度功能,支持Cron定时触发和事件触发模式。比如,每周一清晨跑一次数据归因分析,就可以这样配置:
project.schedule("weeky_retrain", "0 2 * * 1", "train_model", params={...})4.4 模型部署:把模型变成可调用的服务
训练阶段完成之后,真正的业务价值在于把模型上线提供服务。MLRun的一个亮点是支持直接从已记录的模型Artifact部署为一个在线服务,使用内置的机器学习模型服务器。
# 使用轻量级模型服务器部署 serving_func = mlrun.code_to_function( name="iris-serving", kind="serving", image="mlrun/mlrun", ) # 添加模型路由 serving_func.set_model("iris_model", model_file=train_run.outputs["iris_model"]) # 部署到本地或集群 serving_func.deploy()部署完成后,系统会返回一个可供调用的URL。如果你是在本地部署,那这个URL通常是http://localhost:8050/之类。调用方式也很简单,发一个POST请求,传入鸢尾花的四个特征数值即可:
curl -X POST http://localhost:8050/iris_model \ -H "Content-Type: application/json" \ -d '{"inputs": [[5.1, 3.5, 1.4, 0.2]]}'返回结果会包含预测的类别。此时你的第一个“从数据到服务”的Demo流水线就已经完全跑通了。
5. 进阶实践:如何处理更真实复杂的工作流
5.1 并行实验与超参数搜索的正确姿势
Demo完成后,真实项目里马上会遇到下一个问题:训练时想同时跑多组参数,怎么办?常规做法是自己在外部套一个循环,但MLRun提供了更优雅的并行机制。
最直接的方式是创建多个训练Function同时执行。比如,我想比较50棵树和200棵树的效果:
run_a = project.run_function("train_model", params={"n_estimators": 50, "max_depth": 3}) run_b = project.run_function("train_model", params={"n_estimators": 200, "max_depth": 5}) # 等待两个任务都完成 for run in [run_a, run_b]: run.wait_for_completion()底层来看,如果是Kubernetes模式,这两个训练任务会被分配到不同的Pod上真正并行执行。对于需要横向扩展的任务,MLRun也可以和Ray或者Dask集成,实现更大的并行度。
5.2 特征存储:让特征工程不重复“踩坑”
在真实业务中,特征工程往往是重复劳动最严重的环节。每个新模型都重新写一遍特征处理代码,不仅浪费精力,而且特征口径经常对不上。MLRun也提供了一个 Feature Store(特征存储)的解决方案,可以把常用的特征集注册到统一存储中,后续不同模型直接读取同一个特征集,保证了口径一致。
这一块如果展开讲,篇幅又会增加很多。但至少你应该知道,当你使用log_dataset或者注册特征集时,MLRun会自动处理特征的元数据登记、数据新鲜度和时间旅行查询等问题。
5.3 与现有监控体系对接的思考
对于一个真实上线运行的模型,关注的不只是调用量,还有一个更重要的指标叫数据漂移。MLRun内置了模型漂移检测能力,可以在服务运行时自动计算输入数据的分布变化,并发出告警。
这里要提醒一下:漂移检测是必要的,但不要一上来就做得过于复杂。初期的监控应该聚焦在两个核心问题:调用是否正常和输入分布是否发生明显变化。太过花哨的监控维度往往会增加维护成本。根据我自己的实践经验,先做好这两个,再逐步扩展到更细粒度的画像与A/B测试支持。
6. 实战踩坑记录与效率提升经验
6.1 最容易踩的三个坑
第一,任意修改函数内部代码后,必须重新执行set_function。很多新手在函数封装好后,直接在编辑器里改了函数内部的某一行代码,然后直接调用run_function,发现运行的还是旧代码。原因是MLRun默认会对代码做快照,set_function是在“登记当前版本”。如果改了代码但没重新执行set_function,它提交的还是之前的快照。别问我是怎么知道的。
第二,本地执行与集群执行的差异。本地模式下有些mlrun操作执行得很快,但一旦切到集群模式,每次运行都需要经历拉镜像、创建Pod、调度等步骤,所以总耗时增加是正常的,不要误以为系统卡死了。看日志时重点关注“pod scheduled”和“pod ready”这类阶段状态。
第三,Storing数据的路径问题。新手经常在log_dataset后,默认数据应该已经写到了 MongoDB 或某个数据中心存储,其实MLRun默认只是写入了你本地或S3兼容的持久化存储。后续从UI查看Artifact时,如果看到“loading data”一直转圈,多半是数据实际访问路径对UI不可见,需要检查Artifact的路径配置是否在可访问范围内。
6.2 提高日常开发效率的配置清单
这里有一份我实际用下来觉得很顺手的配置清单,供参考:
| 配置项 | 推荐设置 | 原因说明 |
|---|---|---|
| 默认镜像 | 预装常用依赖的自定义镜像 | 避免每次跑任务都临时构建 |
| 项目上下文 | 放在独立目录并纳入Git管理 | 代码与配置统一版本化 |
| 数据路径 | 使用对象存储或共享存储 | 本地磁盘在集群模式下不可见 |
| 日志级别 | 设置为DEBUG排查问题 | 平时INFO,排查时再切换 |
| 服务端口 | 统一规划端口段 | 避免服务一多就端口冲突 |
此外,Jupyter Notebook和MLRun配合有额外的加成效果。MLRun支持将Notebook中的代码直接转化为Function,这意味着你可以在Notebook里做探索性分析,满意之后一键注册为Pipeline的一部分。不过建议定期清理不再使用的Notebook核,因为长期挂着的核会占用内存,降低整体环境稳定性。
6.3 排查问题的思路框架
无论平台有多完善,实际运行中遇到问题是难免的。我自己遇到过最典型的场景:训练任务提交后,一直处于 “Running” 状态但没有任何日志输出。这时候怎么排查?经过多次实战,我总结出一个优先级排序:
先看Function日志,再查Pod调度状态,最后检查容器镜像拉取是否失败。日志没有任何输出,大概率是代码在早期阶段就崩了,此时需要把日志级别调成DEBUG重新跑一次。状态为容器创建中,大概率是镜像拉取或者资源配额不足。逐一排查比慌乱地上网搜索有效率得多。
7. 与主流MLOps工具横向对比的思考
聊到这里,必须回答一个很多人都会纠结的问题:MLRun和其他主流工具横向比较,到底好在哪里?差在哪里?
先说优势。相比Kubeflow,MLRun对Python使用者友好太多,简洁抽象的封装让编写Pipeline更像是写业务代码而不是写YAML。相比Airflow,MLRun提供了ML场景的原生特性,比如模型注册、模型服务、数据集版本管理等,这些在Airflow里全靠自己造轮子。相比MLflow,MLRun的编排调度能力强得多,MLflow在很多场景更像一个实验记录和模型仓库。
再说不足。MLRun的社区活跃度和资料丰富程度不如MLflow,遇到冷门问题时搜到的有效答案较少。其次,它的核心优势一定要配合Kubernetes才能真正全面发挥,如果你只使用本地单机模式,很多高级功能会受限。因此,在有K8s条件的企业环境中,MLRun的潜力才能真正释放出来。
另外还有一点不能忽略:MLRun对开发者自身的容器化知识有一定要求,如果完全不懂容器和K8s基础,很多时候会“知其然不知其所以然”,遇到问题时容易卡住。因此,学习MLRun的同时,建议并行补一下容器基础。
8. 个人使用体验与未来拓展方向
最后这部分不写总结,纯粹说说我用MLRun这段时间的真实感受。
如果你和我一样,不想被Kubernetes细节劝退,又想有一个能自动记录实验、版本化数据、串联训练和服务的工具,MLRun的确是个很好的选择。但真正让我对MLRun另眼相看的,是它在处理“从Notebook到生产”这个环节时的平滑度。以前这个环节需要手写大量工程代码,现在一个deploy()就能完成,省下来的时间可以更好地花在算法调优和业务理解上。
关于未来拓展方向,我的初步想法是结合已部署的模型,开发一套既包含预测结果,又包含可解释性分析的服务。因为MLRun支持任意语言的函数封装,所以完全可以在现有Serving服务后面再挂一个Explainer服务,灵活度相当高。
最后分享一个因人而异的实用习惯:不要在一开始就追求把所有功能都用上。MLRun的功能版图很大,但你的业务未必需要全部都搬过来。先把核心的Project、Function、Task、Run这套循环玩熟,再根据实际需求逐步尝试Feature Store、Model Monitoring等进阶能力,整体推进会更扎实,也更容易坚持下去。