第 8 章是悲观者的章节。在几章假设事情大体工作之后,Kleppmann 转向分布式系统里一切可能出错的东西——清单又长又令人谦卑。定义性特征是部分失败(partial failure):系统的某些部分工作而其他坏了,以你常常甚至无法检测的非确定方式。单台计算机是确定的(它工作或干净崩溃);分布式系统能处于一个令人抓狂的中间状态。本章目标是建立这些故障的准确心智模型——不可靠的网络、不可靠的时钟、进程暂停——这样第 9 章的共识工具才说得通。

⚡ 速览要点
  • 部分失败是本质——分布式系统以单机从不会的非确定、部分方式失败。你必须假设任何能坏的都会坏。
  • 网络不可靠——丢失的请求、死掉的节点、慢响应,从发送者那侧无法区分。唯一工具是超时,而没有完美超时。
  • 时钟不可靠——time-of-day 时钟能跳(NTP、闰秒),所以用墙钟时间戳跨节点排序事件(如 LWW)不安全。
  • 进程不可预测地暂停——GC、VM 挂起、OS 调度能冻结一个节点数秒;一个"leader"可能暂停超过它的租约而不自知。
  • fencing token 防御共享资源——一个单调递增的 token 让存储拒绝一个醒来还以为自己持有锁的陈旧节点。
  • 一个节点不能信任自己——真相由 quorum(多数)定义,而非任何单节点的意见。Byzantine 故障(撒谎的节点)是更难、通常与数据中心无关的情况。
tldr

分布式系统建在三个根本不可靠的基础上——网络、时钟,和代码的及时执行——且没有一个能被信任在界限内行为。实际后果:只通过超时检测故障(不完美)、绝不用墙钟时间跨节点排序事件、用 fencing token 防御共享资源,并让多数 quorum——而非任何单节点——决定什么是真的。

故障与部分失败

在单台计算机上,操作是确定的:要么整台机器工作,要么它崩溃。我们刻意偏好计算机完全崩溃而非返回错误结果。分布式系统放弃这种舒适。许多机器经网络连接,会有部分失败:部分工作而其他坏了,且关键地失败是非确定的——涉及网络的操作可能不可预测地成功、失败或挂起,而你甚至可能不知道是哪个。这种非确定性,而非任何单个故障,是使分布式系统难的原因。云计算方法(商用机器、预期失败、在软件里建容错)不同于超算方法(把整个集群当一台机器、失败时重启)。对互联网服务我们想要持续可用,所以我们必须从不可靠组件构建可靠系统

不可靠的网络

本书里的分布式系统是 shared-nothing 的:机器只通过在异步分组网络上传消息通信。当你发一个请求而没收到响应,你无法判断发生了什么。许多可能性坍缩成同一观察:

从发送者那侧,所有这些看起来相同:无回复。即便在单个数据中心内网络故障也不罕见——书中引用的研究发现它们常见。处理一个无响应节点唯一实用的方式是超时:过一段时间后停止等待并假设失败。

超时与无界延迟

但正确的超时是多长?没有好答案。超时意味宣告死节点死之前长时间等待(慢恢复)。超时更快检测故障但冒假阳性风险——在节点只是慢时宣告它死,这可能是灾难性的:节点的工作被交给其他、增加它们的负载、可能使它们超时,级联成死亡螺旋。延迟无界,因为分组网络用统计复用(statistical multiplexing):交换机处、目的地 OS 处,以及来自其他流量的排队都加可变延迟。相反,传统电路交换电话网保留带宽并提供有界延迟——但代价是对突发数据利用率差。你不能同时有高利用率和有界延迟,所以正确做法是持续测量往返时间并自适应超时,而非硬编码一个。

不可靠的时钟

时间看似简单但在分布式系统里危险,因为每台机器有自己的时钟(一个漂移的石英振荡器),通过网络用 NTP 不完美地同步。两种时钟不能混淆:

方面Time-of-day 时钟单调时钟
测量墙钟时间(如 UTC)自某点以来的流逝时间
能后跳?能——NTP、闰秒不能——总向前
跨节点可比?本应可比,但不可靠不——跨节点无意义
用于时间戳、日历测量时长/超时

信任同步时钟的危险

时钟同步远不如人们假设的可靠:NTP 受网络往返延迟限制,节点漂移,闰秒和误配置导致时钟跳动。最危险的后果是用墙钟时间戳跨节点排序事件——例如,最后写入胜(LWW)冲突解决。若节点 A 的时钟哪怕略快于节点 B 的,一个真正发生更晚的来自 B 的写可能被悄悄丢弃,因为它的时间戳看起来更旧。时钟偏移因此造成几乎无法调试的数据丢失和因果违反。若你必须用时钟排序,你需要置信区间:时间戳其实是一个范围,而非一个点。Google 的 Spanner 用 TrueTime(GPS 和原子钟)界定不确定性,并刻意在提交前等过不确定区间,使排序得到保证。

进程暂停

即便撇开时钟,你也不能假设代码及时运行。一个线程能在任何点被暂停任意长时间:一次垃圾回收 stop-the-world 暂停、一台虚拟机被挂起和迁移、OS 抢占线程、笔记本盖子合上,甚至慢磁盘 I/O。经典故障:一个节点持有租约说"我是 leader 直到时间 T",做了些工作,但在行动前暂停(比如 15 秒 GC 暂停)。等它恢复,租约已过期,另一个 leader 已接管——但暂停的节点不知道并继续行动,仿佛它仍掌权,损坏共享状态。

知识、真相与谎言

反复出现的教训是一个节点不能信任自己对系统状态的判断——它只知道它收到(或没收到)了什么消息,而那些能丢失或延迟。所以分布式系统依赖 quorum:决策需要多数节点间的一致,这样系统不依赖任何单节点。一个节点可能相信自己是 leader,但若 quorum 已经前进,它的信念无关紧要——且危险。

Fencing Token

为保护共享资源免受暂停 leader 问题,用 fencing token:每次锁/租约被授予,锁服务也返回一个每次递增的数。客户端必须把这个 token 随每个对受保护资源的写一起包含,而资源拒绝任何携带比它已见过的更旧 token 的写。所以当一个暂停的旧 leader 醒来并试图用陈旧 token 写,存储拒绝它。

fencing tokens reject a stale leader
client A 拿到锁  → token 33
client A 暂停(长 GC)…
租约过期;client B 拿到锁 → token 34
client B writes(token=34)  → 存储接受,记录 34
client A 醒来,writes(token=33) → 存储看到 33 < 34 → 拒绝

  存储服务强制顺序——客户端不能被信任
  去知道自己已被取代。

Byzantine 故障

上面一切都假设节点诚实但可能慢、崩溃或丢消息。Byzantine 故障更糟:一个节点任意行为——发送损坏或矛盾的消息,也许恶意地(名字来自"Byzantine 将军问题")。Byzantine 容错算法即便某些节点撒谎也继续工作,但它们复杂且昂贵。在典型可信数据中心你能假设无 Byzantine 故障(节点是你的),所以大多数系统不付这个代价。它们在对抗环境里要紧:航天(辐射翻转位),以及参与者互不信任的 P2P/区块链系统。

系统模型

为严格推理,我们定义一个系统模型——关于时序的假设(同步:有界延迟;部分同步:通常有界但偶尔不——现实的那个;异步:无时序假设)和关于节点失败的假设(crash-stopcrash-recoveryByzantine)。正确性表达为安全性(safety)属性("坏事不发生"——必须始终成立)和活性(liveness)属性("好事最终发生"——可能有"最终"这样的限定)。好算法证明它们的安全性属性在模型允许的所有运行里成立。

总结

本章刻意阴郁,这样下一章能建设性。它灌输的最有用的习惯:停止假设网络可靠、时钟准确,或你的代码按时运行。设计时假设请求会丢失、时间戳会撒谎、你的进程会在最糟时刻冻结——然后倚靠 quorum 和 fencing token 而非任何节点的本地信念。

🎯 面试速答

为什么你无法区分死节点和慢节点?一个 shared-nothing 节点只观察到"无回复",那可能是丢失的请求、崩溃的对端,或慢响应——所以超时是唯一(不完美)的检测器。
为什么用墙钟时间戳的 LWW 不安全?节点间时钟偏移意味一个真正更晚的写可能携带"更旧"时间戳并被悄悄丢弃——因果违反造成的数据丢失。
fencing token 解决什么问题?一个在租约过期后才醒来的暂停 leader;一个单调递增的 token 让存储拒绝陈旧写者。
安全性 vs 活性?安全性 = "坏事不发生"且必须始终成立;活性 = "好事最终发生"且可能被"最终"限定。

← 上一篇
事务