Skip to content

10 · 分布式系统的硬道理:部分失败、时间与共识

一句话点题:单机悄悄给了你三个奢侈——操作要么成要么败、全世界有一个统一的「现在」、函数调用必达。一旦跨过两台机器,这三样全没了。 入门篇的「大白话 CAP」只是门口,本章往下挖到真正硌脚的硬骨头:分布式到底难在哪;以及——在 AI 帮你写实现的时代,哪些判断依然只能由你来下。


🧭 进阶篇从这里开始。 入门篇(01–09)教你看懂一个系统、并能从 0 设计一个中小系统;从本章起的进阶篇,处理的是另一类问题——系统一旦做大、做关键,才会露出獠牙的那些「硬骨头」:分布式、失败、规模、演进、组织。

先别紧张,这一章是入门篇的「老朋友变深」,不是另起炉灶。 还记得 05 · 数据与状态 里那个「点赞数可以最终一致、ATM 余额必须强一致」的例子吗?还记得那句「大白话版 CAP:网络一断,一致性和可用性只能二选一」吗?这一章干的事,就是把那几句你已经会背的结论,翻过来看它们背后到底踩了什么坑——为什么会「最终」一致、为什么强一致那么贵、网络到底是怎么「断」的。你已经站在门口了,我们只是往里再走一层。

这恰恰是 AI 时代最值钱的能力。AI 几秒就能吐出一份能跑的 Raft 实现,但「这里到底要不要共识、能容忍多大不一致、网络裂开那一刻你要正确还是要在线」——这些判断题的代价由你的业务承担,AI 给不了你标准答案,因为答案取决于你愿意拿什么换什么。实现越来越廉价,判断越来越值钱。 这条主线会贯穿整个进阶篇。


一、单机的三个奢侈,跨过两台机器就全没了

写单机程序时,有三件事你习以为常,从没意识到它们是奢侈品:

   单机世界(三个奢侈)            分布式世界(三个全没了)
   ──────────────────           ──────────────────────────
   ① 函数调用必达               ① 网络不可靠:丢包、乱序、重复、延迟、分区
   ② 全世界一个"现在"(同一时钟) ② 没有全局时钟:每台机器的表都不一样,"同时"是幻觉
   ③ 要么成功、要么失败          ③ 部分失败:有的节点成了、有的败了、有的不知道死活

前两条都好理解,第三条「部分失败(partial failure)」才是万恶之源。单机里一个操作的结局是二元的:成,或败。分布式里多了一个要命的第三态——「不知道」:

你给另一台机器发了个请求,等了 2 秒没回音。它是没收到?收到了但还在算?算完了但回包丢了?还是整台机器已经宕了?——你无法区分「它死了」和「它只是慢」。 这叫灰色失败(gray failure)

你手里唯一的武器是超时(timeout):等够一段时间还没回,就当它失败。但超时本质是一次猜测——猜短了,会把只是慢的节点误判为死(然后重试,雪上加霜);猜长了,故障恢复就迟钝。整个分布式系统的可靠性设计,很大程度上就是在跟这个「分不清死与慢」的幽灵搏斗。

架构智慧:单机时代你写代码默认「调用会成功」;分布式时代要反过来——默认每一次跨网络的调用都可能失败、超时、或重复送达,然后问自己:这种情况下,我的系统会怎样?这一个思维翻转,是入门和进阶的分水岭。


二、一致性不是一个开关,是一道光谱(把 05 往下挖)

05 · 数据与状态 里我们把一致性拉成了「强一致 ↔ 最终一致」一条线。现在放大中间地带——它其实是好几档,每一档的**价格(延迟与可用性)**都不一样:

   贵 ◀───────────────────────────────────────────────▶ 便宜
   线性一致           顺序一致      因果一致         最终一致
 (Linearizable)    (Sequential)  (Causal)        (Eventual)
 "全网立刻看到      "大家看到的     "有因果的事      "迟早一致,
  同一个最新值,      顺序一致,      顺序不乱,无关    中间窗口各看各的"
  像只有一台机器"    但可能不是实时"  的爱咋咋地"
   ↑ ATM 取钱        ↑ 配置下发      ↑ 群聊消息/评论   ↑ 点赞数/浏览量
  • 线性一致:最强。任何人在任何节点读,都立刻读到「最近一次写」的值,仿佛系统只有一台机器。代价最高——需要节点间协调,延迟和可用性都要为它让路。
  • 因果一致:一个很实用的中间档。有因果关系的操作(你回复了我的消息)对所有人顺序一致;没有因果关系的并发操作(两个陌生人各发各的)谁先谁后无所谓。它比线性一致便宜得多,又能避免「先看到回复、后看到原帖」这种荒谬。
  • 最终一致:最弱也最便宜。写完之后副本们慢慢追平,中间窗口允许各看各的。

关键认知:强一致不是「更好」,是「更贵」。 每往左升一档,你都在为它支付延迟和可用性的账单。架构判断不是「尽量强」,而是「这份数据配得上多强」——ATM 余额配得上线性一致;群聊消息配因果一致就够;点赞数用最终一致都嫌奢侈。把宝贵的强一致配额,花在真正会出事的地方。


三、CAP 只是开场白,PACELC 才是你天天在付的账

05 讲了 CAP 的人话版:网络分区(P)迟早发生,届时只能在一致性(C)和可用性(A)里二选一。 但 CAP 漏了一大半真相——绝大多数时候网络是好的,这时你在付什么账?

补全它的是 PACELC:

   if (P 网络分区) {            // 偶尔发生
       二选一:  C 还是 A       // 要正确,还是要在线?
   } else {                    // ★ 99% 的时间在这里 ★
       二选一:  L 还是 C       // 要低延迟,还是要强一致?
   }
  • 分区时(PAC):网裂成两半,你要么拒绝服务保正确(选 C),要么继续服务但可能给旧数据(选 A)。这是 CAP 讲的部分。
  • 没分区时(ELC):就算网络一切正常,强一致依然要钱——为了让所有副本同意一个值,得等节点间往返协调,这就是延迟(L)。你想更快(低 L),就得放松一致性(C)。

一句话翻译这两行公式:CAP 让你以为「一致性只在网络出故障(分区)那种极端时刻才需要权衡」;PACELC 拍醒你——网络好端端的时候(占 99% 的日子),你每写一次数据、每决定一次「要不要等所有副本都确认」,就已经在偷偷拿延迟换一致性了。 这不是出事才付的「意外险」,是你天天在付的「水电费」。

判断要点:CAP 容易让人以为「一致性只在网络故障时才需要权衡」。错。PACELC 提醒你:即使岁月静好,你每一次"要不要等所有副本确认"的选择,都是在拿延迟换一致性。 这才是你天天在付的账。


四、没有「现在」:逻辑时钟买到的是「因果正确」

💧 深水区(初读可跳过,主线不依赖它;想搞懂的往下看)。这一节是分布式里最「学术」的一块。你只需带走一句话:在分布式里没有统一的「现在」,所以要判断两件事谁先谁后,靠的不是看表,而是看「谁导致了谁」(因果)。 至于下面那三种时钟的名字,绝大多数业务一辈子用不上——读不下去就跳到第五节,毫无损失。下面是给好奇的人讲透的版本。

既然每台机器的物理时钟都有偏差、网络延迟又不确定,那「事件 A 比 B 先发生」这句话在分布式里怎么才算数?

先体会一下这个坑有多真实:你和朋友各看各的手机抢同一张票,两部手机的表可能差了几十毫秒;而「谁先点的」这件事,偏偏就发生在这几十毫秒里。靠对比两块表的读数来判断谁先谁后,是不可靠的——表本身就对不齐。

办法是放弃物理时间,改用因果关系:不问「几点几分」,只问「这件事是不是因为那件事才发生的」。这就是逻辑时钟:

   节点甲:  事件a1 ──发消息m──▶

   节点乙:  事件b1 ────────────── ▶ 收到m ── 事件b2

        逻辑时钟保证:a1(发) 一定排在 b2(收之后) 前面,
        因为 b2 "因果上" 依赖 m,而 m 来自 a1。
        至于 a1 和 b1 谁先?它们没有因果关系 —— 算"并发",不强行排序。

关键招数其实很朴素:让每条消息都「带上」发送方此刻的计数,收到的人把自己的计数往后调到比它更大。 这样「发」一定排在「收」前面,因果顺序就被钉死了。在这个朴素思想上,有三种做法,越往后越精细:

  • Lamport 时钟:给所有事件排出一条总队(谁都能说出先后)。
    • 打个比方:像微信群里按消息到达服务器的先后强行编号——所有消息都有了唯一的序号。缺点是:它分不清「B 是真的在回复 A」还是「A、B 只是恰好同时各发各的」。它只保证「不会先看到回复、后看到原帖」,但会把「其实互不相干的两件事」也硬排出个先后。
  • 向量时钟(Vector Clock):更进一步,能看出哪些事件其实是「同时发生、互不相干」的——也就能发现「写冲突」。
    • 打个比方:两个人同时编辑同一份文档的同一行,向量时钟能识别出「这俩改动谁也不知道对方」,于是系统知道这里撞车了,得让人来决定留谁,而不是稀里糊涂用「表上谁的时间晚」覆盖掉另一个。
  • 混合逻辑时钟(HLC):把「物理表的时间」和「逻辑计数」缝在一起,既贴近真实时间、又保证因果顺序。现代分布式数据库做「某一刻的一致性快照」常用它。
    • 一句话:前两种纯逻辑、读数和真实时间脱节(不好排查问题);HLC 让序号「看起来还像个时间」,兼顾两边。

判断要点:大多数业务系统用不上逻辑时钟——别为了显得高级而引入。但一旦你的系统要保证「因果正确」,它就绕不开:实时协同文档 里多人编辑的合并顺序、实时通讯 里消息的时序、分布式数据库的一致性快照……背后都是「在没有全局钟的世界里,如何确定先后」这同一个问题。


五、共识:最贵的一种协调,千万别滥用

有时候你就是需要一组机器对某件事达成铁板一块的一致:谁是主库?这条复制日志的第 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,把正确性下沉到消费端的幂等设计支付系统 的幂等扣款、通知系统 的去重限频,都是这一招的活样板。

这条「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


🤖 AI 时代,这章为什么不是「过时的底层细节」

你可能会想:这些不都是 AI 能帮我写的底层吗?恰恰相反——

  • AI 写实现,你下判断。 AI 能生成 Raft、重试、幂等去重的代码;但「这里要不要共识、容忍多大不一致、分区时选 C 还是 A」是判断题,代价由你的业务承担。会写 ≠ 会选。
  • AI 原生系统本身就是重度分布式。 看看刚上线的 Agent 模板:AI Agent / 工作流平台 的长任务要可恢复(部分失败)、工具调用要幂等(at-least-once 重试)、记忆要在多步间保持一致;Claude Code 这类编码 agent 的并发子代理、检查点恢复,也都站在这章的硬道理上。LLM 把"不确定性"又叠了一层在分布式的"不确定性"之上——硬道理只会更吃重,不会过时。

🎯 随堂检验

🤔团队想让消息系统做到「恰好一次投递」,最稳妥的架构判断是?
  • A在传输层用更可靠的协议,强行保证恰好一次
  • B接受至少一次投递,把幂等做在消费端,用幂等键去重
  • C改用强一致数据库存所有消息,就不会重复了

本章小结

  • 核心论断:分布式难,难在单机的三个奢侈全没了——网络不可靠、没有全局时钟、部分失败。其中「部分失败」最致命,因为它带来「分不清对方是死了还是只是慢」的灰色失败,而你唯一的武器(超时)只是猜测。
  • 一致性是一道有价目表的光谱:线性 → 顺序 → 因果 → 最终,每往强档升一级,延迟与可用性的账单就涨一截。判断不是「尽量强」,而是「这份数据配得上多强」。
  • 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 的落地。

💬 评论