本页目录
分布式读写路径
一次跨节点读写,quorum、WAL、Raft 日志、一致性哈希同时运转。这条从 coordinator 到 N 个副本再到 read repair 的完整路径,是分布式存储系统的核心数据流。
概述
前面复制策略讲了单 leader / 多 leader / leaderless 三种模式,共识协议讲了 Raft 怎么让多个节点对日志序列达成一致,一致性哈希讲了数据怎么分布到不同节点。但一次跨节点读写,这三件事是同时发生的——写请求落到某个节点后,怎么选副本、怎么写 WAL、怎么等 quorum、数据不一致时怎么在读路径上修?这篇把读写路径完整走一遍。
分布式写路径:一条写请求的旅程
以 leaderless + quorum 模式 (Dynamo/Cassandra 风格) 为例,走一条写请求:
flowchart TD
C["① Client → Coordinator<br/>写 key=user:42<br/>N=3, W=2"]
C --> HASH["② Coordinator 一致性哈希<br/>算出 key 落在 3 个节点<br/>[Node-A, Node-B, Node-C]"]
HASH --> SEND["③ Coordinator 并发写<br/>→ 3 个节点, 等 W=2 个 ACK"]
SEND --> NODE["在每个目标节点上:"]
NODE --> WAL["a. append to WAL<br/>→ fsync<br/>crash recovery 保证"]
WAL --> MEM["b. 写入 MemTable<br/>(内存有序结构)"]
MEM --> ACK["c. 返回 ACK<br/>(含版本号/时间戳)"]
ACK --> QUORUM{"④ Coordinator<br/>收到 ≥2 ACK?"}
QUORUM -->|"✅ W=2 满足"| SUCCESS["返回成功给 Client"]
QUORUM -.->|"未收到"| STALE["剩余节点数据<br/>暂时不一致"]
classDef client fill:#e3f2fd,stroke:#1565c0
classDef coord fill:#fff3e0,stroke:#ef6c00
classDef node fill:#f3e5f5,stroke:#7b1fa2
classDef done fill:#e8f5e9,stroke:#2e7d32
class C client
class HASH,SEND,QUORUM coord
class NODE,WAL,MEM,ACK node
class SUCCESS done
关键:W ≤ N 是"可用性"和"一致性"的调钮。 W=N 时写操作等待全部副本——任何节点故障则写阻塞;W=1 时最快但可能读到旧数据(取决于 R)。平衡点通常是 W + R > N(R 是读 quorum),保证读写集合必有交集。
和 Raft 写路径的区别
Raft 的写是强 leader 串行:所有写必须经 leader → leader 写本地日志 → 复制到 follower majority → committed → apply to state machine。没有 quorum 调钮,没有"部分 ACK 也算成功"——要么 committed(确定性持久),要么没 committed(可能被后续 leader 覆盖)。这和 leaderless quorum 是两个世界:共识协议的确定性 vs 复制策略的可用性优先。
分布式读路径:怎么读到正确的
同上场景,N=3, R=2(读 quorum):
flowchart TD
C2["① Client → Coordinator<br/>read('user:42')"]
C2 --> HASH2["② Coordinator 一致性哈希<br/>找到 N=3 个副本节点"]
HASH2 --> SEND2["并发读请求, 等 R=2 个响应"]
SEND2 --> COMPARE["③ 收到 2 个响应<br/>Node-A: version=5<br/>Node-B: version=3"]
COMPARE --> PICK["取最新 version=5<br/>返回给 Client ✅"]
COMPARE --> REPAIR["④ read repair (异步)<br/>发现 Node-B 落后<br/>后台推送 version=5 → Node-B"]
classDef client fill:#e3f2fd,stroke:#1565c0
classDef coord fill:#fff3e0,stroke:#ef6c00
classDef done fill:#e8f5e9,stroke:#2e7d32
classDef repair fill:#f3e5f5,stroke:#7b1fa2
class C2 client
class HASH2,SEND2,COMPARE coord
class PICK done
class REPAIR repair
R + W > N 时,读写 quorum 必有重叠——读一定能看到最新写。但有个精细点:coordinator 必须拿到至少 R 个响应后比较版本(不是取最快那个),否则可能读到旧数据。
read repair vs hinted handoff:两种修复,两种时机
| read repair | hinted handoff | |
|---|---|---|
| 谁触发 | 读路径上 (coordinator 比对版本) | 写路径上 (写入时目标节点不可达) |
| 修什么 | 已经存在的副本,只是版本落后 | 根本缺失的副本(目标节点挂了) |
| 存在哪 | N/A | 写在 coordinator(或相邻节点)的一个特殊 "hints" 区 |
| 生效时间 | 立即(异步) | 目标节点恢复后,coordinator 把 hint 推过去 |
hinted handoff 的核心思想:写的时候不降级,把写给挂了节点的数据暂存在别处,等它恢复再补。 这样即使某些节点临时不可达,W 仍能达到(N 不变)——写可用性不降。Cassandra 默认存 hint 3 小时,超时丢弃。
sloppy quorum:可用性走到极致
上面说的 quorum 是 strict quorum:写一定要落到一致性哈希算出的那 N 个"正确"节点上。如果那 N 个里挂了太多,即使有 hinted handoff,W 也凑不够。
sloppy quorum 放宽:coordinator 可选不在 N 里的任意节点来凑 W。数据暂时写到了"不该有这条数据的节点"上,等正确节点恢复后再移过去。这进一步提升写可用性,代价是读可能完全看不到刚写的数据(如果读 quorum 恰好也没覆盖那个"临时节点")——最终一致性窗口可能显著拉大。
Dynamo 论文把 strict + sloppy 都列了,Cassandra 默认 strict(hinted handoff 兜底),DynamoDB 的 on-demand 模式更接近 sloppy。
从日志到状态机:WAL 在分布式下的双重身份
在 Raft 里,日志是"共识的载体";在单机存储引擎里,WAL 是"持久性的载体"。实际系统里这两层相互作用:
- Raft 日志管哪些操作已经"在集群层面确定",宕机恢复后日志重放重做状态机。
- 本地 WAL管"单次 apply 还没刷到 SSTable 时别丢"——如果 apply 后、flush 前宕机,WAL 恢复;flush 后 WAL 对应部分可截断。
- 这就是为什么共识协议:Raft里说"committed → apply to state machine",但没有展开 state machine 是什么——state machine 里面就是这一层的 WAL + 存储引擎。
写放大:分布式让已有的放大更复杂
单机 LSM 的写放大来自 compaction(一层层重写)。分布式又多两层:
- 网络放大:N=3 时一条写变成 3 条网络消息(N 个副本)。加上 hinted handoff 可能更多。
- Raft 复制放大:Raft 先写日志(majority),再 apply 到状态机(本地写 WAL + MemTable),一条逻辑写可能触发两次本地 fsync(日志 fsync + WAL fsync)——CockroachDB/Yugabyte 的优化是合并成一次。
所以分布式系统的"写吞吐"不能只看单机存储引擎的吞吐,必须把网络和共识开销算进去。
权衡与失败模式
- W + R ≤ N 时读不到最新写:读写 quorum 不保证重叠 → 始终设 W + R > N。
- read repair 不及时:读发现但不修(忘了异步推) → 监控副本版本差异,定期反熵(见下篇)。
- hinted handoff 堆积:目标节点长期不恢复,hints 撑爆 coordinator 磁盘 → 设 hint 过期时间,监控 hints 队列长度。
- fsync 策略过松:WAL 批量 fsync 时宕机丢最后一批 → 按持久性需求定 fsync 策略(Raft 日志本身已有持久化,本地 WAL 可适当放宽)。
参考
- 论文: "Dynamo: Amazon's Highly Available Key-value Store"(DeCandia 2007)——quorum + hinted handoff + sloppy quorum 的源头
Keywords: quorum write, quorum read, N/W/R, strict quorum, sloppy quorum, read repair, hinted handoff, coordinator, WAL, fsync, Raft log vs WAL, write amplification, network amplification, consistent hashing + replication, 最终一致性窗口