12 · 为失败而设计:韧性工程
一句话点题:高可用不是「祈祷别出事」,而是「预设一定会出事」——你控制不了组件什么时候坏,但你能设计「坏了之后会发生什么」。韧性工程,就是把这件事从运气,变成可以下注、可以验证的工程。
🧭 进阶篇第 3 章。 06 · 质量属性与取舍 给了你「可用性 = 几个 9 = 每年允许停机多久」的标尺;10 · 分布式系统的硬道理 摆出了病理——部分失败、灰色失败(「分不清它是死了还是只是慢」)、自动化在分区时「正确地」闯祸。这一章把它们拧成一根绳:既然失败必然发生、又分不清死与慢,那一个系统该长成什么样,才能在零件不断坏掉的同时整体不倒? 这就是「为失败而设计(Design for Failure)」。
这一章的好消息是:它几乎不靠新名词,全靠生活常识。 保险丝、船舱、留余量、丢车保帅——你早就懂这些道理,本章只是把它们安到系统上。读起来会比上一章轻松很多。
还是那条主线:AI 几秒就能给你写出一个能跑的 happy path。但「这个依赖挂了要不要降级、重试几次才不算雪上加霜、丢哪部分流量保哪部分」——这些判断的代价由你的线上事故承担,AI 给不了。
一、思维翻转:从「多久坏一次」到「坏了多久能恢复」
新人谈可用性,潜台词永远是「让它别坏」:用更贵的服务器、更严的测试、更小心的发布。这条路有个天花板——你永远消灭不了失败。磁盘会坏、网线会被挖断、依赖会超时、有人会手抖打错一条命令(本章的真实案例里,你会看到这条「手抖」反复出现)。
AWS 的 CTO Werner Vogels 把这句话钉进了一代工程师的脑子里:
「Everything fails, all the time.」(一切都会坏,随时都会坏。)
一旦你接受这个前提,度量的重心就会发生一次根本的转移:
旧思维:盯着 MTBF 新思维:盯着 MTTR
───────────────────── ─────────────────────
MTBF = 平均无故障时间 MTTR = 平均恢复时间
「多久坏一次?」 「坏了之后,多久能好?」
越大越好 ← 努力方向 → 越小越好
可用性 ≈ MTBF / (MTBF + MTTR)
↑ 把 MTBF 推到无穷大,代价是天文数字、且仍有上限
↓ 把 MTTR 压到几秒,可用性照样很高 —— 而且现实可达这俩都能抬高可用性,但性价比天差地别。把 MTBF(别坏)从 30 天提到 60 天,要砸的钱和精力是指数级的,且总有意外;而把 MTTR(快恢复)从 30 分钟压到 30 秒,靠的是自动检测 + 自动切换 + 隔离爆炸半径这类可设计、可演练的工程手段。
架构智慧:可用性的杠杆,绝大多数压在 MTTR 那一端,而不是 MTBF。一个「天天有小毛病、但每次几秒就自愈」的系统,比一个「半年不出事、一出事就瘫一天」的系统,可用性高得多、也健康得多。 韧性不是「不摔跤」,是「摔了能立刻爬起来,而且只蹭破一点皮」。
这就是为失败而设计的全部出发点:你的精力不该全花在「祈祷它别坏」,而该花在「假设它一定会坏,那时系统会怎样」。 下面六节,就是把这个「会怎样」拆成可操作的判断。
二、级联失败:一个慢依赖,如何拖垮全站
最反直觉、也最致命的一件事:线上大事故,极少是「一个组件挂了」直接造成的;绝大多数是「一个组件慢了 / 挂了,然后引发连锁反应,把健康的部分也拖垮了」。 这叫级联失败(cascading failure),是把「局部故障」放大成「全站雪崩」的核心机制。
看一个最经典的剧本——一个慢依赖如何顺着调用链「逆流而上」吃掉整个系统:
正常时: 网关 ──▶ 服务A ──▶ 服务B ──▶ 数据库
(每个请求占用 1 个线程/连接,几十毫秒就还回去)
❶ 数据库变慢(GC / 锁 / 热点),B 的调用从 50ms 涨到 5s
│
❷ B 的线程被「卡在等数据库」上,迟迟不释放 → B 的线程池被占满
│
❸ A 调 B 也开始超时;A 的线程同样卡在「等 B」上 → A 线程池耗尽
│
❹ A 一卡,客户端/网关开始【重试】 → 请求量不降反【翻倍、三倍】
│ ↑ 火上浇油:本就过载,重试又加压
▼
❺ 整条链路线程/连接池全部耗尽 → 健康的接口也无线程可用 → 全站 503
└─ 一个慢 DB,十分钟内拖垮了整个平台这里藏着三个把局部故障放大成全局灾难的「放大器」,每一个都值得你刻进脑子:
- 资源耗尽(线程 / 连接池):慢调用的本质,是让请求长时间占着资源不放。线程池、连接池是有限的;一个慢依赖会像泡水的海绵一样,把整个池子吸干。「慢」比「快速失败」更可怕——快速失败至少立刻把资源还回来了。
- 重试风暴(retry storm):系统一旦变慢,上游(客户端、网关、SDK)的「善意重试」会让请求量瞬间翻几倍。本来就过载,你还往里灌更多——这是把火浇上油。 大量事故的「致命一击」,都是重试风暴贡献的。
- 超时层层堆叠:如果每一层的超时设得一样长(比如都设 30s),最内层慢了,会让外层每一层都干等满 30s 才放弃——资源被「集体罚站」。健康的请求挤不进来,等于跟着陪葬。
架构智慧:慢,是比宕机更隐蔽、更致命的故障形态。 宕机是「快速失败」——调用立刻报错,资源立刻释放;而「慢」会让请求抱着资源慢慢死,顺着调用链把资源耗尽,一节一节传染上去。韧性工程的一大半功夫,就是不让「慢」传播开:要么快速失败,要么把它关进笼子。下面三到六节,讲的全是「关笼子」的招。
三、隔离爆炸半径:把故障关进小格子
既然失败必然发生、还会级联传染,那第一道工程哲学就不是「消灭故障」,而是控制爆炸半径(blast radius)——让任何单点的失败,只能炸掉一小格,炸不到全局。 这是从船舶工程借来的智慧。
舱壁(Bulkhead):把资源池切开,故障不串味。 轮船的船体被隔成多个水密舱,一个舱进水,水密门一关,船照样浮着;若整个船舱是连通的,一处破洞就能沉掉整艘船(泰坦尼克正是隔舱不够高、水漫过舱壁顶才沉的)。映射到系统:
❌ 共享一个池(一损俱损):
所有下游调用 ──▶ [ 同一个线程池 / 连接池 ]
↑ 慢依赖 X 占满整个池 → 调用健康依赖 Y 的请求也没线程了 → 全挂
✅ 舱壁隔离(各占各的):
调用支付 ──▶ [池 P:20 线程] ← 支付挂了,最多用尽这 20 个
调用推荐 ──▶ [池 R:10 线程] ← 推荐挂了,只影响推荐,支付毫发无伤
调用搜索 ──▶ [池 S:10 线程] ← 互不侵占,一个舱进水,其余照常Cell-based 架构:把整个系统复制成多个独立「细胞」。 更高一层的隔离——不是隔离一个池,而是把整套服务栈(网关、服务、数据)复制成多个互相隔离的 cell,每个 cell 服务一部分用户。一个 cell 整个烧了,只影响落在它里面的那批用户,其余 cell 毫无感知。AWS 大量内部服务用这种「cell-based」架构来给爆炸半径设上限。
Shuffle sharding:用随机组合,让「连坐」概率趋近于零。 这是 AWS 的一个精妙发明。
💧 深水区(初读可跳过,记住下面这个比方就够了):它就像发扑克牌。 给每个客户随机发「一手牌」(几个节点的组合),而不是让一整桌人共用同一手牌。这样当某个「毒客户」把自己那几张牌(节点)玩坏时,几乎不会有别人和他拿到的是完全相同的一手牌——别人顶多和他重了一两张,手里还有别的牌能正常用。于是「一个坏客户拖垮一整组人」的连坐,被摊薄到几乎不可能发生。下面是把这个比方算成数字的版本。
假设你有 8 个后端节点、要服务很多客户:
普通分片:把客户切成 4 组,每组固定 2 个节点
→ 某 2 个节点被一个「毒客户」打挂,固定绑这 2 个节点的那一整组客户全遭殃
Shuffle sharding:给每个客户随机分配 2 个节点的组合
→ 从 8 个节点里随机挑 2 个,一共有 28 种不同的组合(这就是 C(8,2)=28)
→ 两个客户「恰好抽到完全相同的那 2 个节点」的概率,只有 1/28,很低
→ 一个毒客户打挂它那 2 个节点,几乎不会和别人「完全重叠」
别人哪怕共享了其中 1 个节点,还有另 1 个能用 → 受影响面被摊薄到接近 0(节点越多、每人分到的越多,组合数会爆炸式增长——上千节点时组合数是天文数字,两人「完全撞车」的概率低到可以忽略。这就是 shuffle sharding 威力的来源。)
架构智慧:隔离的核心判断,是先想清楚 「故障域(failure domain)」的边界画在哪——哪些东西必须一起死、哪些绝不能互相拖累。最该被舱壁隔开的,永远是「核心」和「非核心」:别让「猜你喜欢」挂掉时,把「下单支付」一起带走。隔离不是免费的(更多池 = 更多闲置资源、更复杂的容量规划),所以它本身也是一道取舍——把隔离的颗粒度,花在「绝不能被拖垮」的关键路径上。
四、主动自保:熔断、超时预算、降载
隔离是「被动防线」——把故障关在格子里。但格子里那个组件还在挣扎,挣扎本身(慢、重试)就在消耗资源。所以还需要主动自保:系统要能主动识别危险、主动断舍离,而不是傻等着被拖死。
熔断器(Circuit Breaker):像家里的保险丝,自动跳闸。 当对某个依赖的调用失败率超过阈值,熔断器「跳闸」,后续调用直接快速失败(fail fast),根本不发出去——既保护了已经奄奄一息的下游(别再压它了),又让自己立刻释放资源(别再傻等了)。它有三个状态:
失败率超阈值
┌────────┐ ───────────▶ ┌────────┐
│ 关闭 │ │ 打开 │ ← 跳闸!所有调用直接快速失败,
│ Closed │ │ Open │ 不再打扰奄奄一息的下游
│ 正常放行 │ ◀─────────── └───┬────┘
└────────┘ 探测成功 │ 冷却一段时间后
▲ ▼
│ ┌──────────────┐
└───────────── │ 半开 Half-Open │ ← 试探性放【一个】请求过去
连续成功 └──────────────┘ 成功→关闭恢复;失败→重新打开Netflix 的 Hystrix 把熔断器 + 舱壁打成了一个工业级组件,是这套思想最有名的落地。它的「半开」态是灵魂:不盲目恢复,而是先放一个探子过去试水,确认下游真活过来了,才彻底恢复。
超时预算(Timeout Budget):给整条链路一个「总时限」,逐层递减。 上一节说过,各层超时设得一样长会「集体罚站」。正确做法是让超时沿调用链层层递减——上游给下游的时间预算,必须小于它自己剩下的时间:
用户能忍的总时限:3s
▼
网关(预算 3s) ──▶ 服务A(分到 2.5s) ──▶ 服务B(分到 1.5s) ──▶ DB(0.8s)
每一层都给下游【更少】的时间,留出余量给自己处理和返回
反模式:每层都设 30s → 最内层卡住,外层全部干等满 30s,资源被集体罚站背压(Backpressure)与降载(Load Shedding):扛不住时,主动丢一部分,保住整体。 这是最反直觉、却最体现成熟度的一招。当请求量超过处理能力,你有两个选择:
❌ 来者不拒(假装能扛):
请求洪水 ──▶ 队列无限堆积 ──▶ 内存爆 / 延迟飙到分钟级 ──▶ 全员超时 → 全军覆没
↑ 谁都没服务好,大家一起死
✅ 主动降载(丢车保帅):
请求洪水 ──▶ [超过容量?] ──是──▶ 立刻拒绝多余的(快速返回 429/503,甚至排队)
│否
▼
正常处理 ──▶ 被放进来的请求,都得到了【正常、快速】的服务
↑ 牺牲一部分,换大多数人的可用架构智慧(本节最重要):「优雅地拒绝一部分请求」,远胜于「假装全部都能扛、然后一起崩」。 这是从「乐观」到「成熟」的分水岭。背压是「向上游说:我满了,你慢点发」;降载是「我自己决定:超出的部分立刻丢,保住放进来的那批被好好服务」。一个会主动降载的系统,在过载时是「部分可用」;一个来者不拒的系统,在过载时是「全部不可用」。 这正是 模型推理服务 面对超额请求时要排队、要拒绝的原因——GPU 就那么多,硬接只会让所有人一起超时。
五、聪明地重试:退避 + 抖动,且必须以幂等为前提
重试是把双刃剑:用对了能自动跨过瞬时抖动,用错了就是第二节那场重试风暴的元凶。聪明地重试,要同时满足三个条件。
① 指数退避(Exponential Backoff):别催命,越等越久。 失败后别立刻重试,而是等待时间指数级拉长(1s → 2s → 4s → 8s…)。给下游喘息的时间,而不是失败后立刻又怼上去。
② 抖动(Jitter):打散「同时重试」,这是关键中的关键。 只有退避还不够——如果一千个客户端同时失败、又用完全一样的退避节奏,它们会在第 1s、第 2s、第 4s……整整齐齐地一起重试,形成一波波同步的冲击波,反复把刚要恢复的下游再打趴下。加入随机抖动(在退避时间上叠加一个随机量),把这一千个客户端的重试时刻打散开:
❌ 无抖动:故障恢复瞬间,所有客户端同步重试 → 一波波尖峰把下游反复打死
请求量 │ ▲ ▲ ▲
│ ╱│╲ ╱│╲ ╱│╲ ← 同步的冲击波
│────╯ │ ╰───────╯ │ ╰───────╯ │ ╰──
1s 2s 4s
✅ 有抖动:把每个客户端的重试时刻随机打散 → 削平尖峰,下游平稳恢复
请求量 │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ← 被摊平的、可承受的负载
│───────────────────────────────AWS 的工程实践(Marc Brooker 的经典文章《Exponential Backoff And Jitter》)实测:在大量客户端争抢时,加了抖动的退避,能把无效的重复调用砍掉一半以上,完成时间也显著更短。抖动是那种「一行代码的改动,换来数量级的稳定」的高杠杆设计。
③ 重试预算(Retry Budget)+ 幂等前提。 还要给重试上一个总闸:限制「重试请求」占总请求的比例(比如不超过 10%),一旦超过就停止重试——这是从根上掐死重试风暴的保险。而所有这一切的大前提是:
重试的对象,必须是幂等的。 否则「超时了但其实成功了」的请求一重试,就会重复扣款、重复发货、重复下单。
这一条直接承接 11 · 数据一致性工程 的核心:at-least-once 重试 + 消费端幂等 = 效果上的恰好一次。 支付系统 的幂等扣款、通知系统 的去重限频,就是「敢于重试」的地基——没有幂等,你根本不敢重试;不敢重试,瞬时故障就成了永久失败。
架构智慧:重试不是「失败了就再试一次」这么天真。安全的重试 = 指数退避 + 抖动 + 重试预算 + 幂等前提,四样缺一不可。少了退避和抖动,重试会变成压垮下游的风暴;少了预算,风暴没有上限;少了幂等,重试会把数据搞错。AI 生成的代码默认是
retry(3)式的裸重试——这恰恰是最危险的那种。
六、优雅降级:核心保命,非核心可弃
前面几节都在「防止崩」。但有时候下游就是挂了、就是恢复不了——这时韧性的最后一道防线是:「坏一部分」远好过「全挂」。 这就是优雅降级(Graceful Degradation)。
核心判断是一道你必须提前做好的功课:把功能按「掉了会死 vs 掉了能忍」分级。
电商大促,推荐服务挂了。两种活法:
❌ 没分级(一损俱损):
推荐挂 ──▶ 商品详情页加载推荐时报错 ──▶ 整个详情页打不开 ──▶ 用户连下单都做不了
↑ 因为「猜你喜欢」,丢了「成交」
✅ 优雅降级(弃车保帅):
推荐挂 ──▶ 「猜你喜欢」模块显示默认热榜 / 干脆不显示
──▶ 商品详情、加购、下单、支付【全部照常】
↑ 用户几乎无感,核心交易一分钱不少实现优雅降级的工程抓手,是降级开关 / 功能开关(feature flag):把每个非核心功能,都做成可以一键关闭的开关。出事时(或大促前),运维一键关掉「猜你喜欢」「实时弹幕」「个性化排序」,把宝贵的资源(CPU、数据库连接、下游配额)让给核心交易链路。降级的常见形态:
- 降级到缓存 / 默认值:推荐挂了,返回一个预置的热榜;实时库存查不到,显示「有货」让用户先下单,后端再校验。
- 降级功能丰富度:大促时关掉「个性化」,所有人看同一个榜单——省掉昂贵的实时计算。
- 降级到异步:同步处理扛不住,先收下请求、丢进队列、返回「处理中」,慢慢消化。
架构智慧:优雅降级的前提,是你早就想清楚了「什么是核心、什么是非核心」——这件事必须在风平浪静时做完,绝不能等事故现场才临时拍脑袋。一个没有降级预案的系统,在过载时只有「全开」和「全关」两个挡位;一个有降级预案的系统,有一整排可以逐级松开的「泄压阀」。 这一条紧扣 06 章 那句「大促时关掉『猜你喜欢』,保住『下单支付』」——它不是一句口号,而是一套需要提前埋好开关的工程能力。
七、量化可靠性:SLI / SLO / SLA 与错误预算
讲了这么多手段,一个绕不开的问题:到底要做到多可靠? 06 章 已经警告过——「别张口就要五个 9」,每多一个 9,成本数量级上涨。但「做到多可靠」不能靠感觉拍板,得有一套可量化、可管理的语言。Google SRE 把它讲透了,三个词先分清:
SLI (Indicator 指标) ── 你【测量】的那个数:成功率?P99 延迟?
例:「过去 5 分钟,成功响应数 / 总请求数」
SLO (Objective 目标) ── 你给 SLI 定的【内部目标】:成功率 ≥ 99.9%
例:「一个季度内,99.9% 的请求成功且 < 300ms」 ← 团队自己的及格线
SLA (Agreement 协议) ── 写进【合同】、违约要【赔钱】的那条线
通常【松于】SLO(留安全垫):对外承诺 99.5%,内部 SLO 卡 99.9%而把它们用活的,是 错误预算(Error Budget) 这个绝妙的发明:
既然 100% 不可能,就定 SLO = 99.9%
↓
那么 0.1% 就是你被【允许】出错的额度 = 错误预算
(一个月 ≈ 43 分钟的「可以挂」的时间)
↓
预算【还有剩】 ──▶ 大胆发新功能、上线、搞实验(反正还输得起)
预算【烧光了】 ──▶ 冻结一切上新,全员转去搞稳定性,直到把预算「攒」回来架构智慧:错误预算是韧性工程里最聪明的「政治发明」——它把「稳定 vs 迭代速度」这场研发和 SRE 之间永恒的battle,从「靠嗓门大」变成了「用一个共享的数字说话」。它还揭示了一个反直觉的真相:追求 100% 可靠是错的。 100% 意味着你永远不敢发布、不敢实验,迭代速度归零——而用户其实根本感知不到 99.9% 和 100% 的区别(网络本身就没那么可靠)。留出错误预算,是在「主动给自己留出冒险和迭代的空间」。 几个 9 的选择,本质是 06 章 那句:回到业务问「这个系统真的需要那么多 9 吗」——多出来的每个 9,都是从迭代速度和真金白银里换的。
八、混沌工程:用主动注入故障,证明韧性
最后一个、也是最颠覆认知的判断:你以为系统有韧性,和系统真的有韧性,是两回事。 前面六节的所有设计——熔断、降级、舱壁、超时——在没被真正触发过之前,都只是「理论上能扛」。 而没演练过的容灾预案,约等于没有。
悖论:那些「故障时才会触发」的代码路径(熔断逻辑、降级分支、failover 切换),恰恰是平时跑得最少、测试最薄、最可能悄悄坏掉的路径。等到真出事那一刻,你才发现「降级开关三个月前就失灵了」「failover 从库其实没在同步」——这是事故现场最常见的二次打击。
混沌工程(Chaos Engineering) 的回答简单而生猛:别假设,去证明。主动、可控地往生产系统里注入故障,在「可控的小爆炸」中,提前暴露那些「等真出事才会暴露」的脆弱点。 它发源于 Netflix——当年为了上云,他们做了 Chaos Monkey:
Chaos Monkey:在工作时间,【随机】杀掉生产环境里的实例
│
└─▶ 逼着每个团队从第一天起就假设「我的实例随时会被杀」
→ 于是没人敢依赖「单个实例不挂」→ 冗余和自愈成了【默认习惯】
后来扩成「Simian Army(猴子军团)」:
• Latency Monkey ── 注入延迟,演练「慢依赖」(正是第二节那个杀手)
• Chaos Gorilla ── 干掉【整个可用区】,演练区域级容灾
• Chaos Kong ── 干掉【整个区域】,演练最高级别的灾难恢复它的精髓不是「搞破坏」,而是一套科学方法:先定义「正常状态」的稳态指标(如成功率)→ 提出假设「就算杀掉一个实例,成功率也不该掉」→ 在生产(或仿真环境)注入故障 → 看假设是否成立 → 不成立就修。 把「希望它能扛」变成「已经验证过它能扛」。
架构智慧:混沌工程是「为失败而设计」这一整章的验收环节——它逼你把「纸上的韧性」变成「跑过的韧性」。这背后是一个深刻的工程心态转变:与其在凌晨三点被一场没预料到的真故障叫醒、手忙脚乱,不如在周二下午、喝着咖啡、用一场你完全掌控的「计划内故障」,提前找出同样的弱点。 主动找痛,是为了不被动挨打。当然——它有严格前提:必须先有监控能看见影响、有爆炸半径控制能及时止损、能一键中止。 没有这些护栏就往生产注故障,那不叫混沌工程,叫事故。
📌 真实案例:三次「手抖」,如何拖垮半个互联网
韧性工程最好的老师,是真实的大型事故。下面三个,都来自官方事后分析(post-mortem),每一个都精准踩中了本章的某根筋。
① AWS S3 大故障(2017-02-28):一条打错的命令,如何拖垮大半个互联网。 那天上午,一名 S3 工程师按既定排查手册执行命令,本想下线少量计费子系统的服务器,但一个参数输入有误,导致下线的服务器远多于预期——其中误伤了支撑 S3 索引子系统(管理整个区域所有对象的元数据和位置) 和放置子系统的大批服务器。这两个子系统被迫完全重启;而在 S3 的体量下,重启意味着要重建数十亿对象的元数据索引,耗时远超预期。由于无数服务(包括 AWS 自己的控制台、乃至大量第三方网站)都依赖 us-east-1 的 S3,大半个互联网随之瘫痪约 4 小时。
教训精确对应本章:① 「手抖」是常态(Vogels「everything fails」里就包括人);② 爆炸半径失控——一条命令能误伤如此大范围,说明缺乏隔离与「危险操作」的二次防护;③ AWS 事后的整改正是给这类命令加上「移除容量不得超过最小安全阈值」的护栏,本质就是给操作装一个降载式的下限保护。 📎 AWS 官方事后分析:Summary of the Amazon S3 Service Disruption (US-EAST-1)
② Meta / Facebook 全球宕机(2021-10-04):一次配置变更,如何让 35 亿人「消失」约 6 小时。 一次例行维护中,一条本想「评估骨干网容量」的命令,意外把整个骨干网的连接全部撤下;而一个审计工具的 bug 没能拦住这条错误命令。骨干网一断,Facebook 的 DNS 服务器因检测到自身与数据中心失联,主动撤回了自己的 BGP 路由通告——于是从全世界的角度看,Facebook 的 DNS 直接从互联网上「消失」了:服务器其实还活着,但全世界都找不到它。Facebook、Instagram、WhatsApp、Messenger 全球下线约 6 小时。雪上加霜的是:连内部工具、门禁系统都依赖这套网络,工程师一度连机房都进不去、远程也连不上,恢复因此被严重拖慢。
教训精确对应本章:① 级联失败的教科书——一个局部动作,经由「健康检查 → 自动撤回路由」的自动化链条,放大成全局灾难,与 10 章 GitHub 那例如出一辙(自动化在极端情况下「正确地」闯了大祸);② 恢复工具不能依赖于「正在恢复的那个系统」——这是韧性设计里极易被忽视的循环依赖,直接拉长了 MTTR。 📎 Meta 官方事后分析:More details about the October 4 outage
③ Cloudflare 全球故障(2019-07-02):一个正则表达式,如何在几秒内打满全球 CPU。 一次 WAF(Web 应用防火墙)规则的常规上线,其中一条规则含有一个写得不好的正则表达式,触发了灾难性回溯(catastrophic backtracking)——CPU 开销爆炸式增长。致命的是:这条规则没有灰度、没有金丝雀发布,被一次性推送到了全球所有边缘服务器。结果全球每一个处理 HTTP/HTTPS 流量的 CPU 核心瞬间被打满,Cloudflare 网络大面积瘫痪约 27 分钟(它当时承载着可观比例的互联网流量)。
教训精确对应本章:① 资源耗尽(这次是 CPU)和线程池耗尽是同一种病——某段逻辑抱着资源不放,把整体拖垮;② 变更没有爆炸半径控制(全球一次性推送)是把局部 bug 放大成全局灾难的放大器——这正是 cell-based / 金丝雀发布要解决的问题;③ Cloudflare 事后重新加回了被误删的 CPU 用量保护、并改用有运行时上限保证的正则引擎——本质都是「给可能失控的东西装上限」。 📎 Cloudflare 官方事后分析:Details of the Cloudflare outage on July 2, 2019
三个案例,三种「手抖 / 小 bug」,共同的剧本是:一个微小的局部触发,顺着「资源耗尽 / 自动化连锁 / 全局推送」的放大器,炸成了全局灾难。 韧性工程要做的,就是在每一个放大器上装闸:隔离、熔断、降载、爆炸半径控制、灰度发布。
🤖 AI / vibe coding 视角:happy path 原型,与人补的韧性判断
韧性是 AI 时代含金量不降反升的判断力,原因有两层。
第一层:AI 生成的代码,默认只有「乐观路径(happy path)」。 你让 AI 写一段「调用支付接口」的代码,它会给你一段在「网络通、对方秒回、一切正常」前提下完美运行的代码——但默认没有超时、没有重试退避、没有熔断、没有降级、没有隔离。 这正是本章讲的所有东西的反面。vibe coding 的产出,是一个在 demo 里跑得飞快、一上生产就脆得像玻璃的原型:
AI 默认给你的(happy path) 生产真正需要的(人补的韧性判断)
──────────────────────── ──────────────────────────────────
result = call(payment_api) + 超时(别无限等)
# 假设它一定成功、一定秒回 + 退避 + 抖动重试(且接口必须幂等)
+ 熔断(对方挂了别再压它、别拖垮自己)
+ 降级(挂了走兜底,而不是整页崩)
+ 舱壁(用独立池,别拖垮其他调用)
↑ 把这个脆弱的玻璃原型,变成扛得住生产的系统,靠的正是这一整列「人补的判断」把脆弱的 happy-path 原型,锻造成扛得住生产的系统——这中间的全部距离,就是这一章。 AI 能瞬间帮你写出熔断器的代码,但「这个依赖该不该熔断、阈值设多少、降级到什么、丢哪部分流量」——是判断题,代价由你的线上事故承担。
第二层:AI 原生系统本身,把韧性的需求又拔高了一截。 因为它引入了一种新的失控形态——自主循环烧钱:
- AI Agent 平台 的「步数 / 成本 / 超时上限」,本质就是本章的降载 + 熔断:一个会「规划 → 调工具 → 再规划」的自主 agent,若不设硬上限,可能原地打转、无限循环,一晚上烧掉天文数字的 token 费。给自主循环装的这些「刹车」,正是韧性思想在 AI 时代的新形态——自主性越高,越要有硬性的熔断与降载。
- 模型推理服务 面对超额请求,GPU 就那么多,只能排队 + 降载:接不下的请求快速拒绝(返回 429),而不是全塞进来让所有人一起超时——这正是第四节降载判断的直接应用。
- Agent 的工具调用要幂等 + 可重试、长任务要靠检查点可恢复(部分失败)——这些都站在本章和 10、11 的地基上。
架构智慧:LLM 把「不确定性」又叠了一层在系统的「不确定性」之上——模型会跑偏、会幻觉、会陷入循环。所以 AI 原生系统不是「更不需要韧性」,而是更需要、且需要新形态的韧性(给自主循环装刹车)。vibe coding 让写代码变快了,但**「让脆弱的原型扛得住生产」这件事的价值,只升不降**——它恰恰是人最该补、AI 最补不了的那部分判断。
🎯 随堂检验
- A加大线程池和服务器,硬扛住所有请求
- B给推荐调用加熔断和独立线程池,挂了就快速失败并走降级,保住核心链路
- C对推荐接口立刻无限重试,直到它恢复
本章小结
- 核心思维翻转:高可用不是「祈祷别出事」,是「预设一定会出事」(Vogels:Everything fails, all the time)。度量重心从 MTBF(多久坏一次)转向 MTTR(坏了多久能恢复)——韧性是「摔了能立刻爬起来、只蹭破一点皮」。
- 级联失败是头号杀手:大事故极少是「一个组件挂了」,而是「一个组件慢了,经由资源耗尽 + 重试风暴 + 超时堆叠这三个放大器,把全站拖垮」。慢,比宕机更致命。
- 隔离爆炸半径:舱壁(切开资源池)、cell-based(复制成独立细胞)、shuffle sharding(随机组合让连坐趋近于零)——核心是先画清「故障域」,把「核心」和「非核心」舱壁隔开。
- 主动自保:熔断器(三态,像保险丝跳闸 + 半开探测)、超时预算(沿链路递减)、背压与降载(主动丢一部分保整体)。「优雅地拒绝一部分」远胜「假装全扛、然后一起崩」。
- 聪明地重试:指数退避 + 抖动(打散同步重试,一行改动换数量级稳定)+ 重试预算 + 幂等前提(承接 [11])——四样缺一不可,否则重试就是风暴的元凶。
- 优雅降级:提前把功能按「掉了会死 vs 能忍」分级,用降级开关一键关非核心,把资源让给核心链路。「坏一部分」远好过「全挂」。
- 量化可靠性:SLI(测量值)/ SLO(内部目标)/ SLA(合同线);错误预算把「稳定 vs 速度」之争变成「用一个共享数字说话」——追求 100% 是错的,留预算是为了留出迭代和冒险的空间。
- 混沌工程:别假设,去证明——主动注入故障(Chaos Monkey / Simian Army),在可控小爆炸里提前暴露脆弱点。前提是有监控、有爆炸半径控制、能一键中止。
- AI 时代主线:vibe coding 默认只给「乐观路径」(无超时/重试/熔断/降级/隔离);把脆弱的 happy-path 原型变成扛得住生产的系统,靠的正是人补的韧性判断。而 AI 原生系统(agent 的步数/成本上限 = 降载+熔断)只会让韧性更吃重。
承上启下:这一章解决的是「系统会不会倒」——在零件不断坏掉时如何不崩。下一章(进阶篇第 4 章)《13 · 规模化的力学》换一个维度:当系统不是「坏」,而是「被成功撑爆」——用户、数据、流量涨了百倍千倍——架构会在哪里先裂开?分片、热点、缓存、异步化这些「规模化的力学」,又该怎么提前布局。韧性让你扛住故障,规模化让你扛住成功。
💬 评论