软件工程有一条反直觉的规律:集成的频率越高,每次集成的痛苦越小。这条规律从 1990 年代末被 Kent Beck 写进极限编程的实践清单,到今天演化出一整套涵盖构建、测试、部署、发布的自动化体系。CI/CD 不是某一个工具或某一种流程,而是三十年工程实践反复迭代的产物。

持续集成的诞生(1996-2006)

极限编程与 CI 的起源

1996 年,Kent Beck 加入克莱斯勒公司的 C3(Chrysler Comprehensive Compensation System)项目。这个项目此前陷入僵局,Beck 将它作为极限编程(XP)的试验田,引入了十二条核心实践,持续集成是其中之一。CI 的核心主张很简单:开发者每天多次将代码集成到主干,每次集成都触发自动构建和测试。1999 年出版的《Extreme Programming Explained: Embrace Change》正式将 CI 确立为一个命名实践。

CI 要解决的问题在当时有一个形象的说法叫"集成地狱"(integration hell)——团队各自开发数周甚至数月,最后合并代码时发现冲突遍地、Bug 丛生。CI 的药方是缩短集成周期,把痛苦分散到每天的小增量里。Martin Fowler 后来给了一句被广泛引用的评价:“持续集成并不能消除 Bug,而是让它们非常容易发现和改正。”

CruiseControl:第一个 CI 服务器

2001 年,ThoughtWorks 开源了 CruiseControl——业界公认的第一个专用 CI 服务器。CruiseControl 用 Java 编写,能在代码签入后自动触发构建,并通过邮件通知结果。此前团队要做 CI,只能靠 cron 脚本或手动触发,CruiseControl 把这件事变成了一个可配置的基础设施。ThoughtWorks 后来又推出了 CruiseControl.NET 和 CruiseControl.rb,覆盖 .NET 和 Ruby 生态。

ThoughtWorks 在 CI/CD 文化中的角色不止于此——Martin Fowler 长期担任 ThoughtWorks 的首席科学家,Jez Humble 也出自这家公司。CI/CD 方法论的理论化和工具化,很大程度上由 ThoughtWorks 这一组织推动。

Fowler 的十条准则

2006 年 5 月,Martin Fowler 在个人网站上发表了《Continuous Integration》一文(至今仍在更新),给出了 CI 的十条核心实践:

  1. 维护一个单一的源码仓库
  2. 自动化构建
  3. 让构建过程包含自测
  4. 每次提交都在集成机器上构建
  5. 保持构建速度(十分钟以内)
  6. 在生产环境的克隆上测试
  7. 让任何人都能轻松拿到最新可交付物
  8. 所有人都能看到构建结果
  9. 自动化部署
  10. 构建失败时立即修复

这十条准则至今仍是评判一个团队 CI 成熟度的基准线。很多团队号称"在做 CI",实际上只做到了自动化构建,连构建自测和构建速度都没有保障。

从 Hudson 到 Jenkins:一次影响深远的分裂

2005 年,Sun Microsystems 的工程师 Kohsuke Kawaguchi 开发了 Hudson,最初只是一个解决个人痛点的副项目。Hudson 凭借易用性和插件生态迅速增长,到 2009 年已经成为全球使用最广泛的 CI 服务器。

2010 年,Oracle 收购 Sun Microsystems 后,主张对 Hudson 商标的所有权,并试图将项目迁移到 Oracle 基础设施上。开发者社区对 Oracle 的治理方式产生严重不信任。2011 年 1 月,社区投票决定将项目 fork,新项目命名为 Jenkins(两个名字都来自英式管家的形象)。Oracle 保留了 Hudson 之名,但缺乏社区支撑的 Hudson 很快边缘化,Jenkins 则发展为全球最大的开源 CI/CD 生态系统。

持续交付的理论奠基(2010)

《持续交付》与部署流水线

2010 年 8 月,Jez Humble 和 David Farley 出版了《Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation》。这本书由 Martin Fowler 作序,在 CI 的基础上提出了一套完整的交付体系。

书中最核心的概念是部署流水线(Deployment Pipeline):代码从提交到上线,经过一条自动化的阶段序列——提交阶段(编译+单元测试)→ 自动验收测试 → 用户验收测试 → 预发布环境 → 生产环境。每个阶段都是一道质量关卡,任何环节失败都会阻断流水线。

Humble 还明确区分了两个容易混淆的概念。持续交付(Continuous Delivery)是指软件始终处于可发布状态,何时发布是一个业务决策,由人拍板。持续部署(Continuous Deployment)则是每次通过所有自动化测试的提交都自动部署到生产环境,不需要人工审批。多数企业实践的是持续交付;持续部署对自动化测试覆盖率和监控体系的要求极高,只有 Etsy、Amazon、Netflix 等少数组织在大规模实践。

书中还有一条被广泛引用的原则:“如果一件事做起来很痛苦,就更频繁地做它。”(If it hurts, do it more often.)这与 Kent Beck 在 CI 上的思路一脉相承——缩小批次,分散风险。

基础设施即代码

部署流水线要求环境可重复创建,这推动了基础设施即代码(Infrastructure as Code, IaC)的发展。IaC 工具的演进脉络如下:

年份 工具 特点
1993 CFEngine Mark Burgess 开发,最早的配置管理工具
2005 Puppet 声明式 DSL,代理模式
2009 Chef Ruby 编写基础设施代码,代理模式
2012 Ansible 无代理,YAML 声明,2015 年被 Red Hat 收购
2014 Terraform HashiCorp 出品,HCL 语言,云平台无关,plan/apply 工作流

从 CFEngine 到 Terraform,IaC 工具经历了从命令式到声明式、从代理到无代理、从单机到云原生的转变。Terraform 的 plan/apply 模式——先预览变更再执行——成为后来 GitOps 工作流的直接前身。

分支策略的三次范式转移

分支策略是 CI/CD 落地的源头。代码怎样在分支间流转,直接决定了集成频率和发布节奏。

GitFlow

2010 年 1 月 5 日,Vincent Driessen 发表了《A successful Git branching model》,提出了 GitFlow 模型。GitFlow 定义了五种分支角色:main(生产版本)、develop(开发主线)、feature(功能分支)、release(发布准备)、hotfix(线上修复)。这套模型适合版本化发布的产品——每隔数周出一个版本,需要并行维护多个版本。

GitFlow 在 2010-2016 年间成为最流行的分支策略。但 Driessen 本人在 2020 年给原文加了一条注释,承认 GitFlow 并不适合持续部署的场景,建议这类项目使用更简单的模型。

GitHub Flow

2011 年前后,GitHub 的 Scott Chacon 提出了 GitHub Flow:只有 main 分支加上短生命周期的功能分支,通过 Pull Request 审查后合入 main,合入即部署。这套模型极其简洁,适合 Web 服务类产品——没有"版本"的概念,每次合并就是一次发布。

主干开发

主干开发(Trunk-Based Development, TBD)的理念比 Git 更古老——所有开发者直接向一条主干分支提交代码。Paul Hammant 在 trunkbaseddevelopment.com 上系统推广了这种做法。2018 年,Forsgren、Humble 和 Kim 在《Accelerate》一书中,基于 DORA 的大规模调研数据,得出结论:主干开发是高效能软件交付的关键预测指标之一。

TBD 并不要求完全禁止分支,而是要求分支的生命周期极短——通常不超过一天。长生命周期的功能分支才是它所反对的模式。

容器化与云原生 CI/CD

Docker 统一了构建环境

2013 年 3 月,Solomon Hykes 在 PyCon 上发布了 Docker。Docker 对 CI/CD 的影响是根本性的:它让"在我机器上能跑"变成了"在任何机器上都能跑"。CI 服务器不再需要为每种语言和框架维护独立的构建环境,只需要一个 Dockerfile 就能复现完整的构建上下文。

构建环境的可重复性问题,在 Docker 之前一直是 CI 的痛点。不同机器上的依赖版本、系统库版本、编译器版本的微小差异,都可能导致"CI 过了但生产环境挂了"的问题。容器镜像把这些变量全部锁定。

Kubernetes 重塑编排

2014 年 6 月,Google 发布了 Kubernetes,脱胎于其内部的 Borg 和 Omega 系统。Kubernetes 在 2016 年加入 CNCF 后迅速成为容器编排的事实标准。

Kubernetes 对部署策略的影响尤其显著。滚动更新(Rolling Update)成为 Deployment 资源的内置策略,通过 maxSurge 和 maxUnavailable 两个参数就能控制更新节奏。更高级的部署策略——蓝绿、金丝雀——则借助 Istio、Argo Rollouts 等工具实现。

SaaS CI 与 GitHub Actions

CI 工具在 2010 年代经历了从自托管到云托管的转变:

年份 工具 意义
2010 Travis CI 第一个主流 SaaS CI,YAML 配置,与 GitHub 深度集成
2011 CircleCI SaaS CI,强调速度和并行化
2012 GitLab CI 与代码仓库统一产品,2016 年合并为 GitLab CI/CD
2018 GitHub Actions GitHub 原生 CI/CD,2019 年 11 月 GA
2019 Tekton CNCF 项目,Kubernetes 原生流水线

GitHub Actions 把 CI/CD 变成了代码托管平台的内置能力,工作流定义文件(YAML)和代码放在同一个仓库里版本管理。

GitOps:Git 即运维

2017 年,Weaveworks 的 CEO Alexis Richardson 提出了 GitOps 的概念:以 Git 仓库作为基础设施和应用配置的唯一事实来源,由控制器持续将集群状态与 Git 中的声明对齐。

GitOps 的两个代表性工具是 Flux(Weaveworks,2016 年启动,2022 年 CNCF 毕业)和 Argo CD(Intuit,2018 年推出,2022 年 CNCF 毕业)。GitOps 本质上是把 Terraform 的 plan/apply 模式搬到了 Kubernetes 集群管理上——想变更集群状态,提交一个 Git commit 即可,控制器负责执行和校正漂移。

部署策略:从全量到渐进

部署策略解决的核心问题是:新版本怎样进入生产环境,才能在发布速度和故障影响之间取得平衡。Martin Fowler 在博客中将金丝雀发布、滚动重启和 A/B 测试都归入"渐进式方法"(phased approach),认为它们是 ParallelChange 思想在部署领域的实践。

滚动重启(Rolling Restart)

滚动重启逐个替换集群中的节点:摘掉一个旧版本节点,启动一个新版本节点,健康检查通过后再处理下一个。新旧版本在整个过程中长时间共存。

这是最朴素的部署策略。优点是不需要额外资源,任何规模的集群都能用;缺点是回滚速度慢——出了问题需要反向逐个替换。此外,新旧版本长时间共存意味着接口必须做兼容,数据库 schema 变更尤其需要注意。

在 Kubernetes 中,滚动更新是 Deployment 的默认策略:

1
2
3
4
5
6
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1

maxSurge 控制最多可以多出几个 Pod,maxUnavailable 控制最多可以有几个 Pod 不可用。这两个参数决定了更新速度与可用性之间的折中。

蓝绿部署(Blue-Green Deployment)

蓝绿部署维护两套完全相同的生产环境。蓝集群承载当前线上流量,绿集群用于部署和验证新版本。新版本在绿集群上完成所有验证后,通过负载均衡器将流量一次性从蓝切换到绿。此时绿变成新的"蓝",原来的蓝作为回退环境保留。

这一概念由 Daniel North 和 Jez Humble 在 ThoughtWorks 期间提出,并在 2010 年《Continuous Delivery》一书中正式文档化。

蓝绿部署的核心优势是回滚极快——只需要把负载均衡器切回旧集群。缺点同样明显:需要双倍的基础设施资源,且切换瞬间的影响面是全量的。如果新版本带有一个只在高并发下触发的 Bug,蓝绿切换后全部用户同时受影响,比金丝雀发布的爆炸半径大得多。这是大部分公司不直接采用蓝绿部署的原因。

红黑部署(Red-Black Deployment)是蓝绿部署的同义变体,核心思路完全一致,只是换了一种颜色命名。Netflix 等公司倾向于使用这个术语。

金丝雀发布(Canary Release)

金丝雀发布的名字来源于煤矿中的金丝雀——矿工带金丝雀入矿井,金丝雀对有毒气体比人更敏感,一旦金丝雀出问题,矿工立即撤离。部署领域的金丝雀逻辑相同:先把一小部分流量(通常 1%-5%)导向新版本,如果监控指标正常,再逐步扩大比例,直到全量切换。

金丝雀发布的完整流程分为三个阶段。

第一阶段是无流量验证。新版本部署到生产环境后,先不分配任何真实流量,在生产级的基础设施上做冒烟测试。很多大厂的实现跳过了这一步,直接给金丝雀节点灌流量——这等于在用真实用户做冒烟测试。

第二阶段是内部流量验证。通过特性开关或路由规则,先让内部员工使用新版本。Facebook 的员工数量足够构成一个有统计意义的样本。Amazon 使用暗部署(Dark Launch)、蚂蚁金服使用灰度环境来实现类似的目的。

第三阶段是逐步放量。通过负载均衡器的路由策略,按比例将外部流量切入新版本节点。分流维度可以是节点权重、源 IP、地理位置,也可以基于用户属性做精确路由。

与蓝绿部署相比,金丝雀发布在同一套物理环境里完成渐进式替换,不需要双倍资源。代价是出了问题也需要逐步回滚,且在回滚完成之前系统处于有损服务状态。

在 Kubernetes 中,金丝雀发布通常借助 Istio 的 VirtualService 做流量分割:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: my-service
spec:
http:
- match:
- headers:
end-user:
exact: internal-tester
route:
- destination:
host: my-service
subset: v2
- route:
- destination:
host: my-service
subset: v1

上面的规则将 internal-tester 用户的流量路由到 v2,其余流量仍走 v1。

A/B 测试(A/B Testing)

金丝雀发布可以使不同人群体验不同版本,表面上与 A/B 测试非常相似。两者的区别在于目的:金丝雀发布的目的是验证新版本的稳定性,最终目标是全量替换;A/B 测试的目的是比较不同策略(UI 布局、推荐算法、定价方案等)的效果,不一定涉及软件版本的更替。

A/B 测试的根源在 Web 分析和直邮营销领域。Amazon 和 Google 在 2003-2004 年间就开始用 A/B 测试优化页面体验。在部署领域,A/B 测试的实现可以完全不依赖软件版本更新——通过特性开关把用户分组,对不同组施加不同策略即可。

金丝雀发布和 A/B 测试还有一个实际区别:对统计严格性的要求不同。A/B 测试需要控制变量、选定指标、计算统计显著性,是一个实验设计问题;金丝雀发布更关注的是错误率、延迟、资源消耗等运维指标,判定标准通常是"是否明显恶化"而非"是否统计显著"。

影子部署(Shadow Deployment)

影子部署将生产流量镜像复制到新版本服务上,但新版本的响应不返回给用户。真实用户完全无感,新版本在生产级流量下接受验证。

这种策略适合只读场景——API 查询、搜索排序、推荐引擎等。对于有写操作的服务,影子部署会导致数据重复写入或状态不一致,需要额外的隔离机制(比如写入影子数据库而非主库)。Istio 提供了 Traffic Mirroring 功能来支持影子部署。影子部署的资源消耗是双倍的,但因为不影响任何用户,它是所有策略中风险最低的验证手段。

特性开关(Feature Toggles)

特性开关的实践可以追溯到 2009 年。当年的 O’Reilly Velocity 大会上,Flickr 的 John Allspaw 和 Paul Hammond 做了一场名为"10+ Deploys Per Day: Dev and Ops Cooperation at Flickr"的演讲。Flickr 使用一种叫 flippers 的内部工具来控制功能发布——不需要重新部署,只需要翻转一个开关。

Martin Fowler 在 2010 年将这一实践系统化为 Feature Toggles 的概念。特性开关的核心设计要点是解耦"决策点"(代码中检查开关状态的位置)和"决策逻辑"(如何决定开关的值)。

根据 Fowler 的分类,特性开关分为四种。发布开关(Release Toggles)控制新功能的发布时机,功能稳定后应该从代码中删除,生命周期以天或周计。实验开关(Experiment Toggles)用于 A/B 测试,生命周期等同于实验周期。运维开关(Ops Toggles)用于运行时应急——限流、降级、熔断——生命周期可能长达数月甚至数年。权限开关(Permission Toggles)基于用户属性控制功能可见性,比如 VIP 专属功能、内测功能。

Facebook 的 Gatekeeper、Twitter 的 Decider、Google 的 Experiments 系统都是特性开关的大规模实践案例,这些系统在 2008-2011 年间独立发展出来。

特性开关的维护成本常被低估。开关数量增长后需要统一的管理平台、配套的监控告警和定期清理机制。残留的废弃开关会变成代码中的认知负担和潜在故障源。

渐进式交付(Progressive Delivery)

2018 年,RedMonk 分析师 James Governor 提出了 Progressive Delivery 这个术语,定义为"附带精细爆炸半径控制的持续交付"。渐进式交付不是一种具体的部署策略,而是一个统称,涵盖了金丝雀发布、蓝绿部署、A/B 测试、特性开关和影子部署。

渐进式交付的核心思想是将"部署"和"发布"解耦:部署是代码进入生产环境的技术动作,发布是用户看到新功能的业务决策。代码可以先部署但不发布(通过特性开关关闭),在内部验证充分后再逐步对外开放。

支撑渐进式交付的工具包括 LaunchDarkly 和 Split.io(特性开关平台)、Argo Rollouts 和 Flagger(Kubernetes 渐进式发布控制器)。

策略对比

策略 资源占用 回滚速度 爆炸半径控制 适用场景
滚动重启 中等 一般业务,资源受限
蓝绿部署 高(双倍环境) 极快 低(全量切换) 关键业务,要求秒级回滚
金丝雀发布 中等 高(精细分流) 需要精细流量控制的核心服务
A/B 测试 不适用 产品功能效果验证
影子部署 高(双倍计算) 不适用 极高(零用户影响) 只读服务验证,性能基准测试

数据库变更与部署策略的配合

部署策略决定了新旧版本的共存模式,而数据库 schema 变更在共存期间尤其棘手。

滚动重启和金丝雀发布要求新旧版本的应用代码同时兼容同一份数据库 schema。实践上的做法是只做加法不做减法——添加新字段而非修改字段类型,用应用层逻辑处理新旧字段的映射,全量切换完成后再清理旧字段。

蓝绿部署由于两套环境完全隔离,可以选择更激进的迁移方式:在切换前完成 schema 迁移和数据回填,或者采用双写模式在切换期间保持两边数据同步。

无论哪种策略,数据库变更的基本原则不变:不在线修改字段类型,使用版本化的迁移脚本,保留回滚方案,在生产之前充分测试迁移脚本。

DevOps 运动与度量体系

从 Velocity 大会到 devopsdays

2008 年 Agile 大会上,Patrick Debois 和 Andrew Shafer 在走廊里讨论了"敏捷基础设施"的话题——这次即兴对话被视为 DevOps 运动的概念性起点。

2009 年,Flickr 的 John Allspaw 和 Paul Hammond 在 O’Reilly Velocity 大会上发表了"10+ Deploys Per Day"演讲,展示了开发和运维协作下的高频部署实践。Patrick Debois 在比利时看了这场演讲的录像后深受触动,于同年 10 月在根特组织了第一届 devopsdays 大会——DevOps 这个词就此诞生。"devops"这个拼写是为了适应 Twitter 的字符限制,从"devopsdays"的 hashtag 缩略而来。

DORA 四指标

2018 年,Nicole Forsgren、Jez Humble 和 Gene Kim 出版了《Accelerate》,公布了 Google DORA 团队多年调研的结论。DORA 识别出四个衡量软件交付效能的关键指标:部署频率(Deployment Frequency)、变更前置时间(Lead Time for Changes)、变更失败率(Change Failure Rate)和服务恢复时间(Time to Restore Service, MTTR)。

这项研究的价值在于用大规模实证数据证明了 CI/CD、主干开发和松耦合架构与交付效能之间的因果关系。数据表明速度和稳定性不矛盾——高效能团队在四个指标上同时领先。

供应链安全

2020 年 12 月的 SolarWinds 事件是 CI/CD 安全意识的转折点。攻击者在 SolarWinds 的构建流水线中注入恶意代码,通过正常的软件更新渠道分发到数千个客户组织。这次事件暴露了一个事实:CI/CD 流水线本身也是攻击面。

此后,业界围绕构建流水线的安全做了系统性的防御工作。Google 主导的 SLSA(Supply-chain Levels for Software Artifacts)框架在 2021 年发布,定义了从 L1 到 L4 的供应链完整性等级。Sigstore 项目在 2022 年 GA,提供了对构建产物进行密码学签名和验证的开源方案。这些工具正在被集成到主流 CI/CD 平台中,成为流水线的标配环节。

参考资料