专业书籍精读 · Chaos Engineering · 第 1 章
Chaos Engineering: System Resiliency in Practice · Ch 1 · Casey Rosenthal & Nora Jones · 2020
你点开 Netflix 挑片、打开淘宝看首页,这一下背后不是「一个程序」,而是几百个小程序互相喊话:一个管认出你是谁,一个管给你推荐什么,一个管封面图,一个管算钱。《混沌工程》第 1 章不教任何技术,它先问一件扎心的事:为什么这种系统明明每个零件都好着,整体还会挂?
一只机械表零件上百,但它是「复杂但拆得开」的——拆开摆一桌,一个个看懂,你就能推出整只表怎么走。城市早高峰不是这样:没有一辆车坏,路照样堵死。堵不住在任何一辆车里,它住在车与车之间。这一章的全部主张就是:今天的线上系统,已经从「手表」变成了「早高峰」。
过去保可靠靠「读懂它」:画架构图、写文档、开评审会,把每个零件都测过一遍,然后签字说没问题。这套办法有个没说出口的前提——有人装得下整个系统。现在这个前提没了。每个工程师脑子里都只有一块残片,而且因为系统每天都在改,那块残片还在不停过期。评审会给你的是信心,不是可靠,这两件事之间并没有因果。
既然想不明白,就去试:在可控范围内主动制造一次故障,看系统真实怎么反应——而不是猜它应该怎么反应。这是整本书的出发点。诚实说一句代价:在真实系统上制造故障,一定会有真实用户被波及,所以这本书后面大半篇幅都在讲怎么把被波及的人数压到最小。
复杂系统的故障不住在零件里,住在零件之间;没人装得下整个系统,所以可靠性不能靠想明白,只能靠动手试。
想进到具体机制、量级和示意图? → 切到精读版
你以为故障来自「某个坏零件」——找出它、修好它、下次就不会了。这一章要拆掉的正是这个直觉:在复杂系统里,绝大多数严重故障来自一堆各自完全正常、各自完全符合规格的组件之间的接缝。既然整体的行为推不出来,可靠性就不再是一个论证问题,而是一个实验问题——这是全书所有方法唯一的前提。
全书 21 章的第 1 章,Part I「铺垫」的开篇。作者 Casey Rosenthal 在 Netflix 组建了最早的混沌工程团队,Nora Jones 是同期实践者。这一章不给方法,只立前提:Ch2 接着讲系统如何持续向失效边界漂移,Ch3 才给出五条原则。现实里它对着的,是任何一个「几十上百个服务拼起来、还在每天改动」的线上系统。
传统可靠性工程有一个从没写进文档的前提:系统是可被理解的。于是方法全围着「理解」展开——画架构图、写设计文档、开评审会、把测试覆盖率刷上去。
把具体数字摆上来,这个前提立刻就塌了。一个中等规模的互联网后端有几百个服务、每天上千次发布,完整调用图没有任何人画得完;更要命的是接缝的数量涨得比组件快得多:按 n(n-1)/2(任取两个组件配成一对的数量)走,10 个组件最多 45 对两两关系,100 个组件就是 4950 对。组件数线性涨,接缝数平方涨——而故障恰恰住在接缝上。
非线性还会让量级直接失真。一个请求扇出到 100 个后端,每个后端只有 1% 的响应慢于 1 秒,那么有 63% 的用户请求会至少撞上一个慢的:组件级的「1% 小毛病」,到用户那头变成「大多数人都慢」。同理,100 个串行依赖各自做到 99.99% 可用,乘起来只剩约 99.0%,一年近 87 小时不可用。
不解决会怎样:你每次事故后都在修「上次那个」,下一次仍被一个没想到的组合打中;评审会和架构图给你的是信心,不是可靠。
复杂但可拆的系统满足一条性质:整体 = 零件之和。一只机械表、一个编译器,零件数量可以非常大,但只要你把每个零件的行为搞清楚,整体行为就是可推导的。本章给的判据可以直接拿去用:你能不能把每个组件单独验证通过,然后据此宣布整体正确?能——它是 complicated,评审加测试性价比最高;不能——它是 complex,你需要别的东西。
微服务化、自动扩缩容、多区域部署、每天上千次发布,每一样都在把系统往后者推。复杂系统还有一个让人不舒服的性质:非线性——同一个扰动这次毫无反应、下次放大成全站事故,因为系统两次所处的状态不同。所以「上次演练没事」从不构成「这次也没事」,这是后面「持续自动化地跑」那条原则的根。
每个工程师手里的模型都是局部正确、整体过期的。局部正确才最危险:它让人确信自己判断得对。架构图是一种希望,不是一种观测——它描述系统该怎么跑,不是它正在怎么跑;在一个每天发布上千次的系统里,它从画完那刻就开始偏离事实。
还有一条很容易被跳过的推论:事故复盘时,因果链看上去总是显而易见的。于是人们得出「他本该想到」的结论——可事前那条链藏在四位数条接缝里,没有人能把它枚举出来。把「事后显然」当成「事前可预见」,是复杂系统里最贵的一个认知错误,它会把复盘悄悄变成追责。
① 业务逻辑与应用逻辑错位。一家流媒体公司里,用户数据分散在几个微服务里。其中一个服务对「查不到记录」的处理是返回一个默认值——教科书式的防御性编程,别让异常外泄。调用它的那个服务则按「拿到的数据就是用户的真实状态」继续往下算,也完全合规。两段逻辑分开看都对:一个说别抛异常,一个说按数据办事。合起来,一部分用户看到了与自己账户不符的内容。没有任何一个组件有 bug,错在两个正确假设之间的那道缝。
② 客户端引发的重试风暴。某个后端变慢 → 请求超时 → 客户端按既定策略自动重试 → 到达量翻倍 → 更慢 → 重试更多。重试本身是好工程(瞬时抖动本就该重试),可它和「后端已经饱和」叠在一起就变成正反馈:每个失败请求重试 3 次,就能把到达量推到原来的近 4 倍。真正恶劣的是这个环能自己活下去——等你赶到现场,最初让后端变慢的原因可能早已消失,系统却还卡在饱和态里,必须靠外力(限流、主动丢弃、冷启动)才爬得出来。
③ 假日代码冻结。为了过节平稳,公司冻结所有变更。结果冻结期自己成了事故源头。这里结论比细节重要:这套系统的稳态里本来就含着「持续变更」这一项——例行发布顺手重启了进程、清掉了累积状态、换掉了即将到期的东西。把变更拿掉并不等于把系统按住,它只是把系统推进了一个从没人去过、因此从没被验证过的状态。换句话说,「什么都不做」也是一次没做过实验的变更。
把三个摆在一起,本章要你带走的是一个共同模式,四条:每个组件都符合自己的规格;故障发生在接缝上;事后看显然、事前不可枚举;触发条件是状态空间里一个没人去过的点。这四条合起来就是「光靠推理不够用」的证明。
Brooks 在《没有银弹》(No Silver Bullet,1986)里把复杂度切成两种:偶发的(accidental)来自我们的工具、历史包袱和重复实现,可以删;本质的(essential)来自问题本身。本章借这把刀说一件不太受欢迎的事:我们系统里的复杂度,大部分是本质的——个性化推荐、全球多区域、A/B 实验、多端适配、四个 9 的可用性目标,每一项都是业务要的价值,删掉复杂度就等于删掉它。
由此得出全章最反直觉的一句:复杂度是成功的副产品。你的系统之所以这么复杂,是因为它有人用、赚钱、长大了。所以目标不该是消除复杂度(做不到),而是获得在复杂度里安全行动的能力。诚实补一句:这条很容易被引用成「别重构了」。书并不反对削偶发复杂度——该削,而且性价比最高;它说的是削完之后剩下的那部分不会消失,你得为它准备另一套办法。
整体行为推不出来,关于系统的知识又从哪来?本章的回答是经验主义:把系统放进那个条件里,看它怎么反应。
这里有一句该背下来的分界:测试(test)验证你已经知道的断言,实验(experiment)产出你还不知道的知识。单元测试和集成测试写的都是「我断言在 X 下应该得到 Y」——它们只能覆盖你想到的那些 X。混沌实验问的是另一种问题:「在 X 下到底会发生什么?」答案可以是你完全没想到的 Y。这也是为什么混沌工程不属于测试,而属于实验方法。Ch3 的五条原则(建稳态假设、多样化真实事件、在生产跑、自动化持续跑、最小化爆炸半径),全部是这一句的工程化。
面对复杂度,工程上一共就那么几种策略。它们不互斥,但各有各的隐含假设,用错场合就是白花钱:
| 策略 | 它假设什么 | 真的有效时 | 代价 / 失效点 |
|---|---|---|---|
| 文档 + 架构评审 | 有人装得下整个系统 | 组件少、变更慢、接口稳定 | 产出的是信心而非证据;发布一次就开始过期 |
| 单元 / 集成测试 | 你能提前想到该断言什么 | 组件内部逻辑、回归防护 | 只覆盖已想到的条件;接缝上的交互天然测不到 |
| 形式化方法(TLA+ 等) | 关键部分能抽象成可穷举的模型 | 协议、一致性、并发控制 | 建模成本高;验证的是模型,不是生产里跑的那套实现 |
| 可观测性 | 故障已经在发生了 | 缩短定位时间(MTTR) | 被动;只告诉你出事了,不告诉你下次什么会出事 |
| 混沌实验 | 你愿意主动付出可控的损失 | 组件多、变更快、涌现行为多 | 要动真实流量,必有真实用户受影响;实验设计差就只是制造噪音 |
五种不是替代关系,而是覆盖不同的失效类型;本章只论证了「前四种加起来仍留着一个缺口」。
那么什么场景该选什么,本章的判据可以落得很具体:
这一章最实用的产出是一套反驳的语言。评审会上最常听到的「我们每个服务都做到了 99.99%,所以整体没问题」,到这里立刻站不住:100 个串行依赖各自四个 9,乘起来约 99.0%,一年近 87 小时不可用。面试被问「你怎么保证系统可靠」,答「可靠性是涌现属性、推不出来,所以我会在生产上做受控实验」,比背一串中间件名字有分量得多。
大厂实证
① 核心命题:复杂系统的严重故障来自各自正常的组件之间的交互,不来自坏零件。
② 判据:能不能「拆开分别验证 → 据此宣布整体正确」。能是 complicated,不能是 complex。
③ 接缝数按 n(n-1)/2 走:组件线性涨、接缝平方涨——10 个组件 45 对,100 个组件 4950 对。
④ 非线性让量级失真:扇出 100、各 1% 慢 ⇒ 63% 用户慢;100 个串行四个 9 ⇒ 整体约 99.0%。
⑤ 三个遭遇的共同模式:组件合规、故障在接缝、事后显然而事前不可枚举、触发点从没人去过。
⑥ 重试风暴是正反馈环:进去后移除初因也出不来,要靠限流 / 丢弃 / 冷启动掐断。
⑦ 代码冻结不是「把系统按住」,是把它推进一个没被验证过的状态——不变更也是一次变更。
⑧ 复杂度大部分是本质的、是成功的副产品;目标不是消除它,而是获得在其中安全行动的能力。
⑨ 测试验证你知道的断言,实验产出你不知道的知识——这是混沌工程与测试的分界,也是全书方法的起点。