问题

分布式系统把数据复制到 N 个节点上。写入时让 W 个节点确认,读取时查询 R 个节点。W 和 R 取多少,才能保证读到最新写入的数据?

"取多数派"是工程师最常见的直觉回答,但"多数"到底是多少——N 的一半多一个?还是 N 的三分之二?这两个数字分别在什么条件下出现,背后的推导和权衡,远比"取多数派"三个字复杂。

起源:Gifford 加权投票(1979)

NRW 模型的源头是 David K. Gifford 在 1979 年 SOSP 会议上发表的论文 Weighted Voting for Replicated Data。Gifford 的模型比今天常见的等权版本更一般化:每个副本节点被分配一个投票权重 wᵢ,所有节点的总票数 V = Σwᵢ。读仲裁所需票数 Vr 和写仲裁所需票数 Vw 必须满足两个约束:

1
2
约束 1:Vr + Vw > V
约束 2:Vw > V / 2

约束 1 保证任何一次读操作至少触碰到一个持有最新版本的节点——读集合和写集合必然有交集。约束 2 保证任何两次写操作至少有一个公共节点,从而可以对写入排出全序。

当每个节点的权重都等于 1 时,V = N,两个约束退化为等权形式:

1
2
R + W > N
W > N / 2

这就是今天的 NRW 模型。

同年,Robert H. Thomas 独立发表了 A Majority Consensus Approach to Concurrency Control for Multiple Copy Databases,从数据库并发控制的角度提出了多数派投票,与 Gifford 的工作殊途同归。

核心数学:鸽巢原理

W + R > N 的正确性证明只需要一条初等工具——鸽巢原理(又称抽屉原理):如果 n+1 个物体放进 n 个抽屉,至少有一个抽屉里有两个物体。

设上一次成功的写操作写入了节点集合 Sw(|Sw| = W),当前的读操作查询了节点集合 Sr(|Sr| = R)。两个集合的元素都取自同一个全集——大小为 N 的节点集合。

1
|Sw| + |Sr| = W + R > N

鸽巢原理直接给出结论:两个子集的元素数之和超过全集大小,交集非空。Sw ∩ Sr ≠ ∅,读操作至少能碰到一个持有最新数据的节点。

客户端收到 R 个响应后,通过版本号或时间戳从中挑出最新值。

这条性质和 Paxos 正确性证明用的是同一条定理。Paxos 论文中的叙述是"任取两个多数派 Q₁ 和 Q₂,|Q₁| + |Q₂| > N,所以 Q₁ ∩ Q₂ ≠ ∅"——和上面的推导完全一致。

约束 2 的作用

R + W > N 保证读写交叉,但还不够。如果 W ≤ N/2,两次并发写入可以成功写入两个不相交的节点集合。后续读操作虽然能读到某个最新值,但无法判断两个值之间的先后顺序。

W > N/2 保证任何两次写操作的写入集合也有交集。公共节点同时持有两次写入的记录,可以用版本号排出全序。

Dynamo 风格的系统有时放松约束 2,允许 W ≤ N/2。代价是并发写入可能在不同节点集合上各自成功,产生冲突版本。这些系统用版本向量(vector clock)或最后写入胜出(Last-Write-Wins, LWW)来事后解决冲突,而不是在写入时通过 quorum 交叉来预防冲突。

配置空间

固定 N 之后,W 和 R 的取值形成一个连续的权衡谱。不同的 (N, W, R) 组合对应不同的一致性、可用性和性能特征。

N = 3 的配置

配置 W R 写容错 读容错 特点
均衡 2 2 1 1 读写对称,最常见
ROWA 3 1 0 2 读最快,任何一个节点故障都阻塞写入

W = 1 不满足 W > N/2 = 1.5 的约束,无法保证写入全序,因此不是合法的严格仲裁配置。

ROWA(Read-One-Write-All)是 W = N, R = 1 的极端选择。PacificA 协议就采用了这种设计——所有 Secondary 确认后 Primary 才返回成功,换取读操作只需查询 Primary 一个节点。

N = 5 的配置

配置 W R 写容错 读容错
均衡 3 3 2 2
写重读轻 4 2 1 3
ROWA 5 1 0 4

容错公式

写操作最多容忍 N − W 个节点同时故障,读操作最多容忍 N − R 个节点同时故障。系统整体的容错上限取两者中的较小值。

W 和 R 是一对跷跷板:W 越大,每次写入的一致性越强,但能容忍的故障数越少,延迟也越高(要等更多节点响应)。R 同理。CAP 定理在 NRW 模型里具象化为 W 和 R 的数值选择。

W 的取值:过半还是三分之二?

一个广泛流传的印象是"写仲裁占 N 的三分之二"。这个印象在 N = 3 的场景下恰好正确(W = 2 = N × 2/3),但推广到一般情况时会产生误导。根源在于它混淆了两种不同的故障模型。

崩溃故障模型(Crash Fault)

Paxos、ZAB、Raft 处理的是崩溃故障:节点要么正常运行,要么完全停止响应,不会发出错误信息。

在这个模型下,集群需要 N = 2f + 1 个节点来容忍 f 个同时崩溃。多数派大小 = f + 1 = (N + 1) / 2。

N f 多数派 占 N 的比例
3 1 2 66.7%
5 2 3 60.0%
7 3 4 57.1%
9 4 5 55.6%
2f+1 f f+1 → 50%

随着 N 增大,多数派占比单调下降,极限值是 50%。三分之二只是 N = 3 这个最常用部署规模下的巧合。

拜占庭故障模型(Byzantine Fault)

PBFT 等协议处理的是拜占庭故障:节点可能发出任意错误信息,包括恶意伪造。

在这个模型下,集群需要 N = 3f + 1 个节点来容忍 f 个拜占庭节点。仲裁大小 = 2f + 1 = (2N + 1) / 3。

N f 仲裁 占 N 的比例
4 1 3 75.0%
7 2 5 71.4%
10 3 7 70.0%
3f+1 f 2f+1 → 66.7%

随着 N 增大,仲裁占比趋向三分之二。

差一个 f 的直觉

崩溃节点沉默不语,不会投假票,多数即可判定真相。拜占庭节点会投假票:最坏情况下 f 个坏节点全部投了假票,好节点的真票必须压过 f 张假票。f + 1 张真票对 f 张假票才能胜出,总共需要收集 2f + 1 张票——三分之二门槛由此而来。

换句话说,crash-fault 模型下沉默节点"不占名额",只需要在应答者中取多数;Byzantine 模型下恶意节点"占名额还捣乱",需要额外的票数来抵消它们的干扰。

两种模型在 N = 3 时的巧合

故障模型 N f 仲裁
Crash 3 1 2
Byzantine 4 1 3

N = 3 的 crash-fault 仲裁恰好是 2,等于 N 的三分之二。但 N = 3 的 Byzantine 场景需要 N = 4 才能容忍 1 个拜占庭节点(3f + 1 = 4),仲裁是 3。两种模型的数值只在 N = 3 的 crash-fault 配置下巧合地碰上了"三分之二",推广到更大的 N 后差距越来越大。

共识协议与副本仲裁:同源不同路

Paxos/Raft 里的 quorum 和 Dynamo 风格系统里的 NRW 建立在同一条数学性质上(多数派交叉),但解决的问题层次不同。

维度 共识协议(Paxos/Raft) 副本仲裁(Dynamo/Cassandra)
目标 多个提案者就单一值达成一致 多副本读写保证能看到最新值
写入语义 两阶段(Prepare + Accept) 直接并行写 W 个节点
冲突处理 协议内建(高编号提案覆盖低编号) 需要额外机制(版本向量、LWW)
提供的一致性 线性一致性 仅保证读到某个最新副本
读取路径 读 leader 或走一轮 quorum read 查 R 个节点取最新版本

共识协议在 quorum 交叉之上叠加了提案编号排序、日志复制、领导者选举等机制,目标是全局操作的线性一致性。副本仲裁的裸 NRW 只保证"读到的 R 个响应中包含最新值",不对操作的全局顺序做任何承诺。

Quorum 交叉是地基。共识协议在地基上盖了一栋有承重结构的建筑(提案编号、日志、选举),Dynamo 风格的 NRW 只在地基上搭了平房。平房够用的场景不需要多层建筑的复杂度。

Flexible Paxos:打破对称 quorum

2016 年,Heidi Howard 等人发表 Flexible Paxos: Quorum Intersection Revisited,指出标准 Paxos 对 quorum 的要求过于保守。

标准 Paxos 要求所有 quorum 都是多数派(> N/2),不区分 Phase 1(Prepare)和 Phase 2(Accept)的大小。Flexible Paxos 发现:真正需要交叉的只有一对——Phase 1 quorum 必须和 Phase 2 quorum 交叉。

原因在于 Paxos 的安全性证明只在一个地方用到了 quorum 交叉——新提案者在 Phase 1 收集信息时,必须碰到至少一个在旧提案的 Phase 2 中投过票的节点。只要 Q₁ + Q₂ > N(Q₁ 是 Phase 1 大小,Q₂ 是 Phase 2 大小),这个交叉就有保证。两个 Phase 1 quorum 之间、两个 Phase 2 quorum 之间都不需要交叉。

这打开了一个新的配置维度。Q₁ 和 Q₂ 可以不对称:

Q₁(Prepare) Q₂(Accept) 条件 Q₁+Q₂>N 场景
N 1 N+1 > N ✓ 全员 Prepare,单节点 Accept
⌊N/2⌋+1 ⌊N/2⌋+1 标准 Paxos 对称多数派
1 N N+1 > N ✓ 单节点 Prepare,全员 Accept

在领导者稳定的系统中(Raft 和 ZAB 的常态),Phase 1 只在领导者换届时执行,频率极低;Phase 2 每次提交都要跑。Flexible Paxos 允许用大的 Q₁(反正极少执行)换取小的 Q₂(每次提交都执行),直接降低稳态提交延迟。

对 NRW 参数选择的启示:如果读操作远比写操作频繁(或反之),不必对称地分配 R 和 W——让低频操作承担更大的 quorum 开销,高频操作用最小 quorum 即可。

Sloppy Quorum 与 Hinted Handoff

Amazon Dynamo 论文(DeCandia 等,2007)引入了 Sloppy Quorum 这一工程妥协。

严格 quorum 要求 W 个确认必须来自数据所属的 N 个首选节点。当首选节点中有部分不可达时,严格 quorum 会阻塞写入。Sloppy Quorum 放宽了这个要求:允许写入请求被路由到一致性哈希环上的下一个可用节点——即使该节点不属于原来的 N 个首选节点。

收到这种"代管"数据的节点会在本地标记一条提示:“这份数据应该属于节点 X”。等 X 恢复后,代管数据被移交给 X。这个过程就是 Hinted Handoff。

代价很直接:Sloppy Quorum 违反了 W + R > N 的前提。W 个确认可能包含不属于原始 N 的节点,后续读操作如果只查原始 N 中的 R 个节点,就可能漏掉这些写入。Sloppy Quorum 把一致性让给了可用性——一个明确的 AP 倾斜。

Cassandra 通过 consistency level 提供了粒度控制:设为 QUORUM 走严格仲裁,设为 ONE 或 ANY 走 sloppy 路径。运维人员可以逐操作地选择一致性和可用性之间的平衡点。

实际系统的 NRW 选择

系统 复制模型 NRW 暴露方式 默认配置 说明
Cassandra 无主 每次操作指定 consistency level ONE QUORUM = ⌊RF/2⌋+1,RF 是 replication factor
DynamoDB 内部 quorum 不暴露 NRW 最终一致 用户只看到"最终一致读"和"强一致读"两个选项
Riak 无主 直接暴露 N/R/W 参数 N=3, R=2, W=2 均衡配置
Kafka 主从 + ISR min.insync.replicas + acks acks=1 acks=all + min.insync.replicas=2 近似 W=2
ZooKeeper ZAB 共识 不可配 多数派 写入需多数 follower 确认
etcd Raft 共识 不可配 多数派 读可配为 linearizable 或 serializable

Kafka 的 ISR 机制与经典 NRW 有显著差异。Kafka 不使用固定大小的 quorum,而是维护一个动态的 ISR(In-Sync Replicas)集合——只有跟上 leader 复制进度的 follower 才在 ISR 中。acks=all 要求 ISR 中所有副本确认,min.insync.replicas 限定 ISR 的最小规模。

差别在于:ISR 可以缩小到只剩 leader 一个节点,此时 acks=all 退化为单节点确认,不提供任何冗余保护。经典 NRW 的 W 是固定的,不会因节点状态变化而降级。换言之,Kafka 的"quorum"是弹性的,在极端情况下会自动降低安全水位来维持可用性;经典 NRW 则是宁可阻塞写入也不降低安全水位。

NRW 不解决的问题

线性一致性

W + R > N 保证"读到的 R 个响应中包含最新值",但不保证多个客户端看到相同的操作顺序。两个客户端同时执行 quorum read,可能从不同的 R 集合得到不同版本——一个看到新值,另一个看到旧值。在严格的线性一致性定义下,如果一次写入已经对某个客户端可见,它就必须对所有后续读取可见。裸 quorum read 不提供这个保证。

要在 quorum 之上实现线性一致性,常见手段包括:

  • 读修复(Read Repair):读取时发现过期副本后主动推送最新值,使后续读取收敛到一致状态。
  • 两阶段读(ABD 算法):读到最新值后再写回一轮 quorum,确保后续读取能看到这个值。代价是每次读操作的延迟翻倍。
  • 走共识协议的 linearizable read 路径(如 Raft 的 ReadIndex 或 Lease Read),本质上借助 leader 做了一次隐式的全局排序。

并发写入冲突

两个客户端同时写入不同的值时,quorum 机制本身不决定哪个值胜出。冲突解析需要额外策略:

策略 做法 代价
Last-Write-Wins(LWW) 按物理时间戳排序,保留最晚的写入 丢弃并发写入;依赖时钟同步(NTP 的精度有限)
版本向量(Vector Clock) 每个节点维护向量时钟,检测因果无关的并发写入 冲突暴露给应用层决策;向量长度随节点数增长
CRDTs 使用满足交换律和结合律的数据结构,并发修改自动合并 仅适用于特定数据类型(计数器、集合、LWW-Register 等)

Dynamo 选择了版本向量加应用层解析(购物车场景下合并所有冲突版本,宁可多出物品也不丢失物品)。Cassandra 选择了 LWW。Riak 两者都支持,由用户在 bucket 级别选择。

成员变更

NRW 模型的推导假设 N 是固定值。真实系统中节点的加入和退出会改变 N。在新旧配置切换的瞬间,如果新旧配置的 quorum 没有交叉,就可能出现短暂的不一致窗口。

这本质上就是共识协议中成员变更(membership change)问题的翻版。Raft 的联合共识(Joint Consensus)和单节点变更方案都是为了在切换期间保证新旧配置的任何两个 quorum(一个来自旧配置,一个来自新配置)仍然有交叉。

参考资料

  • Gifford, D. K. “Weighted Voting for Replicated Data.” SOSP, 1979.
  • Thomas, R. H. “A Majority Consensus Approach to Concurrency Control for Multiple Copy Databases.” ACM TODS, 1979.
  • Herlihy, M. “A Quorum-Consensus Replication Method for Abstract Data Types.” ACM TOCS, 1986.
  • Lamport, L. “The Part-Time Parliament.” ACM TOCS, 1998.
  • DeCandia, G. et al. “Dynamo: Amazon’s Highly Available Key-value Store.” SOSP, 2007.
  • Lakshman, A. and Malik, P. “Cassandra: A Decentralized Structured Storage System.” LADIS, 2009.
  • Ongaro, D. and Ousterhout, J. “In Search of an Understandable Consensus Algorithm.” USENIX ATC, 2014.
  • Howard, H., Malkhi, D., and Spiegelman, A. “Flexible Paxos: Quorum Intersection Revisited.” OPODIS, 2016.