- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 三个工具为何必须整合为一套技术栈
- 各工具的职责边界与整合点
- 在 Kubeflow Pipeline 中追踪 MLflow 实验
- 从 MLflow 到 KServe:模型部署自动化
- Canary 部署与自动回滚
- 三个工具的版本兼容性矩阵
- 按故障场景应对
- 构建统一运维仪表盘
- 参考资料

三个工具为何必须整合为一套技术栈
当 ML 项目从 notebook 迈向生产环境的那一刻,团队会面对三个相互独立的问题。
- 流水线编排(Pipeline Orchestration):需要把数据预处理、训练、评估用可复现的工作流跑起来 —— Kubeflow Pipelines。
- 实验/模型追踪:需要把超参数、指标(metric)、artifact 集中管理,并晋升进模型注册表(Model Registry) —— MLflow。
- 模型服务(Model Serving):需要把训练好的模型部署成支持 auto-scaling、canary、A/B 测试的 inference endpoint —— KServe。
如果把这三个工具分别独立运维,就会反复出现"训练 artifact 路径和服务清单(manifest)对不上""流水线里注册的模型版本,和服务端引用的版本不一致"这类接合部故障。本文以 Kubeflow Pipelines v2(2.3+)、MLflow 2.17+、KServe 0.14+ 为基准,设计这三个工具的整合点。
各工具的职责边界与整合点
| 阶段 | 负责工具 | 输入 | 输出 | 整合接口 |
|---|---|---|---|---|
| 数据校验 + 预处理 | Kubeflow Pipeline | Raw data (GCS/S3) | 预处理后的数据集 | 通过 Pipeline parameter 传递数据路径 |
| 训练 + 超参数搜索 | Kubeflow Pipeline + MLflow | 预处理后的数据 | MLflow Run(指标、artifact) | 把 MLflow tracking URI 作为 Pipeline 环境变量注入 |
| 模型注册 + 晋升 | MLflow Model Registry | 训练完成的 artifact | Registered Model Version | 把 MLflow 的 model URI 映射到 KServe storageUri |
| 模型服务 + 流量管理 | KServe | Model URI (GCS/S3) | Inference endpoint | KServe InferenceService 引用 MLflow model URI |
| 监控 + 回滚 | KServe + Prometheus | Inference metrics | 告警 / 自动回滚 | 基于 Prometheus 指标的 canary 判定 |
在 Kubeflow Pipeline 中追踪 MLflow 实验
让 Kubeflow Pipeline 的每个 component 都把指标和 artifact 记录进 MLflow,是整合的第一步。
定义 Pipeline Component
"""
以 Kubeflow Pipelines v2 component 的形式执行训练,
同时把实验结果记录进 MLflow 的示例。
"""
from kfp import dsl
from kfp.dsl import Input, Output, Dataset, Model, Metrics
@dsl.component(
base_image="python:3.11-slim",
packages_to_install=[
"mlflow==2.17.2",
"scikit-learn==1.5.2",
"pandas==2.2.3",
"boto3==1.35.0",
],
)
def train_model(
training_data: Input[Dataset],
model_output: Output[Model],
metrics_output: Output[Metrics],
mlflow_tracking_uri: str,
mlflow_experiment_name: str,
n_estimators: int = 200,
max_depth: int = 10,
learning_rate: float = 0.1,
):
import mlflow
import pandas as pd
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import cross_val_score
import json
import os
# 连接 MLflow
mlflow.set_tracking_uri(mlflow_tracking_uri)
mlflow.set_experiment(mlflow_experiment_name)
# 加载数据
df = pd.read_parquet(training_data.path)
X = df.drop(columns=["label"])
y = df["label"]
with mlflow.start_run() as run:
# 记录超参数
mlflow.log_params({
"n_estimators": n_estimators,
"max_depth": max_depth,
"learning_rate": learning_rate,
"feature_count": X.shape[1],
"training_rows": X.shape[0],
})
# 训练
clf = GradientBoostingClassifier(
n_estimators=n_estimators,
max_depth=max_depth,
learning_rate=learning_rate,
random_state=42,
)
cv_scores = cross_val_score(clf, X, y, cv=5, scoring="f1_weighted")
clf.fit(X, y)
# 记录指标
mlflow.log_metrics({
"cv_f1_mean": float(cv_scores.mean()),
"cv_f1_std": float(cv_scores.std()),
})
# 保存模型(MLflow Model Registry 格式)
mlflow.sklearn.log_model(
clf,
artifact_path="model",
registered_model_name="recommendation-classifier",
)
# 同时记录到 Kubeflow Metrics(用于 UI 展示)
metrics_output.log_metric("cv_f1_mean", float(cv_scores.mean()))
metrics_output.log_metric("cv_f1_std", float(cv_scores.std()))
metrics_output.log_metric("mlflow_run_id", run.info.run_id)
# 把模型路径作为 output 传递(供下一个 component 使用)
model_output.uri = f"runs:/{run.info.run_id}/model"
model_output.metadata["mlflow_run_id"] = run.info.run_id
Pipeline 整体构成
from kfp import dsl, compiler
@dsl.pipeline(
name="recommendation-training-pipeline",
description="数据校验 -> 训练 -> 模型注册 -> 服务部署",
)
def training_pipeline(
data_path: str = "gs://ml-data/recommendation/2026-03-04/",
mlflow_tracking_uri: str = "http://mlflow.ml-platform.svc:5000",
mlflow_experiment: str = "recommendation-v3",
serving_namespace: str = "model-serving",
):
# Step 1: 数据校验
validate_task = validate_data(data_path=data_path)
# Step 2: 预处理
preprocess_task = preprocess_data(
raw_data=validate_task.outputs["validated_data"],
)
# Step 3: 训练 + MLflow 追踪
train_task = train_model(
training_data=preprocess_task.outputs["processed_data"],
mlflow_tracking_uri=mlflow_tracking_uri,
mlflow_experiment_name=mlflow_experiment,
n_estimators=300,
max_depth=8,
learning_rate=0.05,
)
train_task.set_cpu_limit("4").set_memory_limit("16Gi")
train_task.set_accelerator_type("nvidia.com/gpu").set_accelerator_limit(1)
# Step 4: 模型质量门禁
gate_task = quality_gate(
metrics=train_task.outputs["metrics_output"],
f1_threshold=0.80,
)
# Step 5: 部署到 KServe(质量门禁通过时)
with dsl.Condition(gate_task.outputs["passed"] == "true"):
deploy_task = deploy_to_kserve(
model=train_task.outputs["model_output"],
serving_namespace=serving_namespace,
)
compiler.Compiler().compile(
pipeline_func=training_pipeline,
package_path="recommendation_pipeline.yaml",
)
从 MLflow 到 KServe:模型部署自动化
把 MLflow Model Registry 中的模型晋升到 "Production" 阶段后,自动创建 KServe InferenceService 的 component。
@dsl.component(
base_image="python:3.11-slim",
packages_to_install=["kubernetes==31.0.0", "mlflow==2.17.2"],
)
def deploy_to_kserve(
model: Input[Model],
serving_namespace: str,
canary_traffic_percent: int = 20,
):
"""
把注册在 MLflow 中的模型部署为 KServe InferenceService。
以 canary 方式为新版本分配一部分流量。
"""
from kubernetes import client, config
import json
import mlflow
config.load_incluster_config()
api = client.CustomObjectsApi()
mlflow_run_id = model.metadata.get("mlflow_run_id")
model_uri = model.uri # runs:/<run_id>/model
# 从 MLflow 中提取 GCS 路径
mlflow_client = mlflow.tracking.MlflowClient()
run = mlflow_client.get_run(mlflow_run_id)
artifact_uri = run.info.artifact_uri # gs://mlflow-artifacts/<exp>/<run>/artifacts
storage_uri = f"{artifact_uri}/model"
inference_service = {
"apiVersion": "serving.kserve.io/v1beta1",
"kind": "InferenceService",
"metadata": {
"name": "recommendation-classifier",
"namespace": serving_namespace,
"labels": {
"mlflow-run-id": mlflow_run_id,
"pipeline": "recommendation-training",
},
"annotations": {
"serving.kserve.io/deploymentMode": "Serverless",
"serving.kserve.io/autoscalerClass": "hpa",
"serving.kserve.io/metrics": "cpu",
"serving.kserve.io/targetUtilizationPercentage": "70",
},
},
"spec": {
"predictor": {
"canaryTrafficPercent": canary_traffic_percent,
"minReplicas": 2,
"maxReplicas": 10,
"model": {
"modelFormat": {"name": "mlflow"},
"storageUri": storage_uri,
"resources": {
"requests": {"cpu": "1", "memory": "2Gi"},
"limits": {"cpu": "2", "memory": "4Gi"},
},
},
},
},
}
try:
api.patch_namespaced_custom_object(
group="serving.kserve.io",
version="v1beta1",
namespace=serving_namespace,
plural="inferenceservices",
name="recommendation-classifier",
body=inference_service,
)
print(f"Updated InferenceService with canary {canary_traffic_percent}%")
except client.exceptions.ApiException as e:
if e.status == 404:
api.create_namespaced_custom_object(
group="serving.kserve.io",
version="v1beta1",
namespace=serving_namespace,
plural="inferenceservices",
body=inference_service,
)
print("Created new InferenceService")
else:
raise
Canary 部署与自动回滚
结合 KServe 的 canary 功能和 Prometheus 指标,实现自动回滚判定。
Canary 晋升/回滚判定脚本
"""
Canary 部署后在一段时间内持续观察指标,
自动判定晋升或回滚的脚本。
以 CronJob 或 Argo Workflow step 的形式运行。
"""
import requests
import time
from dataclasses import dataclass
from typing import Optional
@dataclass
class CanaryConfig:
service_name: str
namespace: str
prometheus_url: str
observation_minutes: int = 15
check_interval_seconds: int = 60
error_rate_threshold: float = 0.02 # 2%
p99_latency_threshold_ms: float = 500 # 500ms
min_request_count: int = 100 # 最小观察请求数
def query_prometheus(config: CanaryConfig, query: str) -> Optional[float]:
"""从 Prometheus 查询指标值。"""
resp = requests.get(
f"{config.prometheus_url}/api/v1/query",
params={"query": query},
timeout=10,
)
results = resp.json().get("data", {}).get("result", [])
if results:
return float(results[0]["value"][1])
return None
def evaluate_canary(config: CanaryConfig) -> dict:
"""评估 Canary 版本的 error rate 和 latency。"""
error_rate_query = (
f'sum(rate(kserve_request_total{{service_name="{config.service_name}",'
f'namespace="{config.namespace}",response_code=~"5.."}}[5m])) / '
f'sum(rate(kserve_request_total{{service_name="{config.service_name}",'
f'namespace="{config.namespace}"}}[5m]))'
)
p99_query = (
f'histogram_quantile(0.99, sum(rate(kserve_request_duration_seconds_bucket'
f'{{service_name="{config.service_name}",namespace="{config.namespace}"}}'
f'[5m])) by (le)) * 1000'
)
request_count_query = (
f'sum(increase(kserve_request_total{{service_name="{config.service_name}",'
f'namespace="{config.namespace}"}}[{config.observation_minutes}m]))'
)
error_rate = query_prometheus(config, error_rate_query) or 0.0
p99_latency = query_prometheus(config, p99_query) or 0.0
request_count = query_prometheus(config, request_count_query) or 0
return {
"error_rate": error_rate,
"p99_latency_ms": p99_latency,
"request_count": request_count,
"error_rate_ok": error_rate < config.error_rate_threshold,
"latency_ok": p99_latency < config.p99_latency_threshold_ms,
"sufficient_traffic": request_count >= config.min_request_count,
}
def run_canary_judgment(config: CanaryConfig) -> str:
"""
在 observation_minutes 时间内观察指标,并决定晋升或回滚。
Returns: "promote" or "rollback"
"""
checks_passed = 0
total_checks = config.observation_minutes * 60 // config.check_interval_seconds
for i in range(total_checks):
result = evaluate_canary(config)
print(f"Check {i+1}/{total_checks}: {result}")
if not result["sufficient_traffic"]:
print("Insufficient traffic, waiting...")
time.sleep(config.check_interval_seconds)
continue
if result["error_rate_ok"] and result["latency_ok"]:
checks_passed += 1
else:
# 立即回滚条件:error rate 超过阈值的 3 倍
if result["error_rate"] > config.error_rate_threshold * 3:
print(f"Immediate rollback: error_rate={result['error_rate']}")
return "rollback"
time.sleep(config.check_interval_seconds)
pass_ratio = checks_passed / max(total_checks, 1)
decision = "promote" if pass_ratio >= 0.9 else "rollback"
print(f"Decision: {decision} (pass_ratio={pass_ratio:.2f})")
return decision
三个工具的版本兼容性矩阵
在实际运维中,版本组合问题频繁出现。这里整理了已验证过的组合。
| Kubeflow Pipelines | MLflow | KServe | Kubernetes | Python | 备注 |
|---|---|---|---|---|---|
| v2.3.0 | 2.17.x | 0.14.x | 1.28-1.30 | 3.11 | 截至 2026 年 3 月的稳定组合 |
| v2.2.0 | 2.15.x | 0.13.x | 1.27-1.29 | 3.10-3.11 | 此前的稳定版本 |
| v2.1.0 | 2.12.x | 0.12.x | 1.26-1.28 | 3.10 | LTS 维护中 |
注意事项:MLflow 2.16 起,model signature 校验默认启用。KServe 的 MLflow 服务器加载没有 signature 的旧模型时,会抛出 MlflowException: Model signature is missing 错误。需要给现有模型补上 signature,或者设置 MLFLOW_MODEL_SIGNATURE_ENFORCEMENT=disabled 环境变量。
按故障场景应对
场景 1:Kubeflow Pipeline 完成后,MLflow 里没有记录 Run
症状:Pipeline 显示成功(green),但 MLflow UI 里没有对应的 Run
错误日志(Pipeline pod):
requests.exceptions.ConnectionError: HTTPConnectionPool(host='mlflow.ml-platform.svc', port=5000):
Max retries exceeded with url: /api/2.0/mlflow/runs/create
原因:Kubeflow Pipeline pod 的 ServiceAccount 没有被 NetworkPolicy 允许
访问 MLflow service
解决:
1. 在 NetworkPolicy 中放行 pipeline runner namespace -> mlflow namespace
2. 确认 MLflow tracking URI 是否为 cluster-internal DNS
3. 给 Pipeline component 加上 retry 逻辑
场景 2:KServe 无法加载 MLflow 模型
症状:InferenceService 卡在 "FailedCreate" 状态
错误日志(kserve-container):
mlflow.exceptions.MlflowException: Could not find an "MLmodel" configuration file
at "gs://mlflow-artifacts/3/abc123def/artifacts/model"
原因:MLflow log_model 的 artifact_path 与 KServe storageUri 的路径不一致
(artifact_path="model",但 storageUri 里缺了 "/model")
解决:
storageUri 需要按 artifact_uri + "/model" 的形式拼接。
错误示例:gs://mlflow-artifacts/3/abc123def/artifacts
正确示例:gs://mlflow-artifacts/3/abc123def/artifacts/model
场景 3:Canary 部署过程中无法回滚到旧版本
症状:即便把 canaryTrafficPercent 设为 0,也回不到旧版本
错误日志(kserve-controller):
RevisionFailed: Revision "recommendation-classifier-predictor-prev"
has no ready pods
原因:KServe 把旧 Revision 的 pod scale-to-zero 之后,
cold start 耗时超过了 readiness probe timeout
解决:
1. 给服务设置 minReplicas >= 1(重要服务应禁用 scale-to-zero)
2. 把 readinessProbe timeout 按模型加载时间调大
3. 回滚时不要靠调整 canary 比例,而要把 storageUri 换回旧版本
# 回滚时应用的 KServe 清单
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: recommendation-classifier
namespace: model-serving
spec:
predictor:
canaryTrafficPercent: 0 # 清除 canary 流量
minReplicas: 2 # 防止 scale-to-zero
model:
modelFormat:
name: mlflow
# 替换为此前稳定版本的 model URI
storageUri: gs://mlflow-artifacts/3/previous-stable-run/artifacts/model
resources:
requests:
cpu: '1'
memory: '2Gi'
limits:
cpu: '2'
memory: '4Gi'
containerSpec:
readinessProbe:
initialDelaySeconds: 30
timeoutSeconds: 10
periodSeconds: 5
构建统一运维仪表盘
把三个工具需要采集的核心指标,整合进同一个 Grafana 仪表盘。
| 领域 | 指标 | 来源 | 告警阈值 |
|---|---|---|---|
| Pipeline | 执行成功率、平均耗时 | Kubeflow Pipeline API | 成功率 < 95% |
| 训练 | cv_f1_mean 趋势、训练时长 | MLflow Tracking | f1 < 上一版本 - 0.02 |
| Registry | Pending 模型数、晋升等待时长 | MLflow Model Registry | 等待 > 24h |
| 服务 | error rate、p99 latency、RPS | KServe + Prometheus | error > 1%, p99 > 500ms |
| Canary | canary 与 stable 的指标对比 | Prometheus | canary error > stable * 2 |
| 基础设施 | GPU 使用率、Pod OOM 次数 | Kubernetes metrics | OOM > 0/day |
测验
Q1. 为什么要把 MLflow tracking URI 作为环境变量注入到 Kubeflow Pipeline 中?
||因为 Pipeline component 是以 Kubernetes Pod 的形式运行的,需要通过 cluster-internal DNS 访问 MLflow 服务器,而且各环境(dev/staging/prod)的 URI 各不相同。||
Q2. 在把 MLflow 的 model artifact_path 与 KServe storageUri 对接时,最常见的错误是什么?
||在 MLflow log_model 中设置了 artifact_path="model" 时,却在 storageUri 里漏掉了 "/model" 后缀,导致出现 "MLmodel file not found" 错误。||
Q3. 在 Canary 部署的自动回滚判定中,为什么不能只看 error rate?
||即便 error rate 很低,如果 p99 latency 骤增,用户体验也会变差;而且如果不设最小请求数(traffic volume)门槛就下判定,得出的结论在统计上也可能毫无意义。||
Q4. 在 KServe 中,什么情况下需要对重要服务禁用 scale-to-zero?
||模型加载时间过长、cold start 超过 readiness probe timeout 的情况,或者回滚时需要旧版本 pod 立即可用的情况,都需要把 minReplicas 设为 >= 1。||
Q5. MLflow 2.16+ 中的 model signature enforcement,对既有模型会有什么影响?
||KServe 加载没有 signature 的旧模型时会抛出 MlflowException,导致服务失败。需要给既有模型补上 signature,或者禁用 enforcement。||
Q6. 如果 Pipeline 的 quality gate 只用绝对值(0.80)设置 F1 threshold,会产生什么问题?
||无法感知相对上一版本的性能下滑。比如即便从原本的 0.92 骤降到 0.81,只要通过了绝对 threshold,模型照样会被部署。需要同时加上相对比较(相对上一版本的 delta)条件。||
Q7. 整合这三个工具时,最先需要标准化的接口是什么?
||是模型 artifact 路径的规则。只有 MLflow 存储的 artifact URI 格式与 KServe 引用的 storageUri 格式保持一致,才能实现从流水线到服务的全自动化。||