数据库基础体系 · 第 131/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

数据库共识基础:Raft、Paxos、Leader、日志复制和成员变更

在分布式数据库中,“多副本”与“共识”不是同一个概念。

多副本解决的是数据保存多份;共识解决的是:当副本之间存在网络延迟、消息丢失、节点宕机,甚至旧 Leader 仍在发送消息时,多个节点如何对一串操作达成不可冲突的决定。

例如,三个副本同时收到以下请求:

1. 账户 A 扣款 100
2. 账户 A 扣款 50
3. 账户 A 扣款 80

系统不仅要决定哪些请求成功,还必须让所有正确副本以相同顺序执行已提交请求。否则,即使每个副本都有完整数据,也可能出现不同余额。

本文围绕以下问题展开:

  • 共识算法到底保证什么;
  • Raft 和 Paxos 如何在故障下选出或维持 Leader;
  • 日志复制何时算“提交”;
  • Leader、任期、选票和多数派分别承担什么职责;
  • 成员变更为什么危险,以及 Raft 如何避免双多数派;
  • 共识层、事务层、存储层和 SQL 层之间如何分工;
  • 常见错误配置和故障现象如何解释。

一、先区分四个容易混淆的概念

1. 数据复制

复制是把数据从一个节点传到另一个节点。例如:

节点 A:x = 10
节点 B:x = 10

这只能说明某个时刻两个节点内容相同,不能说明:

  • 更新顺序是否一致;
  • 更新是否已经被足够副本持久化;
  • 发生网络分区时谁可以继续写;
  • 节点恢复后是否会覆盖新数据。

异步主从复制通常只能提供“最终可能追上”的语义。主库确认提交后,从库可能尚未收到对应日志。

2. 复制状态机

复制状态机把客户端操作抽象为确定性命令:

日志:
1. SET x = 10
2. INCR x 3
3. SET y = 7

每个副本只要:

  1. 以相同顺序获取相同日志;
  2. 使用相同初始状态;
  3. 按确定性规则执行;

最终就能得到相同状态。

共识算法主要负责第 1 步:让副本对日志中的命令及其顺序达成一致。它通常不负责 SQL 解析、锁管理、索引维护或事务隔离。

3. 共识

共识协议需要在一组节点中决定一个或多个值。典型要求包括:

一致性

两个正确节点不能对同一个日志位置决定不同的值。

设日志位置为 ii,如果节点 A 已决定:

log[i] = command_1

那么节点 B 不能决定:

log[i] = command_2

其中 command_1 != command_2

完整性

系统决定的值必须是某个客户端提出过的值,而不是协议凭空制造的业务命令。

最终性

一个已经决定的值不会因为 Leader 崩溃、网络恢复或节点重启而被替换。

可用性或活性

在满足一定条件时,客户端请求最终能够完成。常见条件包括:

  • 网络最终恢复;
  • 至少有足够节点存活;
  • 存在能够互相通信的多数派;
  • 不发生无限期消息延迟。

一致性与活性是两个不同目标。系统可以通过“停止写入”保住一致性,但这不代表它仍然可用。

4. Leader

Leader 是某个时期负责协调操作的节点,通常承担:

  1. 接收客户端请求;
  2. 为请求分配日志位置;
  3. 向 Follower 复制日志;
  4. 推进提交位置;
  5. 向状态机应用已提交命令;
  6. 向客户端返回结果。

Leader 不是“永远正确的主节点”,而是带有任期或编号的协议角色。它可能因为网络分区而失去多数派,却不知道自己已经失去领导权。因此,Follower 不能只因为收到旧 Leader 的消息就接受它。


二、为什么多数派能够提供安全性

假设集群有 NN 个节点,每次决定操作需要一个 quorum,即多数派。

N=3N=3 时,多数派大小为 2:

{A, B}
{A, C}
{B, C}

任意两个多数派至少有一个共同节点,因为:

2+2>32 + 2 > 3

一般地,若 quorum 大小为 qq,要保证任意两个 quorum 相交,需要:

2q>N2q > N

多数派的直觉是:如果一个值已经被某个 quorum 接受,那么之后的新 quorum 至少会包含其中一个节点。协议可以利用这个交集节点,得知此前已经接受的值,从而阻止新值覆盖它。

例:三节点集群

初始节点为:

A、B、C

写入命令 SET x=1

  1. Leader A 将命令写入自身;
  2. A 复制给 B;
  3. A 和 B 已经形成多数派;
  4. SET x=1 可以被标记为 committed;
  5. C 此时可以尚未收到该命令;
  6. A 将已提交命令应用到状态机并回复客户端。

若随后 A 宕机:

  • B 与 C 仍可选出 B;
  • B 已经知道 SET x=1
  • 新 Leader 不能安全地选择一个与该已提交命令冲突的值。

为什么不能只写两个节点中的一个

三节点集群中,若只要求一个副本确认:

A 写入 SET x=1
A 宕机
B、C 选出新的 Leader

新集群可能决定:

SET x=2

此时两个操作都曾被系统“确认”,但系统没有交集机制阻止冲突。

多数派不等于永久可用

三节点集群最多容忍一个节点失效:

f=N12f = \left\lfloor \frac{N-1}{2} \right\rfloor

当两个节点同时不可用时,只剩一个节点,没有多数派。此时协议通常必须停止提交新操作,即使剩余节点仍然拥有部分数据。


三、Raft 的核心状态与时间模型

Raft 是一种复制状态机共识算法。它把协议状态组织成几个清晰部分。

1. 三种角色

每个节点在任一时刻处于以下角色之一:

  • Follower:被动接收 Leader 的消息;
  • Candidate:发起选举;
  • Leader:处理客户端请求并复制日志。

典型状态转移:

Follower --选举超时--> Candidate
Candidate --获得多数选票--> Leader
Candidate --发现更高任期--> Follower
Leader --收到更高任期消息--> Follower

2. 任期 Term

Term 是单调递增的逻辑时间:

term 1 -> term 2 -> term 3 -> ...

节点会记录自己见过的最大 term。收到消息时:

  • 消息 term 小于本地 term:拒绝或忽略;
  • 消息 term 大于本地 term:更新本地 term,并退回 Follower;
  • term 相同:按照角色和消息类型处理。

Term 解决的是“旧 Leader 的消息还能不能被接受”这一问题。

3. 选举过程

假设三个节点为 A、B、C,A 原本是 Leader。

正常阶段

A: Leader, term=4
B: Follower, term=4
C: Follower, term=4

A 周期性发送心跳。心跳本质上是没有新日志的 AppendEntries 消息,也承担 Leader 存活证明的作用。

A 失联

若 B 在选举超时时间内没有收到 A 的消息:

  1. B 将 term 增加到 5;
  2. B 变成 Candidate;
  3. B 投票给自己;
  4. B 请求 A、C 投票;
  5. 若 C 投票给 B,B 获得 2 票;
  6. B 成为 term 5 的 Leader。

节点通常在一个 term 内最多投一票。这样可以防止同一个任期出现两个不同 Leader,前提是选举需要多数派。

4. 随机选举超时

如果所有 Follower 同时超时,就可能同时成为 Candidate。Raft 通常使用随机选举超时,使某个节点先发起选举并获得多数票。

随机化解决的是冲突概率问题,不是安全性基础。即使选举反复分裂,安全性仍应保持;只是活性可能变差。


四、Raft 日志复制:从请求到提交

假设 A 是 Leader,客户端发送:

SET x = 10

第一步:Leader 追加本地日志

A 在日志位置 5 写入:

index=5, term=5, command=SET x=10

Raft 日志条目至少可以抽象为:

(index, term, command)

其中:

  • index 是日志位置;
  • term 是创建该条目的 Leader 任期;
  • command 是状态机命令。

第二步:复制给 Follower

A 向 B、C 发送 AppendEntries:

prevLogIndex = 4
prevLogTerm  = 4
entries      = [(5, 5, SET x=10)]
leaderCommit = 4

prevLogIndexprevLogTerm 用来确认前置日志是否匹配。

如果 B 的位置 4 也是 term 4:

B: [1/t1, 2/t1, 3/t3, 4/t4]

B 可以追加位置 5。

如果 B 的位置 4 是 term 3:

B: [1/t1, 2/t1, 3/t3, 4/t3]
A: [1/t1, 2/t1, 3/t3, 4/t4]

B 必须拒绝这次 AppendEntries。Leader 随后回退 next index,寻找双方最后一个匹配位置,并删除 Follower 上与 Leader 冲突的未提交后缀,再复制正确日志。

第三步:多数派确认

若 A 和 B 都拥有位置 5:

A: [1, 2, 3, 4, 5]
B: [1, 2, 3, 4, 5]
C: [1, 2, 3, 4]

位置 5 已经复制到多数派。Leader 可以推进:

commitIndex = 5

然后按顺序应用:

lastApplied: 4 -> 5
状态机:x 从旧值变为 10

A 向客户端返回成功。

提交、应用、回复不是同一个时刻

需要区分三个位置:

  • matchIndex:某个 Follower 已经复制到的日志位置;
  • commitIndex:Leader 确认可以提交的最大位置;
  • lastApplied:状态机已经执行到的最大位置。

典型顺序是:

日志写入 Leader
    ↓
复制到多数派
    ↓
commitIndex 前进
    ↓
状态机 apply
    ↓
向客户端返回结果

具体产品可能在磁盘持久化、状态机执行和客户端响应上采用不同实现,但不能把“收到日志”“写入内存”“提交”“执行”“返回”混为一谈。


五、Raft 的关键安全条件:为什么“多数派复制”还不够

一个容易忽略的细节是:Raft 不仅要求新 Leader 的日志足够新,还规定 Leader 只能直接提交当前任期创建的日志条目。

1. 日志新旧如何比较

比较候选人的日志:

  1. 先比较最后一条日志的 term;
  2. term 更大的日志更新;
  3. term 相同则 index 更大的更新。

候选人请求投票时,Follower 只有在候选人日志“不落后”时才投票。

2. 旧任期日志的陷阱

考虑五节点集群:

A 是 term 1 的 Leader
日志位置 3 的条目 term=1

A 把该条目复制给 B、C,但还没有推进为 committed。随后 A 崩溃。

现在 D 在 term 2 成为 Leader,它的日志中没有这个条目。D 可能覆盖 A、B、C 上位置 3 的 term 1 条目。

这并不违反 Raft:此前该条目没有提交。

更复杂的是,旧条目可能已经存储在多数派,但协议无法仅凭“这个条目在多数派上”安全判断它已提交,因为某些任期与选举路径可能导致误判。Raft 通过要求当前 Leader 先提交一个自己任期内的条目,间接确认此前日志前缀也受到当前领导关系保护。

3. 为什么已提交日志不能被覆盖

若 term 5 的 Leader 提交了位置 10:

log[10] = command_A

这意味着至少一个 term 5 的多数派持有它。之后的候选人若要赢得多数票,必须与这个多数派相交。由于投票者不能投给日志落后的候选人,新 Leader 必须包含该已提交前缀。

因此:

已提交日志只能成为所有后续合法 Leader 的日志前缀

这就是 Raft 日志安全性的核心结论。


六、Paxos:与 Raft 相同的问题,不同的组织方式

Paxos 解决的也是在故障和消息延迟下对值达成一致的问题。它的经典表述通常围绕以下角色:

  • Proposer:提出值;
  • Acceptor:接受提案;
  • Learner:学习已经决定的值。

在工程实现中,一个节点可能同时扮演多个角色。

1. 单次 Paxos 的两阶段

每个提案有一个唯一且递增的编号 nn

Prepare 阶段

Proposer 向多数派 Acceptor 发送:

Prepare(n)

Acceptor 若没有承诺过更大的编号,则:

  1. 承诺不再接受编号小于 nn 的提案;
  2. 返回此前接受过的最高编号及其值(如果存在)。

Accept 阶段

Proposer 收集多数派响应后:

  • 若没有 Acceptor 曾接受过值,可以提出自己的值;
  • 若有 Acceptor 已接受过值,必须选择其中编号最高的那个值。

然后发送:

Accept(n, value)

如果多数派接受,值就被决定。

2. Paxos 的安全性推导

假设值 vv 已由多数派 Q1Q_1 接受。

后续提案编号更大,准备阶段必须获得另一个多数派 Q2Q_2。因为:

Q1Q2Q_1 \cap Q_2 \neq \varnothing

交集中的 Acceptor 已经见过 vv,会在 Prepare 响应中报告它。新 Proposer 因而不能任意改用另一个值。

这保证了两个不同值不会同时被决定。

3. Paxos 与日志复制

单次 Paxos 只能决定一个值。复制状态机需要决定一系列值:

slot 1 -> command_1
slot 2 -> command_2
slot 3 -> command_3

因此工程系统通常使用 Multi-Paxos,或者为每个日志位置运行一个 Paxos 实例。

Multi-Paxos 的优化是:在稳定时期选出一个稳定的协调者,后续多个日志位置可以省略重复的 Prepare 阶段。这使它在工程结构上逐渐接近“由 Leader 连续追加日志”。

4. Raft 与 Paxos 的主要差异

两者都可以提供复制状态机共识,但组织重点不同:

方面 Raft Paxos
核心抽象 Leader、任期、连续日志 提案编号、Acceptor、多实例
日志形态 强调日志前缀一致 可从单值实例组合成日志
选主表达 明确的 Leader 选举 经典 Paxos 不要求固定 Leader
实现教学 状态和流程较直观 理论简洁,但工程化细节较多
稳定期 Leader 连续复制日志 Multi-Paxos 复用协调者

不能简单说“Raft 就是简化版 Paxos”。两者共享多数派交集和安全性思想,但状态机组织、选举规则、日志修复和成员变更协议都有具体差异。


七、Leader 不是线性一致读的充分条件

很多系统将写请求只发给 Leader,但读请求更复杂。

1. 直接读本地状态的风险

假设:

A 是旧 Leader
网络分区后 A 与多数派失联
B 在多数派中成为新 Leader
客户端向 B 写入 x=2
客户端随后又访问 A 读取 x

A 可能仍然返回旧值 x=1。即使 A 还没有意识到自己已经失去 Leader,它也不能安全地提供线性一致读。

2. 线性一致读的要求

线性一致性要求每个操作看起来像在调用和返回之间的某个瞬间原子执行,并且尊重真实时间顺序:

写 x=2 完成
之后发起读 x

读不能返回旧值 x=1

共识日志可以决定写入顺序,但读路径还必须确认当前节点仍然拥有领导权,并且状态机已经应用到足够位置。常见机制包括:

  • 让 Leader 与当前多数派进行一次确认;
  • ReadIndex 一类的协议;
  • 在满足严格条件时使用 Leader lease;
  • 直接把读作为日志操作提交。

这些机制的正确性前提和时钟假设不同,不能仅凭“请求发给 Leader”就宣称线性一致。

3. 读副本通常牺牲实时一致性

Follower 本地读可能看到:

Follower 已应用到 index=100
Leader 已提交到 index=105

这种读适合允许滞后的场景,但不能自动等价于线性一致读。产品通常会区分强读、租约读、Follower read 或最终一致读,具体语义必须以产品文档为准。


八、日志冲突、回退和恢复

1. 冲突日志示例

旧 Leader A 在 term 4 写入:

A: [1/t1, 2/t2, 3/t4, 4/t4]
B: [1/t1, 2/t2, 3/t4]
C: [1/t1, 2/t2]

A 在位置 4 的日志尚未提交就宕机。

新 Leader B 在 term 5 写入另一条命令:

B: [1/t1, 2/t2, 3/t4, 4/t5]

A 恢复后,它的位置 4 是 term 4,与 Leader 的 term 5 冲突。B 发送带有前置位置和任期的 AppendEntries:

prevLogIndex = 3
prevLogTerm  = 4
entries      = [(4, 5, command_B)]

A 确认位置 3 匹配后,删除自身未提交的旧位置 4,并追加新条目。

2. 已提交条目不会这样被替换

只有未提交的冲突后缀可以删除。若某条目已经提交,它必须位于所有后续合法 Leader 的共同日志前缀中。

因此,“节点重启后日志发生变化”不一定是数据丢失。需要判断变化发生在:

  • 已提交日志;
  • 未提交日志;
  • 已应用状态机;
  • 尚未应用的日志。

3. 节点落后过多:快照

如果 Follower 长时间离线,Leader 不必保留并重放全部历史日志。系统可以生成状态机快照:

snapshot_index = 1,000,000
snapshot_term  = 50
state           = 当前数据库状态

恢复流程大致是:

  1. Follower 安装快照;
  2. 将状态恢复到 index 1,000,000;
  3. 再接收其后的增量日志;
  4. 继续追赶 Leader。

快照不是新的共识决定。它压缩的是已经提交并应用过的历史状态。快照安装同样需要校验元数据,避免把错误状态当作合法前缀。


九、成员变更:为什么直接替换配置会产生双多数派

成员变更包括:

  • 增加副本;
  • 删除副本;
  • 替换故障节点;
  • 从三节点扩展到五节点;
  • 从五节点缩减到三节点。

它不是普通配置更新,因为 quorum 的定义会变化。

1. 直接切换的危险

旧配置:

C_old = {A, B, C}

多数派大小为 2。

新配置:

C_new = {C, D, E}

多数派大小也为 2。

如果系统直接同时承认两套配置,网络分区后可能出现:

旧配置多数派:{A, B}
新配置多数派:{D, E}

它们互不相交,可以分别提交不同日志:

旧侧提交 command_A
新侧提交 command_B

这就破坏了共识安全性。

即使新旧集群大小不同,也可能出现同样问题。危险的根源不是节点数量,而是配置切换期间允许两个不相交的 quorum 同时生效。

2. Raft Joint Consensus

Raft 使用联合配置,也称 joint consensus。

先进入联合配置:

C_joint = C_old ∪ C_new

在联合阶段,一条日志必须同时获得:

  • 旧配置的多数派;
  • 新配置的多数派。

例如:

C_old = {A, B, C}
C_new = {B, C, D}
C_joint = {A, B, C, D}

某条配置变更日志必须满足:

old majority:{A, B} 或其他旧多数派
new majority:{B, C} 或其他新多数派

因为两个阶段的 quorum 都参与确认,联合配置与旧配置、新配置之间存在必要交集。

然后按顺序执行:

  1. Leader 提交进入 joint configuration 的日志;
  2. 集群按照联合配置工作;
  3. Leader 提交只包含新成员的配置日志;
  4. 新配置正式生效;
  5. 旧成员停止承担新配置中的投票职责。

3. 为什么新节点要先追日志

新增节点 D 即使已经加入配置,也可能没有完整日志:

A/B/C: index=1000
D:     index=700

若立即让 D 参与关键 quorum,需要确认它能够复制并持久化最新日志。否则,成员变更可能让可用性下降,甚至让配置变更无法提交。

很多系统会先将新节点作为非投票副本或 learner:

  1. 接收快照和增量日志;
  2. 追到接近当前提交位置;
  3. 再提升为正式投票成员。

learner 是常见实现概念,但不同产品的名称和运维接口可能不同,不能假设所有 Raft 实现都提供相同 API。

4. 删除成员不能等于立即关机

删除节点的协议步骤完成前,直接关机可能导致:

  • 新配置日志无法复制到多数派;
  • 集群处于无法提交配置的状态;
  • 旧节点仍然认为自己属于集群;
  • 恢复后出现重复身份或旧日志竞争。

正确顺序应由具体产品的成员管理接口决定,但原则是:先让协议提交配置变化,再停止被移除节点,并验证剩余集群已经使用新配置。


十、节点数量与容灾能力

对于 N=2f+1N=2f+1 个投票节点,最多容忍 ff 个节点失效并保持多数派:

投票节点数 多数派 最多容忍失效
3 2 1
5 3 2
7 4 3

这不是“节点越多越好”。

节点增加通常会带来:

  • 提交需要等待更多网络路径或更复杂的 quorum;
  • Leader 发送复制消息的成本增加;
  • 成员变更和故障排查更复杂;
  • 跨地域部署时尾延迟更高。

跨可用区部署时,还需要同时考虑故障域。如果三个节点都在同一台物理机或同一可用区,协议层面虽然是三副本,实际容灾能力仍可能接近单副本。


十一、共识、事务和 SQL 的边界

1. 共识不等于事务

共识决定:

命令 A 是否排在命令 B 前面

事务还需要处理:

  • 原子性;
  • 隔离性;
  • 锁或 MVCC;
  • 唯一约束;
  • 崩溃恢复;
  • 多条 SQL 的提交边界。

一个 Raft 日志条目可以是:

BEGIN
UPDATE accounts ...
UPDATE ledger ...
COMMIT

也可以让整个事务作为一个不可拆分命令进入共识日志。到底采用哪种方式,是数据库架构选择。

如果一个事务同时涉及多个独立 Raft 组,还需要额外的事务协调机制。单个 Raft 组的日志顺序不能自动提供跨分片原子提交。

2. 共识提交不等于业务提交

当日志条目已经复制到多数派时,协议层可能认为它 committed。但数据库还可能尚未:

  • 完成索引更新;
  • 执行约束检查;
  • 刷新本地状态;
  • 向客户端返回事务结果。

反过来,数据库的本地事务已经提交,也不代表它已经在分布式副本多数派上提交。必须明确产品把这两层如何连接。

3. PostgreSQL 和 MySQL 的边界

PostgreSQL 的 SQL 语言文档以及 MySQL 8.4 参考手册描述的是各自数据库服务器的 SQL、事务和复制相关语义;它们并不意味着“使用 PostgreSQL 或 MySQL 就自动获得 Raft/Paxos 共识”。

常见部署需要区分:

  • 单实例数据库的 WAL/binlog;
  • 主从或异步复制;
  • 半同步复制;
  • 外部高可用管理器;
  • 基于共识的分布式数据库存储层。

例如,异步主从复制中,主库返回成功后,从库可能尚未接收日志。即使复制方向是单向的,也不能把它直接称为 Raft 日志复制。

MySQL Group Replication 等组件具有自己的组成员和故障处理语义;具体保证、配置限制和版本行为应以对应产品文档为准,不能把普通异步复制、组复制和 Raft 互相替换。


十二、故障路径:从现象判断协议状态

场景一:Leader 宕机,少数派仍存活

三节点集群中 A 是 Leader,A 宕机,B、C 正常:

剩余节点:2
多数派:2

B 或 C 可以选出新 Leader,已提交日志应继续保留。未提交日志可能被新 Leader 的日志修复过程覆盖。

场景二:Leader 失去多数派,但仍在运行

A 与 B、C 网络隔离:

A:单独一侧
B、C:多数派一侧

A 可能继续接收客户端请求,但无法获得多数派,因此不能提交新日志。B、C 可以选出新 Leader。

若 A 仍向客户端返回“成功”,说明系统的客户端确认路径存在问题;正确的共识提交不能只依赖 Leader 本地写入。

场景三:客户端重试造成重复命令

客户端发送:

INCR x

Leader 已经提交,但响应在网络中丢失。客户端重试后,日志可能出现两次:

1. INCR x
2. INCR x

共识协议保证这两条日志按顺序复制,不保证客户端请求天然幂等。

数据库或上层服务通常需要请求 ID:

request_id = 8f...
command    = INCR x

状态机记录已处理的请求 ID,重复请求返回原结果而不再次执行。这个去重机制属于应用或数据库事务层,不是 Raft 自动提供的功能。

场景四:节点磁盘损坏后重新加入

节点不能仅凭旧数据目录重新声明自己拥有最新状态。恢复流程通常需要:

  1. 清理或隔离损坏状态;
  2. 以唯一节点身份重新加入;
  3. 从 Leader 接收快照或日志;
  4. 追到合法配置要求的进度;
  5. 观察其复制状态和持久化状态。

直接复制文件或手工修改日志,可能破坏日志索引、term、快照边界和成员身份。


十三、常见误解与反例

误解一:三副本就是强一致

反例:

A:最新
B:最新
C:落后

如果客户端被路由到 C,并且 C 允许本地读,它仍可能读到旧值。三副本只说明存在三份数据,不说明所有读都经过线性一致路径。

误解二:写入 Leader 磁盘就算提交

反例:

A 本地持久化 command_1
B、C 尚未收到
A 随后宕机

如果新 Leader 没有 command_1,协议可以丢弃这条未提交日志。持久化与提交分别回答:

  • 本节点重启后能否恢复;
  • 集群是否已经决定该命令。

误解三:网络分区两边都可以继续写,之后再合并

这通常会产生冲突日志或冲突事务。共识系统一般只允许拥有有效多数派的一侧继续提交;少数派应拒绝提交或进入只读状态。

“最后写入覆盖”“按时间戳合并”属于其他冲突解决模型,不是 Raft/Paxos 的日志一致性语义。

误解四:Leader 一定知道自己已经退位

Leader 可能因为网络延迟而继续工作。它只有在:

  • 收到更高 term;
  • 通过协议确认自己仍与多数派联系;
  • 或收到其他明确的领导信息;

之后才会更新角色。时钟超时本身不是绝对证明。

误解五:成员变更只是改一个配置文件

如果各节点对成员列表的理解不同,就可能分别计算 quorum,导致两个互不相交的多数派。成员变更必须进入共识日志,并按协议阶段生效。


十四、诊断共识系统时应观察什么

排查故障时,不要只看“节点是否在线”,还要对照协议状态。

Leader 和 term

检查:

当前 Leader
当前 term
节点角色
最近一次心跳或选举时间

如果多个节点声称自己是同一 term 的 Leader,说明实现、状态恢复或观测数据存在严重问题,应立即停止依赖该结论并检查日志。

日志进度

至少关注:

lastLogIndex
commitIndex
lastApplied
每个 Follower 的 matchIndex

典型解释:

  • lastLogIndex 增长,commitIndex 不增长:可能没有多数派;
  • commitIndex 增长,lastApplied 不增长:状态机执行阻塞;
  • 某个 Follower 长期落后:网络、磁盘、快照安装或节点负载异常;
  • 节点日志 term/index 不匹配:正在进行冲突修复或恢复不完整。

多数派与故障域

同时确认:

  • 当前投票成员数量;
  • 实际存活数量;
  • 是否有节点只是 learner;
  • 节点是否分布在不同可用区;
  • 成员变更是否处于 joint configuration;
  • 磁盘是否满、fsync 是否阻塞。

“进程存活”不等于“可以参与有效 quorum”。

客户端错误

以下错误不应简单重试无限次:

  • not leader
  • term is stale
  • no quorum
  • configuration change in progress
  • commit timeout

客户端应重新发现 Leader,并使用请求 ID 防止重试导致重复副作用。对于超时请求,不能直接假定“肯定失败”:它可能已经提交,只是响应丢失。


十五、性能取舍:共识路径上的真正成本

对一个需要多数派确认的写请求,延迟下限通常包含:

客户端 -> Leader
Leader -> 足够副本
副本持久化
确认返回 Leader
Leader -> 客户端

因此跨地域部署时,提交延迟往往受跨地域网络往返时间和最慢必要副本影响。批量追加日志、流水线复制、并行发送和批量 fsync 可以改善吞吐,但不能取消 quorum 的安全条件。

还要区分:

  • 协议层确认:日志已由多数派接受或持久化;
  • 状态机完成:命令已经执行;
  • 客户端可见:响应已返回;
  • 其他副本可读:读副本已经应用。

不同系统可能允许在这些阶段之间进行流水线处理,但对外承诺的语义必须明确。


十六、建立正确心智模型

可以用下面这条链路理解一个基于共识的数据库写入:

客户端请求
  ↓
Leader 接收并排序
  ↓
生成带 term/index 的日志条目
  ↓
复制到多数派
  ↓
推进 commitIndex
  ↓
各节点按顺序 apply 到状态机
  ↓
数据库事务和约束逻辑生效
  ↓
按产品定义返回客户端结果

其中:

  • Raft/Paxos 解决“哪些命令、以什么顺序被决定”;
  • Leader 提供稳定时期的协调入口,但不是永久真相;
  • 多数派 通过 quorum 交集保护已决定值;
  • 日志复制 把共识决定转化为复制状态机输入;
  • 成员变更 保护 quorum 结构不在切换期间断裂;
  • 事务层 负责原子性、隔离性和业务约束;
  • 读路径 还要单独证明是否满足线性一致或其他一致性模型。

只要严格区分“本地写入”“日志复制”“多数派提交”“状态机应用”和“客户端可见”,就能正确解释大多数 Leader 切换、日志回退、读到旧值、写入超时以及成员变更故障。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。