专业书籍精读 · 持续交付 · 第 2 章
Continuous Delivery · Ch 2 · Jez Humble & David Farley · 2010
几乎每家公司都有那么一台「谁也不敢碰的服务器」——上面跑着要紧的业务,可它当年怎么装起来的、后来被谁改过什么,早已没人说得清。它哪天坏了,公司就只能靠几个老员工的记忆去考古重建。《持续交付》第 2 章讲的就是怎么让这种机器彻底消失:把「一套系统是怎么组装起来的」完整写下来、存起来,每改一笔就留一条记录。
一家连锁咖啡店要开第二家分店。如果第一家的味道全靠店长的手感——豆子多少克凭感觉、机器压力当年调过一次也没记录,那第二家永远开不出一样的味道。反过来,如果每一步都写进一本谁都能照做的手册,那么开第十家店、甚至第一家被水淹了要重建,都只是照手册再来一遍。配置管理就是给软件系统建这本手册——而手册自己也要进档案室,改一笔就留一条记录:谁、什么时候、改了什么。
三件事。一是改动只发生在机器上、不发生在手册上:有人半夜登上服务器改了个参数,问题当场解决,可这一改从此只活在那台机器里。二是日积月累,每台机器都长得不一样了:同样的包装上去,有的好好的、有的出怪毛病,谁也说不清差在哪。三是没有「昨天那个好状态」可退——出事想退回去,才发现不知道昨天是什么样。
这一章的主张能浓缩成两句大白话。第一句:凡是决定系统长什么样的东西,都要进档案室——不只是程序,还有各种设置、安装脚本、用到的第三方零件,甚至「这台机器该装什么系统、打哪些补丁」。验收标准很硬气:一个新同事在一台崭新的电脑上,从档案室取一份、敲一条命令,就该能把整套系统建起来。
第二句:同一批货发往所有分店,只换标签。打好的那一个软件包,从测试一路到上线全程用同一个,绝不为每个环境各打一个;环境之间的差异(连哪个数据库、发不发真邮件、开哪些功能)在装机那一刻从外面塞进去。这样你测过的东西和交给用户的才是同一个东西——所有检验能作数全靠这一点。
还有一条铁律:以后不许在机器上手工改任何东西。要改就改档案室里那份定义,再让自动化推到所有机器上。
机器坏了可以随手扔掉、重建一台一样的;改错了能退回任意历史状态;出了事能查到是谁在什么时候改了哪一项。诚实的代价是:把一切写成脚本、还要长期维护这本手册,前期明显更慢——而且设置项一多,手册自己也会长成一套需要小心维护的复杂东西。
判断配置管理做得好不好,只要一个问题:给你一份档案和一批空白机器,你能不能把整套系统原样重建出来? 能,你就永远有退路;不能,你的系统就还活在某几个人的记忆里。
想进到具体机制、四问体检表、对比表和示意图? → 切到精读版
你以为配置管理是「把代码放进 Git」这种基础卫生,其实它问的是一个狠得多的问题:给你一个版本库和一批空白机器,你能不能把整套系统——操作系统、补丁、中间件、应用、配置、数据——原样重建出来?《持续交付》第 2 章先给定义与一张体检表,再立三条硬规矩:一切纳入版本控制(version control);同一份制品(artifact)部署到所有环境,环境差异靠外部配置在部署 / 运行时注入;环境只能由自动化从版本库建立,任何手工改动都算故障。在不可重现的环境上,任何测试结论都不成立——这是第 5 章那条流水线的地基。
Part I「基础」的第 2 章,紧接第 1 章的三个发布反模式而来——手工部署、只在开发完成后才碰类生产环境、手工管理生产环境配置——这一章正面回答后两个。它同时是全书的地基:第 3 章持续集成要求主干随时可构建(前提是本章的「每天签入主干」),第 5 章的流水线要求「同一份制品逐关晋级」(前提是本章的「构建与环境无关」),第 11 章展开环境,第 12 章管数据,第 13 章管依赖。今天的对应物:Git、Terraform / Ansible、容器镜像与 registry、Kubernetes 的 ConfigMap 与 Secret、Consul / etcd / Apollo 这类配置中心,以及 GitOps。
先摆个具体场景:一个订单服务跑在 30 台机器上,日常 3,000 QPS、大促十倍。半年里为救两次线上事故,有人在其中 3 台上手工把 JVM 堆和连接池调大了,没留记录。于是你会连撞三件事:p99 延迟(一百个请求里第 99 慢的那个)只在某几台上尖刺、查一周查不出来;预发环境怎么压都复现不了生产的问题;机房故障要重建集群时,你手上只有一份过期文档。本章把「配置管理做到了没有」变成一张能当场回答的体检表:
判据很锋利:一问答不上来就不算做到位——而绝大多数团队卡在第一问。不解决会怎样?你丢掉的不只是效率,而是「已知良好状态」这个概念本身:没有它,回滚只是换一种猜法,容量测试与灰度失去可比性,灾难恢复承诺的恢复时间无从验证。Google 在 SRE 的公开材料里说,大约 70% 的故障来自对运行中系统的变更——而配置变更恰恰最常绕过评审、测试与灰度。
原书的定义是:配置管理指的是这样一个过程——项目相关的所有产物、以及它们之间的关系,都被存储、检索、唯一标识和修改。两个词值得单独拎出来。唯一标识:你要能一口说出「此刻生产上跑的是哪一版制品、哪一版配置、哪一版环境定义」,而三者的对应不能靠人脑记。修改:改动必须走一条受控通路,而不是谁有权限谁就上手。工具只是手段——很多团队代码在 Git 里,环境和配置却在某个人的脑子里加几台机器上,这就叫没做到。
清单是列举式的:源代码、测试、数据库脚本、构建与部署脚本、依赖库、配置文件、文档,甚至编译器与工具链的版本。验收判据一句话说完:新人在一台崭新机器上取一份代码、跑一条命令,就能把系统构建并部署到任意环境。附带两条纪律:每天至少往主干(trunk)签入一次(改动小 → 冲突小、持续集成才有意义、出事时嫌疑范围就是这一次提交),以及写有意义的提交信息——构建红了的时候,别人的第一现场就是它。
唯一的例外是:构建产物不进版本库。它能由源码确定性地重新生成、体积大、还会把历史撑爆;它该待的地方是制品库,按版本号唯一标识、写一次不再改。这条例外不是妥协,它正是流水线能「同一份制品逐关晋级」的物理基础。
外部库也是配置的一部分。一个中等规模的 Java 服务,传递依赖动辄几百个 jar;任何一个版本浮动,你那份「没改过的源码」就会构建出不同的产物。本章要求把依赖版本钉死并纳入管理:2010 年的答案是 Maven / Ivy 仓库加内部镜像,今天是锁文件(package-lock.json、go.sum)加内部制品代理。判据很实在:断了外网、或者上游某个版本被作者删掉(这真的发生过),你还能不能重现出上周那个产物?
先把时机分清:配置可以在四个时刻进入系统——构建时(编译期写进产物)、打包时(按环境打不同的包)、部署时(装机时注入)、运行时(启动读取或运行中动态拉取)。本章立场明确:构建与打包必须与环境无关,坚决反对为每个环境构建不同的二进制。理由不是洁癖:一旦为生产单独构建一次,你测过的东西和你交付的东西就不是同一个东西,流水线上每一关的结论都随之失效。
建模上,本章把配置看成一组以 (应用, 版本, 环境)三元组为键的键值对——同一应用的不同版本可能需要不同的配置项,同一版本在不同环境取不同的值。值放哪里?版本库里的文件、关系数据库、目录服务(LDAP / Active Directory)、专门的配置服务,各有代价(见下一节)。
本章给配置定的三条原则今天依然成立。配置要能被测试:部署完立刻跑冒烟测试(smoke test,一小组「起来了吗、依赖连得上吗」的检查);启动时校验必填项,缺了就直接启动失败(fail fast),而不是等半夜第一个真实请求打进来才炸。配置要能报表:你得一眼看出四个环境此刻各是什么值,否则「预发能过、生产不行」永远查不明白。配置要有版本与审计。
最后一条常被忽略的告诫:别过度可配置。配置项一多,它就变成一门没有类型检查、没有 IDE、没有测试的编程语言,而写它的往往是压力最大的那个夜班。判据:如果改一项配置需要程序员来判断后果,那它本质上就是代码——该有评审、有测试、有灰度。
应用从不在真空里跑。一个环境包含:机器与容量、操作系统与补丁级别、中间件及其版本与配置、网络与防火墙、DNS 与证书、依赖的外部系统,还有数据。本章给出两条铁律:
2010 年的工具是 Puppet 与 CfEngine;今天的镜像加基础设施即代码只是把同一条铁律推到极端:不可变基础设施(immutable infrastructure)干脆取消「改」这个动作,一律重建。这也顺带回答了一个常见疑问——为什么容器化之后配置管理好像变简单了?不是问题消失了,是「不许手工改」被环境本身强制执行了。
可吵的地方不在「要不要纳入版本控制」,而在三处具体选择:配置在哪一刻注入、值放在哪里、环境用哪种姿态维护。
表 1 · 配置在哪个时机注入(代价照实写)
| 时机 | 做法 | 好处 | 代价 | 本章立场 |
|---|---|---|---|---|
| 构建时 | 编译期把环境值写进产物 | 运行期零外部依赖,最省事 | 每个环境一个二进制,测过的 ≠ 发出去的;改一个值要重新构建 | 反对 |
| 打包时 | 打包阶段按环境生成不同的包 | 沿用现成构建工具即可 | 同上,只是晚了一步;包的数量 = 环境的数量 | 反对 |
| 部署时 | 装机时给同一份制品注入配置文件 / 环境变量 | 一份制品走到底;改配置不用重新构建 | 改值要重新部署(今天以秒计,可接受) | 推荐的主战场 |
| 运行时 | 启动读取,或运行中从配置中心动态拉取 | 不重启就能改行为,适合功能开关、限流阈值 | 多一个外部依赖与故障模式;动态改一样是生产变更,得评审、要灰度 | 可用,但按变更对待 |
表 2 · 配置值放在哪里
| 方案 | 好处 | 代价 | 适合 |
|---|---|---|---|
| 版本库里的文件 | 天然有历史、diff、评审,与代码同一套工具(今天叫 GitOps) | 改一次要走一遍流水线;凭据不能明文放 | 环境定义、基础设施参数、大多数应用配置 |
| 环境变量 | 语言与平台无关、极简、不易误提交进代码库 | 没有历史与类型,层级结构表达别扭;改了要重启进程 | 容器化服务的连接串与凭据注入 |
| 关系数据库 | 查询与出报表方便,一处改全局生效 | 版本与审计得自己做;自己成了新单点 | 本就重度依赖数据库的单体系统 |
| 目录服务 LDAP / AD | 企业里现成、集中管理 | 表达能力弱,改动流程重 | 企业内网的账号、拓扑类信息 |
| 专用配置服务 | 动态生效、可灰度、可审计(ZooKeeper / etcd / Consul / Apollo) | 自身必须高可用;客户端缓存与降级要设计;容易变成没人管的后门 | 规模大、服务多、需要动态开关 |
| 密钥管理系统 | 加密、可轮换、按需下发、访问留痕(Vault / KMS) | 多一层依赖与运维成本 | 所有凭据——本书几乎没讲,是后来补的课 |
表 3 · 环境用哪种姿态维护
| 姿态 | 重建一台要多久 | 漂移风险 | 代价 |
|---|---|---|---|
| 手工维护(雪花) | 数小时到数天,靠人考古 | 高,而且无法测量 | 前期零投入,后期完全不可控 |
| 脚本化收敛 | 分钟级(Puppet / Chef / Ansible) | 中——只覆盖被管的那些项,没写进清单的照旧漂 | 要写并长期维护清单,脚本必须幂等 |
| 不可变镜像重建 | 秒到分钟(容器镜像 / AMI) | 低——根本不存在「改」这个动作 | 要有镜像流水线与制品库;有状态服务另需方案 |
三张表连起来看,本章推荐的组合其实很朴素:一份与环境无关的制品,加受版本控制的环境定义,加部署时注入、少量运行时可调的配置;所有改动只走「改版本库 → 自动化推送」这一条通路。
这一章是所有后续实践的地基:没有可重现的环境,第 3 章的持续集成只是一台绿灯装饰品,第 5 章的流水线关卡也证明不了什么——你在环境 A 上测过的东西,本来就不是要上环境 B 的那个东西。今天最直接的化身是 GitOps:期望状态写在 Git 里,控制器持续把集群实际状态收敛过去,手工改动会被自动改回去。面试高频题「你们生产配置放在哪、谁能改、怎么回滚、怎么审计」考的就是这一章;架构评审上最该问的一句是「这套东西能不能从版本库重建出来」。
70% 与 Facebook 的做法都指向同一个反面。① 命题:配置管理不是「用了 Git」,而是回答一个问题——凭版本库和一批空白机器,能不能把整套系统原样重建出来。定义里的关键词:一切产物及其关系都要可唯一标识、可受控修改。
② 体检表四问:能重现任一环境吗?能对任一项做增量改动并部署吗?能追到每次变更是谁在何时改了什么吗?能过审计、且人人拿得到信息吗?——一问答不上就是没做到。
③ 一切纳入版本控制:代码、测试、数据库脚本、构建与部署脚本、依赖、配置、文档乃至工具链;唯一例外是构建产物,它进制品库、按版本唯一标识。配套两条纪律:每天至少往主干签入一次;提交信息要有意义。
④ 依赖也是配置,版本要钉死。判据是断网或上游删版本后,你还能不能重现上周那个产物。
⑤ 应用配置的核心主张:构建与打包必须与环境无关,同一份制品部署到所有环境,差异在部署时(少量在运行时)注入;配置以(应用, 版本, 环境)为键,要能测试、能出报表、有版本与审计。
⑥ 告诫:别过度可配置——需要程序员判断后果的配置,本质上就是代码。
⑦ 环境两条铁律:完全由版本库中的定义自动创建(脚本幂等);任何手工改动都不许,生产锁到改不进去为止。不可变基础设施是这条铁律的极端形式。
⑧ 实证:DORA 说配置入版本控制比代码入版本控制更能预测交付效能;Facebook 每天改配置数千次并配上评审与金丝雀;反面是 AWS 2017 年手敲错一个输入、S3 中断约四小时。诚实的代价:把一切脚本化并长期维护,前期一定更慢。