第八章:分布式系统的挑战
这一章是全书的一个转折点。前七章讨论的是"如何让分布式系统正常工作",第八章则揭示一个残酷现实:分布式系统里没有什么是可靠的——网络会延迟、时钟会漂移、进程会暂停、节点会"假死"。Kleppmann 的核心论点是:在分布式系统中,你无法区分"节点挂了"和"节点很慢",也无法依赖时钟和网络来建立全局秩序。所有分布式算法,都必须在这种不确定性之上建立。
本章结构:先讲网络不可靠,再讲时钟不可靠,然后讲进程暂停,最后讨论这些不确定性带来的根本性知识问题。
一、本章的核心命题
分布式系统的根本困难,不在于"机器会坏",而在于"你无法确定它到底坏没坏"。
在单机系统中,一个进程崩溃是明确的。但在分布式系统中:
- 一个节点没响应,可能是挂了,可能是网络慢,可能是 GC 停顿,可能是对方在处理但没回
- 你无法区分这些情况
- 这就是所谓的部分失效(Partial Failure)
Kleppmann 的比喻:
单机编程像在自家房子里走动,你知道每个房间在哪;分布式编程像在异国他乡,你连方向都不确定,还要跟一群说着不同语言、可能随时消失的人合作。
二、不可靠的网络
1. 网络的基本事实
- 数据包可能丢失、延迟、重复、乱序
- 发送方无法知道对方是否收到
- 超时是唯一的手段,但超时无法区分"慢"和"死"
2. 网络故障的常见误解
误解一:网络是可靠的
- 实际上,数据中心网络也会有丢包、延迟抖动
- 大规模系统中,网络故障是常态
误解二:延迟是固定的
- 实际上,延迟波动极大(从微秒到秒级)
- 原因:排队、拥塞、路由变化、GC
误解三:TCP 能保证可靠
- TCP 只保证"最终送达或明确失败"
- 无法保证延迟上限
- 应用层超时后,TCP 可能还在重传
3. 网络分区(Network Partition)
定义:网络被分成多个部分,部分之间无法通信。
后果:
- 节点之间无法达成一致
- 可能出现脑裂(Split-brain):多个节点都认为自己是主节点
- 这是分布式系统中最棘手的问题之一
Kleppmann 的提醒:
网络分区不是"会不会发生",而是"什么时候发生"。设计系统时必须假设分区会发生。
4. 超时的困境
超时设置的两难:
- 太短:误判慢节点为故障,触发不必要的切换,可能造成脑裂
- 太长:故障节点迟迟不被发现,服务中断时间长
没有完美答案:
- 网络延迟是动态的,无法用固定超时适应所有情况
- 自适应超时(如 Phi Accrual 故障检测器)是一种改进,但仍不完美
实践建议:
- 用实验测量网络延迟分布(如 AWS 的混沌工程)
- 超时设置为 p99.9 延迟的若干倍
- 接受"误判"是必然的,设计容错机制
三、不可靠的时钟
1. 时钟的两种类型
(1)墙上时钟(Time-of-Day Clock)
- 返回当前日期时间
- 与 NTP 同步
- 问题:
- 可能跳变(NTP 校正、闰秒)
- 不同机器可能不同步
- 精度有限
(2)单调时钟(Monotonic Clock)
- 返回自某起点以来的时间
- 保证单调递增
- 适合测量时间间隔
- 不适合比较不同机器的时间
关键区分:
墙上时钟用于"现在几点",单调时钟用于"过了多久"。跨机器比较必须用墙上时钟,但墙上时钟不可靠。
2. 时钟同步的问题
- NTP 同步有误差(毫秒到秒级)
- 网络延迟导致同步不准
- 闰秒导致时间跳变
- 虚拟机时钟可能被暂停
Kleppmann 的警告:
不要用时间戳来决定分布式系统中的事件顺序。时钟不可靠,时间戳不能作为全局顺序的依据。
3. 依赖时钟的陷阱
(1)最后写入胜出(LWW)
- 用时间戳解决冲突
- 问题:时钟不同步,可能丢掉"更晚"的写入
- 代表:Cassandra
(2)时间戳作为事务 ID
- 问题:时钟回拨导致 ID 重复或乱序
- 解决:混合逻辑时钟(HLC)
(3)租约(Lease)
- 用时间保证"我在这段时间内是主节点"
- 问题:时钟漂移导致租约失效,可能脑裂
4. 时钟的合理使用
- 测量时间间隔:用单调时钟
- 跨机器排序:用逻辑时钟(Lamport 时钟、版本向量)
- 需要时间戳时:用 HLC,或接受不精确
四、进程暂停
1. 进程可能随时暂停
原因:
- GC 停顿:Java 等语言的 GC 可能停顿数秒
- 虚拟机暂停:迁移、快照、宿主机资源紧张
- CPU 调度:操作系统可能抢占
- 磁盘 I/O 阻塞:慢磁盘导致进程挂起
后果:
- 进程在"暂停"期间,对外界来说像是挂了
- 恢复后,可能发现世界已经变了
- 租约过期、锁被释放、主节点被切换
2. 进程暂停的经典场景
场景:租约续期
- 节点 A 持有租约,需要定期续期
- A 发生 GC 停顿,错过续期
- 节点 B 认为 A 挂了,接管
- A 恢复后,仍以为自己持有租约
- 脑裂
场景:分布式锁
- 节点 A 获取锁,正在操作
- A 停顿,锁超时释放
- 节点 B 获取锁,开始操作
- A 恢复,继续操作
- 数据损坏
Kleppmann 的结论:
进程暂停是分布式系统中最容易被忽视的问题。任何依赖"进程会在预期时间内响应"的设计,都是脆弱的。
3. 应对进程暂停
- Fencing Token:给每个锁请求一个递增编号,存储层拒绝旧编号的写入
- 租约 + 心跳:但仍有窗口
- 减少依赖:尽量设计无状态服务
- GC 调优:减少停顿时间
五、知识、真相与谎言
1. 分布式系统的根本问题
你无法知道另一个节点在想什么。
- 它可能挂了
- 它可能活着但网络不通
- 它可能活着但被暂停
- 它可能活着但状态错误
这就是"分布式系统的知识问题":
在一个分布式系统中,每个节点只能根据自己的局部信息做判断,而这些信息可能是不完整、不准确的。
2. 主节点与"真相"
问题:谁来决定"谁是主节点"?
- 主节点自己认为自己是主节点
- 从节点可能认为主节点挂了
- 没有全局的"真相"
解决:共识算法(Paxos、Raft)
- 通过多数派投票决定
- 但多数派也可能被网络分区分裂
3. 拜占庭故障(Byzantine Fault)
定义:节点可能发送错误、矛盾、恶意的消息。
- 普通故障:节点崩溃或停止响应
- 拜占庭故障:节点继续运行,但行为异常
应对:
- 拜占庭容错算法(PBFT、PoW)
- 区块链场景常用
- 但代价高,大多数系统假设无拜占庭故障
Kleppmann 的提醒:
大多数分布式系统假设节点是"诚实的但可能崩溃",只有少数场景(如区块链)需要处理拜占庭故障。
4. 系统模型
为了讨论分布式算法,需要定义系统模型:
(1)网络模型
- 可靠网络:消息不丢,但有延迟
- 公平丢失网络:消息可能丢,但最终送达
- 不可靠网络:消息可能丢,不保证送达
(2)节点模型
- 崩溃-停止:节点崩溃后永久停止
- 崩溃-恢复:节点可能崩溃后恢复
- 拜占庭:节点可能任意行为
(3)时间模型
- 同步系统:网络延迟和时钟有上限
- 异步系统:无时间假设
- 部分同步:大部分时间同步,偶尔异步
Kleppmann 的实践观点:
真实系统大多是部分同步的。算法设计要在异步模型下正确,在同步模型下高效。
六、本章的核心思想总结
| 主题 | 核心观点 |
|---|---|
| 网络 | 不可靠,无法区分"慢"和"死",超时是唯一手段 |
| 网络分区 | 必然发生,脑裂是最大风险 |
| 时钟 | 墙上时钟不可靠,不能用于全局排序 |
| 进程暂停 | GC、虚拟机、调度都可能暂停进程,导致租约失效 |
| 知识问题 | 节点只能基于局部信息判断,没有全局真相 |
| 系统模型 | 网络、节点、时间模型,是讨论分布式算法的前提 |
三个贯穿全书的判断:
- 分布式系统的根本困难是不确定性:你无法确定另一个节点的状态,也无法确定自己的判断是否正确。
- 时钟和网络不可靠,是设计约束而非 bug:任何依赖它们建立全局秩序的设计都是脆弱的。
- 进程暂停是最隐蔽的敌人:它让"看起来正常"的节点实际上已经失去同步。
七、这一章在全书中地位
第八章是从"能用"到"可靠"的转折点:
- 第五章数据复制:复制滞后和冲突,根源是网络和时钟不可靠
- 第六章数据分区:再平衡和路由,依赖网络和协调服务
- 第七章事务:分布式事务面临网络分区和进程暂停
- 第九章一致性与共识:在第八章的不确定性之上,建立一致性和共识
- 第十章批处理、第十一章流处理:大规模数据处理同样面临这些问题
一句话概括本章:
分布式系统没有全局真相。网络不可靠、时钟不可靠、进程可能暂停,节点只能基于局部信息做判断。理解这些根本性挑战,才能理解为什么分布式算法如此复杂,以及为什么"简单"的分布式系统往往不可靠。这一章为第九章的一致性与共识奠定了问题基础。