CAP、Raft 与多数派:分布式系统如何达成一致?

前面几篇我们学习了 MySQL 主从复制、Kafka 消息可靠性,以及跨服务的分布式事务。

这些技术虽然解决的问题不同,但都涉及一个共同问题:

当数据分布在多台机器上时,如何保证它们对同一件事情的认识不会发生冲突?

例如,一个服务注册中心运行在三台服务器上:

text 复制代码
Node A

Node B

Node C

它们共同维护一些重要信息:

text 复制代码
当前有哪些服务实例?

哪个节点是 Leader?

某个配置的最新值是什么?

正常情况下,三个节点通过网络通信,能够协调更新这些信息。

但如果 Node A 与另外两个节点突然失去网络连接呢?

此时可能出现:

text 复制代码
Node A:

认为自己仍然是 Leader

而:

text 复制代码
Node B、Node C:

认为 Node A 已经不可用

如果两边都允许随意修改集群状态,就可能产生两套互相冲突的数据。

这就是分布式系统中非常典型的一致性问题。

为了理解它,我们需要先学习 CAP,再逐步理解 Raft 和多数派机制。

一、为什么单机系统不需要考虑多数派?

先看一个简单的单机程序。

假设某个配置保存在一台 MySQL 中:

text 复制代码
config:

order.timeout = 1000

管理员修改配置:

text 复制代码
order.timeout = 2000

只要数据库事务正确提交,后续符合相应读取语义的查询就可以获取新值。

在这个过程中,我们只需要考虑这一个数据库如何管理事务与并发。

但如果希望系统具备更好的故障恢复能力,将相同的数据复制到三个节点:

text 复制代码
          Node A
        /        \
       /          \
   Node B ------ Node C

问题就变了。

现在管理员希望修改:

text 复制代码
order.timeout = 2000

我们必须考虑:

  • 应该由哪个节点接收修改?
  • 一个节点修改成功,其他节点还没修改怎么办?
  • 如果某个节点突然失去连接,它还能继续提供服务吗?
  • 网络恢复以后,三个节点的数据应该以谁为准?

如果三个节点可以各自独立决定最终状态,就可能出现:

text 复制代码
Node A:timeout = 2000

Node B:timeout = 1000

Node C:timeout = 3000

这显然不是我们希望看到的结果。

因此,对于需要统一维护状态的分布式系统,通常需要某种协调机制,来决定:

哪些修改能够被正式确认,以及所有节点最终应该按照什么顺序应用这些修改。

不过在讨论协调机制之前,先要理解分布式系统无法回避的网络问题。


二、CAP:网络出现问题时,为什么不能什么都要?

CAP 是分布式系统中一个非常基础的理论。

它包含三个概念:

text 复制代码
C:Consistency(一致性)

A:Availability(可用性)

P:Partition Tolerance(分区容错性)

很多介绍会直接说:

"分布式系统只能在 CAP 中选择两个。"

但这种说法很容易让人产生误解。

CAP 真正关注的是:当网络分区发生时,系统无法同时满足它所定义的强一致性和完全可用性。

我们分别来看。

1. C:一致性

CAP 中的 Consistency,通常指线性一致性(Linearizability)这一类强一致性要求。

可以先简单理解为:

对于同一份数据,系统表现得像只有一份权威副本,并且已经成功完成的操作应当按照统一的先后关系对外可见。

例如:

text 复制代码
初始配置:

timeout = 1000

管理员成功修改:

text 复制代码
timeout = 2000

之后另一个客户端发起读取。

如果要求线性一致性,那么它不应该因为访问了某个落后的副本,就读取到已经被成功更新操作取代的旧值。

这里要注意:

CAP 中的 Consistency 和 MySQL ACID 中的 Consistency 并不完全是同一个概念。

MySQL ACID 的一致性更关注事务是否维护数据库约束和业务状态规则。

CAP 中的一致性主要关注多个节点对同一份数据提供的统一可见性语义。

2. A:可用性

CAP 中的 Availability 具有比较严格的含义。

简化理解:

系统中的非故障节点收到请求后,都能够在有限时间内返回符合接口要求的响应,而不是无限等待或拒绝处理。

例如:

text 复制代码
Client
   |
   v
Node A
   |
   v
返回请求结果

但要注意,这里的可用性并不只是指:

text 复制代码
服务器进程还在运行

如果某个节点仍然运行,却无法完成客户端请求,那么它不能仅仅因为进程存活就被认为满足 CAP 所要求的可用性。

3. P:分区容错性

Partition Tolerance 指网络发生分区时,系统仍然需要按照所选择的设计约束处理请求。

例如:

text 复制代码
       网络连接中断

Node A       X       Node B

Node A 和 Node B 都可能正常运行,但它们之间的消息无法正常传递。

这种情况下,双方都不知道对方的最新状态。

这里最重要的一点是:

网络分区不等于服务器一定宕机。

可能出现:

text 复制代码
Node A 正常运行

Node B 正常运行

但 A 和 B 无法通信

甚至两边都有客户端继续访问。

所以网络分区比简单的单机故障更加麻烦。

CAP 的真正取舍是什么?

假设 Node A 和 Node B 因网络故障失去联系。

此时客户端分别访问两个节点。

如果系统坚持强一致性,就不能在无法协调的情况下,让两边独立确认互相冲突的写入。

这可能意味着某一侧需要等待或者拒绝部分请求。

如果系统更重视分区期间双方都能继续响应,则可能允许两侧分别处理操作,再在网络恢复以后协调数据冲突。

但这时就无法同时保证所有操作都符合统一的线性一致性顺序。

所以在网络分区期间:

CP 思路:优先保持强一致性,必要时牺牲部分节点的请求可用性。

AP 思路:优先保持分区期间的请求可用性,但需要接受较弱的一致性语义,并处理可能出现的数据分歧。

这里并不是说 CP 系统一遇到网络分区就全部停止工作。

如果某一侧仍然具备形成一致决定的条件,它可能继续正常提供服务。

Raft 使用多数派维持一致性,就是一个典型例子。


三、为什么三个节点的集群,需要一个 Leader?

回到最初的配置管理系统。

假设三个节点共同维护:

text 复制代码
order.timeout = 1000

管理员现在希望把它修改为:

text 复制代码
order.timeout = 2000

如果三个节点都可以独立接收并确认写入,就需要复杂的并发冲突协调。

例如:

text 复制代码
Client A → Node A → timeout = 2000

Client B → Node B → timeout = 3000

两项修改同时发生时,系统必须决定哪个值最终生效。

Raft 采用一种相对容易理解的思路:

在正常运行期间,由一个 Leader 负责组织日志写入和复制,其他节点作为 Follower 接收并复制日志。

例如:

text 复制代码
             Node A
             Leader
             /    \
            /      \
           v        v
       Node B      Node C
       Follower    Follower

现在客户端需要修改配置。

请求交给 Leader:

text 复制代码
Client
   |
   v
Node A(Leader)
   |
   +----> Node B
   |
   +----> Node C

Leader 负责将修改组织为日志条目,并向其他节点复制。

当日志满足提交条件后,再将相应状态修改作为正式结果对外确认。

这样可以避免多个普通 Follower 独立决定不同的写入顺序。

但有一个问题:

如果 Leader 自己发生故障,系统怎么办?

这就是 Raft 的 Leader 选举机制要解决的问题。


四、Raft:通过 Leader、日志复制和多数派达成一致

Raft 是一种用于实现复制状态机一致性的算法。

它的目标不是让所有节点任何时刻的本地数据都完全一样,而是让节点能够对已经提交的操作形成一致的顺序,并且在故障恢复后维持安全性。

Raft 的核心可以先分成三个部分:

text 复制代码
Leader Election
Leader 选举

Log Replication
日志复制

Safety
一致性安全保证

我们先从 Leader 选举开始。

1. Raft 中的三种节点角色

Raft 节点主要具有三种角色:

Follower

正常情况下接收 Leader 的心跳与日志复制请求。

Candidate

当节点认为需要重新选举 Leader 时,可以转变为候选者并请求其他节点投票。

Leader

负责组织日志写入、复制及提交进度。

例如:

text 复制代码
Node A:Leader

Node B:Follower

Node C:Follower

正常运行时:

text 复制代码
Leader
   |
   | 定期发送心跳
   v
Followers

Follower 通过这些通信判断当前 Leader 是否仍然能够正常联系。

2. Leader 不再响应时会发生什么?

假设 Node A 突然崩溃。

text 复制代码
Node A ×

Node B ✓

Node C ✓

Node B 和 Node C 一段时间内没有收到有效 Leader 心跳。

某个节点的选举超时先到达,它就可能成为 Candidate。

例如 Node B:

text 复制代码
Node B
Follower
   |
   | 选举超时
   v
Candidate
   |
   v
请求投票

Node B 会增加自己的 Term,并向其他节点发送投票请求。

Term 可以理解为 Raft 使用的逻辑任期编号。

例如:

text 复制代码
Term 5:

Node A 是 Leader

Node A 故障后:

text 复制代码
Term 6:

Node B 发起选举

Term 不是一个现实时间单位,而是用于区分不同选举任期的逻辑编号。

3. 为什么不是谁先宣布自己是 Leader,谁就成功?

因为 Raft 要求候选者获得多数派投票。

对于三个节点的集群:

text 复制代码
Node A

Node B

Node C

多数派数量为:

text 复制代码
2

如果 Node B 获得自己的投票,再获得 Node C 的投票:

text 复制代码
Node B:投自己一票

Node C:投给 B

总计:

text 复制代码
2 / 3

Node B 就可以赢得当前任期的选举。

例如:

text 复制代码
Node A ×

Node B → Leader

Node C → Follower

需要注意,每个节点在同一个 Term 中通常只能投出一张选票。

而且并不是任意 Candidate 都有资格获得投票。

Raft 还会比较候选者与投票者的日志新旧程度,避免一个日志明显落后的节点随意赢得选举。

这与后面讲到的"已经提交的日志不能丢失"密切相关。

4. 为什么需要随机选举超时?

假设 Node B 和 Node C 总是在完全相同的时刻发起选举:

text 复制代码
Node B → Candidate

Node C → Candidate

B 投自己一票。

C 也投自己一票。

结果:

text 复制代码
Node B:1 票

Node C:1 票

谁都没有达到多数派。

因此,Raft 使用随机化的选举超时来降低节点同时发起选举、持续分票的概率。

如果某次选举没有成功,节点可以进入后续任期重新尝试。


五、多数派:为什么三个节点坏一个,还能继续工作?

这是我们之前学习分布式系统时重点讨论过的问题。

假设有三个节点:

text 复制代码
A、B、C

Raft 需要获得超过集群成员总数一半的投票,才能形成多数派。

计算方式是:

text 复制代码
Majority = floor(N / 2) + 1

其中:

text 复制代码
N = 集群配置中的投票成员数量

例如:

集群节点数 多数派数量 最多允许失去的节点数
1 1 0
2 2 0
3 2 1
4 3 1
5 3 2

这里的故障容忍数量,是在剩余节点仍能正常通信、原有集群成员配置未改变等前提下讨论的。

1. 三个节点坏一个

假设:

text 复制代码
A ×

B ✓

C ✓

剩余:

text 复制代码
2 个节点

原集群的多数派要求:

text 复制代码
2 / 3

B 和 C 可以组成多数派。

如果原 Leader 是 A,那么 B 和 C 还可以通过选举产生新的 Leader。

因此,集群仍然能够继续形成一致决定。

这就是:

三节点 Raft 集群通常能够容忍一个节点故障。

2. 三个节点坏两个

假设:

text 复制代码
A ×

B ×

C ✓

此时只剩下 Node C。

它能不能认为:

text 复制代码
现在只有我一个活着
所以我就是多数派

不能。

因为多数派是根据集群配置中的投票成员总数计算的,而不是根据当前还活着多少节点重新计算。

原集群是三个投票成员:

text 复制代码
N = 3

多数派始终要求:

text 复制代码
2

只剩一个节点,就无法形成新的多数派决定。

这时它通常不能安全地继续提交新的 Raft 日志。

但这不意味着节点进程一定停止,也不代表本地所有数据都无法访问。

例如,在允许读取可能过时的数据的特定系统设计中,节点仍然可能提供本地读取。

只不过它无法仅凭自己保证这些读取始终满足最新的线性一致性语义。

3. 为什么不能根据存活节点重新计算多数派?

假设出现网络分区:

text 复制代码
原集群:

A、B、C

现在分成两边:

text 复制代码
分区一:

A
text 复制代码
分区二:

B、C

如果双方都按自己这一侧的存活节点数重新计算多数派:

Node A 可能认为:

text 复制代码
本分区只有 1 个节点

1 / 1 已经是多数派

Node B、C 也认为:

text 复制代码
本分区有 2 个节点

2 / 2 已经是多数派

结果双方都可能自行确认写入。

这会破坏整个集群对数据修改的统一顺序。

所以多数派必须以共同认可的集群配置为依据。

不能因为网络断开,就由不同分区自行缩小投票成员总数。


六、多数派为什么能够避免脑裂?

前面多次提到 Brain Split(脑裂)。

现在可以从多数派角度理解它。

假设原来的 Leader 是 Node A。

text 复制代码
             A(Leader)
              /       \
             /         \
            B --------- C

突然,Node A 与 B、C 之间的网络中断。

text 复制代码
Node A           Node B、Node C
   |                   |
   X   网络分区        |
   |                   |

Node A 可能暂时认为自己仍然是 Leader。

与此同时,Node B 和 Node C 因为无法联系 A,可能通过投票选出新 Leader。

例如:

text 复制代码
分区一:

Node A
旧 Term 的 Leader
text 复制代码
分区二:

Node B
新 Term 的 Leader

Node C
Follower

这时是不是出现两个 Leader 了?

从节点暂时持有的角色认知来看,可能有两个节点都认为自己是 Leader。但它们不能都安全地提交互相冲突的新日志。

原因是多数派机制。

1. Node A 为什么不能继续成功提交写入?

Node A 所在分区只有自己一个节点:

text 复制代码
A:1 / 3

达不到多数派:

text 复制代码
2 / 3

即使它接收到了客户端写入请求,并将日志暂时追加到自己的本地日志中,也无法获得多数派复制确认,因此不能把这个新写入当成已经提交的结果向客户端成功确认。

2. Node B 为什么可以继续?

Node B 和 Node C 仍然能够正常通信。

text 复制代码
B + C = 2 / 3

它们形成多数派,并且能够完成新 Leader 选举。

因此,新 Leader B 可以继续组织日志复制,并在满足提交条件后确认新写入。

3. 为什么不会同时出现两个独立的多数派?

因为在三个节点中:

text 复制代码
任意两个包含 2 个节点的集合
必然至少共享 1 个节点

例如:

text 复制代码
多数派 1:A、B

多数派 2:B、C

它们共同包含:

text 复制代码
B

这种多数派之间必然存在交集的性质,是 Raft 维持一致性的重要基础。

结合每个节点在同一任期内的投票规则、日志新旧比较和日志复制规则,可以防止两个彼此冲突的决定都被安全提交。

这就是多数派避免脑裂造成数据分叉的核心思路。

不过需要明确:

多数派本身只是 Raft 安全性设计的一部分,不能脱离任期规则、投票限制和日志匹配规则,认为简单数票就已经实现完整 Raft。

4. 网络恢复以后会怎样?

假设 Node A 恢复与 B、C 的通信。

它发现当前集群已经进入更高的 Term:

text 复制代码
Node A:Term 5

Node B:Term 6

Node A 收到合法的更高任期消息后,需要更新自己的任期,并退出原来的 Leader 角色。

随后再按照新 Leader 的日志复制与恢复机制,逐步追上集群状态。

因此,Raft 不是靠旧 Leader 自觉永远不再运行来避免冲突,而是通过任期、多数派和日志规则来协调节点恢复。


七、Raft 的日志复制到底怎么工作?

前面重点讨论了 Leader 选举。

但 Raft 不只是选出 Leader。

它更重要的任务是:

让多个节点按照一致的顺序应用已经提交的操作。

假设我们维护一项配置:

text 复制代码
order.timeout = 1000

客户端请求修改为:

text 复制代码
order.timeout = 2000

现在 Node A 是 Leader:

text 复制代码
Node A:Leader

Node B:Follower

Node C:Follower

第一步:Leader 追加日志

Node A 接收修改请求后,将操作组织为一条日志记录。

例如:

text 复制代码
Index = 10

Term = 5

Command = SET order.timeout 2000

这里:

  • Index 表示日志位置。
  • Term 表示该日志条目所属的任期。
  • Command 表示要复制并应用的操作。

Leader 先将条目追加到自己的日志中。

text 复制代码
Node A:

Index 8
Index 9
Index 10:SET timeout 2000

但此时不能因为 A 已经写入本地日志,就认为这项修改已经得到集群确认。

第二步:向 Follower 复制日志

Leader 向 B、C 发送日志复制请求。

text 复制代码
              Node A
              Leader
                |
          +-----+-----+
          |           |
          v           v
       Node B       Node C

假设 B 成功写入:

text 复制代码
Node A:

Index 10 ✓
text 复制代码
Node B:

Index 10 ✓

但 C 因网络延迟暂时没有完成:

text 复制代码
Node C:

Index 10 尚未收到

现在已有:

text 复制代码
A + B = 2 / 3

第三步:满足提交条件

对于这个由当前任期 Leader 创建的新日志条目,复制到多数派后,Leader 可以依据 Raft 的提交规则推进 Commit Index。

这里有一个技术细节:Raft 对以前任期的旧日志条目还有更严格的提交规则,不能简单认为任何历史日志只要出现在多数派上就可以单独按副本数量直接提交。

为了理解主流程,我们先讨论当前任期新追加的日志。

当条目被正式提交后,节点才能按照日志顺序将对应操作应用到状态机。

例如:

text 复制代码
SET order.timeout 2000

于是配置状态发生改变。

第四步:通知其他节点提交进度

Leader 会在后续通信中告诉 Follower 最新的提交位置。

Follower 按照已经提交的日志顺序应用操作。

例如最终:

text 复制代码
Node A:

timeout = 2000
text 复制代码
Node B:

timeout = 2000
text 复制代码
Node C:

timeout = 2000

这就是 Raft 复制状态机的基本思想。

为什么叫"复制状态机"?

可以这样理解:

text 复制代码
初始状态:

timeout = 1000

所有节点按照相同顺序应用:

text 复制代码
1. SET timeout 1500

2. SET timeout 2000

3. SET timeout 3000

在操作具有确定性、状态机实现正确的前提下,节点最终可以得到相同状态:

text 复制代码
timeout = 3000

Raft 主要负责让节点对已经提交的日志顺序形成一致认识。

具体业务操作怎样修改状态,则属于上层状态机的职责。


八、为什么"写入 Leader 成功"还不等于"提交成功"?

这个问题和我们之前学习 Kafka Producer ACK 时讨论过的故障窗口有些相似,但 Raft 和 Kafka 的具体协议并不相同。

假设当前是:

text 复制代码
Term 5

Node A:Leader

Node B:Follower

Node C:Follower

Node A 收到一条新操作:

text 复制代码
Index 11:

SET timeout 5000

A 将它写入本地日志。

但这时网络发生分区:

text 复制代码
Node A

      X

Node B、Node C

结果:

text 复制代码
Node A:

Index 11 = SET timeout 5000

而:

text 复制代码
Node B、Node C:

还没有 Index 11

此时 A 只有:

text 复制代码
1 / 3

所以 Index 11 不能作为已提交日志确认。

如果 B、C 选出新 Leader 呢?

B 和 C 形成多数派,选出 Node B 作为 Term 6 的 Leader。

Node B 随后可能接收另一项配置修改:

text 复制代码
Index 11:

SET timeout 3000

并复制到 C:

text 复制代码
Node B:

Index 11 = SET timeout 3000
text 复制代码
Node C:

Index 11 = SET timeout 3000

这项当前任期的日志可以在满足提交条件后被正式确认。

网络恢复以后,A 原来本地追加但未提交的 Index 11,可能需要根据新 Leader 的日志进行修正或覆盖。

最终集群保留已经正式提交的结果:

text 复制代码
timeout = 3000

而不是让 A 的未提交修改强行覆盖新 Leader 的已提交记录。

这里要特别强调:

未提交的日志不是已经对客户端确认成功的操作。

如果 Node A 先前没有获得多数派,就不应该对客户端宣称配置修改已经完成。

这也是为什么 Raft 宁可在失去多数派时停止确认新的写入,也要维护已经提交的日志不会发生冲突。

为什么以前已经提交的日志不会被随意覆盖?

因为 Raft 不只检查投票数量。

候选者要赢得选举,还必须满足相应的日志新旧条件。

结合多数派交集和日志匹配规则,能够保证一个合法当选的 Leader 不会丢失此前已经提交的日志。

所以:

多数派决定了哪些节点有资格形成一致决定,日志规则则进一步保证已经提交的历史不会被后续 Leader 破坏。


九、Raft 能保证所有节点随时都有最新数据吗?

不能。

这点很容易被误解。

假设一个新的配置修改已经在 A、B 两个节点上复制成功并提交。

text 复制代码
Node A:

Index 10 ✓
text 复制代码
Node B:

Index 10 ✓

但 Node C 因为网络延迟暂时落后:

text 复制代码
Node C:

Index 10 尚未应用

这并不违反 Raft 的基本一致性要求。

Raft 允许 Follower 暂时落后。

只要集群仍然满足提交与恢复规则,落后的节点之后可以追上 Leader。

因此:

Raft 保证已提交日志的安全性和统一顺序,不代表所有副本在每个瞬间都具有完全相同的本地状态。

这里还涉及读取问题。

从任意 Follower 读取,一定是最新值吗?

不一定。

如果 Node C 尚未应用最新的已提交日志,直接读取 C 的本地状态,就可能得到旧值。

例如:

text 复制代码
Node A:timeout = 2000

Node B:timeout = 2000

Node C:timeout = 1000

所以 Raft 系统如果需要提供线性一致性读取,就不能简单认为:

text 复制代码
任何副本直接读取本地内存

都必然得到最新结果。

实际实现通常需要通过适当的 Leader 确认、提交进度检查或其他安全读取机制来保证读取语义。

例如旧 Leader 发生网络隔离时,它可能暂时不知道自己已经失去领导权。

如果仍然直接把本地数据当作最新结果返回,就可能出现过时读取。

因此:

Raft 的日志复制机制为强一致状态管理提供基础,但具体读取接口是否保证线性一致性,仍然取决于系统如何实现读取协议。


十、Raft 与 MySQL 主从复制有什么区别?

前面学习 MySQL 主从复制时,我们看到:

text 复制代码
Primary
   |
   +---- Replica 1
   |
   +---- Replica 2

Raft 也有:

text 复制代码
Leader
   |
   +---- Follower 1
   |
   +---- Follower 2

看起来非常相似。

但不能因为它们都有主节点和副本,就认为两种机制完全一样。

MySQL 常见异步主从复制

在常见异步复制架构中:

text 复制代码
Primary COMMIT 成功
        |
        v
向 Replica 复制变更

主库可以在副本尚未收到相关事务时,对客户端确认本地事务成功。

因此,如果主库随后故障,而备用副本没有收到最新事务,就可能在故障切换时丢失已在原主库确认的部分数据。

Raft 多数派提交

Raft 的典型写入流程是:

text 复制代码
Leader 追加日志
       |
       v
复制到多数派
       |
       v
满足提交条件
       |
       v
确认写入

它明确把多数派和日志安全规则纳入提交协议。

这样,在集群成员配置和持久化等假设成立时,合法选出的新 Leader 可以维持已经提交的日志历史。

所以二者关注的层次有所不同:

对比 MySQL 常见异步主从复制 Raft
主要目标 数据复制、读扩展、故障恢复基础 复制状态机一致性
写入确认 主库通常不等待副本应用完成 按 Raft 多数派提交规则确认
领导权切换 需要相应故障切换机制 协议本身包含 Leader 选举
副本落后 可以存在 可以存在
故障时的数据安全 取决于复制与切换机制 通过多数派及日志规则保护已提交日志

这里比较的是常见的单主异步 MySQL 复制与 Raft,不能据此推断 MySQL 的所有复制和高可用方案都没有共识机制。

同样,不能简单把 Kafka 的 acks=all 和 Raft 多数派提交看成完全相同的协议。

它们都关注副本确认和故障恢复,但具体机制、提交规则和对外保证并不相同。


十一、多数派是不是越多越好?

理解三节点集群以后,一个自然的问题是:

如果三个节点能够容忍一个故障,那么是不是节点越多越好?

从故障容忍能力来看,增加投票成员确实可能提高集群可容忍的节点故障数量。

例如:

text 复制代码
3 个节点:

多数派 2

最多容忍 1 个节点不可用
text 复制代码
5 个节点:

多数派 3

最多容忍 2 个节点不可用

但节点数量增加也会带来成本。

1. 更多节点不意味着提交延迟一定更低

Raft 需要获得多数派确认。

更多节点可能意味着更多复制通信和更复杂的成员管理。

虽然复制请求可以并行发送,提交也不需要等所有节点,但系统仍然需要考虑网络分布、节点负载和副本持久化开销。

2. 四节点不一定比三节点多容忍一个故障

例如:

text 复制代码
3 节点:

多数派 = 2

最多失去 1 个
text 复制代码
4 节点:

多数派 = 3

最多失去 1 个

四节点集群虽然多了一台机器,但按照简单多数派模型,能够容忍的节点故障数量并没有增加。

这也是为什么常见的小规模共识集群经常选择三个或五个投票成员。

当然,真实部署仍然需要结合故障域、网络拓扑和具体系统要求决定。

3. 多数派不能解决所有基础设施问题

即使五个节点能够容忍两个节点失联,如果它们都部署在同一个容易同时故障的机房,那么一次机房故障仍然可能使整个集群不可用。

所以除了节点数量,还需要考虑:

text 复制代码
节点是否分散在不同故障域

网络链路是否可靠

磁盘持久化是否满足要求

监控能否及时发现节点异常

故障恢复是否自动化

多数派解决的是集群一致性决策问题,而不是替代所有高可用设计。


十二、CAP 和 Raft 到底是什么关系?

现在再回头看 CAP,就会更加清楚。

假设三个节点发生网络分区:

text 复制代码
原集群:

A、B、C

分成:

text 复制代码
分区一:

A

和:

text 复制代码
分区二:

B、C

对于采用 Raft 管理一致状态的系统:

A 这一侧失去了多数派,通常不能继续提交新的写入。

B、C 这一侧仍然可以形成多数派,选举 Leader 并继续处理一致性写入。

因此,Raft 的行为体现了 CAP 中一种偏向一致性的取舍。

面对网络分区,它不是允许两边都独立确认冲突的写入,而是要求写入获得多数派支持。

这可能导致少数派分区的客户端无法完成写入。

所以:

Raft 不是同时突破 CAP 对一致性和可用性的限制,而是通过多数派机制明确规定网络分区时哪些节点可以继续形成一致决定。

这也是为什么我们不能把"系统还有一台机器活着"直接等同于"分布式服务仍然完整可用"。

对于需要共识维护的状态,能否继续提供安全写入,取决于系统是否仍有合法的多数派。


十三、把这些知识放到真实系统中理解

假设我们正在实现一个分布式配置管理系统。

集群包含:

text 复制代码
Config Node A

Config Node B

Config Node C

其中 A 是 Leader。

配置值:

text 复制代码
order.timeout = 1000

管理员请求修改:

text 复制代码
order.timeout = 2000

正常情况下

text 复制代码
Client
   |
   v
Leader A
   |
   v
追加配置变更日志
   |
   v
向 B、C 复制
   |
   v
多数派确认
   |
   v
提交日志
   |
   v
应用配置变更

当相应日志满足提交条件后,系统可以确认此次修改成功。

一个 Follower 崩溃

假设:

text 复制代码
A ✓

B ✓

C ×

A、B 仍然满足:

text 复制代码
2 / 3

因此,可以继续形成提交多数派。

C 恢复以后,再根据日志复制机制追上集群状态。

Leader 崩溃

假设:

text 复制代码
A ×

B ✓

C ✓

B、C 可以通过 Raft 选举产生新 Leader。

之后由新 Leader 继续处理写入。

不过,切换过程中可能存在短暂不可用时间,不能理解为 Leader 故障完全不会影响客户端请求。

只剩一个节点

假设:

text 复制代码
A ×

B ×

C ✓

C 无法形成:

text 复制代码
2 / 3

因此,不能仅凭自己安全地提交新的 Raft 日志。

如果业务要求强一致写入,就需要等待更多节点恢复,或者通过受控的集群成员变更与灾难恢复流程处理,而不是直接让 C 自行宣布集群只剩一个成员。

网络分区后恢复

假设 A 被隔离,B、C 形成新多数派并选出新 Leader。

在分区恢复以后:

text 复制代码
A 发现更高 Term
       |
       v
退出旧 Leader 角色
       |
       v
按照新 Leader 的日志恢复
       |
       v
最终追上已提交状态

这样,集群就能在故障恢复后继续维持统一的日志历史。

这就是 Raft 多数派机制的一个典型应用过程。


十四、总结:多数派的价值,是让分布式系统知道什么时候可以安全地作决定

这一篇从网络分区出发,介绍了 CAP、Raft 和多数派。

可以把这些概念放在三个层次理解。

概念 主要解决的问题
CAP 网络分区时,一致性与可用性存在什么取舍
Leader Election 当前应该由哪个节点组织写入
Term 如何区分不同选举任期
Log Replication 怎样让节点复制相同的操作日志
Majority / Quorum 什么样的节点集合有资格形成一致决定
Commit Index 哪些日志已经正式提交
Replicated State Machine 怎样按照统一的已提交日志顺序更新状态

其中最重要的是四个结论。

第一,网络分区不等于服务器宕机

即使两台机器都在正常运行,也可能因为无法通信而无法判断对方的最新状态。

因此,分布式系统不能只根据节点是否存活来决定谁有权修改共享状态。

第二,三个节点坏掉一个,剩下两个仍然能够形成多数派

因为:

text 复制代码
3 节点集群

多数派 = 2

但是:

text 复制代码
只剩 1 个节点

就无法继续形成新的多数派提交。

多数派不是根据当前存活节点数量随意重新计算的。

第三,多数派与任期、日志规则共同防止脑裂

在网络分区期间,旧 Leader 可能暂时不知道自己已经失去领导权。

但如果它不能获得多数派,就不能安全地确认新日志提交。

而获得多数派的新 Leader,还需要满足日志安全规则,确保此前已经提交的日志不会丢失。

第四,Raft 保证的是已提交日志的安全性,不是所有副本瞬时相同

Follower 可以暂时落后。

强一致读取也需要正确的读取协议,不能简单从任意副本获取本地状态就认为一定是最新值。

最后,可以用一句话总结:

Raft 的核心不是让所有节点永远不出错,而是在节点故障和网络分区时,仍然能够明确哪些操作可以被安全提交,以及故障恢复后所有节点应该遵循哪一份已提交的历史。

理解了这一点,前面学习的服务注册、主从复制和分布式高可用机制就会更容易区分。

下一篇继续讨论分布式锁。

我们会重点解释一个看似矛盾的问题:

既然 Redis 分布式锁已经保证同一时间只有一个持锁者,为什么锁过期后,旧持锁者仍然可能继续修改数据?

这就涉及 Lease、网络断连以及 Fencing Token。

相关推荐
前端的日常1 小时前
一个人+AI做情侣食谱小程序,0 费用的服务器
前端·后端
IT_陈寒2 小时前
Vue的v-for里用了个临时变量,页面直接挂了
前端·人工智能·后端
ServBay3 小时前
本地模型已经跑起来了,Copilot Fusion 为什么还要联网?
后端·ai编程·github copilot
武子康3 小时前
H3-World 的动作没按时发生,先检查时间线变成了什么
人工智能·后端·agent
苍何3 小时前
腾讯 Buddy One 来了,办公全家桶一键打通WorkBuddy(附教程)
后端
方方洛3 小时前
ray教程-18-性能调优与反模式
人工智能·分布式·机器学习
GIS数据转换器4 小时前
基于CIM的城市智慧供热监管服务平台
java·大数据·开发语言·人工智能·后端·物联网·struts
adfreedom4 小时前
如何避免每个业务系统中都有几十个枚举查询
后端
m0_639906944 小时前
SpringBoot自动配置原理
java·spring boot·后端