流水线基础:流水线定义、阶段划分、触发机制、产物管理
流水线(Pipeline)是 CI/CD 的核心执行载体,它将“代码提交”到“生产部署”之间的所有自动化步骤,组织为一条可重复、可观测、可追溯的执行链。理解流水线,不能只把它看作“一串脚本”,而要把握四个维度:定义方式(如何描述)、阶段划分(如何拆分)、触发机制(何时运行)、产物管理(产出什么、如何传递)。
一、核心原理:流水线的声明式描述
现代流水线普遍采用声明式(Declarative)定义,而非命令式脚本。声明式的核心价值在于:你描述“想要什么状态”,而不是“一步步怎么做”。以 Jenkins 为例,一个最小流水线如下:
pipeline {
agent any
stages {
stage('Build') {
steps { sh 'make build' }
}
stage('Test') {
steps { sh 'make test' }
}
}
}
这段定义表达了三个关键信息:执行环境(agent)、阶段列表(stages)、每个阶段的具体动作(steps)。声明式流水线天然具备以下特性:
- 可读性:结构清晰,新人也能快速理解整体流程。
- 可重试:失败后可从指定阶段恢复,而非从头执行。
- 可并行:不同阶段或同一阶段的不同分支可并行执行。
- 可观测:每个阶段的开始、结束、耗时、状态都被平台记录。
二、阶段划分:从提交到上线的标准路径
阶段(Stage)是流水线的骨架。合理的阶段划分应遵循“高内聚、低耦合”原则——每个阶段只做一类事情,阶段之间通过产物(Artifact)或测试报告传递信息。一个典型的部署流水线通常包含以下阶段:
| 阶段 | 核心任务 | 典型产物 | 失败处理 |
|---|---|---|---|
| 检出(Checkout) | 拉取源码、切换分支、获取指定提交 | 工作区代码 | 直接失败,无需继续 |
| 构建(Build) | 编译、打包、生成可执行文件或镜像 | JAR/WAR、Docker 镜像、二进制文件 | 快速失败,节省资源 |
| 测试(Test) | 单元测试、集成测试、静态扫描 | 测试报告、覆盖率报告 | 失败则阻断后续阶段 |
| 部署(Deploy) | 推送镜像、更新服务、执行数据库迁移 | 部署版本号、环境状态 | 需考虑回滚策略 |
| 验证(Verify) | 冒烟测试、健康检查、监控指标确认 | 验证日志、监控快照 | 失败触发自动回滚 |
阶段划分的粒度需要平衡:过粗(如“构建+测试”合并)会导致问题定位困难;过细(如每个测试用例一个阶段)则增加调度开销。经验法则是:每个阶段应能在 5 分钟内判断成败,且失败后能明确知道是代码问题、环境问题还是配置问题。
三、触发机制:让流水线在正确时机运行
触发机制决定了流水线的“启动条件”。错误的触发策略会导致资源浪费或漏检。常见的触发方式包括:
- 代码提交触发(Push):最常用,每次 push 到指定分支即触发。适合开发分支的快速验证。
- 合并请求触发(Merge Request / Pull Request):在 MR 创建或更新时触发,通常只运行测试和静态检查,不部署。
- 定时触发(Schedule):如每晚 2 点运行全量测试,适合耗时较长的回归测试。
- 手动触发(Manual):生产环境部署通常需要人工确认,避免自动化误操作。
- 上游依赖触发(Upstream):当依赖的镜像或库更新时,自动触发下游项目重建。
以 GitLab CI 为例,通过 rules 关键字可以精细控制触发条件:
deploy-prod:
stage: deploy
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
allow_failure: false
- when: never
script:
- ./deploy.sh --environment=prod
这段配置表达:只有 main 分支的提交才显示“手动部署”按钮,其他分支一律不执行该任务。这种“条件 + 动作”的声明方式,比在脚本里写 if 判断更直观、更安全。
四、产物管理:连接阶段的“接力棒”
产物(Artifact)是流水线阶段之间传递的“中间成果”。没有产物管理,每个阶段都要重新拉代码、重新构建,效率极低且无法保证一致性。产物管理需要关注三个层面:
1. 产物的定义与留存:在流水线中明确声明哪些文件需要保留。例如 Jenkins 中:
post {
success {
archiveArtifacts artifacts: 'build/libs/*.jar', fingerprint: true
}
}
这段配置将构建生成的 JAR 包归档,并记录其指纹(哈希值),便于追溯“哪个构建产生了哪个包”。
2. 产物的版本与命名:产物必须与构建号或提交号关联。推荐命名规则:应用名-版本号-构建号.扩展名,例如 order-service-1.4.2-87.jar。避免使用“latest”或“final”这类模糊名称。
3. 产物的传递与消费:下游阶段(如部署)应直接从产物库拉取,而不是重新构建。在 GitLab CI 中,通过 dependencies 关键字控制:
deploy:
stage: deploy
dependencies:
- build
script:
- scp target/app.jar user@server:/opt/app/
这里 dependencies: [build] 表示 deploy 任务只下载 build 任务产生的产物,避免下载无关文件。
五、案例:一个完整的微服务部署流水线
假设有一个 Java 微服务 payment-service,采用 GitLab CI + Kubernetes 部署。流水线定义如下:
stages: [build, test, package, deploy]
build:
stage: build
script:
- mvn compile
artifacts:
paths: [target/classes/]
test:
stage: test
script:
- mvn test
artifacts:
reports:
junit: target/surefire-reports/TEST-*.xml
package:
stage: package
script:
- mvn package -DskipTests
- docker build -t registry.example.com/payment-service:$CI_COMMIT_SHORT_SHA .
- docker push registry.example.com/payment-service:$CI_COMMIT_SHORT_SHA
dependencies: [build]
deploy-staging:
stage: deploy
script:
- kubectl set image deployment/payment-service payment-service=registry.example.com/payment-service:$CI_COMMIT_SHORT_SHA
environment: staging
only: [main]
dependencies: [package]
这条流水线的执行逻辑:
- build 阶段编译源码,保留
target/classes供后续阶段使用。 - test 阶段运行单元测试,生成 JUnit 报告,测试失败则流水线终止。
- package 阶段跳过测试(因为已测过),构建 Docker 镜像并推送仓库,镜像标签为提交 SHA。
- deploy-staging 阶段仅 main 分支触发,使用
kubectl set image滚动更新到预发环境。
该案例体现了三个关键设计:阶段间通过产物传递(build 的 classes 给 test,package 的镜像给 deploy)、触发条件分层(测试每次提交都跑,部署仅 main 分支)、镜像不可变(用提交 SHA 作为标签,避免覆盖)。
六、误区与复盘:流水线设计的常见陷阱
表现为:在一个 stage 里写几百行 shell,包含编译、测试、部署、发通知。后果:无法定位失败环节,无法并行,无法复用。
正确做法:每个 stage 只做一件事,失败后能立即判断是代码问题还是环境问题。
表现为:每次构建都归档全部产物,从不清理。数月后磁盘爆满,构建开始失败。
正确做法:设置保留策略,如“保留最近 30 次构建的产物”或“按天清理 7 天前的包”。
表现为:开发、测试、生产使用完全相同的阶段和参数,导致生产部署时才发现配置差异。
正确做法:通过环境变量或配置文件区分,但阶段结构保持一致。例如
deploy 阶段根据 $ENVIRONMENT 选择不同的 Kubernetes 命名空间。
复盘要点:设计流水线时,先画一张“阶段-产物-触发条件”的表格,明确每个阶段的输入、输出、失败策略。这张表比写代码更重要。流水线的本质是把部署流程工程化,而不是简单地把命令串起来。好的流水线应该让新人也能看懂流程,让机器自动处理重复劳动,让失败快速暴露且可追溯。
上图展示了流水线的完整执行链路:左侧的触发机制决定何时启动,中间的阶段序列按顺序执行,产物在阶段间传递,右侧的产物管理确保可追溯性,底部则强调失败处理和质量门禁。理解这张图,就掌握了流水线设计的核心框架。