KV存储(4):复制、Quorum 读写与一致性边界

1 分钟阅读

发布于:

分片解决了“把 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 后 ack
  • write_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 复制系统闭环。