定义一个棘手的技术问题
一个推荐系统的生命力在于其模型的迭代速度。不同于传统Web服务的无状态特性,推荐系统的CI/CD不仅涉及服务代码的构建和部署,更核心的挑战在于如何自动化、可追溯地处理模型训练、验证、打包和上线这一系列复杂且状态相关的流程。这套流程通常被称为MLOps。
我们面临的具体挑战是为一套基于矩阵分解算法(如ALS)的推荐系统构建模型部署流水线。其技术痛点非常明确:
- 巨型产物(Large Artifacts): 训练完成的模型文件通常在几百MB到数GB之间。传统的CI/CD流水线在处理这种大小的产物时,传递、存储和缓存的效率都面临严峻考验。
- 异构计算资源: 模型训练和验证阶段需要带有NVIDIA GPU的计算节点,而服务构建和部署阶段则在标准CPU节点上进行。流水线必须能够灵活调度和管理这两种截然不同的执行环境。
- 流程强依赖性: 整个流程是严格线性的:数据预处理 -> 特征工程 -> 模型训练 -> 模型评估 -> 模型打包 -> 服务部署。任何一个环节的失败都必须中断整个流程,并提供清晰的失败上下文。
- 版本与可追溯性: 每一个上线的模型都必须与训练它的代码版本、数据集版本以及评估指标严格对应。当线上出现问题时,我们需要能够快速回滚到某个稳定的模型版本。
问题核心是:选择哪一个CI/CD平台——深度集成、自托管能力强的GitLab CI/CD,还是以性能和云原生体验著称的CircleCI——能更优雅、更高效地解决上述MLOps特有的工程挑战?这不是一个简单的工具选型,而是对两种不同DevOps哲学的深度考量。
方案A:GitLab CI/CD - 一体化生态与自控力
GitLab的突出优势在于其一体化的解决方案。代码仓库、CI/CD、包注册表(Package Registry)、容器注册表(Container Registry)甚至制品库(Generic package registry)都无缝集成在同一个平台中。对于MLOps场景,这意味着我们可以将模型、代码和CI流水线置于同一套权限和管理体系下。
优势分析
- 自托管Runner: 这是解决异构计算资源问题的关键。我们可以轻易地注册一台配备了Tesla V100 GPU的物理机或虚拟机作为特定的GitLab Runner,并为其打上
gpu标签。流水线中的训练作业可以精确地调度到这台机器上执行。这不仅解决了硬件依赖,还保证了核心数据和模型始终在内部可控的网络环境中。 - 集成的制品库: GitLab的Generic package registry可以被巧妙地用作一个简易的模型注册表。我们可以将训练好的模型文件(例如,一个
tar.gz压缩包)连同其元数据(如评估指标metrics.json)一起打包,作为一个版本化的“包”上传。流水线的下游作业可以直接通过API或预定义变量拉取特定版本的模型包。 - 流水线可视化与依赖控制: GitLab的
needs关键字可以清晰地定义作业间的依赖关系,构建出一个有向无环图(DAG),完美匹配我们强依赖的流程。
劣势分析
- 维护成本: 自托管Runner的灵活性带来了维护的复杂性。我们需要自己负责Runner的操作系统、NVIDIA驱动、CUDA工具包以及Docker环境的安装、更新和故障排查。
- 缓存机制: GitLab的缓存在处理GB级别的模型文件时可能表现不佳。虽然它支持S3作为分布式缓存后端,但配置相对复杂,且在跨Runner节点间缓存大文件时,网络IO可能成为新的瓶颈。
- 性能感知: 相比CircleCI,GitLab CI/CD的作业启动和执行过程给人的感觉会稍慢一些,尤其是在共享Runner的场景下。对于追求极致迭代速度的团队,这可能是个问题。
核心实现概览: .gitlab-ci.yml
这是一个针对我们推荐系统模型部署的.gitlab-ci.yml的生产级实现。
# .gitlab-ci.yml
variables:
# 使用时间戳和Git SHA生成唯一的模型版本
MODEL_VERSION: "${CI_COMMIT_TIMESTAMP}_${CI_COMMIT_SHORT_SHA}"
# 定义模型产物包的文件名
MODEL_PACKAGE_NAME: "recommendation-model"
# Python和依赖库版本
PYTHON_VERSION: "3.9"
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
stages:
- setup
- build
- test
- deploy
# ------------------------- Stage: setup -------------------------
# 使用模板来减少重复的缓存和前置脚本定义
.python_job_template: &python_job
image: python:${PYTHON_VERSION}-slim
cache:
key:
files:
- poetry.lock
paths:
- .venv
policy: pull-push
before_script:
- apt-get update && apt-get install -y curl
- curl -sSL https://install.python-poetry.org | python3 -
- poetry config virtualenvs.in-project true
- poetry install --no-root --no-dev
# ------------------------- Stage: build -------------------------
build_feature_pipeline:
stage: build
<<: *python_job
script:
- echo "Building feature engineering pipeline components..."
# 这里的脚本会打包特征处理相关的代码和配置
- poetry run build-feature-pipeline --output artifacts/feature_pipeline.tar.gz
artifacts:
paths:
- artifacts/feature_pipeline.tar.gz
expire_in: 1 week
# ------------------------- Stage: test -------------------------
train_and_validate_model:
stage: test
# 使用带有GPU标签的特定Runner
tags:
- gpu
# 使用包含CUDA和CUDNN的自定义镜像
image: nvidia/cuda:11.4.2-cudnn8-devel-ubuntu20.04
variables:
# 传递给训练脚本的超参数
ALS_RANK: "100"
ALS_ITERATIONS: "10"
before_script:
# 在GPU Runner上安装Python和依赖
- apt-get update && apt-get install -y python3.9 python3.9-venv python3-pip git
- python3.9 -m venv .venv
- source .venv/bin/activate
- pip install --upgrade pip
- pip install -r requirements.txt
- echo "NVIDIA Driver and CUDA Toolkit check:"
- nvidia-smi # 验证GPU是否可用
script:
- echo "Starting model training with version ${MODEL_VERSION}..."
- source .venv/bin/activate
# 假设有一个训练脚本,它会拉取数据、进行训练和评估
# --model-version, --output-path, 和 --metrics-path 是脚本参数
- python scripts/train.py \
--data-source s3://recommendation-data/latest.parquet \
--model-version ${MODEL_VERSION} \
--output-path artifacts/model.pkl \
--metrics-path artifacts/metrics.json \
--rank ${ALS_RANK} \
--iterations ${ALS_ITERATIONS}
# 验证模型的关键指标,例如RMSE。如果低于阈值,则job失败
- python scripts/validate_metrics.py --metrics-file artifacts/metrics.json --threshold 0.85
artifacts:
paths:
# 将模型和评估指标作为产物传递
- artifacts/model.pkl
- artifacts/metrics.json
expire_in: 1 week
# 依赖于特征管道构建完成
needs: ["build_feature_pipeline"]
# ------------------------- Stage: deploy -------------------------
package_and_upload_model:
stage: deploy
image: curlimages/curl:7.85.0
script:
- echo "Packaging model and metrics into a single archive..."
- tar -czvf "${MODEL_PACKAGE_NAME}-${MODEL_VERSION}.tar.gz" artifacts/model.pkl artifacts/metrics.json
# 使用GitLab Package Registry API上传模型包
# 这是一个非常关键的步骤,实现了模型的版本化存储
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
--upload-file "${MODEL_PACKAGE_NAME}-${MODEL_VERSION}.tar.gz" \
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/${MODEL_PACKAGE_NAME}/${MODEL_VERSION}/${MODEL_PACKAGE_NAME}-${MODEL_VERSION}.tar.gz"
- echo "Model version ${MODEL_VERSION} uploaded to GitLab Package Registry."
needs: ["train_and_validate_model"]
trigger_service_deployment:
stage: deploy
trigger:
# 触发推荐服务代码仓库的下游流水线
project: 'my-group/recommendation-service'
branch: 'main'
strategy: depend
variables:
# 将模型版本信息传递给下游流水线
# 下游流水线会根据这个版本号去Package Registry拉取对应的模型
MODEL_PACKAGE_NAME: ${MODEL_PACKAGE_NAME}
MODEL_VERSION: ${MODEL_VERSION}
needs: ["package_and_upload_model"]
流程图解
graph TD
subgraph "GitLab CI/CD Pipeline"
A[build_feature_pipeline] --> B(train_and_validate_model);
B -- on GPU Runner --> C(package_and_upload_model);
C -- Upload to GitLab Registry --> D[GitLab Package Registry];
C --> E(trigger_service_deployment);
E -- Trigger with MODEL_VERSION --> F[Recommendation Service Pipeline];
end
style B fill:#f9f,stroke:#333,stroke-width:2px
这个流程清晰地展示了GitLab CI/CD的优势:利用tags调度到GPU Runner,通过artifacts在作业间传递中间产物,最后利用CI_JOB_TOKEN和API将最终模型存入集成的Package Registry,并触发下游部署。整个过程都在GitLab生态内闭环。
方案B:CircleCI - 性能、缓存与可组合性
CircleCI的设计哲学是云原生和性能优先。它通过强大的缓存机制、可复用的Orbs以及灵活的Workflows来加速CI/CD流程。对于MLOps场景,CircleCI的Machine Executor是其应对异构计算资源的关键。
优势分析
- 高性能执行环境: CircleCI的作业启动速度通常快于GitLab。其Machine Executor提供了对GPU实例的直接访问(如AWS EC2的
g4dn.xlarge),性能稳定且无需自己维护底层环境。 - 卓越的缓存: CircleCI的缓存机制非常精细和强大。
save_cache和restore_cache指令可以基于key(如requirements.txt的校验和)进行多层次缓存,无论是Python依赖、数据集样本还是中间层模型,都可以被高效缓存,极大地缩短了重复执行的时间。 - Orbs与可组合性: CircleCI Orbs是可复用的配置包。我们可以使用社区提供的
aws-s3orb来轻松地与S3交互,将S3作为我们的模型存储。或者,我们可以编写自己的Orb,将模型上传、下载和验证的逻辑封装起来,供团队内多个项目使用。 - 工作流(Workflows): 与GitLab的
stages类似,Workflows提供了强大的作业编排能力,可以构建复杂的DAG,并且在UI上展示得非常直观。
劣势分析
- 生态系统分散: 使用CircleCI意味着你需要依赖外部服务来存储模型(如AWS S3, GCS或Artifactory)和容器镜像(如Docker Hub, ECR)。这增加了配置的复杂性和多平台管理的成本。
- 安全与合规: 数据和模型需要传输到CircleCI的云环境以及第三方存储中。对于有严格数据安全和合规要求的组织,这可能是一个障碍,需要额外的安全审查和网络配置(如使用Runner自托管)。
- 配置略显冗长: CircleCI的YAML配置虽然功能强大,但为了实现精细的缓存和步骤控制,配置文件可能会变得比GitLab的更长、更复杂。
核心实现概览: .circleci/config.yml
这是一个使用CircleCI实现相同逻辑的配置文件。
# .circleci/config.yml
version: 2.1
# 定义可复用的Orbs
orbs:
aws-s3: circleci/aws-[email protected]
# 定义可复用的执行器
executors:
python-executor:
docker:
- image: cimg/python:3.9.12
resource_class: medium
gpu-executor:
# 使用CircleCI提供的带有NVIDIA GPU的机器执行器
machine:
image: ubuntu-2004:202111-01
# 可根据需求选择不同的GPU资源类型
resource_class: gpu.nvidia.small
environment:
# 在机器上安装特定版本的CUDA
CUDA_VERSION: "11.4.2"
# 定义可复用的命令
commands:
install_python_deps:
description: "Install Python dependencies using pip"
steps:
- restore_cache:
keys:
- v1-dependencies-{{ checksum "requirements.txt" }}
- v1-dependencies-
- run:
name: Install dependencies
command: |
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
- save_cache:
paths:
- ".venv"
key: v1-dependencies-{{ checksum "requirements.txt" }}
install_cuda_driver:
description: "Install NVIDIA driver and CUDA toolkit on machine executor"
parameters:
version:
type: string
default: "11.4.2"
steps:
- run:
name: Install NVIDIA Driver and CUDA
# 这是一个耗时步骤,但在真实项目中可以被烘焙到自定义镜像中优化
command: |
sudo apt-get update
sudo apt-get install -y software-properties-common
sudo add-apt-repository ppa:graphics-drivers/ppa -y
sudo apt-get update
# 这里省略了完整的驱动安装脚本,实际中会更复杂
echo "Installing CUDA Toolkit version << parameters.version >>..."
# 伪代码:下载并静默安装CUDA Toolkit
echo "CUDA installed."
nvidia-smi
# 定义作业
jobs:
build_feature_pipeline:
executor: python-executor
steps:
- checkout
- install_python_deps
- run:
name: Build feature engineering pipeline
command: |
. .venv/bin/activate
mkdir -p artifacts
# 伪代码:打包特征处理组件
echo "feature pipeline" > artifacts/feature_pipeline.tar.gz
- persist_to_workspace:
root: artifacts
paths:
- feature_pipeline.tar.gz
train_and_validate_model:
executor: gpu-executor
steps:
- checkout
- install_cuda_driver:
version: ${CUDA_VERSION}
- install_python_deps
- run:
name: Train and Validate Model
# CircleCI的作业默认超时时间较短,对于长时间训练任务需要增加
no_output_timeout: 30m
command: |
. .venv/bin/activate
mkdir -p artifacts
python scripts/train.py \
--data-source s3://recommendation-data/latest.parquet \
--model-version "${CIRCLE_SHA1}" \
--output-path artifacts/model.pkl \
--metrics-path artifacts/metrics.json
python scripts/validate_metrics.py --metrics-file artifacts/metrics.json --threshold 0.85
- persist_to_workspace:
root: artifacts
paths:
- model.pkl
- metrics.json
upload_model_to_s3:
executor: python-executor
steps:
- checkout
- attach_workspace:
at: ./artifacts
- run:
name: Package model artifacts
command: |
tar -czvf "model-${CIRCLE_SHA1}.tar.gz" -C ./artifacts .
- aws-s3/copy:
from: "model-${CIRCLE_SHA1}.tar.gz"
to: "s3://my-recommendation-models/models/model-${CIRCLE_SHA1}.tar.gz"
aws-access-key-id: AWS_ACCESS_KEY_ID_VAR
aws-secret-access-key: AWS_SECRET_ACCESS_KEY_VAR
region: AWS_REGION_VAR
# 编排工作流
workflows:
version: 2
model_ci_cd_workflow:
jobs:
- build_feature_pipeline
- train_and_validate_model:
requires:
- build_feature_pipeline
- upload_model_to_s3:
requires:
- train_and_validate_model
# 可以在这里添加一个job,用于触发下游服务部署,例如通过API调用
# - trigger_deployment:
# requires:
# - upload_model_to_s3
流程图解
graph TD
subgraph "CircleCI Workflow"
A[build_feature_pipeline] --> B(train_and_validate_model);
B -- on Machine Executor (GPU) --> C(upload_model_to_s3);
C -- Using aws-s3 Orb --> D[AWS S3 Bucket];
end
subgraph "External Systems"
D
end
style B fill:#f9f,stroke:#333,stroke-width:2px
CircleCI的方案展示了其可组合的特性。我们通过persist_to_workspace在不同硬件环境(CPU docker 和 GPU machine)的作业间传递数据,并利用aws-s3 orb轻松地将最终产物推送到外部存储。整个流程配置清晰,但依赖于对CircleCI概念(Executors, Orbs, Workspaces)的深入理解。
最终选择与理由
在对两个方案进行深入评估后,我们最终决定选择GitLab CI/CD。这个决策并非基于技术上的绝对优劣,而是源于对当前团队结构、技术栈和长期维护成本的综合考量。
决策依据:
- 最小化工具链复杂性: 我们的团队已经深度使用GitLab进行代码托管和项目管理。引入CircleCI会增加一个新的平台,带来额外的学习成本、账号管理和上下文切换开销。将MLOps流程也纳入GitLab,可以保持技术栈的统一性,降低认知负荷。对于一个中等规模的团队而言,平台的整合价值超过了单一工具的极致性能。
- 数据安全与控制: 推荐系统模型是公司的核心资产,训练数据也可能包含敏感信息。使用自托管的GitLab GPU Runner,可以确保整个模型训练和处理流程都在我们自己的VPC内完成,数据不离开内部网络。这极大地简化了安全合规审计流程。
- 集成的模型注册表作为起点: 虽然GitLab的Generic package registry功能简单,但它为我们提供了一个“开箱即用”的模型版本化存储方案。它与CI/CD的无缝集成(通过
CI_JOB_TOKEN)避免了复杂的外部存储认证配置。在MLOps实践的初期阶段,这个简易的方案足以满足需求,未来可以平滑迁移到更专业的工具如MLflow或DVC,而流水线的主体结构无需大改。 - 维护成本的可接受性: 尽管自托管Runner需要维护,但我们的SRE团队已经具备丰富的虚拟机和Docker维护经验。为Runner标准化一个包含NVIDIA驱动和CUDA的Golden Image,可以大大降低日常维护工作量。我们认为,这种可控的维护成本,相比于管理多个云服务和处理潜在的网络安全问题,是更优的选择。
CircleCI在纯粹的CI性能和云原生集成方面无疑非常出色。如果我们的项目是一个开源项目,或者团队文化更倾向于“best-of-breed”(选择每个领域的最佳工具并集成它们),那么CircleCI可能会是更有吸引力的选项。但在我们当前追求内部流程闭环、简化工具链和强化数据管控的背景下,GitLab的一体化生态系统提供了更具实践价值的解决方案。
架构的扩展性与局限性
我们选择的GitLab CI/CD方案,虽然解决了当前的核心问题,但其边界和未来演进路径也必须清晰。
扩展性:
- 多模型支持: 当前流水线可以作为一个模板。通过GitLab的
include关键字,我们可以为不同类型的模型(如CF、FM、DeepFM)定义各自的训练作业,并共享相同的打包和部署阶段。 - A/B测试集成:
trigger_service_deployment作业可以被扩展,传递更丰富的元数据给下游服务部署流水线,例如模型的评估指标。下游流水线可以根据这些信息,自动化地配置服务网关(如Istio)的流量切分规则,实现模型的金丝雀发布或A/B测试。 - 集成专业MLOps工具: 当模型和实验数量激增时,我们可以引入MLflow。训练作业可以增加一个步骤,将模型、参数和指标记录到MLflow Tracking Server。GitLab Package Registry则退化为只存储最终上线的模型文件,而模型的元数据、血缘关系和实验对比则由MLflow管理。
局限性:
- 非专业的模型注册表: GitLab Package Registry缺乏模型治理的关键特性,如模型阶段转换(Staging, Production, Archived)、模型血缘可视化、以及与特定API端点的绑定。这是一个临时的解决方案,而非长久之计。
- 实验跟踪缺失: 当前流水线只关注“部署”,而非“实验”。数据科学家通常需要运行上百次参数调优实验,这个流水线无法高效地管理和比较这些实验的结果。
- 对大数据处理的依赖: 流水线假设数据已经预处理完毕。在更复杂的场景中,CI/CD流水线可能需要先触发一个Spark或Flink作业来生成训练数据,这会进一步增加流水线的复杂性和跨系统依赖。
这个基于GitLab CI/CD的MLOps流水线,是我们在工程效率、安全性和维护成本之间做出的一个务实权衡。它为团队建立了一套自动化、可重复的模型交付基线,但我们清楚地认识到,这只是MLOps漫长道路上的第一步。随着业务复杂度的提升,向更专业的MLOps平台演进将是必然的选择。