# 10 · 分布式系统的硬道理:部分失败、时间与共识 > 一句话点题:**单机悄悄给了你三个奢侈——操作要么成要么败、全世界有一个统一的「现在」、函数调用必达。一旦跨过两台机器,这三样全没了。** 入门篇的「大白话 CAP」只是门口,本章往下挖到真正硌脚的硬骨头:分布式到底难在哪;以及——在 AI 帮你写实现的时代,哪些判断依然只能由你来下。 --- > **🧭 进阶篇从这里开始。** 入门篇(01–09)教你**看懂一个系统、并能从 0 设计一个中小系统**;从本章起的进阶篇,处理的是另一类问题——**系统一旦做大、做关键,才会露出獠牙的那些「硬骨头」**:分布式、失败、规模、演进、组织。 > > 这恰恰是 AI 时代最值钱的能力。AI 几秒就能吐出一份能跑的 Raft 实现,但「这里到底**要不要**共识、能**容忍多大**不一致、网络裂开那一刻你要**正确还是要在线**」——这些**判断题**的代价由你的业务承担,AI 给不了你标准答案,因为答案取决于你愿意拿什么换什么。**实现越来越廉价,判断越来越值钱。** 这条主线会贯穿整个进阶篇。 --- ## 一、单机的三个奢侈,跨过两台机器就全没了 写单机程序时,有三件事你习以为常,从没意识到它们是奢侈品: ``` 单机世界(三个奢侈) 分布式世界(三个全没了) ────────────────── ────────────────────────── ① 函数调用必达 ① 网络不可靠:丢包、乱序、重复、延迟、分区 ② 全世界一个"现在"(同一时钟) ② 没有全局时钟:每台机器的表都不一样,"同时"是幻觉 ③ 要么成功、要么失败 ③ 部分失败:有的节点成了、有的败了、有的不知道死活 ``` 前两条都好理解,**第三条「部分失败(partial failure)」才是万恶之源**。单机里一个操作的结局是二元的:成,或败。分布式里多了一个要命的第三态——**「不知道」**: > 你给另一台机器发了个请求,等了 2 秒没回音。它是**没收到**?**收到了但还在算**?**算完了但回包丢了**?还是**整台机器已经宕了**?——**你无法区分「它死了」和「它只是慢」。** 这叫**灰色失败(gray failure)**。 你手里唯一的武器是**超时(timeout)**:等够一段时间还没回,就当它失败。但超时本质是一次**猜测**——猜短了,会把只是慢的节点误判为死(然后重试,雪上加霜);猜长了,故障恢复就迟钝。**整个分布式系统的可靠性设计,很大程度上就是在跟这个「分不清死与慢」的幽灵搏斗。** > **架构智慧**:单机时代你写代码默认「调用会成功」;分布式时代要反过来——**默认每一次跨网络的调用都可能失败、超时、或重复送达**,然后问自己:这种情况下,我的系统会怎样?这一个思维翻转,是入门和进阶的分水岭。 --- ## 二、一致性不是一个开关,是一道光谱(把 05 往下挖) [05 · 数据与状态](05-数据与状态.md) 里我们把一致性拉成了「强一致 ↔ 最终一致」一条线。现在放大中间地带——它其实是好几档,每一档的**价格(延迟与可用性)**都不一样: ``` 贵 ◀───────────────────────────────────────────────▶ 便宜 线性一致 顺序一致 因果一致 最终一致 (Linearizable) (Sequential) (Causal) (Eventual) "全网立刻看到 "大家看到的 "有因果的事 "迟早一致, 同一个最新值, 顺序一致, 顺序不乱,无关 中间窗口各看各的" 像只有一台机器" 但可能不是实时" 的爱咋咋地" ↑ ATM 取钱 ↑ 配置下发 ↑ 群聊消息/评论 ↑ 点赞数/浏览量 ``` - **线性一致**:最强。任何人在任何节点读,都立刻读到「最近一次写」的值,仿佛系统只有一台机器。代价最高——需要节点间协调,延迟和可用性都要为它让路。 - **因果一致**:一个很实用的中间档。**有因果关系的操作**(你回复了我的消息)对所有人顺序一致;**没有因果关系的并发操作**(两个陌生人各发各的)谁先谁后无所谓。它比线性一致便宜得多,又能避免「先看到回复、后看到原帖」这种荒谬。 - **最终一致**:最弱也最便宜。写完之后副本们慢慢追平,中间窗口允许各看各的。 > **关键认知**:**强一致不是「更好」,是「更贵」。** 每往左升一档,你都在为它支付延迟和可用性的账单。架构判断不是「尽量强」,而是「这份数据**配得上**多强」——ATM 余额配得上线性一致;群聊消息配因果一致就够;点赞数用最终一致都嫌奢侈。**把宝贵的强一致配额,花在真正会出事的地方。** --- ## 三、CAP 只是开场白,PACELC 才是你天天在付的账 [05](05-数据与状态.md) 讲了 CAP 的人话版:**网络分区(P)迟早发生,届时只能在一致性(C)和可用性(A)里二选一。** 但 CAP 漏了一大半真相——**绝大多数时候网络是好的,这时你在付什么账?** 补全它的是 **PACELC**: ``` if (P 网络分区) { // 偶尔发生 二选一: C 还是 A // 要正确,还是要在线? } else { // ★ 99% 的时间在这里 ★ 二选一: L 还是 C // 要低延迟,还是要强一致? } ``` - **分区时(PAC)**:网裂成两半,你要么拒绝服务保正确(选 C),要么继续服务但可能给旧数据(选 A)。这是 CAP 讲的部分。 - **没分区时(ELC)**:就算网络一切正常,**强一致依然要钱**——为了让所有副本同意一个值,得等节点间往返协调,这就是**延迟(L)**。你想更快(低 L),就得放松一致性(C)。 > **判断要点**:CAP 容易让人以为「一致性只在网络故障时才需要权衡」。**错。PACELC 提醒你:即使岁月静好,你每一次"要不要等所有副本确认"的选择,都是在拿延迟换一致性。** 这才是你天天在付的账。 --- ## 四、没有「现在」:逻辑时钟买到的是「因果正确」 既然每台机器的物理时钟都有偏差、网络延迟又不确定,那「事件 A 比 B 先发生」这句话在分布式里**怎么才算数**?靠看墙上的表是不行的——两台机器的表对不齐,差几十毫秒,而很多事就发生在这几十毫秒里。 办法是**放弃物理时间,改用因果关系**。这就是**逻辑时钟**: ``` 节点甲: 事件a1 ──发消息m──▶ ╲ 节点乙: 事件b1 ────────────── ▶ 收到m ── 事件b2 ↑ 逻辑时钟保证:a1(发) 一定排在 b2(收之后) 前面, 因为 b2 "因果上" 依赖 m,而 m 来自 a1。 至于 a1 和 b1 谁先?它们没有因果关系 —— 算"并发",不强行排序。 ``` - **Lamport 时钟**:给所有事件定一个**全序**(谁都能排出先后),但分不清「真有因果」还是「恰好并发」。 - **向量时钟(Vector Clock)**:更进一步,能**识别出哪些事件是并发的**(从而发现「写冲突」)。 - **混合逻辑时钟(HLC)**:把物理时间和逻辑计数缝在一起,既接近真实时间、又保证因果——现代分布式数据库做一致性快照常用它。 > **判断要点**:大多数业务系统**用不上**逻辑时钟——别为了显得高级而引入。但一旦你的系统要保证「**因果正确**」,它就绕不开:[实时协同文档](../templates/collaborative-doc/README.md) 里多人编辑的合并顺序、[实时通讯](../templates/realtime-chat/README.md) 里消息的时序、分布式数据库的一致性快照……背后都是「在没有全局钟的世界里,如何确定先后」这同一个问题。 --- ## 五、共识:最贵的一种协调,千万别滥用 有时候你**就是需要**一组机器对某件事达成**铁板一块的一致**:谁是主库?这条复制日志的第 100 条是什么?这把分布式锁现在归谁?这就是**共识(Consensus)**问题,**Raft / Paxos** 就是解它的。 共识买到的东西很硬核:**即使部分节点宕机或失联,存活的多数派仍能对「一个值 / 一条日志的顺序」达成唯一、一致的决定。** 它是分布式世界里「单一权威真相」的来源。 但它**非常贵**: ``` 每做一个决定,都要走一轮"多数派投票": 提议者 ──▶ [节点1][节点2][节点3][节点4][节点5] ╲ │ │ ╱ 等到 > 半数 确认,才算数(这一来一回 = 延迟) • 至少 3 或 5 个节点(容忍 1~2 个挂) • 写吞吐受限于单一 leader • 每次写多一轮网络往返(延迟涨) • 成员变更(加减节点)很微妙、易出错 ``` > **判断要点(本节最重要)**:**共识贵,只用在「必须有唯一权威顺序」的少数地方**——选主、集群元数据、分布式锁、复制日志/状态机。**绝大多数业务数据不该直接跑共识。** 一个把每条业务写入都塞进共识组的系统,是在为根本不需要的「全局唯一顺序」支付高昂的延迟与吞吐代价。 > > ❌ 反模式:把 Raft 集群当通用数据库使。✅ 正确:让共识只管那一小撮「全局只能有一个答案」的元数据,业务数据该分片分片、该最终一致就最终一致。 --- ## 六、「Exactly-once」是个幻觉:把正确性交给幂等 新人最爱问:「消息系统怎么保证**恰好一次(exactly-once)**投递?」**残酷真相:传递层做不到。** 因为网络会丢、会重,你发出一条消息后没收到 ack,只有两种选择: ``` 重发(可能造成重复) ──▶ 至少一次 (at-least-once):不丢,但可能重 不重发(可能造成丢失)──▶ 至多一次 (at-most-once):不重,但可能丢 "恰好一次" 作为传递保证 —— 不存在。 ``` 真正能做到的,是**换个地方解决**: > **at-least-once 投递(允许重复)+ 消费端幂等(重复了也只生效一次)= 业务效果上的「恰好一次」。** 「幂等」就是:同一个操作执行一次和执行十次,结果一样。实现办法通常是给每个操作一个**幂等键**,消费端用一张**去重表**记下「这个键处理过了」,再来直接跳过。 > **判断要点**:别在传输层追求虚幻的 exactly-once,**把正确性下沉到消费端的幂等设计**。[支付系统](../templates/payment-system/README.md) 的幂等扣款、[通知系统](../templates/notification-system/README.md) 的去重限频,都是这一招的活样板。 > > 这条「at-least-once + 幂等」是下一章的伏笔——它正是 **Saga、Outbox、事件溯源**这些「在分布式里把数据弄对」的工程手法的地基。 --- ## 📌 真实案例:43 秒的网络分区,如何让 GitHub 乱了 24 小时 2018 年 10 月 21 日,GitHub 一次例行更换光纤设备,导致**美东主数据中心与美东网络枢纽断联 43 秒**——仅仅 43 秒。但这 43 秒精准踩中了本章的每一根硬骨头: 1. **部分失败 + 失败检测**:断联期间,美东数据中心**继续接收了写入**(它没死,只是和外界失联了);而管理数据库拓扑的编排器 **Orchestrator(基于 Raft 共识)** 这边,把「美东失联」**判定为「美东主库不可用」**——这正是「分不清死与慢」的灰色失败。 2. **共识的双刃**:美西和公有云的 Orchestrator 节点凑齐了**多数派**,按 Raft 共识**自动把主库切到了美西**,让写入转向美西。一切都「按配置正确执行」。 3. **没有全局真相,数据分叉了**:43 秒后网络恢复,两岸各自都接收过写入,**成了「两个都自认为是主、且各持对方没有的数据」的分叉集群**。自动调和会有丢数据风险,于是只能**人工小心修复**——服务降级整整 **24 小时 11 分钟**。 事后 GitHub 的头号整改措施是:**禁止 Orchestrator 跨区域提升主库**。换句话说——一个为「单节点故障」设计的自动切换机制,在「整个区域分区」面前,自动做出了应用层根本扛不住的决定。 > 教训精确对应本章:**部分失败让你分不清死与慢;没有全局时钟让失败检测必然有误判;共识能在多数派间达成一致,却也能在分区时"正确地"把系统切进一个灾难性的拓扑。** 自动化越强,越要想清楚它在极端情况下会「正确地」干出什么。 > > 📎 [GitHub 官方事后分析:October 21 post-incident analysis](https://github.blog/news-insights/company-news/oct21-post-incident-analysis/) --- ## 🤖 AI 时代,这章为什么不是「过时的底层细节」 你可能会想:这些不都是 AI 能帮我写的底层吗?恰恰相反—— - **AI 写实现,你下判断。** AI 能生成 Raft、重试、幂等去重的代码;但「这里要不要共识、容忍多大不一致、分区时选 C 还是 A」是**判断题**,代价由你的业务承担。会写 ≠ 会选。 - **AI 原生系统本身就是重度分布式。** 看看刚上线的 Agent 模板:[AI Agent / 工作流平台](../templates/ai-agent-platform/README.md) 的长任务要**可恢复**(部分失败)、工具调用要**幂等**(at-least-once 重试)、记忆要在多步间**保持一致**;[Claude Code](../templates/claude-code/README.md) 这类编码 agent 的并发子代理、检查点恢复,也都站在这章的硬道理上。**LLM 把"不确定性"又叠了一层在分布式的"不确定性"之上**——硬道理只会更吃重,不会过时。 --- ## 🎯 随堂检验 --- ## 本章小结 - **核心论断**:分布式难,难在单机的三个奢侈全没了——**网络不可靠、没有全局时钟、部分失败**。其中「部分失败」最致命,因为它带来「**分不清对方是死了还是只是慢**」的灰色失败,而你唯一的武器(超时)只是猜测。 - **一致性是一道有价目表的光谱**:线性 → 顺序 → 因果 → 最终,每往强档升一级,延迟与可用性的账单就涨一截。判断不是「尽量强」,而是「这份数据配得上多强」。 - **PACELC 比 CAP 完整**:分区时在 C/A 间选;**不分区时(99% 的时间)还在 L/C 间选**——强一致即使在岁月静好时也要花延迟买。 - **没有全局钟,就用因果**:逻辑时钟(Lamport / 向量 / HLC)买的是「因果正确」。多数系统用不上,但协同、时序、快照绕不开。 - **共识最贵,别滥用**:Raft/Paxos 给你「单一权威真相」,代价是多数派往返、节点下限、吞吐受限。只用在选主、元数据、锁、复制日志这类「全局必须唯一」处。 - **Exactly-once 是幻觉**:传递层只有 at-least-once 或 at-most-once;**at-least-once + 消费端幂等 = 效果上的恰好一次**。 - **AI 时代主线**:实现越来越廉价,选型与容错边界的**判断**越来越值钱;而 AI 原生系统本身就是重度分布式,这些硬道理只会更重要。 > **承上启下**:这一章把分布式的「病理」摆了出来——为什么会乱、会丢、会分叉。下一章(进阶篇第 2 章)《数据一致性工程》,我们接着这章结尾的「at-least-once + 幂等」往下走,讲在没有跨服务事务的世界里,**怎么真的把数据弄对**:Saga、Outbox、幂等去重、事件溯源与 CQRS 的落地。