流水线的价值不是把若干命令搬进自动化平台,而是建立一条可追溯、可重复、能在错误扩散前停下来的交付路径。设计时应从反馈速度与风险暴露顺序出发:成本低、执行快、失败概率高的检查靠前,昂贵或会改变环境状态的动作靠后。
阶段划分与快速失败
一条常见路径可以拆成六段,各段只输出下一段真正需要的结果:
| 阶段 | 核心动作 | 失败后的处理 |
|---|---|---|
| lint | 格式、静态规则、配置语法 | 立即停止,不占用构建资源 |
| 构建 | 编译、打包、生成元数据 | 保留编译日志,禁止部署 |
| 单测 | 隔离验证业务逻辑,输出覆盖率 | 阻断制品发布 |
| 制品 | 生成镜像或软件包,签名并入库 | 已发布版本不得覆盖 |
| 部署 | 按目标环境应用变更 | 标记发布批次,允许回滚 |
| 验证 | 冒烟、健康检查、关键业务探针 | 停止放量或触发回滚 |
快速失败不等于所有检查都串行。lint、依赖漏洞检查和不依赖编译产物的单测可以并行启动;构建完成后,再并行执行集成测试、镜像扫描与制品签名。关键是明确依赖图,而不是用一个巨型 job 顺序执行全部脚本。
stages: [check, build, test, publish, deploy, verify]
lint:
stage: check
script: ["make lint"]
unit:
stage: test
needs: ["build"]
script: ["make test-unit"]
smoke_staging:
stage: verify
needs: ["deploy_staging"]
script: ["./ci/smoke.sh https://staging.example.internal"]
并行化不能突破共享资源边界。若多组测试共用同一个数据库,数据互相污染会制造随机失败。更稳妥的做法是为每个 job 创建独立 schema 或临时实例,并为 runner 设置并发上限,防止流水线把依赖服务压垮。
缓存不是制品
依赖缓存用于保存可重新下载的第三方包,如 Maven、npm 或 Go 模块缓存;构建缓存保存编译中间结果,如对象文件、增量编译目录。两者都允许丢失,缓存未命中只应变慢,不应改变构建结果。可部署制品则必须长期保存、校验完整性并可审计,不能用缓存目录代替制品库。
缓存 key 应包含真正影响结果的输入:操作系统、运行时版本、架构与锁文件摘要。把分支名作为唯一 key 会造成同一分支更新依赖后仍读到旧内容;把提交哈希作为唯一 key 又几乎无法复用。
deps-${os}-${arch}-${runtime}-${hash(lockfile)}
build-${compiler_version}-${hash(source_and_flags)}
缓存恢复可使用由精确到宽泛的前缀,但保存时只写精确 key。来自不可信分支的缓存不要供受保护分支复用,避免缓存投毒。缓存目录中也不应出现访问令牌、签名私钥或环境配置。
不可变制品与版本号
同一次提交只构建一次。测试、预发布和生产环境使用同一个制品,通过“提升”而不是重新构建进入下一环境,否则编译器版本、依赖源或构建时间差异都可能让已验证内容与生产内容不一致。
版本号需要同时满足人可读与机器唯一。可采用语义化版本加提交短哈希,如 2.4.1+g8ac91ef;容器镜像额外记录内容摘要。部署清单应固定到摘要:
image: registry.internal/order@sha256:4d2c...9a10
标签可以移动,摘要不会。回滚时选择上一份已验证摘要,而不是重新推送同名标签。制品元数据还应记录源提交、构建任务、依赖清单和签名,便于定位一份二进制究竟从何而来。
门禁放在风险发生之前
代码质量门应位于制品发布之前,包括单测结果、最低覆盖率策略和静态分析。安全扫描也要放在风险发生之前:源代码与依赖扫描适合在构建阶段并行执行;镜像漏洞扫描必须在镜像生成后、部署前完成;动态安全测试依赖运行中的系统,通常放在 staging 部署后、生产提升前。覆盖率不是越高越安全,应重点阻断覆盖率突然下降或关键模块无测试。
门禁规则要固定严重级别、例外审批人和到期时间。扫描器暂时不可用时不能默默通过,可根据风险选择阻断或进入人工审批。历史遗留问题可建立基线,只阻止新增高风险项,避免一次性门槛让流水线长期失去执行力。
环境提升与配置差异
dev → staging → prod 应是一条制品晋级链。环境差异只存在于外部配置,例如副本数、域名、资源额度和凭据引用,不应通过重新编译产生三个镜像。通用模板与环境值分离,并在部署前渲染、校验差异:
deploy/
base/
env/dev.yaml
env/staging.yaml
env/prod.yaml
敏感值只保存引用,真实密钥由运行环境的密钥系统注入。生产提升可设置人工审批,但审批页面必须展示提交范围、制品摘要、测试证据和基础设施变更,而非只放一个“确认”按钮。审批是风险判断,不是替代自动检查。
流水线本身也是代码
流水线定义、脚本与部署模板应进入版本控制,经过评审并由测试验证。复用宜围绕稳定能力建立模板,例如“构建并签名容器”或“部署后冒烟”,业务仓库只传入少量参数。模板版本要固定,升级通过合并请求完成;直接引用主分支会让大量项目在未评审时同时改变行为。
可以为脚本编写本地可执行入口,使开发者在提交前运行与 CI 相同的 make lint、make test。对流水线配置做语法测试,对部署模板做静态渲染,对关键发布脚本用临时环境演练,才能避免“自动化系统本身没有测试”的盲区。
一个巨型 job 会让失败定位、并行调度和局部重试都变困难;把部署、验证与回滚揉成一段脚本还可能在重试时重复修改环境。另一类危险做法是把密钥写进脚本或变量默认值,日志展开、调试输出和仓库历史都可能留下副本。发布链路应做到阶段职责单一、输入输出明确、凭据短期注入,并且每次环境变更都能追溯到不可变制品与经过评审的流水线版本。
重试、超时与失败路径
重试只适合网络抖动、临时限流等瞬态错误,编译失败和断言失败不应自动重跑,否则会掩盖不稳定测试。每个外部调用都要设置超时,job 也要有总时限;取消旧提交的流水线可以节省资源,但正在执行生产变更的任务不能被粗暴终止,应进入可恢复的部署事务。
部署脚本需要区分“未开始”“执行中”“已完成”三种状态。若任务在应用资源后、记录结果前中断,重试必须通过查询实际环境判断后续动作,而不是盲目再执行一次。可用并发锁保证同一环境同一时刻只有一个发布者,同时让只读验证任务继续运行。
流水线失败信息应直接指出阶段、命令、制品版本和日志位置。对偶发失败建立分类统计,修复测试隔离或外部依赖,不要用无限重试换取表面上的成功率。定期从代码提交演练到 staging,并抽查生产回滚路径,才能确认门禁、权限和历史制品在需要时仍然可用。
评论(2)
看完突然理解了我们老大为什么坚持这么设计。
想问下你实际压测的 QPS 大概什么量级?
请 登录 后参与评论。