Wiki: 深入浅出分布式系统共识协议(Consensus Protocol)底层原理
在分布式系统中,共识协议(Consensus Protocol)被称为分布式架构的“灵魂”。它解决了计算机科学中最具挑战性的问题:在网络可能延迟、丢包,且部分节点可能随时宕机的情况下,一组独立的机器如何对某个决定(或一连串操作)达成完全一致的意见?
本文将以目前业界最主流的 Raft 协议 为例,深度剖析分布式共识协议的理论基石、底层运行机制以及它如何从数学上预防“脑裂”。
1. 共识协议的理论基石:状态机复制(State Machine Replication)
要理解共识协议,首先要理解它是如何让多台机器的数据保持一致的。几乎所有现代共识协议(如 Paxos、Raft、ZAB)都基于同一个数学推论——状态机复制(SMR):
[ 相同初始状态 ]
|
v
[ 相同的输入日志序列 (Log) ] <--- (由共识协议确保顺序和内容完全一致)
|
v
[ 状态机执行 (State Machine) ]
|
v
[ 最终一致的状态 ]
- 状态机(State Machine):一个输入决定一个输出的确定性程序。只要给它输入相同的指令,它就必然产生相同的结果。
- 底层机理:共识协议的本质,并不是去同步最终的数据库文件,而是去同步一份绝对一致的、有序的“操作日志(Log)”。只要保证所有节点上的日志顺序和内容完全一致,每个节点在本地顺序执行这些日志后,最终得到的数据库状态就必然完全一致。
2. 共识协议如何工作?(以 Raft 协议的三大核心机制为例)
Paxos 协议因过于晦涩难懂而在学术界出名,而 Raft 协议 凭借其结构清晰、易于实现的特点,成为了当今工业界(如 etcd、Consul)的绝对首选。Raft 的运行原理可以拆解为三个子问题:
机制一:领导者选举(Leader Election)—— 谁说了算?
Raft 采用强领导者(Strong Leader)模型。在集群中,任何时刻只能有一个 Leader,所有的写请求都必须由 Leader 统一接收并分发。
[ Follower ] --(超时无心跳)--> 变成 [ Candidate ]
^ |
| (收到新 Leader 心跳) | (获得过半数选票)
| v
+--------------------------- [ Leader ]
- 节点的三种状态:
- Leader(领导者):负责接收客户端请求,同步日志,发送心跳。
- Follower(跟随者):默默接收 Leader 的心跳和日志。
- Candidate(候选人):当 Follower 听不到 Leader 的心跳时,会毛遂自荐,发起投票。
- 选举超时(Election Timeout)的艺术:
- 如果 Leader 宕机,Followers 在一段时间内(如 150ms - 300ms)没有收到 Leader 的心跳,就会认为 Leader 死了。
- 随机化机制(Randomized Start):为了防止所有 Follower 同时发起选举导致选票瓜分(Split Vote),Raft 让每个节点的“等待超时时间”在 150ms-300ms 之间随机。谁的计时器先到零,谁就先发起投票,从而极大地提高了选举一次成功的概率。
- 任期(Term):
- 每次选举都会产生一个新的
Term(类似于朝代号,是一个递增的数字)。如果一个节点发现自己的 Term 比别人小,会立刻自动降格为 Follower。
- 每次选举都会产生一个新的
机制二:日志复制与两阶段提交(Log Replication)
一旦选出 Leader,它就开始履行职责。当客户端向 Leader 写入数据时,共识在底层是通过以下“两阶段”达成的:
Client Leader Follower 01 Follower 02
| | | |
|-- 写请求 -->| | |
| |-- 1. 广播 Log (未提交) ->| |
| |------------------ 1. 广播 Log (未提交) ---->|
| | | |
| |<-- 2. 确认 (ACK) -----| |
| |<----------------- 2. 确认 (ACK) -----------|
| | (半数以上确认) | |
| |-- 3. 提交并应用 (Commit) |
|<-- 成功 ---| | |
- 阶段 1:未提交阶段(Uncommitted)
Leader 收到写请求,先将操作写入自己的本地 Log,然后向所有 Follower 广播该 Log 项。此时,该日志处于“未提交”状态。 - 多数派确认(Quorum):
Follower 收到 Log 后写入本地,并向 Leader 回复 ACK。一旦 Leader 收到超过半数(Majority,即 $N/2 + 1$)节点的确认,Leader 就认为该日志已经安全,正式将其标记为 Committed(已提交),并写入本地状态机。 - 阶段 2:通知应用:
Leader 向客户端返回成功,并在下一次心跳中通知所有 Follower:“我这边已经 Commit 了,你们也赶紧在本地 Commit 并应用到状态机吧。”
机制三:防止脑裂(Brain-Split Prevention)—— 多数派约束
在网络发生物理割裂时,集群会被分成两个孤立的网络。共识协议是如何确保不会同时有两个 Leader 乱写数据的?
假设一个 5 节点的集群,分裂成了 2 个节点的“少数派分区”和 3 个节点的“多数派分区”:
[ 少数派分区 (2个节点) ] [ 多数派分区 (3个节点) ]
+--------------------------+ +--------------------------+
| Host A (旧 Leader) | | Host C (新选出的Leader)|
| Host B | | Host D |
| | | Host E |
+--------------------------+ +--------------------------+
无法获得过半数(3票)确认 成功获得过半数(3票)确认
===> 所有写入全部失败! ===> 正常接受并提交写入!
- 少数派分区无法选举新 Leader:
如果旧 Leader 在少数派分区(Host A/B),由于它们只有 2 个节点,无法凑齐 5 节点集群所需的 3 票($5/2 + 1$),因此该分区无法进行任何有效写入。 - 多数派分区可以选出新 Leader:
多数派分区(Host C/D/E)拥有 3 个节点,刚好满足过半数要求。它们可以选出新 Leader,并正常提交(Commit)写入。 - 网络恢复后的合流(Rejoin):
当网络恢复后,旧 Leader(Host A)收到新 Leader(Host C)的强力心跳,且发现自己的 Term(朝代)落后了,会立刻降格为 Follower。
日志对齐:Agent A/B 会被强制丢弃在分裂期间产生的所有未提交日志,并完全同步 Agent C 的日志,从而实现全集群的数据最终绝对一致。
3. 为什么普通应用软件(如 Jenkins)无法直接使用共识协议?
共识协议如此强大,为什么不直接塞进 Jenkins、Jira 这些应用中来实现多写双活呢?因为共识协议的代价极其昂贵:
- 写入吞吐量暴跌:
单机写文件只需要一次磁盘 I/O。而使用共识协议,每一次写入都需要经历:网络广播 -> 等待多数派响应 -> 本地刷盘 -> 再次广播确认。这会引入毫秒级的网络延迟,导致写吞吐量(TPS)大幅下降。 - 极高的开发侵入性:
普通的应用程序读写文件都是随意的(如直接修改 XML、写日志)。而共识协议要求所有的写操作必须强顺序、可序列化、且可重放。这需要重构整个应用底层的内存数据结构和文件 I/O 引擎。 - 硬件成本翻倍:
由于多数派(Quorum)的数学限制,共识集群的节点数必须是奇数(3, 5, 7)。要容忍 1 台机器宕机,至少需要部署 3 台节点,这对于普通应用来说性价比极低。
4. 总结
共识协议通过 强领导者控制、两阶段多数派提交 以及 严格的一致性哈希/任期对比,在不可靠的网络硬件之上构建出了一套绝对可靠的数据一致性逻辑。
它是分布式存储和分布式数据库的底层基石,但由于其高昂的通信成本和重构代价,对于普通的企业级应用(如 Jenkins),采用我们上一篇 Wiki 介绍的“共享存储 + 虚拟 IP 漂移”的主备(Active-Passive)方案,依然是目前性价比最高、技术最可行的工程解。
浙公网安备 33010602011771号