架构设计

DDIA 第八章:分布式系统的麻烦|网络、时钟与真相|学习笔记|2026-09-16

通过支付超时、重试风暴、进程暂停、Redis 租约、fencing token、网络分区和多数派选举等案例,系统理解分布式系统中的部分故障、不可靠网络与时钟、租约失效、脑裂、系统模型,以及安全性与活性的工程取舍。

🌐 本章核心:分布式系统最难处理的不是组件一定会失败,而是发生部分故障时,观察者无法确定远端究竟发生了什么。超时只表示“不知道”,本地认知不等于全局真相。 学习日期:2026-09-16 学习方式:3 次互动精讲 + 支付与订单案例 + Redis/数据库租约设计 + 情境问答 + 综合测验 学习结果:最终测验通过,能够分析超时、重试、时钟、进程暂停、租约、fencing token、脑裂、多数派,以及安全性与活性的取舍。

一、全章主线

第 8 章不直接给出某个万能算法,而是建立设计分布式系统之前必须接受的现实:

  1. 网络不可靠:请求和响应可能丢失、延迟或排队。
  2. 时钟不可靠:不同机器的物理时钟会漂移、回拨或跳跃。
  3. 进程会暂停:GC、虚拟化、I/O 和调度可能让线程长时间停止。
  4. 节点无法独自判断真相:节点存活不代表仍然拥有领导权。
  5. 算法依赖系统模型:必须明确允许哪些故障,以及要保持哪些安全性和活性。

速记:超时表示“不知道”,不是“失败”;本地认知不等于全局真相。

二、部分故障与不可靠网络

单机程序通常表现为整体工作或整体失败;分布式系统则可能只有一部分组件异常,其余部分继续运行。

当订单服务调用支付服务但没有收到响应时,至少存在以下可能:

  • 请求在途中丢失。
  • 请求仍在网络、操作系统或线程池队列中。
  • 支付服务暂时暂停或已经崩溃。
  • 支付已经成功,但响应丢失。
  • 支付已经成功,响应只是延迟。
  • 订单服务自身过载,未能及时处理响应。

调用方看到的现象完全相同:截止时间内没有响应。因此,超时是一种本地判断,不能证明远端操作失败,也不能证明节点死亡。

三、超时、重试与幂等

策略 收益 风险
较短超时 更快怀疑真正故障 误判慢节点,引发重试与故障转移
较长超时 降低误判概率 用户等待和真正故障恢复更慢
动态故障检测 根据延迟和抖动调整怀疑程度 仍然无法证明节点已经死亡

流量高峰中,服务响应时间从 100 ms 上升到 800 ms,而客户端在 300 ms 超时并立即重试三次,会形成:

负载升高 → 响应变慢 → 大量超时 → 大量重试
    ↑                              ↓
    └──────── 负载进一步升高 ──────┘

工程规则:

  • 使用指数退避、随机抖动、最大次数和总截止时间。
  • 区分一次请求超时与两次尝试之间的退避时间。
  • 只重试临时故障,不盲目重试业务拒绝和参数错误。
  • 同一次业务操作的所有重试使用相同幂等键。
  • 幂等防止重复业务效果,但不会消除重复计算、连接和数据库压力。
  • 总截止时间应向下游传播,不能让每一层重新获得完整预算。

四、支付幂等

支付请求示例:

POST /payments
Idempotency-Key: pay-O-123

支付服务应保证:

  • 同一业务操作的所有重试使用同一个键。
  • 幂等键具有唯一约束。
  • 首次成功后持久化处理结果。
  • 后续相同键返回原结果,不再次扣款。
  • 相同键携带不同金额或参数时拒绝请求。

仅使用内存锁不能保证幂等:进程重启会丢锁,多实例之间不共享锁,长时间暂停可能让租约过期。对于本地业务,应让幂等记录和业务写入处于同一数据库事务;对于第三方支付,应使用渠道幂等键、PENDING/UNKNOWN 状态、结果查询和对账。

五、墙上时钟与单调时钟

时钟 适合用途 主要风险
墙上时钟 time-of-day 日志、展示、日历规则 漂移、NTP 校时、回拨和跳跃
单调时钟 monotonic RPC 耗时、超时、退避 不同机器的数值不能直接比较

墙上时钟只能表达“这台机器认为现在是几点”,不能严格证明跨节点事件顺序。NTP 可以减小误差,但不能让误差归零。

如果使用物理时间戳执行 Last Write Wins,时钟较慢节点产生的真实新写入可能拥有更小时间戳,从而被静默丢弃。

测量持续时间必须使用单调时钟。若墙上时钟在任务期间向后调整 2 秒,两次读数相减会少算 2 秒,甚至得到负数。

六、进程暂停、租约与 fencing token

进程可能因为 GC、操作系统调度、虚拟机暂停、缺页或 I/O 长时间停止。暂停期间租约可能过期;进程恢复后却沿着旧调用栈继续执行,成为僵尸客户端。

Worker A 获得 token=41
A 长时间暂停,租约过期
Worker B 获得 token=42 并完成写入
A 恢复,继续尝试写入
最终资源发现 41 < 42,拒绝 A

关键结论:

  • 租约只是在一段时间内授予执行资格。
  • 写入前由客户端检查一次租约不够,检查后仍可能再次暂停。
  • 每次重新授予租约时生成严格递增的 fencing token。
  • 最终资源保存最后接受的 token,并原子拒绝旧 token。

数据库条件写入骨架:

UPDATE protected_resources
SET value = ?, last_fencing_token = ?
WHERE id = ?
  AND last_fencing_token < ?;

受影响行数为 0 时,Worker 必须认为自己已经过期并停止操作。

七、Redis 软锁与数据库权威租约

Redis 可以使用以下形式减少并发和惊群:

SET soft-lock:{order}:123 <owner_uuid> NX PX 30000

但 Redis 锁不应成为关键业务正确性的唯一来源。推荐职责划分:

  • Redis:软锁、TTL、owner UUID,减少重复执行和数据库竞争。
  • 数据库:权威 lease、单调 fencing token、最终条件写入。

完整流程:

  1. Worker 生成稳定的 acquire ID。
  2. 获得 Redis 软锁。
  3. 在数据库事务中锁定资源租约行;租约为空或过期时递增 token。
  4. 执行业务时携带 token。
  5. 数据库只接受比当前记录更新的 token。
  6. 续租和释放都必须校验 owner ID 与 token。
  7. Redis 主从切换或软锁失效时,数据库仍只承认权威租约。

如果所有数据都位于同一个数据库且竞争不大,直接使用数据库事务、行锁或乐观版本号通常更简单。

八、多次有序写入

同一租约中的多次写入可以使用组合版本:

(fencing_token, operation_sequence)

例如:

(42, 1) < (42, 2) < (43, 1)

如果后一个写入是完整状态,可以按组合版本保留最新状态。如果每个操作都必须执行,则需要:

  • 每个操作拥有稳定 operation ID。
  • 保存操作日志和执行结果。
  • 强制 sequence 等于 last sequence + 1。
  • 业务修改、操作日志和 sequence 推进处于同一事务。
  • 同一资源尽量只允许一个写请求在途。

典型严格顺序场景包括有余额约束的取款、订单状态机、数据库迁移和增量补丁。

九、多数派、脑裂与任期

5 节点集群发生网络分区:

多数派:A、B、C
少数派:D、E

A、B、C 可以得到 3 票并选出新 Leader;D 即使仍然运行,也不能继续安全提交写入。

多数派的关键性质是任意两个多数派必定相交。共识协议使用 term、epoch 或类似 fencing token 的机制拒绝旧 Leader:

旧 Leader D:term=7
新 Leader A:term=8
D 恢复后携带 term=7 的写入必须被拒绝

如果少数派也向客户端确认写入,会产生脑裂、日志分叉、写入丢失,并可能破坏库存、余额和唯一性等业务不变量。

十、系统模型

模型 假设
Crash-stop 节点崩溃后不再恢复
Crash-recovery 节点可能重启,依赖持久化状态恢复
Byzantine 节点可能撒谎、发送矛盾消息或恶意行为
同步网络 网络与处理时间存在已知上限
部分同步网络 通常延迟有界,异常时可能任意长
异步网络 不假设延迟或暂停存在固定上限

普通数据库通常按非拜占庭、部分同步环境设计:节点可能崩溃、暂停和断网,但不会故意构造谎言。

十一、安全性与活性

性质 含义 示例
Safety 安全性 坏事永远不发生 不重复扣款;旧 token 不能覆盖新 token
Liveness 活性 好事最终会发生 最终选出 Leader;网络恢复后继续服务

网络分区时,少数派拒绝写入会损失活性或可用性,但仍保持安全性;两个分区都提交冲突写入则直接破坏安全性。

十二、学习问答与纠偏

  • 正确判断:支付请求超时不能证明支付失败。
  • 正确提出:使用稳定事务 ID 或幂等键重试,避免重复扣款。
  • 补全理解:幂等记录与本地业务写入应由数据库唯一约束和事务共同保护,不能只靠内存锁。
  • 正确识别:客户端立即重试会放大负载,形成重试风暴。
  • 补全理解:指数增长的主要是退避时间;单次超时应由真实延迟分布和总截止时间决定。
  • 正确区分:RPC 耗时使用单调时钟,日期展示使用墙上时钟。
  • 曾将时钟向后调整理解为耗时增加;修正为计算耗时减少,甚至可能为负数。
  • 正确识别:旧 Worker 租约过期后没有权利继续写入。
  • 正确理解:fencing token 必须由最终数据库强制检查。
  • 正确判断:5 节点分区中 3 节点一侧可以形成多数派。
  • 补全理解:少数派旧 Leader 的写入不是“数据分区”,而是可能导致脑裂、日志分叉和已确认写入丢失。
  • 最终综合题通过:能够为重复支付、僵尸 Worker、脑裂和响应丢失分别选择幂等、fencing token、多数派与权威状态查询。

十三、最终综合案例

订单 O-123 的处理过程:

  1. W1 获得 10 秒租约和 token 41。
  2. W1 用幂等键 pay-O-123 调用支付;支付成功但响应延迟。
  3. W1 超时并发生 15 秒 GC 暂停。
  4. 协调服务发生 3/2 网络分区,多数派向 W2 发放 token 42。
  5. W2 使用相同幂等键查询或重试支付,获得原成功结果。
  6. W2 用 token 42 将订单更新为 PAID。
  7. W1 恢复后携带 token 41 写入,被数据库拒绝。
  8. 数据库提交响应丢失时,调用方使用相同 operation ID 重试或查询权威状态。
风险 防护机制
重复支付 支付渠道幂等键、参数校验和对账
僵尸 Worker 租约、fencing token、最终资源条件写入
脑裂 多数派、任期号和旧 Leader 拒绝机制
响应丢失 相同 operation ID 的幂等重试与权威状态查询

十四、工程检查清单

  • 能解释部分故障为什么比完全故障更难处理
  • 能说明超时为什么只能表达怀疑
  • 能设计有限重试、退避、抖动和总截止时间
  • 能区分墙上时钟和单调时钟
  • 能解释进程暂停如何产生僵尸客户端
  • 能使用 fencing token 保护最终资源
  • 能说明 Redis 锁与数据库权威租约的职责边界
  • 能解释多数派如何避免两个冲突决定同时提交
  • 能区分安全性问题和活性问题

十五、一句话复盘

分布式系统设计的核心不是消除所有故障,而是在无法确定远端状态的情况下,仍然用幂等、版本、fencing、事务和多数派守住用户依赖的安全保证。

相关学习