ML工程化落地:从Notebook到Kubernetes的模型服务化实战

发布时间:2026/7/3 6:10:06
ML工程化落地:从Notebook到Kubernetes的模型服务化实战 1. 项目概述这不是一次模型训练而是一场工程交付“From Notebook to Production: Running ML in the Real World (Part 4)”——这个标题里藏着一个被太多人轻描淡写、却让无数团队在临门一脚时彻底卡死的真相Notebook 是思考的草稿纸Production 是交付的合同书。它不讲怎么调参、不教怎么画 loss 曲线而是直面那个没人愿意多说但每天都在发生的现实你本地跑通的 Jupyter 里那串闪着绿光的model.predict()一旦扔进真实业务系统大概率会变成一个沉默的 500 错误、一个持续超时的 API、或者更糟——一个每天悄悄把预测结果偏移 3% 却没人发现的“幽灵模型”。我做过 7 个从零落地的 ML 工程项目其中 4 个在 Part 3模型验证之后就停滞了超过 6 周原因全出在 Part 4不是模型不行是它根本没准备好“上班”。这个系列的第四部分核心关键词就是ML 工程化落地、模型服务化、可观测性、CI/CD for ML、生产环境稳定性。它面向的不是刚学完 Scikit-learn 的新手也不是只管发论文的研究员而是那个被老板拍着桌子问“上个月说好的推荐功能用户什么时候能用上”的 ML 工程师、数据平台负责人或是技术决策者。它解决的问题非常具体如何让一个在.ipynb里活得好好的模型在 Kubernetes 集群里扛住每秒 2000 次请求如何确保新模型上线后线上指标没崩老用户没收到乱七八糟的推荐当监控报警说“p99 延迟突增”你该先看模型推理耗时还是看特征提取 pipeline 的 Kafka 消费延迟这些都不是理论题是凌晨两点 Slack 里弹出来的告警截图。很多人以为 Part 4 就是“把 pickle 文件扔到 Flask 里跑个 API”实测下来这种做法在小流量验证阶段可能撑一周但只要接入真实订单、支付、用户行为等关键链路不出三天就会暴露出三类致命短板第一无版本追溯——你根本不知道当前线上跑的是哪次 commit、哪个超参组合、基于哪天的数据训练的第二无依赖隔离——模型依赖的pandas1.3.5和业务系统要求的pandas2.0.3冲突整个服务起不来第三无健康反馈——API 返回 200但内部模型输出全是 NaN下游业务照常处理错误像雪球一样滚到财务对账环节才被发现。Part 4 的本质就是用软件工程的确定性去对抗机器学习本身的不确定性。它不追求模型绝对最优而追求“可解释、可回滚、可度量、可协作”的交付确定性。下面我们就一层层拆开这个过程不讲虚的只说我在电商、金融、IoT 三个领域踩过坑、改过三次架构后最终沉淀下来的实操路径。2. 整体设计思路为什么必须放弃“Flask Pickle”这一条捷径2.1 传统简易方案的三大幻觉与真实代价在开始讲正确路径前必须先戳破一个广泛存在的幻觉很多团队尤其是资源紧张的初创公司或业务部门自建的数据小组会本能地选择一条看似最短的路径——用 Flask/FastAPI 写个轻量接口joblib.load()加载本地.pkl模型文件再套个 Nginx 做反向代理然后告诉产品“接口 ready 了”。这条路径在 POC 阶段确实快但它的“快”是建立在透支未来工程债基础上的。我把它称为“三幻觉陷阱”幻觉一“模型文件小部署就是复制粘贴”真实情况是一个中等复杂度的 XGBoost 模型.pkl文件可能只有 5MB但它的运行时依赖树却深不见底。xgboost1.7.6依赖numpy1.21.0,2.0.0而你的特征工程代码又用了feature-engine2.5.0它强制要求pandas1.5.0。当你把所有包pip install -r requirements.txt装进 Docker 镜像时pip会自动降级numpy到1.23.5来满足兼容性结果模型加载时报AttributeError: module numpy has no attribute bool——因为numpy 1.24才引入了新的布尔类型别名。这个错误不会在本地 notebook 里出现因为你的开发环境是手动维护的“特供版”但在线上标准化镜像里它必然爆发。代价每次模型更新都要人工核对 17 个依赖包的精确版本号且无法自动化验证。幻觉二“API 返回 200 就代表服务正常”真实情况是HTTP 状态码只反映 Web 框架层是否崩溃完全不反映模型层是否健康。我们曾在一个风控模型上线后发现/predict接口平均响应时间稳定在 80msp99 也低于 150ms一切监控绿油油。但两周后业务方反馈“拒绝率异常升高”排查发现模型在处理某类加密 ID 特征时因上游数据源字段长度突变从 32 位 MD5 变成 64 位 SHA256导致特征向量化后维度错位模型内部计算溢出输出全为inf。而我们的 FastAPI 接口对inf值做了json.dumps()兼容处理返回了看似正常的{score: Infinity}下游 Java 服务解析 JSON 时直接抛NumberFormatException被业务逻辑 catch 后默认走“高风险”分支。代价模型逻辑错误被 HTTP 层掩盖问题暴露延迟长达 14 天期间产生 2300 误拒订单。幻觉三“用 Git 管理 notebook 就等于管理了模型”真实情况是Git 只能追踪.ipynb文件的文本 diff但无法告诉你第 42 次 commit 里训练的模型用的是train_data_v3.parquet还是train_data_v3_cleaned.parquet也无法关联那次训练所用的sklearn1.2.2是从 conda-forge 还是 PyPI 安装的更无法回答“这个模型在 A/B 测试中对比 baseline 提升了 1.2% 的点击率但 ROI 却下降了 0.8%是因为特征泄露还是样本偏差”——因为 notebook 里没有结构化记录实验元数据。代价无法复现线上问题无法做归因分析每一次故障排查都变成考古现场。这三条幻觉叠加的结果就是团队陷入“部署-救火-回滚-再部署”的死亡循环工程师 70% 的时间在处理环境不一致、依赖冲突、日志缺失等基础设施问题而不是优化模型本身。Part 4 的设计起点就是主动放弃这条看似平坦的捷径转而拥抱一套有明确契约、可审计、可自动化的 ML 工程范式。2.2 核心架构选型为什么是 MLflow KServe Prometheus/Grafana 组合经过 4 轮架构迭代我们在 2023 年底将生产环境统一收敛到以下技术栈并持续稳定运行至今组件选型关键理由实测效果模型注册与生命周期管理MLflow Model Registry开源、轻量、与训练代码无缝集成支持 StageStaging/Production、版本描述、模型签名input/output schema、A/B 测试标签原生支持 Python/PyTorch/TensorFlow/XGBoost 等主流格式模型上线审批流程从 3 天缩短至 2 小时回滚操作从手动修改配置文件变为一行 CLI 命令mlflow models transition-model-version-stage --name fraud-detector --version 12 --stage Production模型服务化引擎KServe (v0.12) on KubernetesCNCF 毕业项目专为 ML 设计内置 vLLM、Triton、SKLearn、XGBoost 等多种推理服务器支持自动扩缩容KPA、金丝雀发布、A/B 测试、模型解释SHAP与 Istio 服务网格深度集成单个模型服务实例在 4c8g 节点上稳定支撑 1800 QPS新模型金丝雀发布时可精确控制 5% 流量切过去10 分钟内无异常则自动提升至 100%可观测性体系Prometheus Grafana OpenTelemetryPrometheus 抓取 KServe 暴露的/metrics端点含model_latency_seconds,model_request_total,model_prediction_result_count等 32 个核心指标Grafana 构建“模型健康仪表盘”包含 p99 延迟热力图、错误率趋势、特征分布漂移告警OpenTelemetry 注入请求 trace串联从 API Gateway → KServe InferenceService → 特征存储 → 模型推理全链路平均故障定位时间MTTD从 47 分钟降至 6.3 分钟首次实现“预测结果异常”与“特征数据质量下降”的自动关联告警如当user_age字段空值率 5% 时自动触发模型输入校验失败告警这个组合不是为了堆砌新技术而是每个组件都精准解决一个核心痛点MLflow 解决“模型是什么、谁批准的、用什么数据训的”——它把模型从一个二进制文件升级为一个带有完整上下文的、可审计的工程资产。KServe 解决“模型怎么安全、稳定、可控地对外提供服务”——它把模型服务从一个裸露的 HTTP 接口变成一个具备企业级运维能力的微服务。Prometheus/Grafana 解决“模型现在好不好、哪里不好、为什么不好”——它把模型健康从“凭感觉”变成“看数字、定阈值、自动告警”的科学管理。提示不要试图用一个工具解决所有问题。我们曾试过用 SageMaker Hosting 直接托管虽然省去了 Kubernetes 运维但失去了对底层资源的精细控制如 GPU 显存分配策略且与内部 CI/CD 流水线集成成本极高也试过纯自研 Flask 服务加自定义监控结果在第 3 个模型上线后监控埋点代码就占了业务逻辑的 40%维护成本爆炸。工程选型的第一原则是让每个工具只做它最擅长的一件事并用清晰的接口API/Protocol连接它们。2.3 关键设计决策背后的“为什么”在落地这套架构时有几个关键决策点其背后都有血泪教训支撑决策一坚持容器化且镜像构建必须与模型训练环境完全一致我们要求所有模型服务镜像必须使用mlflow.pyfunc.log_model()生成的conda.yaml作为基础环境定义并通过Dockerfile显式声明FROM continuumio/miniconda3:4.12.0再COPY conda.yaml . RUN conda env update -n base -f conda.yaml。绝不允许pip install -r requirements.txt这种模糊方式。原因很简单Conda 环境能精确锁定 C 库如libgfortran,libopenblas的 ABI 版本而 pip 无法保证。我们曾因scipy在不同 Linux 发行版上链接的libgfortran版本不一致导致同一模型在 Ubuntu 镜像里正常在 CentOS 镜像里直接Segmentation Fault。实操心得在 CI 流水线中增加一步conda list --explicit environment.lock将完整环境快照存入制品库这是事后复现问题的唯一救命稻草。决策二模型输入/输出必须强 Schema 化且 Schema 由 MLflow 自动推导并持久化KServe 要求每个InferenceService必须定义predictor.modelClassName和predictor.tensorrt.enabled等字段但更重要的是我们强制所有模型在mlflow.pyfunc.log_model()时传入signaturemlflow.models.infer_signature(train_X, train_y)。MLflow 会自动生成 JSON Schema 描述输入字段名、类型、形状如{type: tensor, tensor-spec: {dtype: float32, shape: [-1, 12]}}。这个 Schema 会被写入模型MLmodel文件并由 KServe 在启动时加载用于运行时输入校验。当上游传来一个 13 维特征向量时KServe 会在请求进入模型推理前就返回400 Bad Request而不是让模型内部报ValueError: Expected input with 12 features, got 13 instead。这不仅是健壮性提升更是接口契约的显性化——它让数据科学家和后端工程师第一次有了共同的语言。决策三拒绝“模型即服务”拥抱“模型即管道”Part 4 的终极目标不是让单个模型跑起来而是让整个 ML Pipeline 可交付。因此我们定义的最小可部署单元从来不是一个.pkl文件而是一个Model Serving Bundle它包含模型文件model.pkl或model.onnx特征预处理代码preprocessor.py封装fit_transform()和transform()后处理逻辑postprocessor.py如分数归一化、业务规则兜底输入/输出 Schema 定义schema.json服务配置kserve_config.yaml定义资源请求、扩缩容策略、金丝雀权重这个 Bundle 作为一个整体通过 CI 流水线构建、测试、签名、推送到私有 Helm Chart 仓库。上线时helm upgrade fraud-detector ./charts/fraud-detector --set model.version12一条命令完成模型、预处理、后处理、配置的原子性更新。这才是真正的“端到端可重复”。3. 核心实操环节从训练代码到生产服务的完整流水线3.1 训练脚本改造让 notebook 的“灵感”变成可交付的“资产”假设你有一个在 Jupyter 中调试好的信用评分模型核心代码如下简化版# credit_score_train.ipynb cell import pandas as pd import numpy as np from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score # Load data df pd.read_parquet(s3://my-bucket/data/train_v202310.parquet) # Feature engineering df[income_to_debt_ratio] df[income] / (df[debt] 1e-6) df[is_high_risk_occupation] df[occupation].isin([freelancer, unemployed]) # Train-test split X df.drop([user_id, label], axis1) y df[label] X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) # Train model model RandomForestClassifier(n_estimators100, max_depth10, random_state42) model.fit(X_train, y_train) # Evaluate y_pred_proba model.predict_proba(X_test)[:, 1] print(fAUC: {roc_auc_score(y_test, y_pred_proba):.4f})这段代码在 notebook 里跑得飞起但它离生产还有十万八千里。要让它成为 Part 4 的合格输入必须进行四步手术式改造第一步剥离数据加载抽象为可配置的数据接口不能硬编码s3://my-bucket/data/train_v202310.parquet。我们创建data_loader.py# data_loader.py import pandas as pd from typing import Tuple, Optional def load_training_data( data_version: str v202310, sample_fraction: float 1.0 ) - Tuple[pd.DataFrame, pd.Series]: 加载训练数据支持版本化和采样 :param data_version: 数据版本标识如 v202310 :param sample_fraction: 采样比例用于快速迭代 :return: (features_df, labels_series) # 实际项目中这里会对接内部数据目录服务 path fs3://my-bucket/data/train_{data_version}.parquet df pd.read_parquet(path) if sample_fraction 1.0: df df.sample(fracsample_fraction, random_state42) X df.drop([user_id, label], axis1) y df[label] return X, y第二步将特征工程逻辑封装为独立、可序列化的 Preprocessor 类不能把income_to_debt_ratio这种计算散落在训练脚本里。创建preprocessor.py# preprocessor.py import pandas as pd import numpy as np from sklearn.base import BaseEstimator, TransformerMixin from typing import List, Dict, Any class CreditScorePreprocessor(BaseEstimator, TransformerMixin): 信用评分专用预处理器支持 fit/transform def __init__(self, high_risk_occupations: Optional[List[str]] None): self.high_risk_occupations high_risk_occupations or [freelancer, unemployed] self.feature_names_in_ None def fit(self, X: pd.DataFrame, yNone): # 记录原始列名用于后续 transform 时保持顺序 self.feature_names_in_ X.columns.tolist() return self def transform(self, X: pd.DataFrame) - pd.DataFrame: X X.copy() # 安全计算比率 X[income_to_debt_ratio] X[income] / (X[debt] 1e-6) # 生成布尔特征 X[is_high_risk_occupation] X[occupation].isin(self.high_risk_occupations) # 确保输出列顺序与 fit 时一致并添加新特征 new_features [income_to_debt_ratio, is_high_risk_occupation] all_features self.feature_names_in_ new_features return X[all_features] def get_feature_names_out(self, input_featuresNone) - List[str]: return self.feature_names_in_ [income_to_debt_ratio, is_high_risk_occupation]第三步重构训练脚本显式记录所有实验元数据train.py不再是 notebook 的复制粘贴而是 MLflow 的“契约入口”# train.py import mlflow from mlflow.models import infer_signature from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score import pandas as pd import joblib import sys import os # 设置 MLflow Tracking URI指向公司内部 MLflow Server os.environ[MLFLOW_TRACKING_URI] http://mlflow.internal:5000 def main(data_version: str v202310): # 1. 开启 MLflow Run with mlflow.start_run(run_namefcredit-score-{data_version}) as run: # 2. 记录参数 mlflow.log_param(data_version, data_version) mlflow.log_param(model_type, RandomForestClassifier) mlflow.log_param(n_estimators, 100) mlflow.log_param(max_depth, 10) mlflow.log_param(random_state, 42) # 3. 加载数据 from data_loader import load_training_data X, y load_training_data(data_versiondata_version) # 4. 初始化并拟合预处理器 from preprocessor import CreditScorePreprocessor preprocessor CreditScorePreprocessor() X_processed preprocessor.fit_transform(X) # 5. 划分训练/测试集 X_train, X_test, y_train, y_test train_test_split( X_processed, y, test_size0.2, random_state42 ) # 6. 训练模型 model RandomForestClassifier( n_estimators100, max_depth10, random_state42 ) model.fit(X_train, y_train) # 7. 评估并记录指标 y_pred_proba model.predict_proba(X_test)[:, 1] auc roc_auc_score(y_test, y_pred_proba) mlflow.log_metric(auc, auc) # 8. 推断输入/输出 Schema关键 # 注意infer_signature 的输入必须是处理后的 X_train不是原始 X signature infer_signature(X_train, model.predict_proba(X_train)) # 9. 将模型、预处理器、后处理器如有一起打包为 PyFunc 模型 # 这里我们创建一个自定义的 PyFuncModel 类 class CreditScoreModel(mlflow.pyfunc.PythonModel): def __init__(self, model, preprocessor): self.model model self.preprocessor preprocessor def predict(self, context, model_input): # model_input 是 pandas DataFrame processed_input self.preprocessor.transform(model_input) return self.model.predict_proba(processed_input)[:, 1] # 10. 使用 mlflow.pyfunc.log_model 打包 mlflow.pyfunc.log_model( artifact_pathmodel, python_modelCreditScoreModel(model, preprocessor), signaturesignature, input_exampleX_train.iloc[:3], # 提供一个输入样例用于 KServe 自动生成测试请求 registered_model_namecredit-score-model # 注册到 Model Registry ) # 11. 记录数据版本和预处理器代码便于复现 mlflow.log_artifact(preprocessor.py) mlflow.log_artifact(data_loader.py) if __name__ __main__: data_version sys.argv[1] if len(sys.argv) 1 else v202310 main(data_version)注意mlflow.pyfunc.log_model()是整个链条的枢纽。它生成的MLmodel文件里不仅包含模型权重还固化了preprocessor.py的代码、conda.yaml环境定义、以及signature。这意味着无论你在哪台机器上mlflow.pyfunc.load_model(models:/credit-score-model/Production)加载出来的都是一个“开箱即用”的、包含完整推理逻辑的函数对象无需额外安装任何东西。第四步编写模型服务的“胶水”代码——KServe 的 InferenceService 定义在kserve/目录下创建inference-service.yaml# kserve/inference-service.yaml apiVersion: kserve.kserve.io/v1beta1 kind: InferenceService metadata: name: credit-score namespace: ml-production annotations: # 启用 OpenTelemetry trace kserve.io/tracing: true spec: predictor: # 使用 SKLearn 推理服务器它原生支持 mlflow.pyfunc 模型 sklearn: # 指向 MLflow Model Registry 中的 Production 版本 storageUri: models://http://mlflow.internal:5000/credit-score-model/Production # 资源请求防止 OOM resources: limits: memory: 2Gi cpu: 1000m requests: memory: 1Gi cpu: 500m # 自动扩缩容策略 autoscalingConfig: minReplicas: 2 maxReplicas: 10 targetUtilizationPercentage: 70 # 金丝雀发布配置上线新版本时用 canaryTrafficPercent: 5这个 YAML 文件就是 KServe 的“服务蓝图”。它告诉 KServe“请为名为credit-score的模型启动一个至少 2 个副本、最多 10 个副本的服务当 CPU 利用率超过 70% 时自动扩容并且初始只把 5% 的流量导向它”。所有这些配置都与 MLflow 中注册的模型版本强绑定实现了“模型即代码、配置即模型”的理念。3.2 CI/CD 流水线让每一次提交都自动走向生产我们使用 GitLab CI 构建端到端流水线.gitlab-ci.yml核心节选如下stages: - test - build - deploy variables: MLFLOW_TRACKING_URI: http://mlflow.internal:5000 KUBECONFIG: /etc/kube/config # 阶段1单元测试与静态检查 test: stage: test image: python:3.9 script: - pip install pytest flake8 black - pytest tests/ -v - flake8 --max-line-length120 . - black --check . # 阶段2模型训练与注册 train-and-register: stage: build image: continuumio/miniconda3:4.12.0 script: - conda env update -n base -f environment.yml - python train.py $DATA_VERSION # 将 MLflow Run ID 作为产物传递给下一阶段 artifacts: paths: - mlflow_run_id.txt # 只在 main 分支或打 tag 时触发 only: - main - tags # 阶段3服务部署仅当模型注册成功且通过审批后 deploy-to-production: stage: deploy image: bitnami/kubectl:latest script: # 1. 从上一阶段获取 Run ID查询 MLflow 获取模型版本号 - MODEL_VERSION$(curl -s $MLFLOW_TRACKING_URI/api/2.0/mlflow/model-versions/search?filtername%3Dcredit-score-model | jq -r .model_versions[] | select(.current_stageProduction) | .version) # 2. 渲染 KServe YAML 模板注入模型版本 - sed -i s/storageUri:.*/storageUri: \models:\/\/http:\/\/mlflow.internal:5000\/credit-score-model\/$MODEL_VERSION\/g kserve/inference-service.yaml # 3. 应用到 Kubernetes - kubectl apply -f kserve/inference-service.yaml -n ml-production # 仅当 MR 被指定 approver 批准后才执行 rules: - if: $CI_PIPELINE_SOURCE merge_request_event when: manual allow_failure: false这个流水线的关键在于状态驱动train-and-register阶段成功后模型自动注册到 MLflow Registry 的Staging阶段然后由数据科学家在 MLflow UI 上手动将其 Promote 到Production阶段此时deploy-to-production阶段才会被触发它从 MLflow API 动态拉取当前Production版本号并注入到 KServe YAML 中最后kubectl apply。整个过程无人工干预但每一步都有明确的审批点既保证了自动化效率又守住了质量红线。3.3 生产环境监控不只是看 p99更要读懂模型的“心跳”KServe 默认暴露/metrics端点Prometheus 抓取后我们构建了三个核心 Grafana 仪表盘仪表盘一“模型健康总览”核心指标model_request_total{status~2..} / model_request_total成功率关键告警当rate(model_request_total{status~5..}[5m]) 0.015xx 错误率 1%时立即触发 PagerDuty深度洞察叠加model_latency_seconds_bucket{le0.1}和model_latency_seconds_bucket{le0.5}观察延迟分布是否右移——如果le0.1的占比从 95% 降到 80%说明有少量请求变慢可能是特征存储抖动而非模型本身问题。仪表盘二“输入数据质量”我们在 KServe 的predictor容器中注入了一个 OpenTelemetry Processor它在每次predict()调用前对输入 DataFrame 执行轻量级校验# telemetry_processor.py def validate_input(df: pd.DataFrame): # 检查空值率 null_rate df.isnull().mean().max() if null_rate 0.05: logger.warning(fHigh null rate detected: {null_rate:.3f}) # 上报自定义指标 counter meter.create_counter(input_null_rate) counter.add(1, {rate: f{null_rate:.3f}}) # 检查数值范围基于训练时的统计 if income in df.columns: if (df[income] 0).any(): logger.error(Negative income detected!)Grafana 中展示input_null_rate和input_negative_income_count当input_null_rate 5% 持续 10 分钟自动创建 Jira ticket 并通知数据工程师。仪表盘三“模型性能漂移”我们每天定时运行一个批处理作业用最新 24 小时的线上请求日志脱敏后调用model.predict_proba()计算预测分数的分布histogram并与上周基线对比。使用 KServe 的explainer功能对随机 1000 个请求调用 SHAP 计算特征重要性观察income_to_debt_ratio的平均贡献值是否发生显著变化t-test p-value 0.01。当prediction_score_mean偏离基线 2 个标准差或shap_income_importance变化 15%Grafana 仪表盘会亮起红色预警并附上漂移分析报告链接。实操心得监控不是越多越好而是要聚焦“影响业务结果”的指标。我们曾经堆砌了 87 个监控图表结果真正有用的只有这 3 个。记住一个能触发明确行动的告警胜过一百个安静的仪表盘。比如“p99 延迟 200ms”这个告警应该自动触发kubectl top pods -n ml-production | grep credit-score查看 CPU/Memory而不是只发一封邮件。4. 常见问题与实战排障那些文档里不会写的细节4.1 “模型加载失败ModuleNotFoundError: No module named preprocessor”现象KServe Pod 日志显示ModuleNotFoundError: No module named preprocessor但你在train.py中明明mlflow.pyfunc.log_model()了。根因mlflow.pyfunc.log_model()确实会把preprocessor.py作为 artifact 保存但 KServe 的 SKLearn 推理服务器默认只加载model.pkl并不会自动把同目录下的.py文件加入sys.path。解决方案在train.py中不要只 logpreprocessor.py而是创建一个requirements.txt显式声明依赖# 在 train.py 的 mlflow.log_model() 之前 with open(requirements.txt, w) as f: f.write(scikit-learn1.2.2\n) f.write(pandas1.5.3\n) # 注意这里必须包含 preprocessor.py 所在的 package f.write(.\n) # 表示当前目录即包含 preprocessor.py 的目录是一个可安装的 package mlflow.log_artifact(requirements.txt)然后在preprocessor.py同级目录下创建一个空的__init__.py文件使其成为一个合法的 Python package。这样mlflow.pyfunc在加载时会执行pip install -e .从而把preprocessor模块正确安装到环境中。4.2 “KServe 服务启动后/healthz 返回 200但 /predict 一直 503”现象kubectl get ingress显示路由正常curl http://kservice.credit-score.ml-production.svc.cluster.local/healthz返回{status:ok}但curl -X POST ... /predict持续返回503 Service Unavailable。排查路径kubectl describe pod -l serving.kserve.io/inferenceservicecredit-score—— 查看 Pod 事件常见原因是FailedScheduling资源不足或CrashLoopBackOff容器启动失败。如果 Pod Runningkubectl logs -c kfserving-container pod-name—— 查看 KServe 主容器日志重点搜索failed to load model或timeout。最关键的一步kubectl exec -it pod-name -c kfserving-container -- sh然后手动尝试加载模型python -c import mlflow; m mlflow.pyfunc.load_model(gs://my-bucket/mlflow/1/abc123/model); print(m.predict([[1,2,3]]))这会暴露真实的 Python 导入错误比如ImportError: libgomp.so.1: cannot open shared object file缺少 OpenMP 库此时需要在