Quorum 复制协议:读写多数派与 Paxos 变体

Quorum 复制协议与 Paxos 变体:从读写多数派 W+R>N 的数学条件讲起,剖析 Sloppy Quorum、读修复与提示移交的工程实现,梳理 Basic Paxos、Multi-Paxos、Fast Paxos、EPaxos 与 Flexible Paxos 的演进脉络,并厘清 Quorum 与共识的边界及生产调参要点

多副本是分布式存储的基本手段,但副本一旦多于一个,「写几个副本才算成功」「读几个副本才算可信」就变成了必须显式回答的问题。Quorum(法定人数)给出的答案最简洁:只要写集合与读集合必然相交,读就能看到最新写入。这个 W + R > N 的不等式是所有多数派协议的基础。

但 Quorum 只解决了「读得到」,没有解决「同时写怎么办」。真正需要多个客户端并发修改同一份状态时,必须请出 Paxos 家族。本文先讲透 Quorum 的数学与工程实现,再梳理 Paxos 变体的演进,最后厘清一个常被混淆的边界:Quorum 不等于共识。

一句话:W + R > N 保证「读一定能看到最新写」,但不保证「并发写不冲突」。

1. 复制的基本矛盾

1.1 三个目标不可兼得

任何复制协议都在三个目标间做取舍:

目标含义冲突点
一致性任何读都能看到最新写需要等待多数派
可用性少数节点故障仍可读写需要放宽等待
延迟单次操作尽快返回等待多数派意味着等待 RTT

CAP 定理说的是分区发生时必须在一致性(C)与可用性(A)之间选一个;而 PACELC 补充说:即使没有分区,也要在延迟(L)与一致性(C)之间选一个。Quorum 参数就是把这两个取舍具体化的旋钮。

1.2 复制的两种范式

范式机制代表系统一致性
主从复制单一主节点定序,从节点回放MySQL、Redis、Kafka取决于同步策略
无主复制客户端直接写多副本Dynamo、Cassandra、RiakQuorum 决定
共识复制多数派投票定序日志etcd、ZooKeeper、Spanner线性一致

主从复制把「定序」集中在主节点,简单但主节点是瓶颈与单点;无主复制把定序交给客户端与版本号,扩展性好但需要冲突解决;共识复制用多数派投票同时解决定序与容错,代价是每次写要一轮 RTT。这三种范式的细节对比可延伸阅读数据库复制 。

2. Quorum 机制

2.1 读写多数派

设副本数为 N,写需要 W 个副本确认,读需要 R 个副本响应。核心条件是:

若 W + R > N,则任意读集合与任意写集合必然相交
相交的副本上一定保存着最新写入 -> 读一定能看到最新值

例:N=3, W=2, R=2
  写集合 {A,B},读集合 {B,C} -> 交集 {B},B 有新值
  写集合 {B,C},读集合 {A,B} -> 交集 {B},B 有新值

这个推导有个隐含前提:写是单调的、带版本号的,读时能识别哪个副本更新。如果版本信息缺失,读到交集也判断不出哪个值新,条件就失效了。

2.2 参数组合

NWR特性适用
322均衡,容忍 1 节点故障通用配置
331写重读轻,写延迟高读多写少
313写快读慢,一致性弱写多读少、可容忍旧读
533容忍 2 节点故障跨机房、高可用要求高
524写优先日志类写入
542读优先元数据读取

N 为奇数是有意为之:多数派为 floor(N/2)+1,偶数 N 不增加容错能力却增加成本,因此 3、5、7 是主流。跨机房部署时 N 通常取「机房数 × 每机房副本数」,并把多数派落在「包含主机房」的组合上。

2.3 读修复与提示移交

无主复制下,Quorum 读只能保证「读到最新值」,但不保证旧副本被修正。修正依赖两个后台机制:

读修复(Read Repair)
  客户端读到多个版本后,把最新值回写到落后的副本
  优点:延迟低,随读流量自然发生
  缺点:冷数据不被读 -> 永远不修复

反熵(Anti-Entropy)
  后台进程用 Merkle 树比对副本差异,批量同步
  优点:覆盖冷数据
  缺点:周期长,开销大

提示移交(Hinted Handoff)
  目标副本不可达时,把写暂存到其他节点并附提示
  目标恢复后转发过去
  优点:提升写可用性
  缺点:暂存节点也挂了则丢数据,需要与反熵配合

三者是互补关系,缺一不可。只做读修复的集群在冷数据上会长期不一致;只做提示移交的集群在暂存节点故障时会静默丢写。

2.4 Sloppy Quorum

严格 Quorum 要求写集合必须是「归属该键的副本」,节点故障时写就会失败。Sloppy Quorum 放宽这一点:写可以落到任意 W 个健康节点,只要通过提示移交最终送达。

模式写集合可用性一致性
严格 Quorum必须是归属副本低(归属副本挂了就写不了)强(读必见最新写)
Sloppy Quorum任意 W 个健康节点高弱(可能读到旧值)

代价很明确:Sloppy Quorum 破坏 W + R > N 的相交保证。因为写可能落在读集合完全不相交的节点上,读就看不到最新写。DynamoDB 默认开启 sloppy quorum 以换取高可用,需要强一致的读必须显式声明 ConsistentRead。

2.5 跨机房布局

三机房部署时,N=3, W=2, R=2 意味着每次写都要等一个跨机房确认,每次读也要等一个跨机房响应:

布局 A:多数派落在单机房(N=3, 机房内 3 副本)
  写:机房内 2 副本确认 -> 延迟低(同机房 RTT)
  故障:该机房整体故障 -> 全系统不可写
  适用:延迟敏感、可接受机房级不可用

布局 B:每机房 1 副本(N=3, 三机房各 1)
  写:需 2 个机房确认 -> 跨机房 RTT
  故障:任一机房故障仍可读写
  适用:容灾优先

布局 C:主机房 2 副本 + 备机房 1 副本(N=3)
  写:主机房内 2 副本确认即可 -> 延迟低
  故障:主机房故障 -> 备机房单副本无法组成多数派,不可写
  折中:延迟接近 A,容灾接近 A

没有免费的选择:多数派落在哪里,故障边界就落在哪里。要做跨机房容灾,就必须接受跨机房 RTT;要低延迟,就必须接受机房级不可写。真实系统常用「主机房多数派 + 定期演练机房切换」的折中方案。

3. Paxos 家族

3.1 要解决什么问题

Quorum 解决了「读得到最新值」,但没解决「多个客户端同时写」:两个写都拿到多数派确认,值就分叉了。Paxos 的目标是让多数派在同一轮里只接受一个值,从而把并发写收敛成单一决定。

3.2 Basic Paxos

Basic Paxos 分两个阶段,角色包括 Proposer、Acceptor、Learner:

阶段 1(Prepare)
  Proposer 选编号 n,向多数派发送 Prepare(n)
  Acceptor 若 n > 已承诺编号 -> 承诺不再接受 < n 的提案
             并返回已接受的最大编号提案(若有)

阶段 2(Accept)
  Proposer 收到多数派承诺后,发送 Accept(n, v)
    v 的选择规则:若任一响应携带已接受的提案,取其中编号最大者的值
                 否则用 Proposer 自己的值
  Acceptor 若未承诺更高编号 -> 接受并持久化

决议达成:多数派 Accept 即认为 v 被选定

两个关键规则值得单独记:

  • 编号必须全局唯一且单调递增(通常用「轮次 + 节点 ID」保证)
  • 阶段 2 的值必须优先沿用已接受的值,否则会违反安全性

3.3 Multi-Paxos

Basic Paxos 每决定一个值都要两轮 RTT,吞吐极低。Multi-Paxos 的优化是选出一个稳定的 Leader,让阶段 1 只做一次:

变体优化点代价
Multi-PaxosLeader 复用阶段 1,后续只需一轮需要选主与租约
Fast Paxos允许客户端直接提交,省掉 Proposer 一跳需要 3f+1 而非 2f+1 个节点
Flexible Paxos阶段 1 与阶段 2 的多数派可以不同(只需相交)降低阶段 2 延迟
EPaxos无 Leader,按冲突图排序实现复杂,冲突多时退化

Flexible Paxos 的洞察很有意思:安全性只要求「阶段 1 的多数派集合」与「阶段 2 的多数派集合」两两相交,并不要求各自都是多数派。例如 5 节点里阶段 1 用 3 个、阶段 2 用 3 个,只要任意两集合都相交即可。这给跨机房部署留出了优化空间——把阶段 1 固定在主机房 3 个节点,阶段 2 允许跨机房 3 个。

3.4 Paxos 与 Raft

Raft 常被称为「更易理解的 Paxos」,两者本质相同,差异在工程表达:

维度PaxosRaft
定序对象单个值(Multi-Paxos 扩展为日志)日志本身
选主未规定,需自行设计显式定义(任期 + 投票)
日志空洞允许,靠状态机跳过禁止,Leader 强制补齐
成员变更未规定单节点变更 / 联合共识
实现难度高(协议描述抽象)中(协议描述完整)

Raft 用「日志必须连续」这条强约束换来了可理解性,代价是 Leader 需要补齐从节点缺失的日志条目。落地的 Raft 实现细节见 https://plumephp.com/distributed-raft-implementation/;共识算法与 Quorum 的整体关系,可对照 https://plumephp.com/consensus-algorithms/。

4. Quorum 与共识的边界

4.1 Quorum 不是共识

这是最容易被混淆的一点:

能力Quorum(W+R>N)共识(Paxos/Raft)
读能看到最新写是(严格模式)是
并发写收敛为单值否是
线性一致否是
支持原子 CAS否(需额外机制)是
单次操作延迟1 轮2 轮(Basic)/ 1 轮(Multi)

Quorum 给的是「读一致性」,共识给的是「写定序」。需要「比较并交换」「唯一性约束」「选主」这类语义时,Quorum 无能为力,必须上共识。这也解释了为什么 etcd、ZooKeeper 这类协调组件用 Raft/ZAB 而不是 Quorum。

4.2 选主与租约

共识系统里选主是核心,Quorum 系统里也常需要「主」来定序。两种常见做法:

多数派选主 + 任期
  Leader 通过多数派投票产生,携带任期号
  任何写必须携带当前任期,旧任期写被拒绝
  优点:不依赖时钟
  缺点:需要一轮投票

租约(Lease)
  Leader 获得一段时间的独占权,期内无需再投票
  优点:期内零额外开销
  缺点:依赖时钟有界漂移(租约时长 > 最大时钟漂移)

租约的性能优势明显,但它把正确性押在了时钟上。如果节点时钟漂移超过租约余量,可能出现两个 Leader 同时认为自己有效——这就是脑裂。因此生产系统要么给租约留足够大的安全边际(租约 10s、最大漂移 100ms),要么干脆用不依赖时钟的多数派选举。

4.3 与分布式锁的关系

分布式锁是 Quorum 与共识交汇的典型场景:锁的语义是「同一时刻只有一个持有者」,本质是唯一性约束,必须由共识保证。基于 Quorum 的锁(如 Redis 多节点加锁)在分区时可能出现双持有者,需要 fencing token 兜底。锁的实现取舍与 fencing 机制,见 https://plumephp.com/distributed-locking/。

4.4 线性一致读的实现路径

共识系统里读也可以很快。三种做法,成本与强度递增:

方式机制是否线性一致延迟
从任意节点读直接读本地日志否(可能读到旧值)最低
ReadIndexLeader 确认自己仍是 Leader,再等状态机应用到读索引是一轮心跳
走日志(Log Read)把读也当一条日志提交是一轮日志复制

ReadIndex 是性价比最高的一档:它不做日志复制,只做一次多数派心跳确认「Leader 仍然有效」,然后等待本地状态机推进到读发生时的提交索引。etcd 的 Serializable 与 Linearizable 读就分别对应第一种与第二种。注意它仍需等待心跳往返,跨机房部署时这个 RTT 依然存在。

5. 工程实现要点

5.1 参数调优

# 典型无主复制集群配置(示意)
replication:
  factor: 3              # N=3,每机房 1 副本
  write_quorum: 2        # W=2
  read_quorum: 2         # R=2,满足 W+R>N
  sloppy_quorum: false   # 关闭,换取强读
  read_repair:
    enabled: true
    chance: 0.1          # 10% 概率触发阻塞式读修复,避免每次都等
  hinted_handoff:
    enabled: true
    max_hint_window: 3h  # 超过 3h 未送达则放弃,交给反熵
  anti_entropy:
    interval: 1h         # Merkle 树比对周期

read_repair.chance 是常见的性能旋钮:阻塞式读修复要等所有副本响应,概率设太高会把 P99 拖长;设太低则修复慢,需要反熵兜底。

5.2 故障与降级

故障严格 Quorum 表现应对
1 个副本宕机(N=3)读写正常无需干预
2 个副本宕机(N=3)读写全部失败需人工介入或降级为单副本读
主机房整体故障视多数派位置而定多数派跨机房时可继续
网络分区少数派分区不可写客户端需处理写失败并重试

「多数派跨机房」是设计的关键决策:多数派落在单机房,则该机房故障时全系统不可写(但延迟低);多数派跨机房,则单机房故障仍可写(但每次写要跨机房 RTT)。

5.3 可观测性

  • 副本落后量:每个副本的版本与最新版本的差值
  • 读修复触发率与修复数据量
  • 提示移交队列长度与积压时长
  • 提案编号冲突率:Paxos 里编号冲突频繁说明选主不稳定
  • Leader 切换次数:频繁切换通常意味着网络抖动或超时阈值太小

5.4 与客户端重试的配合

Quorum 写失败时客户端会重试,而重试本身会放大负载,形成「故障 -> 超时 -> 重试 -> 更慢 -> 更多超时」的正反馈。三条基本约束:

1. 重试必须有上限与退避
   max_attempts = 3, backoff = 50ms * 2^n, 加随机抖动
2. 重试必须幂等
   写入携带幂等键(客户端生成的唯一 ID),服务端去重
3. 重试不能跨副本乱写
   同一幂等键的重试应指向同一批副本,避免版本分叉

尤其第 3 条:如果重试时换了副本集合,可能与第一次写形成两个并发版本,读修复会陷入「谁是权威版本」的僵局。正确做法是让客户端缓存「上次写到的副本集合」,重试时优先复用。

6. 常见坑

坑后果修法
误以为 Quorum 等于强一致并发写下数据分叉需要 CAS/唯一性时上共识
Sloppy Quorum 下声称强读读不到最新写关闭 sloppy 或显式一致读
忽略读修复概率配置P99 被阻塞式修复拖长设低概率 + 反熵兜底
租约时长小于时钟漂移双 Leader 脑裂租约 > 最大漂移的 10 倍以上
多数派落在单机房该机房故障全系统不可写明确权衡延迟与容灾
提案编号非全局唯一Paxos 安全性被破坏编号 = 轮次 + 节点 ID

总结

主题关键内容
复制矛盾一致性、可用性、延迟三者不可兼得,PACELC 补上延迟维度
Quorum 数学W + R > N 保证读写集合相交,前提是带版本号
参数组合N=3 时 2/2 最均衡,N 取奇数,多数派位置决定容灾边界
工程机制读修复、反熵、提示移交三者互补;Sloppy Quorum 牺牲相交保证
Paxos 家族Basic 两阶段、Multi 复用选主、Fast 省一跳、Flexible 解耦两阶段多数派
边界Quorum 给读一致性,共识给写定序;租约依赖时钟有界漂移

把 Quorum 与共识分清,是设计复制系统的第一步:先问「这份数据需要并发写收敛吗」。不需要收敛的(缓存、会话、计数器),Quorum 加读修复足够,且延迟更低;需要收敛的(锁、配置、唯一性约束),必须交给 Paxos/Raft 这类共识协议。多数生产系统的真实形态是两者并存——用共识维护「谁在哪、谁是主」这类元数据,用 Quorum 承载海量业务数据的读写。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「distributed-systems」更多文章

  1. 边缘计算架构与就近接入
  2. 分布式系统成本优化与容量治理
  3. 单体到分布式:遗留系统迁移与绞杀者模式