Skip to content
Published on

AI 平台技术栈设计:Kubeflow、MLflow、KServe 集成运维

分享
Authors
AI 平台技术栈设计:Kubeflow、MLflow、KServe 集成运维

三个工具为何必须整合为一套技术栈

当 ML 项目从 notebook 迈向生产环境的那一刻,团队会面对三个相互独立的问题。

  1. 流水线编排(Pipeline Orchestration):需要把数据预处理、训练、评估用可复现的工作流跑起来 —— Kubeflow Pipelines。
  2. 实验/模型追踪:需要把超参数、指标(metric)、artifact 集中管理,并晋升进模型注册表(Model Registry) —— MLflow。
  3. 模型服务(Model Serving):需要把训练好的模型部署成支持 auto-scaling、canary、A/B 测试的 inference endpoint —— KServe。

如果把这三个工具分别独立运维,就会反复出现"训练 artifact 路径和服务清单(manifest)对不上""流水线里注册的模型版本,和服务端引用的版本不一致"这类接合部故障。本文以 Kubeflow Pipelines v2(2.3+)、MLflow 2.17+、KServe 0.14+ 为基准,设计这三个工具的整合点。

各工具的职责边界与整合点

阶段负责工具输入输出整合接口
数据校验 + 预处理Kubeflow PipelineRaw data (GCS/S3)预处理后的数据集通过 Pipeline parameter 传递数据路径
训练 + 超参数搜索Kubeflow Pipeline + MLflow预处理后的数据MLflow Run(指标、artifact)把 MLflow tracking URI 作为 Pipeline 环境变量注入
模型注册 + 晋升MLflow Model Registry训练完成的 artifactRegistered Model Version把 MLflow 的 model URI 映射到 KServe storageUri
模型服务 + 流量管理KServeModel URI (GCS/S3)Inference endpointKServe InferenceService 引用 MLflow model URI
监控 + 回滚KServe + PrometheusInference 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 PipelinesMLflowKServeKubernetesPython备注
v2.3.02.17.x0.14.x1.28-1.303.11截至 2026 年 3 月的稳定组合
v2.2.02.15.x0.13.x1.27-1.293.10-3.11此前的稳定版本
v2.1.02.12.x0.12.x1.26-1.283.10LTS 维护中

注意事项: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 Trackingf1 < 上一版本 - 0.02
RegistryPending 模型数、晋升等待时长MLflow Model Registry等待 > 24h
服务error rate、p99 latency、RPSKServe + Prometheuserror > 1%, p99 > 500ms
Canarycanary 与 stable 的指标对比Prometheuscanary error > stable * 2
基础设施GPU 使用率、Pod OOM 次数Kubernetes metricsOOM > 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 格式保持一致,才能实现从流水线到服务的全自动化。||

参考资料