跳到主要内容

软件交付:从 Git 提交到真实用户

这一章讲三件事: 交付流水线到底分几段、每段干什么;分支策略怎么影响交付; 以及新版本怎么一步步安全地放到用户面前。 读完你会知道「代码合并了」和「用户用上了」之间隔着什么,以及每一层的保险丝。

1. 这一章讲什么

第 05、06 章的两道闸都过了,代码合并进主干——然后呢?原书的立场很明确:你应该了解你的代码最终如何出现在用户面前,那些在 Git 提交和实时流量之间的步骤不应该是一个谜——即使它由别人自动执行1

本章在全书链条里的位置:它是「运维之海」站点的入口。交付做完,系统才算真正「长」在用户身上;接下来第 08 章讲它坏了怎么办。

2. 顶层全景

构建 build ──→ 发布 release ──→ 部署 deploy ──→ 展开 rollout
编译+打包 包进仓库+公告 安装到机器 把用户流量移到新版本
(带版本号、 (变更日志/ (原子安装、 (特性开关/金丝雀/
分平台) 发行说明) 可回滚) 蓝绿/摸黑启动)

用户流量 ──────────────────────────────┘ ← 展开完成才算「交付」

图说:四个词在行业里常被混用;原书先钉死定义再谈最佳实践。
主走查在 3.4:一个 deb 包走到 1% 金丝雀流量的全程。

3. 核心原理

3.1 四个阶段,四个词

先把术语统一——原书开篇就承认,交付阶段没有行业标准的定义,「发布」和「部署」跟不同的人聊可能是完全不同的环节2。本书沿用原书的四段划分3:

  • 构建:把代码变成软件包。软件包应该不可变、带版本号——构建一次,到处安装,不再每台机器现编4;
  • 发布:把包发行到集中的仓库,同时更新发行说明和变更日志;
  • 部署:把包安装到预生产和生产环境。此时用户还访问不到——只是装上了而已;
  • 展开:把用户逐渐转移到新版本上。展开完成,交付才算完成。

这四段各自有一套最佳实践,下面按段走。

3.2 分支策略:交付的地面轨道

交付前还有一个决定性变量:代码在版本控制里怎么流。原书把分支策略分成两大系5:

基于主分支的开发(主干式):所有人都在主干上工作,分支只用于单个小特性或修 bug,几小时到几天内就合回去。频繁合并有个名字——持续集成(CI):变化迅速传给所有人,谁也不会跑偏太远;代价是主干上的 bug 拖累所有人,所以合并前要跑快速自动化测试,团队的普遍共识是主干应该总是可以发布6

基于特性分支的开发:多个人在长期存续的分支上各养一个特性,发布前才汇入发布分支。最流行的是 Gitflow(开发分支+发布分支+热修复分支的组合),由文森特·德里森在 2010 年归纳7。但原书立刻补了一句今天读来最重要的话:德里森本人后来修改了那篇博文,不再鼓励把 Gitflow 用于做持续集成和交付的软件8

原书的推荐与这个演变一致:除非真的需要长期特性分支,否则坚持主干式——管特性分支本身就会变得很复杂9。这条判据同时回答了第 04 章埋的问题:为什么 API 和数据要向前向后兼容(第 10 章展开)——因为只有兼容,新旧版本才能共存,部署才不必排队。

3.3 构建与发布:包的纪律

构建出来的包要带身份。 包名里就要能读出版本和平台,比如 Linux 的包名 mysql-server-8.0_8.0.21-1_amd64.deb——同一个 MySQL 版本,分别为 AMD64、ARM64、Intel 386 三种架构各打一包10。没有版本号的包,你无法把「正在运行的东西」对应回「哪份源码、哪些特性、哪份文档」。

不同的资源分开打包。 配置、schema、图片、语言包和代码的发布节奏、构建时间、验证需求都不同——分开打,才能各自独立地向前和向后滚动;面向客户的完整安装包则是一个「元包」:装所有包的包11。原书用 Python 的打包生态画了一整套洋葱:.py 源文件 → sdist(源码压缩包)→ wheel(连编译好的本地库一起带)→ PEX(连依赖一起带)→ 冻结(把解释器也一并打进包里,即原书说的 Freezers)→ 容器/镜像(连操作系统一起带)12离代码越远的一层,包含的「环境」越多——你选哪层打包,决定了部署时还要装什么。

发布段的四条纪律。 第一,责任在你:即使组织有发布工程团队,归根结底你有责任确保软件得到适当的部署和良好的运转——他们不像你那样了解你的代码13。第二,发到专用仓库(Docker Hub、PyPI、Maven Central 这类):仓库为部署而生、可搜索、可回滚;用 Git 仓库当发布仓库是权宜之计,开发的小提交和部署的大并发会互相拖累14。第三,已发布的包不可变:所有运行「1.2.3」的实例在字节(数据的最小存储单位)层面上一模一样——会变的版本号,等于没有版本号15。第四,频繁发布:较长的发布周期给人一种错误的安全感,实践中快速的发布反而更稳定——每个版本的变化少、风险小,出了 bug 要回看的改动少,而且代码在开发者脑子里还是新鲜的16

让用户知道发生了什么。 变更日志(逐条列出本版修了哪些工作单)主要给支持团队和开发团队看;发行说明(新特性和修复的汇总)专门给用户看17。想看一套完整的仪式,原书指了 Apache 软件基金会:发布经理用密钥签名、附校验和,委员会投票验收候选版,再公告到邮件列表18

3.4 主走查:一个包从主干到 1% 的流量

把全章机制串在一根线上。场景:你的团队给一个 Linux 服务改了个 bug,要走完整条流水线(具体版本号和百分比为演示编的,流程照实)。

① 构建 CI 从主干切出的合并提交打出包:
myservice_2.13.7-1_amd64.deb ← 包名自带版本与架构
② 发布 包上传到内部包仓库,变更日志自动从工作单生成,
发行说明写给人看:「修复登录偶发超时」
③ 部署 部署脚本把 2.13.7 装到全新目录 /opt/myservice-2.13.7/,
不覆盖任何旧文件;装完把软链接 /opt/myservice
原子化地翻转到新目录 ← 原子部署
(回滚 = 把软链接翻回旧目录,秒级)
④ 展开 负载均衡器把 1% 的用户流量路由到新版本,其余 99%
仍走 2.13.6;盯错误率、P99 延迟半小时,无异常再放
宽到 10% → 50% → 100% ← 金丝雀部署
若指标劣化:流量全部切回旧版,或用特性开关
一键关掉新路径

三处机制值得停一下。为什么部署要「装到新位置+翻软链接」:原书对原子部署的解释是——安装脚本多步骤,不要假设每步都成功;部分完成的安装不能覆盖上一个好版本,而「新位置安装+原子翻转链接」让同一台机器可以多次安装同一个包,回滚也只是再翻回去19

为什么敢把 1% 的用户先送上去:这就是金丝雀部署——金丝雀版本先接到全量中一个很小的子集(原书的图例是 1% 的入站流量),它是新版本的早期预警系统;运转不良只伤一小撮用户,而且可以立刻切回旧版20

如果连「一部分用户」都切不动怎么办:那是蓝绿部署的场子——两个完整的集群(各自是一组一模一样的机器),蓝色跑旧版、绿色跑新版,流量切换是原子化的整批翻转;代价是每个环境必须扛得住 100% 的用户流量21

3.5 展开的五种降险模式

主走查里出现了两种,原书总共给了五种,各有分工:

模式机制适合
特性开关代码包在 if 里,按标志位决定走新路径还是旧路径;可以是布尔、允许列表(只对名单里的用户开)、百分比斜坡(从测试账号开始逐步放量)、甚至小函数22功能级灰度;代价是状态管理:数据库通常不受开关控制,新旧代码要读写同一批表,代码必须向前向后兼容,关掉再开时状态还在23
熔断器特殊的开关:二进制、永久性、自动化——由运维事件(延迟峰值、异常)触发而非人来扳24防性能劣化(超过阈值——预先设好的临界值——自动禁用特性);也防不可逆损害——发邮件、转账这类做了就不能撤的操作,拿不准就熔断25
金丝雀小比例流量先进新版高流量、多实例的服务
蓝绿两套环境整批翻转流量切不出子集、或新旧版本无法并行服务的场景
摸黑启动(影子流量)新代码接真实流量但丢弃结果——用户看不到它,它是坏了也没人受伤26验证系统迁移;分暗中读取(共用生产数据存储)和暗中写入(独立存储)两档27

两个实操提醒。其一,别贪多:这些模式全都增加运维复杂度——要同时支持多个版本、跟踪哪些开关开着;精心设计、留给大改动用28。其二,开关要还债:淘汰掉的旧开关要清理——原书提醒:开关一多,代码就很难推理——很难想清楚所有开关组合下的行为——还会滋生 bug;清理要有纪律:建任务票、渐进地删29

摸黑启动还有一个高阶玩法:开源工具 Diffy 向后端发影子流量——两个实例跑生产代码、一个跑候选代码,新版与旧版的差异是「新代码的效果」,两个旧版实例之间的差异是「环境的噪声」——自动把噪声和真差异分开30。原书还给了 Twitter 的真实案例:一个测试特别棘手的老服务,团队靠自研的「暗中写入」对比流对象,拿真实流量当测试集,边缘案例一个个现形、补成测试用例——测试套件变厚、开发周期变短,「黑夜带来了黎明」31

3.6 独立部署:一条禁令的代价

最后补一条本章最贵的教训。如果服务 A 必须先于服务 B 部署,交付就会排队甚至死锁。原书给了 LinkedIn 的历史:当年手动部署,服务互相调用、形成循环依赖,一次部署要拆成 20 多个环节,工程师和 SRE(网站可靠性工程师)在聊天频道里守到凌晨,一步失败全线卡住——LinkedIn 最终禁止了顺序部署:服务必须支持独立部署,否则不允许交付;禁令带来大量改造工作,但换来了部署完全自动化、开发者每天可发布多次、所有人多睡了很多觉32

这条禁令的实现手段,一半在别人手里:通信协议必须让新旧版本互相操作——这正是第 10 章「兼容性」整章要讲的东西。

4. 作者的判断与证据

  • 「主干式优于 Gitflow」:作者判断,并援引了 Gitflow 作者本人 2010 年博文的事后修正——这是本章最硬的外部证据。
  • 「快速发布产生更稳定软件」:作者判断,论证是机制性的(单版变更少、回看范围小、代码还在脑子里),没有给统计。
  • LinkedIn 顺序部署禁令:亲历故事,有具体规模(20 多个环节、通宵 IRC),是「独立部署」判据的证据。
  • Twitter 暗中写入案例:亲历故事,展示摸黑启动作为「安全网→敢重构」的杠杆。
  • 五阶段降险模式:业界通行实践的转述,原书自警「不要疯狂使用」。

5. 边界与局限

  • 术语本就没有行业标准:原书自认,本章的「构建/发布/部署/展开」是本书口径,和别的团队协作时要先对词。
  • 云原生时代的外科手术缺席:书成于 2021 年前夜,自动管理成排容器的基础设施(如 Kubernetes)自带的滚动更新、就地回滚没有讨论;金丝雀/蓝绿在那些平台上多是内置能力。
  • 特性开关的治理:讲了「要清理」,没讲开关数量爆炸后怎么审计、怎么验证「所有开关组合都测过」。
  • 数据库 schema 变更与展开的协调只有一句「需要特别小心」,展开见第 10 章。

6. 可带走的

  1. 先对词再干活:构建→发布→部署→展开,四段各有各的事,「部署了」不等于「上线了」;
  2. 包要带版本、按平台拆、不同资源分开发;离代码越远的一层带的环境越多;
  3. 已发布的包不可变——字节级一致才谈得上推理和回滚;
  4. 快速发布不是冒险,是保险:单版变更少、可回看范围小、代码还在脑子里;
  5. 变更日志给支持/开发看,发行说明给用户看,两种都要写;
  6. 部署要原子:装到新目录+翻转软链接,回滚就是翻回去;
  7. 展开五模式按需选:开关管功能、熔断器管自动刹车、金丝雀管流量、蓝绿管整批、摸黑启动管「看真实流量但不冒险」;
  8. 开关是债:清理旧开关要建任务票、渐进地删;
  9. 争取独立部署——「必须先部署 A 再部署 B」是排队和凌晨告警的源头,兼容性是解药;
  10. 代码展开完成、指标看过了,才许开香槟。

7. 原文地图

主题原书章原文位置
不该是谜第8章 软件交付text/17-ch08.txt:3(搜「出现在用户面前」)
四阶段第8章 软件交付text/17-ch08.txt:23(搜「rollout」) · text/17-ch08.txt:28(搜「只是被安装了而已」)
术语无标准第8章 软件交付text/17-ch08.txt:15(搜「行业标准的定义」)
主干式与 CI第8章 软件交付text/17-ch08.txt:45(搜「主干或主线」) · text/17-ch08.txt:67(搜「持续集成(CI)」) · text/17-ch08.txt:74(搜「总是可以发布」)
Gitflow 与修正第8章 软件交付text/17-ch08.txt:88(搜「Gitflow」) · text/17-ch08.txt:113(搜「不再鼓励」)
包名与版本第8章 软件交付text/17-ch08.txt:133(搜「mysql-server」) · text/17-ch08.txt:151(搜「纳入版本管理」)
元包第8章 软件交付text/17-ch08.txt:168(搜「元包」)
Python 打包洋葱第8章 软件交付text/17-ch08.txt:172(搜「Python 打包」) · text/17-ch08.txt:141(搜「wheel」)
责任在你第8章 软件交付text/17-ch08.txt:243(搜「归根结底」)
发布仓库第8章 软件交付text/17-ch08.txt:253(搜「存储资源库」) · text/17-ch08.txt:277(搜「运维上的问题」)
版本不变性第8章 软件交付text/17-ch08.txt:287(搜「字节层面上都一样」)
频繁发布第8章 软件交付text/17-ch08.txt:293(搜「较长的发布周期」)
变更日志 vs 发行说明第8章 软件交付text/17-ch08.txt:320(搜「专门给用户看」)
ASF 流程第8章 软件交付text/17-ch08.txt:327(搜「指定一名发布经理」)
自动部署与持续交付第8章 软件交付text/17-ch08.txt:355(搜「手动步骤」) · text/17-ch08.txt:362(搜「持续交付」) · text/17-ch08.txt:371(搜「Terraform」)
主走查:原子部署第8章 软件交付text/17-ch08.txt:383(搜「全部完成要么什么都没做」) · text/17-ch08.txt:389(搜「软链接」)
LinkedIn 禁令第8章 软件交付text/17-ch08.txt:422(搜「20 多个部署环节」) · text/17-ch08.txt:433(搜「最终禁止」)
特性开关第8章 软件交付text/17-ch08.txt:482(搜「feature flag」) · text/17-ch08.txt:486(搜「允许列表」) · text/17-ch08.txt:493(搜「状态突变」) · text/17-ch08.txt:502(搜「淘汰或不再使用」)
熔断器第8章 软件交付text/17-ch08.txt:515(搜「熔断器是一种特殊的」) · text/17-ch08.txt:525(搜「永久性的」) · text/17-ch08.txt:529(搜「不可逆行动」)
主走查:金丝雀 1%第8章 软件交付text/17-ch08.txt:544(搜「金丝雀部署用于」) · text/17-ch08.txt:547(搜「1%的入站流量」)
蓝绿部署第8章 软件交付text/17-ch08.txt:551(搜「蓝绿部署指的是」) · text/17-ch08.txt:568(搜「100%的用户流量」)
摸黑启动第8章 软件交付text/17-ch08.txt:602(搜「影子流量」) · text/17-ch08.txt:597(搜「暗中写入」)
Diffy第8章 软件交付text/17-ch08.txt:607(搜「Diffy」)
Twitter 案例第8章 软件交付text/17-ch08.txt:632(搜「生产环境的稳定性」) · text/17-ch08.txt:642(搜「黑夜带来了黎明」)
监控展开第8章 软件交付text/17-ch08.txt:465(搜「错误率、响应时间」) · text/17-ch08.txt:478(搜「开香槟庆祝」)

Footnotes

  1. 出处:「第8章 软件交付」第 7 段(text/17-ch08.txt:7,搜「该是一个谜」)。

  2. 出处:「第8章 软件交付」第 15 段(text/17-ch08.txt:15,搜「行业标准的定义」)。

  3. 出处:「第8章 软件交付」第 23 段(text/17-ch08.txt:23,搜「rollout」)与第 28 段(text/17-ch08.txt:28,搜「只是被安装了而已」)。

  4. 出处:「第8章 软件交付」第 24 段(text/17-ch08.txt:24,搜「并且被标记了版本」)与第 128 段(text/17-ch08.txt:128,搜「更加一致」)。

  5. 出处:「第8章 软件交付」第 52 段(text/17-ch08.txt:52,搜「两个主要系列」)。

  6. 出处:「第8章 软件交付」第 67 段(text/17-ch08.txt:67,搜「持续集成(CI)」)与第 74 段(text/17-ch08.txt:74,搜「总是可以发布」)。

  7. 出处:「第8章 软件交付」第 87 段(text/17-ch08.txt:87,搜「文森特·德里森」)与第 88 段(text/17-ch08.txt:88,搜「Gitflow」)。

  8. 出处:「第8章 软件交付」第 113 段(text/17-ch08.txt:113,搜「修改了他最初」)与第 113 段(text/17-ch08.txt:113,搜「不再鼓励」)。

  9. 出处:「第8章 软件交付」第 112 段(text/17-ch08.txt:112,搜「基于主分支的分支策略」)与第 112 段(text/17-ch08.txt:112,搜「管理特性分支会变得很复杂」)。

  10. 出处:「第8章 软件交付」第 133 段(text/17-ch08.txt:133,搜「mysql-server」)与第 137 段(text/17-ch08.txt:137,搜「AMD」)。

  11. 出处:「第8章 软件交付」第 164 段(text/17-ch08.txt:164,搜「分开单独地打包」)与第 168 段(text/17-ch08.txt:168,搜「元包」)。

  12. 出处:「第8章 软件交付」第 172 段(text/17-ch08.txt:172,搜「Python 打包」)、第 141 段(text/17-ch08.txt:141,搜「wheel」)与第 200 段(text/17-ch08.txt:200,搜「Freezers」)。

  13. 出处:「第8章 软件交付」第 243 段(text/17-ch08.txt:243,搜「归根结底」)。

  14. 出处:「第8章 软件交付」第 253 段(text/17-ch08.txt:253,搜「存储资源库」)与第 277 段(text/17-ch08.txt:277,搜「运维上的问题」)。

  15. 出处:「第8章 软件交付」第 287 段(text/17-ch08.txt:287,搜「字节层面上都一样」)。

  16. 出处:「第8章 软件交付」第 293 段(text/17-ch08.txt:293,搜「较长的发布周期」)与第 298 段(text/17-ch08.txt:298,搜「更少」)。

  17. 出处:「第8章 软件交付」第 320 段(text/17-ch08.txt:320,搜「专门给用户看」)。

  18. 出处:「第8章 软件交付」第 327 段(text/17-ch08.txt:327,搜「指定一名发布经理」)与第 329 段(text/17-ch08.txt:329,搜「签名」)。

  19. 出处:「第8章 软件交付」第 383 段(text/17-ch08.txt:383,搜「全部完成要么什么都没做」)与第 389 段(text/17-ch08.txt:389,搜「软链接」)。

  20. 出处:「第8章 软件交付」第 544 段(text/17-ch08.txt:544,搜「金丝雀部署用于」)与第 547 段(text/17-ch08.txt:547,搜「1%的入站流量」)。

  21. 出处:「第8章 软件交付」第 551 段(text/17-ch08.txt:551,搜「蓝绿部署指的是」)与第 568 段(text/17-ch08.txt:568,搜「100%的用户流量」)。

  22. 出处:「第8章 软件交付」第 482 段(text/17-ch08.txt:482,搜「feature flag」)与第 486 段(text/17-ch08.txt:486,搜「允许列表」)。

  23. 出处:「第8章 软件交付」第 493 段(text/17-ch08.txt:493,搜「状态突变」)与第 495 段(text/17-ch08.txt:495,搜「向前和向后兼容」)。

  24. 出处:「第8章 软件交付」第 515 段(text/17-ch08.txt:515,搜「熔断器是一种特殊的」)与第 525 段(text/17-ch08.txt:525,搜「永久性的」)。

  25. 出处:「第8章 软件交付」第 529 段(text/17-ch08.txt:529,搜「不可逆行动」)。

  26. 出处:「第8章 软件交付」第 602 段(text/17-ch08.txt:602,搜「影子流量」)与第 583 段(text/17-ch08.txt:583,搜「没有用户受到影响」)。

  27. 出处:「第8章 软件交付」第 594 段(text/17-ch08.txt:594,搜「暗中读取」)与第 597 段(text/17-ch08.txt:597,搜「暗中写入」)。

  28. 出处:「第8章 软件交付」第 456 段(text/17-ch08.txt:456,搜「疯狂」)与第 461 段(text/17-ch08.txt:461,搜「更大规模的更改」)。

  29. 出处:「第8章 软件交付」第 503 段(text/17-ch08.txt:503,搜「很难推理」)与第 506 段(text/17-ch08.txt:506,搜「渐进地、适时地进行清理」)。

  30. 出处:「第8章 软件交付」第 607 段(text/17-ch08.txt:607,搜「Diffy」)与第 611 段(text/17-ch08.txt:611,搜「消除误报」)。

  31. 出处:「第8章 软件交付」第 632 段(text/17-ch08.txt:632,搜「生产环境的稳定性」)与第 642 段(text/17-ch08.txt:642,搜「黑夜带来了黎明」)。

  32. 出处:「第8章 软件交付」第 422 段(text/17-ch08.txt:422,搜「20 多个部署环节」)、第 433 段(text/17-ch08.txt:433,搜「最终禁止」)与第 439 段(text/17-ch08.txt:439,搜「的睡眠时间」)。