DDIA 导读(八):分布式系统的麻烦
2026/9/13 10:43:30 网站建设 项目流程

本文是《Designing Data-Intensive Applications》(DDIA,中文译名《数据密集型应用系统设计》)第 8 章的导读。DDIA 是 Martin Kleppmann 所著的分布式系统经典,本系列逐章导读,把书的核心概念讲清楚。

一句话主旨

分布式系统与单机系统的根本区别不是"节点多",而是"部分失效"——单机要么全好要么全挂,分布式系统却会有一部分节点活着、一部分死了,而且你常常分不清"死了"和"慢了"。本章把分布式系统的三大不可靠——网络、时钟、进程暂停——逐一拆开,告诉你为什么在分布式系统里连"判断一个节点是否挂了"都没有可靠答案,以及为什么你写出的分布式代码几乎一定有 bug。


核心概念拆解

1. 部分失效——分布式系统的根本困难

单机系统是个"全有全无"的世界:要么正常工作,要么整机崩溃——电源拔了,CPU 不转了,所有组件同时停。但分布式系统不一样:一个节点挂了,其他节点还在跑;一条网线断了,另一个机房还在服务。

分布式: 部分失效

系统仍在跑
但行为不确定

节点A 正常

节点B 挂了

节点C 正常

节点D 慢了(是死是慢?)

单机: 全有全无

故障

重启

正常

整机崩溃
所有组件同时停

部分失效之所以可怕,不是"有节点挂了",而是"你不知道哪些挂了"。网络不可靠,你发一个请求没回应——对方是死了?还是网络分区了?还是只是慢了?你无从区分。单机世界里"超时"是个明确信号(进程没了),分布式世界里"超时"只是"我等不及了"——对方可能还活着,甚至已经把请求处理完了,只是响应还没传回来。

问题→方案:问题——分布式系统里部分节点失效是常态,且你无法可靠区分"失效"“慢”“网络分区”。场景——任何跨节点调用都可能超时,超时后你不知道对方做了什么。方案——不要信任超时作为确定性信号:超时只代表"我决定不再等了",不代表对方没做。所有设计都要假设"请求可能已到达但响应丢失",因此操作必须幂等(重试不会产生副作用),否则重试会导致重复执行。

2. 不可靠网络——超时不是确定性信号

大多数分布式系统跑在 IP 网络上,采用"异步网络"模型:网络只管"尽力交付",不保证——不保证消息到达、不保证顺序、不保证延迟上界。消息可能丢失(网络拥塞、路由器丢包),可能乱序,可能延迟任意久。

节点客户端节点客户端网络不保证: 不丢、有序、有延迟上界超时: 我不再等了但 N 可能已处理完请求!只是响应丢了请求(可能到达,可能没到)??? (没回应)重试(必须幂等!)

超时的两难

  • 超时设太短 → 正常请求被误判失败,重试导致重复处理(需要幂等)
  • 超时设太长 → 真挂了也要干等,拖慢整个系统

DDIA 的建议:不要用固定的超时,而是用动态调整的超时——根据历史延迟分布(如 φ 延迟预测)自适应设置,快的时候快超时,慢的时候多等等。但这仍不能消除不确定性——超时永远只是"猜测",不是"判决"。

网络分区:网络故障把系统切成互不可达的两组(甚至更多组),每组内部正常通信,组间完全断联。分区不罕见——网络抖动、交换机故障、配置错误都可能触发。分区的难点是:被隔离的组不知道自己是"被分区的少数派"还是"完整的多数派"——它只能看到"我这边能通信",无法得知"对方还在不在"。这也是 CAP 定理的物理根源(下一章细讲)。

3. 不可靠时钟——墙上钟会撒谎

分布式系统常用时钟来做两件事:给事件排序(谁先谁后)和测量时长(超时、租约)。但分布式系统的时钟有两类,混用会出事:

单调钟 Monotonic

只增不减
不受 NTP 影响

适合: 测量时长(超时)

不适合: 跨节点比较

墙上钟 Time-of-day

从 NTP 同步
可回拨/跳变

适合: 给事件打时间戳

不适合: 测量时长

墙上钟(Time-of-day clock):返回人类时间(如 14:32:05),通常从 NTP 同步。问题是它可能回拨或跳变——NTP 检测到时钟偏快会调慢(甚至回拨),管理员手动调时间会跳变。用墙上钟测量时长(end - start)可能得到负数或零。

单调钟(Monotonic clock):只保证单调递增,适合测量时长(超时计时),但语义上不适合给事件排序——不同节点的单调钟起点不同,无法跨节点比较。

时钟漂移:即使有 NTP,不同节点的时钟也不是精确同步的——硬件晶振有误差,典型每天漂移几毫秒到几百毫秒。你以为"两个节点的时间戳只差 1ms",实际可能差几秒。

时钟不可信的后果——租约与 fencing token:很多系统用"租约"(lease)来选主——主节点持有一个带过期时间的租约,过期前它是主,过期后让别人接管。但如果主节点的时钟慢了,它可能以为自己租约还没过期,继续以主身份写入,而实际上新主已经接管——两个"主"同时写,脑裂。DDIA 的解法是fencing token:每次租约发放时附带一个单调递增的 token 号,存储层只接受 token 号比上次大的写请求——即使旧主的时钟撒谎,它带的 token 号更小,存储层直接拒绝。

新主(token=34)存储层旧主(token=33)新主(token=34)存储层旧主(token=33)时钟慢了, 以为租约没过期写入(token=33)33 < 上次34, 拒绝!写入(token=34)34 > 上次, 接受

4. 进程暂停——GC 停顿比你想的常见

即使网络和时钟都完美,还有一关:进程可能被暂停任意久。一个正在执行的节点可能因为垃圾回收(GC)停顿几十秒甚至几分钟,在这期间它对外界"消失"了——不响应心跳、不处理请求——但从它自己的视角,只是"打了个盹",醒来后继续执行,完全不知道自己暂停了多久。

ZooKeeper(协调)存储层主节点ZooKeeper(协调)存储层主节点持有租约, 过期前续约⏸ GC 停顿 60 秒新主已接管GC 结束, 醒来如果无 fencing token → 脑裂!租约过期, 选新主继续写入(以为还是主!)

进程暂停的陷阱在于:被暂停的节点醒来后不知道自己暂停过。它的内存状态完好,代码从上次执行处继续,它"觉得"自己一直是主。如果协调层(如 ZooKeeper)在暂停期间已经把主让给了别人,这个"僵尸主"的写入就会和新主冲突——又是脑裂。

这进一步说明 fencing token 的必要性:光靠租约过期不够(进程暂停让旧主"感觉"租约还没过期),还需要存储层用 fencing token 做最后的防线——不管旧主怎么"以为",token 号比新主小就拒绝。DDIA 反复强调:不要假设进程不会暂停,所有依赖"我知道自己是唯一主"的逻辑都可能是错的。

5. 拜占庭故障与 FLP 不可能定理——信任的边界

本章末尾点到两个更硬的结论:

拜占庭故障(Byzantine Faults):前面讨论的所有故障都假设节点是"诚实但可能失效"的——它要么正常工作,要么挂了,不会"撒谎"(不会故意发错误数据)。但如果有节点故意作恶发错误数据(被黑、硬件错误导致数据损坏),那就是拜占庭故障。本章的共识协议不处理拜占庭故障——它们假设节点是诚实的。处理拜占庭故障需要 BFT(拜占庭容错)协议,开销大得多,本书不深入。

本书不讨论拜占庭场景,因为大多数数据中心系统假设节点是受信任的——你可以控制硬件、运维、网络。拜占庭容错主要用在区块链、跨组织协作等"不信任"场景。

FLP 不可能定理:Fischer、Lynch、Paterson 在 1985 年证明——在异步网络(延迟无上界)中,只要有一个节点可能崩溃,就不存在一个一定能终止的确定性共识算法。换句话说:理论上,异步网络的共识不可能做到既正确又一定能达成。这听起来像分布式系统没救了,但实践中有出路——FLP 是"最坏情况下可能不终止",而真实网络不会永远最坏;实际共识协议(Raft/Paxos)通过引入随机性或超时来打破 FLP 的理论障碍,在绝大多数情况下能终止。


关键取舍

本章没有"方案选择"——它是一章警告,告诉你分布式系统的物理现实是什么。但有一个核心取舍贯穿全书:

取舍选择 A选择 B代价
信任超时超时即判死,快速故障转移超时只是"不等了",谨慎处理残留请求A 快但可能误杀+重复处理;B 稳但复杂
信任时钟用墙上钟排序、租约过期用 fencing token 做最后防线光靠时钟会脑裂;加 token 多一层但安全
假设不暂停进程一直跑,心跳准假设随时可能暂停,所有"我是主"的判断都可疑前者简单但不可靠;后者需要 fencing

一句话总结这章:在分布式系统里,网络不可靠、时钟不准、进程会暂停——这三件事让"判断真相"变得极其困难。任何基于"我知道对方状态"或"我知道当前时间"的假设都可能是错的。解法的方向不是"消除不确定性"(做不到),而是设计成在不确定下仍安全:幂等操作容忍重试、fencing token 容忍脑裂、共识协议容忍节点失效。这些解法正是下一章(一致性与共识)的主题。


下一篇:第 9 章——一致性与共识。全书的钉子户:线性一致性、Raft/Paxos、CAP。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询