KV存储(4):复制、Quorum 读写与一致性边界
发布于:
分片解决了“把 keyspace 切开”与“扩缩容迁移”的问题,但它不解决可靠性。要让 KV 存储在节点故障、网络分区下仍可用,需要引入复制。复制引入了新的核心问题:
- 写入如何提交:写到几个副本算成功?
- 读取如何选副本:如何避免读到旧数据?
- 一致性边界在哪里:系统提供的是强一致、读己之写、还是最终一致?
- 修复如何做:副本分歧如何检测、如何回补?
这一篇只做“复制 + quorum”的基础闭环:把 N/R/W 的含义讲清楚,并给出工程上可观测、可调优的指标与常见坑。
1. 复制模型:每个分片都有 N 个副本
对一个分片(token range / shard),复制因子为 N(例如 3):
Shard S:
replica0 on NodeA
replica1 on NodeB
replica2 on NodeC
系统会定义:
- 写 quorum W:一次写入需要写成功的副本数
- 读 quorum R:一次读取需要读取(或确认)的副本数
经典条件:
若 (R + W > N),则读写 quorum 交集非空。
交集意味着:读和写至少在一个副本上“相遇”,提供一种安全边界(但仍需要版本/冲突语义配合)。
2. 写路径:coordinator、并发写与提交点
2.1 coordinator 模式
一次写入常见由一个协调者(coordinator)负责:
Client -> Coordinator
Coordinator -> send write to N replicas in parallel
Coordinator -> wait for W acks
Coordinator -> return success/failure
协调者可以是:
- 请求路由到的任意节点(server-side routing)
- 或客户端直连某个副本后,该副本临时充当协调者
2.2 版本:没有版本就没有“新旧”的判定
复制系统必须能比较“哪个写更新”。常见版本方案:
- 单调递增序列号(需要单主或全局序列源)
- 逻辑时钟(Lamport)
- 向量时钟(vector clock,用于并发冲突检测)
- 时间戳(需要明确容忍时钟偏差的语义)
如果系统选择“最后写 wins”(LWW),就必须明确:
- 时间戳来源(客户端/服务端)
- 时钟漂移处理
- 与重试/重复写的关系
3. Quorum 的直觉:R/W/N 的三角
3.1 三个常用组合
以 N=3 为例:
- 强一些(读写都多数派):W=2,R=2
- 写快读强(写多数派,读 1):W=2,R=1(读可能旧,需 read-repair)
- 读快写强(写 1,读多数派):W=1,R=2(写返回快,但数据可能在少数副本上)
3.2 交集性质到底保证了什么
当 (R+W>N) 时,读 quorum 与写 quorum 交集非空,因此:
- 如果读会“合并/选择最新版本”,并且副本能提供“可比较”的版本信息
- 那么读有机会看到最近一次已提交写(至少在交集中有一个副本包含它)
但要注意:这不是自动强一致。还缺两块:
- 副本是否真的持久化(ack 的含义)
- 读的合并规则(如何选版本/如何处理并发写)
4. 写一致性:ack 的语义(内存 ack vs 落盘 ack)
写成功的 ack 可能表示:
- 已落盘(更安全,延迟更高)
- 已写入 memtable/WAL buffer(更快,但崩溃可能丢)
工程上经常提供多档写一致性:
write_durable=true:WAL fsync 后 ackwrite_durable=false:写到 WAL buffer 先 ack,后台 fsync
这直接影响故障语义:
- coordinator 返回成功不代表写不会丢(取决于 durable 级别)
- “读到旧值”与“写丢失”是两类不同问题,排查路径不同
5. 读路径:读 1 vs 读多数派
5.1 读 1(R=1)
优点:延迟低、成本低。
缺点:可能读到落后副本。常见补救:
- 读返回时附带版本
- 后台 read-repair(异步修复)
- 读路径做“带版本的快速一致性检查”(多拉一份元信息)
5.2 读多数派(R=2)
优点:更接近“读到最新”。
缺点:延迟与可用性受最慢副本影响(tail latency)。
可用性提醒:若需要 R=2,在 N=3 下只允许最多 1 个副本不可用;否则读失败。
6. 故障场景:网络分区时系统会怎样
6.1 以 N=3,W=2,R=2 为例
当分区导致某一侧只剩 1 个副本可达:
- 写需要 2 个 ack → 写失败(保护一致性)
- 读需要 2 个副本 → 读失败(保护一致性)
这是一个典型的 CAP 选择:在分区下牺牲可用性以保一致性边界。
6.2 以 W=1,R=1 为例
在分区下更“可用”,但会产生更多副本分歧,修复与冲突处理的成本上升。
工程上常见做法:
- 对“强语义路径”用更强的 R/W
- 对“可容忍陈旧”的路径用弱的 R/W
- 把一致性作为 per-request 或 per-table 的可配置项(tunable consistency)
7. 写入并发与冲突:没有冲突模型,Quorum 只是“门槛”
并发写的两类情况:
- 可比较:一个版本明确更新于另一个(例如同一个 leader 分配序列号)
- 并发不可比较:需要冲突解决(向量时钟/多值返回/应用合并)
如果系统选择 LWW:
- 需要把“并发写”强行线性化为“较新胜出”
- 这会把时钟问题、重试问题带入一致性语义
更系统的冲突处理在后续笔记单独展开。
8. 修复:反熵与 read-repair 的位置
复制系统长期运行一定会出现副本分歧:
- 某副本短暂不可用导致写缺席
- 网络抖动导致部分请求超时重试
- 磁盘坏块导致局部数据丢失
修复手段通常两类:
- read-repair:读路径发现分歧,顺便修复(可异步)
- anti-entropy:后台周期性比对副本(Merkle tree 等)并回补
这两者是“读延迟/后台带宽/一致性收敛速度”的三角权衡。
9. 工程观测指标:把一致性问题从“玄学”变成“可测”
建议最少监控(按分片/副本维度聚合):
- 写路径:写成功率、W 未达成次数、写延迟 P99、每副本 ack 延迟分布
- 读路径:读命中副本分布、R 未达成次数、读延迟 P99、读到旧版本比例(若可检测)
- 分歧/修复:read-repair 次数、anti-entropy backlog、修复流量、修复失败率
- 副本健康:各副本落后程度(version lag)、pending hints(若有 hinted handoff)
常见线上现象与排查方向:
- 写 P99 抖动:慢副本/磁盘队列/网络抖动 → coordinator 等 W ack 被 tail 拖慢
- 读偶尔旧:R=1 + 某副本落后 → 看 version lag 与 read-repair 是否跟得上
- 分歧持续不收敛:后台修复带宽不足或失败 → 看 backlog 与错误码
10. 小结
复制 + quorum 提供了一个可调的一致性边界,但它不是“按下开关就强一致”。系统必须补齐:
- ack 的持久化语义(写成功到底意味着什么)
- 版本与冲突模型(新旧如何比较,并发如何处理)
- 修复路径(分歧如何被检测与收敛)
后续 3 篇笔记会把修复、冲突与删除语义(tombstone/TTL)补齐,形成可落地的 KV 复制系统闭环。