GitLab CI/CD实现机器学习模型工程化落地

1. 项目概述:为什么“模型跑通”只是万里长征第一步?

你是不是也经历过这样的场景:凌晨两点,Jupyter Notebook里那个准确率92.3%的XGBoost模型终于调参成功,你兴奋地截图发到团队群,配文“搞定!”。结果三天后,产品同学在钉钉上戳你:“那个推荐模型,能接进APP首页吗?用户点击后实时返回结果。”你点开自己本地的.ipynb文件,突然发现——它压根没写过一行能被HTTP请求调用的代码;训练数据硬编码在/Users/angelica/data/raw/路径下;依赖包只记得装了scikit-learn==1.2.2,但忘了记pandas版本;更别说测试集划分逻辑藏在第17个cell里,连自己第二天都得重读十分钟才能看懂。这不是个例,这是绝大多数数据科学项目的常态。“模型跑通”和“服务上线”之间,隔着一条叫“工程化鸿沟”的深谷——而GitLab CI/CD Pipeline,就是我们亲手搭起来的第一座钢索桥。它不负责帮你设计特征工程,也不替你优化AUC,但它会强制你把“能运行”的代码,变成“可交付、可验证、可回滚、可协作”的生产资产。我带过的12个落地项目里,有9个卡在从开发环境到测试环境的迁移环节,问题几乎全出在环境不一致、流程无记录、发布靠手动这三座大山。GitLab不是银弹,但它是一套可审计、可沉淀、可复用的“交付操作系统”。它让数据科学家第一次真正拥有对代码生命周期的掌控力——从git push那一刻起,编译、测试、打包、部署,全部自动触发、全程留痕、失败即止。这篇文章不讲抽象概念,只拆解我在金融风控、电商推荐、工业设备预测三个真实项目中,用GitLab把模型服务推上K8s集群的完整路径。每一步配置我都贴出实测有效的YAML片段,每一个坑我都标出报错日志原文和修复命令。如果你正被“模型总在生产环境崩”、“每次上线都要求运维帮忙改配置”、“新同事接手项目要花一周理清依赖”这些问题困扰,那接下来的内容,就是你该抄的作业。

2. 核心思路拆解:为什么选GitLab而不是GitHub Actions或Jenkins?

在动手写第一行.gitlab-ci.yml之前,我必须先说清楚一个关键决策:为什么是GitLab?去年我们团队做过一次横向对比测试,用同一套LSTM时间序列预测代码,在GitLab CI、GitHub Actions、Jenkins三种平台跑完从代码提交到API服务可用的全流程。结果很反直觉——GitHub Actions平均耗时最短(4分12秒),但生产环境首次故障率高达67%;Jenkins稳定性最好(故障率8%),但配置维护成本让两位工程师每周多花15小时;GitLab CI综合得分最高,故障率仅11%,且配置变更耗时从Jenkins的平均47分钟降到GitLab的6分钟。这个结果背后,是三个不可替代的底层能力。

首先是原生集成的制品仓库(Container Registry)。GitHub Actions需要额外配置Docker Hub或自建Harbor,而GitLab自带Registry,且权限与项目组完全绑定。我们在电商项目中,模型训练镜像、API服务镜像、数据预处理镜像全部存于同一命名空间下,gitlab.example.com:5000/ecommerce/recommender:20231025这样的地址,直接嵌入K8s Deployment YAML,无需额外认证。更重要的是,当某次训练镜像因CUDA版本冲突导致GPU节点启动失败时,我们通过Registry的镜像层分析功能,5分钟内定位到nvidia/cuda:11.7.1-devel-ubuntu20.04基础镜像被上游更新破坏,立刻锁定旧版镜像并通知团队停用新tag——这种深度可观测性,是第三方Registry难以提供的。

其次是CI/CD流水线与代码仓库的强一致性保障。GitHub Actions的workflow文件放在.github/workflows/目录,而Jenkinsfile常置于项目根目录,两者都可能被开发者误删或修改。GitLab则要求所有CI配置必须存在于项目根目录的.gitlab-ci.yml中,且该文件本身受Git版本控制。这意味着:当你git checkout v2.3.1时,自动加载对应版本的CI脚本;当你git revert commit_id时,CI行为也同步回退。我们在金融风控项目中曾遇到一个致命问题:v2.2.0版本的模型评估脚本里,误将roc_auc_scoreaverage='macro'参数写成'micro',导致线上AUC虚高3.2个百分点。由于CI脚本随代码回滚,我们一键恢复v2.1.0的CI配置后,所有历史tag的构建结果自动修正,避免了人工排查数百个历史构建记录的灾难。

最后是Runner资源调度的确定性。Jenkins的Slave节点常因负载不均导致构建排队,GitHub Actions的Ubuntu runner共享资源,高峰期CPU被限制在2核。GitLab Runner支持标签化精准调度,我们在K8s集群中部署了三类Runner:gpu-runner(绑定NVIDIA GPU节点)、cpu-runner(普通计算节点)、test-runner(内存16GB+SSD的专用测试机)。当.gitlab-ci.yml中声明tags: [gpu-runner]时,任务必然落在GPU节点,且独占该节点资源。这直接解决了LSTM训练任务因抢占CPU导致的OOM崩溃问题——在Jenkins上,这类崩溃平均每周发生2.3次;迁移到GitLab后,连续147天零OOM。

提示:不要迷信“最新技术”。我们曾为追求时髦,在IoT设备预测项目中短暂试用GitHub Actions,结果因runner缓存机制缺陷,导致pip install -r requirements.txt每次下载不同版本的torch,最终模型推理结果波动超15%。GitLab的cache策略明确支持按requirements.txt文件哈希值缓存,这才是生产环境需要的确定性。

3. 核心细节解析:数据科学项目特有的CI/CD四道关卡

数据科学项目的CI/CD绝非简单复制Web应用的流程。我把它拆解为四个必须攻克的关卡,每一关都藏着让模型在生产环境“活下来”的关键细节。这些不是理论,而是我在三个项目中踩坑后总结的血泪清单。

3.1 关卡一:环境一致性——如何让“本地能跑”等于“服务器必跑”

数据科学家最常犯的错误,是把环境当成黑盒。pip list输出的37个包,谁是核心依赖?谁是开发工具?谁是隐藏的版本炸弹?我们在电商推荐项目中,曾因matplotlib3.7.0版本引入了一个PIL兼容性bug,导致特征可视化脚本在服务器上静默失败,而本地开发机因已安装PIL 9.5.0侥幸通过。解决方案是三层环境隔离法

第一层:requirements.txt只放运行时依赖。严格剔除jupyteripykernelblack等开发工具。用pipreqs --force --no-pin .生成初始列表,再人工审核——重点检查tensorflowpytorch是否指定CUDA版本(如torch==2.0.1+cu117),pandas是否锁定小版本(pandas>=1.5.3,<1.5.4防API变更)。

第二层:environment.yml定义Conda环境。针对需要特定C++库的场景(如xgboost编译),用Conda管理比pip更可靠。我们的金融风控模型必须使用gcc 9.4编译,而Ubuntu 20.04默认gcc 10.3environment.yml中明确声明:

name: ds-prod channels: - conda-forge - defaults dependencies: - python=3.9 - gcc_linux-64=9.4 - xgboost=1.7.5 - pip - pip: - -r file:requirements.txt

第三层:Docker多阶段构建。这是终极保险。我们的标准Dockerfile包含三个阶段:

# 阶段1:构建环境(含编译工具) FROM continuumio/miniconda3:23.5.2 AS builder COPY environment.yml . RUN conda env create -f environment.yml && conda clean -a # 阶段2:运行时环境(精简无编译工具) FROM continuumio/miniconda3:23.5.2 COPY --from=builder /opt/conda/envs/ds-prod /opt/conda/envs/ds-prod ENV PATH="/opt/conda/envs/ds-prod/bin:$PATH" COPY . /app WORKDIR /app # 阶段3:生产镜像(移除conda,仅保留python) FROM python:3.9-slim COPY --from=0 /opt/conda/envs/ds-prod/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages COPY --from=0 /opt/conda/envs/ds-prod/bin/ /usr/local/bin/ COPY . /app WORKDIR /app CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

这样构建出的镜像体积从1.2GB压缩到327MB,且彻底剥离了conda和gcc,杜绝了“本地conda环境能跑,Docker里pip install失败”的经典陷阱。

3.2 关卡二:数据可靠性——没有可信数据,模型就是空中楼阁

CI流程中,90%的数据相关故障源于“测试数据不可控”。我们在工业设备预测项目中,曾因测试数据集被误删,导致CI流程卡在load_test_data()函数长达22分钟才超时。更危险的是,有人偷偷用生产数据的子集做测试,导致CI通过但模型在真实场景失效。解决方案是数据契约(Data Contract)驱动的测试

首先,在项目根目录创建data_contract.yaml,明确定义测试数据规范:

version: "1.0" datasets: - name: "test_features" path: "data/test/features.parquet" schema: columns: - name: "timestamp" type: "datetime64[ns]" nullable: false - name: "temperature" type: "float64" min: -40.0 max: 120.0 - name: "vibration_freq" type: "float64" min: 0.0 max: 10000.0 row_count: min: 1000 max: 5000

然后在CI的test阶段,插入数据校验步骤:

test-data: stage: test image: python:3.9-slim script: - pip install great-expectations - python -m great_expectations checkpoint run data_contract_checkpoint artifacts: - reports/* allow_failure: false

great_expectations会自动读取data_contract.yaml,验证测试数据是否符合约定。一旦vibration_freq出现负值或行数不足1000,CI立即失败,并生成HTML报告指出具体哪一行哪一列违规。这比写assert df.shape[0] > 1000的硬编码断言,可靠了不止一个数量级。

3.3 关卡三:模型可重现性——确保“这次的结果”能被“下次完美复现”

模型训练的随机性是生产环境的隐形杀手。同一个random_state=42,在不同numpy版本下可能产生不同结果。我们在金融风控项目中,因numpy 1.23.5升级到1.24.0,导致特征重要性排序变化,触发了下游规则引擎的误判。解决方案是四重锁定机制

  1. 代码锁定git commit哈希值作为模型元数据。在训练脚本末尾添加:
import subprocess commit_hash = subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('utf-8').strip() model.metadata['git_commit'] = commit_hash
  1. 环境锁定environment.ymlrequirements.txt的SHA256校验。CI中增加步骤:
sha256sum environment.yml requirements.txt > env_checksums.txt

并将该文件存入模型存储桶,与模型文件同名但后缀为.checksum

  1. 数据锁定:训练数据集的dvc repropachyderm版本号。我们采用DVC,在CI中执行:
dvc pull -r train_data dvc metrics show --all-commits

确保训练数据版本与代码版本严格对应。

  1. 硬件锁定:记录GPU型号和驱动版本。在训练开始前注入:
nvidia-smi --query-gpu=name,driver_version --format=csv,noheader,nounits

这些信息全部写入模型的model.pkl元数据,当运维人员在K8s中看到模型异常时,只需查model.pkl就能瞬间定位是代码、环境、数据还是硬件变更所致。

3.4 关卡四:服务健壮性——让API不只是“能响应”,更要“懂业务”

数据科学家写的API,常犯两个错误:一是返回500 Internal Server Error却不带任何上下文,二是对输入数据不做业务校验。我们在电商推荐API中,曾因前端传入user_id="abc"(字符串而非整数),导致pandas.merge静默返回空DataFrame,API返回空列表,前端以为“无推荐商品”,实际是严重BUG。解决方案是业务语义层校验

在FastAPI的Pydantic模型中,不只定义类型,更定义业务规则:

from pydantic import BaseModel, Field, validator from typing import List class RecommendationRequest(BaseModel): user_id: int = Field(..., ge=1, le=999999999, description="用户ID,必须为1-999999999的整数") item_ids: List[int] = Field(..., min_items=1, max_items=50, description="待打分商品ID列表,1-50个") @validator('user_id') def user_id_must_be_valid(cls, v): if v % 1000 == 0: # 业务规则:ID末三位不能全为0 raise ValueError('user_id末三位不能全为0') return v

同时,在API入口处增加熔断器:

from circuitbreaker import circuit @circuit(failure_threshold=5, recovery_timeout=60) @app.post("/recommend") def recommend(request: RecommendationRequest): # 主逻辑 pass

当连续5次调用失败(如数据库连接超时),自动熔断60秒,返回503 Service Unavailable并附带Retry-After: 60头。这比让前端无限重试导致雪崩,专业了太多。

4. 实操过程详解:从空仓库到K8s服务的七步流水线

现在,让我们把前面所有原则,落地为一条可执行的GitLab CI/CD流水线。以下是在工业设备预测项目中实测有效的完整流程,所有YAML和代码均来自生产环境。我会逐行解释每个步骤的设计意图和避坑要点。

4.1 步骤一:初始化与环境准备(.gitlab-ci.yml)

stages: - setup - test - build - deploy - monitor variables: # 全局变量,避免硬编码 PYTHON_VERSION: "3.9" DOCKER_REGISTRY: "$CI_REGISTRY" IMAGE_NAME: "$CI_REGISTRY_IMAGE" # K8s集群配置 KUBE_CONFIG: "$HOME/.kube/config" KUBE_CONTEXT: "prod-cluster" setup-env: stage: setup image: python:${PYTHON_VERSION}-slim before_script: - apt-get update && apt-get install -y curl jq && rm -rf /var/lib/apt/lists/* script: - pip install --upgrade pip setuptools wheel - pip install -r requirements.txt artifacts: - .gitlab-ci.yml - requirements.txt - environment.yml cache: key: "$CI_COMMIT_REF_SLUG" paths: - .pip-cache/ tags: - cpu-runner

关键设计点

  • before_script中安装curljq,为后续K8s操作铺路。很多教程忽略这点,导致kubectl apply命令找不到。
  • artifacts明确列出CI流程必需的配置文件,确保后续阶段能继承。若漏掉environment.ymlbuild阶段将无法重建Conda环境。
  • cache使用$CI_COMMIT_REF_SLUG(分支名)作为key,而非默认的default,避免maindev分支共用缓存导致冲突。

4.2 步骤二:数据与代码双重校验(test阶段)

test-data-contract: stage: test image: python:${PYTHON_VERSION}-slim script: - pip install great-expectations - python -c " import json with open('data_contract.yaml') as f: contract = json.load(f) print(f'Data contract loaded for {len(contract[\"datasets\"])} datasets') " - great_expectations checkpoint run data_contract_checkpoint allow_failure: false tags: - cpu-runner test-unit: stage: test image: python:${PYTHON_VERSION}-slim script: - pip install pytest pytest-cov - pytest tests/ --cov=src --cov-report=html --cov-fail-under=80 artifacts: - htmlcov/ coverage: '/^TOTAL.*\\s+([0-9]{1,3})%$/' tags: - cpu-runner

避坑要点

  • test-data-contract必须放在test-unit之前。因为数据校验失败应阻断所有后续测试,避免浪费资源。
  • coverage正则表达式必须精确匹配pytest输出。/^TOTAL.*\\s+([0-9]{1,3})%$/捕获TOTAL 100%中的数字,若写成.*([0-9]+)%,可能误匹配100%100%之间的空格,导致覆盖率计算错误。
  • --cov-fail-under=80是硬性红线。我们在金融项目中设定为85%,因为特征工程模块必须100%覆盖,否则无法保证数据清洗逻辑的稳定性。

4.3 步骤三:模型训练与持久化(build阶段)

train-model: stage: build image: continuumio/miniconda3:23.5.2 script: - conda env create -f environment.yml - conda activate ds-prod - python src/train.py --config configs/train_prod.yaml artifacts: - models/*.pkl - models/*.joblib tags: - gpu-runner build-api-image: stage: build image: docker:23.0.6 services: - docker:dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - docker build -t $IMAGE_NAME:latest -f Dockerfile.api . - docker push $IMAGE_NAME:latest tags: - cpu-runner

实操心得

  • train-model使用continuumio/miniconda3:23.5.2而非python:3.9,因为前者预装了conda,避免在gpu-runner上重复安装。
  • build-api-image必须启用docker:dind服务,否则docker build命令会报Cannot connect to the Docker daemon。这是GitLab CI新手最常见的错误之一。
  • docker login命令中,$CI_REGISTRY_USER$CI_REGISTRY_PASSWORD是GitLab预置的CI变量,无需手动配置,但必须确保项目设置了CI/CD Variables中的CI_REGISTRY_PASSWORD为Protected。

4.4 步骤四:K8s部署与健康检查(deploy阶段)

deploy-to-k8s: stage: deploy image: bitnami/kubectl:1.27.4 before_script: - mkdir -p $HOME/.kube - echo "$KUBE_CONFIG" | base64 -d > $HOME/.kube/config script: - kubectl config use-context $KUBE_CONTEXT - kubectl set image deployment/recommender-api api=$IMAGE_NAME:latest - kubectl rollout status deployment/recommender-api --timeout=300s after_script: - kubectl get pods -n default -l app=recommender-api -o wide tags: - cpu-runner health-check: stage: deploy image: curlimages/curl:8.2.1 script: - | until curl -f http://recommender-api.default.svc.cluster.local:8000/health; do echo "Waiting for API to be ready..." sleep 5 done - curl http://recommender-api.default.svc.cluster.local:8000/health | jq '.status' tags: - cpu-runner

关键细节

  • after_scriptkubectl get pods命令,会在GitLab CI日志中打印出新Pod的IP和所在节点,方便运维快速定位问题。
  • health-check使用curlimages/curl:8.2.1而非alpine/curl,因为后者缺少jq,无法解析JSON响应。
  • 健康检查URL使用K8s内部服务域名recommender-api.default.svc.cluster.local,而非外部Ingress地址,避免DNS解析失败导致误判。

4.5 步骤五:模型监控与告警(monitor阶段)

monitor-model-drift: stage: monitor image: python:${PYTHON_VERSION}-slim script: - pip install evidently - python -c " from evidently.report import Report from evidently.metrics import DataDriftTable import pandas as pd # 加载线上服务的实时请求日志(从S3或Kafka) log_df = pd.read_parquet('s3://logs/prod/recommender-20231025.parquet') # 加载训练时的基准数据 baseline_df = pd.read_parquet('data/train/features.parquet') report = Report(metrics=[DataDriftTable()]) report.run(reference_data=baseline_df, current_data=log_df) report.save_html('drift_report.html') " artifacts: - drift_report.html tags: - cpu-runner

经验分享

  • evidentlyDataDriftTable会自动计算每个特征的PSI(Population Stability Index),当PSI>0.25时标记为“严重漂移”。我们在电商项目中,将此阈值设为0.15,因为商品价格特征对模型影响极大。
  • 报告生成后,我们通过GitLab的artifacts自动存档,并设置Webhook,当drift_report.html生成时,自动发送Slack消息给算法团队。

4.6 步骤六:回滚机制——当新模型出问题时,如何5分钟内切回旧版?

真正的生产就绪,不在于“上线多快”,而在于“回滚多稳”。我们在金融风控项目中,实现了全自动回滚:

rollback-on-failure: stage: deploy image: bitnami/kubectl:1.27.4 when: on_failure before_script: - mkdir -p $HOME/.kube - echo "$KUBE_CONFIG" | base64 -d > $HOME/.kube/config script: - kubectl config use-context $KUBE_CONTEXT - PREV_REVISION=$(kubectl rollout history deployment/recommender-api --revision=1 | tail -2 | head -1 | awk '{print $1}') - kubectl rollout undo deployment/recommender-api --to-revision=$PREV_REVISION - kubectl rollout status deployment/recommender-api --timeout=120s tags: - cpu-runner

原理说明

  • when: on_failure确保仅在deploy-to-k8s失败时触发。
  • kubectl rollout history命令获取历史版本列表,tail -2 | head -1提取倒数第二个版本(即上一个稳定版本),awk '{print $1}'取出版本号。
  • kubectl rollout undo执行回滚,整个过程平均耗时47秒。

4.7 步骤七:安全扫描——堵住模型供应链的漏洞

最后一步,也是最容易被忽视的一步:安全。我们在所有镜像构建后,强制进行CVE扫描:

scan-image: stage: build image: anchore/engine-cli:latest before_script: - anchore-cli --u admin --p foobar --url http://anchore-engine:8228 system wait script: - anchore-cli --u admin --p foobar --url http://anchore-engine:8228 image add $IMAGE_NAME:latest - anchore-cli --u admin --p foobar --url http://anchore-engine:8228 evaluate check $IMAGE_NAME:latest --detail tags: - cpu-runner

安全实践

  • 使用anchore/engine-cli而非trivy,因为前者能深度扫描Python包的依赖树,发现requests<2.28.0urllib3CVE-2023-43804漏洞,而trivy仅扫描OS包层。
  • 扫描结果中,若发现CRITICAL级别漏洞,CI自动失败,并在GitLab MR中生成评论,标注漏洞详情和修复建议。

5. 常见问题与排查技巧实录

在将这套流程推广到团队其他项目时,我们收集了高频问题TOP10,并附上现场排查记录和终极解决方案。这些不是教科书答案,而是深夜三点在服务器日志里扒出来的真相。

5.1 问题1:CI流水线卡在docker build,日志显示Error response from daemon: dial tcp: lookup docker on 192.168.0.1:53: no such host

现场记录

  • 环境:GitLab CE 15.10,Runner部署在AWS EC2(Ubuntu 22.04)
  • 复现步骤:在.gitlab-ci.yml中启用docker:dind服务后,docker build命令始终失败
  • 日志关键行:time="2023-10-25T03:14:22Z" level=fatal msg="Error initializing graph driver: driver not supported"

根本原因
docker:dind容器默认使用vfs存储驱动,而EC2实例的根文件系统是ext4vfs驱动在ext4上性能极差且不稳定。GitLab官方文档未强调此点,导致大量用户踩坑。

解决方案
在Runner的config.toml中,为docker:dind服务指定overlay2驱动:

[[runners]] name = "docker-runner" url = "https://gitlab.example.com/" token = "xxx" executor = "docker" [runners.docker] image = "docker:23.0.6" privileged = true volumes = ["/cache", "/certs/client:/certs/client:ro"] [runners.cache] Type = "s3" # 关键配置:强制dind使用overlay2 [runners.docker.services] [[runners.docker.services]] name = "docker:dind" command = ["--storage-driver=overlay2"]

重启Runner后,docker build耗时从超时(30分钟)降至2分14秒。

5.2 问题2:pytest覆盖率报告中,src/models/目录显示0%,但实际写了单元测试

现场记录

  • 项目结构:
    src/ __init__.py models/ __init__.py predictor.py utils/ __init__.py helpers.py tests/ test_predictor.py
  • test_predictor.pyfrom src.models.predictor import Predictor导入正常,但pytest --cov=src报告src/models/为0%

根本原因
pytest-cov--cov=src参数,要求被测代码必须以src为Python包根目录运行。但test_predictor.pyfrom src.models...的导入方式,导致predictor.py被当作顶层模块加载,其__file__路径为/builds/project/src/models/predictor.py,而--cov只监控/builds/project/src/下的模块,src/models/被排除在外。

解决方案
在项目根目录创建pyproject.toml,强制pytestsrc为源码根:

[tool.pytest.ini_options] pythonpath = ["src"] testpaths = ["tests"] addopts = [ "--cov=src", "--cov-report=html", "--cov-fail-under=80" ]

同时,将test_predictor.py中的导入改为相对导入:

# tests/test_predictor.py from models.predictor import Predictor # 改为不带src前缀

重新运行后,覆盖率正确显示src/models/为92%。

5.3 问题3:K8s部署后,Pod状态为CrashLoopBackOffkubectl logs显示ModuleNotFoundError: No module named 'sklearn'

现场记录

  • Dockerfile使用多阶段构建,stage 2基于python:3.9-slim
  • docker run -it <image> python -c "import sklearn"本地测试正常
  • 但K8s中Pod启动即崩溃

根本原因
python:3.9-slim镜像基于Debian,而sklearn的某些C扩展(如_cython_blas)依赖libglib2.0-0库。slim镜像中该库被精简移除,但pip install scikit-learn未报错,导致运行时动态链接失败。

解决方案
在Dockerfile的stage 2中,显式安装缺失的系统库:

FROM python:3.9-slim # 关键修复:安装sklearn依赖的系统库 RUN apt-get update && apt-get install -y \ libglib2.0-0 \ libsm6 \ libxext6 \ && rm -rf /var/lib/apt/lists/* # 继续COPY和RUN...

此问题在scikit-learn>=1.2.0版本中尤为常见,因新版加强了Cython优化。

5.4 问题4:great_expectations数据校验通过,但线上服务仍因数据格式异常崩溃

现场记录

  • data_contract.yaml中定义temperaturefloat64min: -40.0,max: 120.0
  • CI中great_expectations报告ALL EXPECTATIONS PASSED
  • 但线上API收到temperature="NaN"字符串,导致pandas.to_numeric()失败

根本原因
great_expectationscolumn_type检查只验证Pandas读取后的数据类型,而"NaN"字符串在pd.read_parquet()时被自动转为np.nan,类型仍是float64,通过校验。但业务逻辑中,np.nan未被处理,导致下游计算崩溃。

解决方案
data_contract.yaml中,增加null_count约束,并在CI中启用expect_column_values_to_not_be_null

datasets: - name: "test_features" path: "data/test/features.parquet" schema: columns: - name: "temperature" type: "float64" # 新增:禁止空值 expect_column_values_to_not_be_null: true # 新增:全局空值检查 null_count: max: 0

同时,在训练脚本中,强制填充空值:

df['temperature'] = df['temperature'].fillna(df['temperature'].median())

双保险下,空值问题彻底消失。

5.5 问题5:kubectl rollout status超时,但Pod实际已Running

现场记录

  • deploy-to-k8s阶段kubectl rollout status设置--timeout=300s
  • 日志显示Waiting for deployment "recommender-api" rollout to finish: 0 of 3 updated replicas are available...
  • kubectl get pods显示所有Pod为Running状态

根本原因
kubectl rollout status检查的是Deployment的Replicas状态,而Pod的Running状态不等于Ready。我们的API服务在启动后需加载1.2GB模型文件,livenessProbereadinessProbe未配置,导致K8s认为Pod未就绪。

解决方案
在K8s Deployment YAML中,添加探针配置:

livenessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 120 # 给足模型加载时间 periodSeconds: 30 readinessProbe: httpGet: path: /readyz port: 8000 initialDelaySeconds: 60 periodSeconds: 10

/readyz端点返回{"status": "ready", "model_loaded": true},仅当模型完全加载后才返回200

6. 最后一点个人体会:别让CI/CD成为新的“黑盒”

写完这篇万字长文,我想说点掏心窝