# 12 · 为失败而设计:韧性工程 > 一句话点题:**高可用不是「祈祷别出事」,而是「预设一定会出事」——你控制不了组件什么时候坏,但你能设计「坏了之后会发生什么」。韧性工程,就是把这件事从运气,变成可以下注、可以验证的工程。** --- > **🧭 进阶篇第 3 章。** [06 · 质量属性与取舍](06-质量属性与取舍.md) 给了你「可用性 = 几个 9 = 每年允许停机多久」的标尺;[10 · 分布式系统的硬道理](10-分布式系统的硬道理.md) 摆出了病理——部分失败、灰色失败、自动化在分区时「正确地」闯祸。这一章把它们拧成一根绳:**既然失败必然发生、又分不清死与慢,那一个系统该长成什么样,才能在零件不断坏掉的同时整体不倒?** 这就是「为失败而设计(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 个节点的组合(C(8,2)=28 种组合) → 两个客户「恰好分到完全相同的两个节点」的概率极低 → 一个毒客户打挂它那 2 个节点,几乎不会和别人「完全重叠」 别人哪怕共享了其中 1 个节点,还有另 1 个能用 → 受影响面被摊薄到接近 0 ``` > **架构智慧**:隔离的核心判断,是先想清楚 **「故障域(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,甚至排队) │否 ▼ 正常处理 ──▶ 被放进来的请求,都得到了【正常、快速】的服务 ↑ 牺牲一部分,换大多数人的可用 ``` > **架构智慧(本节最重要)**:**「优雅地拒绝一部分请求」,远胜于「假装全部都能扛、然后一起崩」。** 这是从「乐观」到「成熟」的分水岭。背压是「向上游说:我满了,你慢点发」;降载是「我自己决定:超出的部分立刻丢,保住放进来的那批被好好服务」。**一个会主动降载的系统,在过载时是「部分可用」;一个来者不拒的系统,在过载时是「全部不可用」。** 这正是 [模型推理服务](../templates/inference-serving/README.md) 面对超额请求时要排队、要拒绝的原因——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 · 数据一致性工程](11-数据一致性工程.md) 的核心:**at-least-once 重试 + 消费端幂等 = 效果上的恰好一次。** [支付系统](../templates/payment-system/README.md) 的幂等扣款、[通知系统](../templates/notification-system/README.md) 的去重限频,就是「敢于重试」的地基——**没有幂等,你根本不敢重试;不敢重试,瞬时故障就成了永久失败。** > **架构智慧**:重试不是「失败了就再试一次」这么天真。**安全的重试 = 指数退避 + 抖动 + 重试预算 + 幂等前提**,四样缺一不可。少了退避和抖动,重试会变成压垮下游的风暴;少了预算,风暴没有上限;少了幂等,重试会把数据搞错。**AI 生成的代码默认是 `retry(3)` 式的裸重试——这恰恰是最危险的那种。** --- ## 六、优雅降级:核心保命,非核心可弃 前面几节都在「防止崩」。但有时候下游就是挂了、就是恢复不了——这时韧性的最后一道防线是:**「坏一部分」远好过「全挂」。** 这就是优雅降级(Graceful Degradation)。 核心判断是一道你必须提前做好的功课:**把功能按「掉了会死 vs 掉了能忍」分级。** ``` 电商大促,推荐服务挂了。两种活法: ❌ 没分级(一损俱损): 推荐挂 ──▶ 商品详情页加载推荐时报错 ──▶ 整个详情页打不开 ──▶ 用户连下单都做不了 ↑ 因为「猜你喜欢」,丢了「成交」 ✅ 优雅降级(弃车保帅): 推荐挂 ──▶ 「猜你喜欢」模块显示默认热榜 / 干脆不显示 ──▶ 商品详情、加购、下单、支付【全部照常】 ↑ 用户几乎无感,核心交易一分钱不少 ``` 实现优雅降级的工程抓手,是**降级开关 / 功能开关(feature flag)**:把每个非核心功能,都做成可以**一键关闭**的开关。出事时(或大促前),运维一键关掉「猜你喜欢」「实时弹幕」「个性化排序」,把宝贵的资源(CPU、数据库连接、下游配额)**让给核心交易链路**。降级的常见形态: - **降级到缓存 / 默认值**:推荐挂了,返回一个预置的热榜;实时库存查不到,显示「有货」让用户先下单,后端再校验。 - **降级功能丰富度**:大促时关掉「个性化」,所有人看同一个榜单——省掉昂贵的实时计算。 - **降级到异步**:同步处理扛不住,先收下请求、丢进队列、返回「处理中」,慢慢消化。 > **架构智慧**:优雅降级的前提,是**你早就想清楚了「什么是核心、什么是非核心」**——这件事必须在风平浪静时做完,绝不能等事故现场才临时拍脑袋。**一个没有降级预案的系统,在过载时只有「全开」和「全关」两个挡位;一个有降级预案的系统,有一整排可以逐级松开的「泄压阀」。** 这一条紧扣 [06 章](06-质量属性与取舍.md) 那句「大促时关掉『猜你喜欢』,保住『下单支付』」——它不是一句口号,而是一套需要提前埋好开关的工程能力。 --- ## 七、量化可靠性:SLI / SLO / SLA 与错误预算 讲了这么多手段,一个绕不开的问题:**到底要做到多可靠?** [06 章](06-质量属性与取舍.md) 已经警告过——「别张口就要五个 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 章](06-质量属性与取舍.md) 那句:回到业务问「这个系统真的需要那么多 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)](https://aws.amazon.com/message/41926/) **② Meta / Facebook 全球宕机(2021-10-04):一次配置变更,如何让 35 亿人「消失」约 6 小时。** 一次例行维护中,一条本想「评估骨干网容量」的命令,**意外把整个骨干网的连接全部撤下**;而一个**审计工具的 bug 没能拦住这条错误命令**。骨干网一断,Facebook 的 DNS 服务器因检测到自身与数据中心失联,**主动撤回了自己的 BGP 路由通告**——于是从全世界的角度看,**Facebook 的 DNS 直接从互联网上「消失」了**:服务器其实还活着,但全世界都找不到它。Facebook、Instagram、WhatsApp、Messenger 全球下线约 **6 小时**。雪上加霜的是:连内部工具、门禁系统都依赖这套网络,工程师**一度连机房都进不去、远程也连不上**,恢复因此被严重拖慢。 > 教训精确对应本章:① **级联失败的教科书**——一个局部动作,经由「健康检查 → 自动撤回路由」的自动化链条,放大成全局灾难,与 [10 章](10-分布式系统的硬道理.md) GitHub 那例如出一辙(自动化在极端情况下「正确地」闯了大祸);② **恢复工具不能依赖于「正在恢复的那个系统」**——这是韧性设计里极易被忽视的循环依赖,直接拉长了 MTTR。 > 📎 [Meta 官方事后分析:More details about the October 4 outage](https://engineering.fb.com/2021/10/05/networking-traffic/outage-details/) **③ 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](https://blog.cloudflare.com/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 平台](../templates/ai-agent-platform/README.md) 的「**步数 / 成本 / 超时上限**」,本质就是本章的**降载 + 熔断**:一个会「规划 → 调工具 → 再规划」的自主 agent,若不设硬上限,可能**原地打转、无限循环,一晚上烧掉天文数字的 token 费**。给自主循环装的这些「刹车」,正是韧性思想在 AI 时代的新形态——**自主性越高,越要有硬性的熔断与降载。** - [模型推理服务](../templates/inference-serving/README.md) 面对超额请求,GPU 就那么多,只能**排队 + 降载**:接不下的请求快速拒绝(返回 429),而不是全塞进来让所有人一起超时——这正是第四节降载判断的直接应用。 - Agent 的工具调用要**幂等 + 可重试**、长任务要靠**检查点可恢复**(部分失败)——这些都站在本章和 [10](10-分布式系统的硬道理.md)、[11](11-数据一致性工程.md) 的地基上。 > **架构智慧**:**LLM 把「不确定性」又叠了一层在系统的「不确定性」之上**——模型会跑偏、会幻觉、会陷入循环。所以 AI 原生系统不是「更不需要韧性」,而是**更需要、且需要新形态的韧性**(给自主循环装刹车)。vibe coding 让写代码变快了,但**「让脆弱的原型扛得住生产」这件事的价值,只升不降**——它恰恰是人最该补、AI 最补不了的那部分判断。 --- ## 🎯 随堂检验 --- ## 本章小结 - **核心思维翻转**:高可用不是「祈祷别出事」,是「预设一定会出事」(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 · 规模化的力学》换一个维度:当系统不是「坏」,而是「**被成功撑爆**」——用户、数据、流量涨了百倍千倍——架构会在哪里先裂开?分片、热点、缓存、异步化这些「规模化的力学」,又该怎么提前布局。韧性让你扛住故障,规模化让你扛住成功。