Skip to content

Commit 1151b5e

Browse files
committed
docs: gossip 和 raft 协议完善,绘制配图帮助理解
1 parent a81bfb2 commit 1151b5e

8 files changed

Lines changed: 216 additions & 65 deletions

File tree

docs/distributed-system/protocol/gossip-protocol.md

Lines changed: 111 additions & 55 deletions
Large diffs are not rendered by default.
Binary file not shown.
-9.56 KB
Binary file not shown.
Binary file not shown.

docs/distributed-system/protocol/images/gossip/反熵-闭环.drawio

Lines changed: 0 additions & 1 deletion
This file was deleted.
-10.8 KB
Binary file not shown.
Binary file not shown.

docs/distributed-system/protocol/raft-algorithm.md

Lines changed: 105 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -139,22 +139,65 @@ Leader 收到客户端请求后,会生成一个 entry,包含`<index,term,cmd
139139

140140
Follower 不会自行决定提交点;它们从 Leader 的 AppendEntries RPC 中携带的 leaderCommit 得知当前可提交的最大索引,并将本地 commitIndex 更新为 min(leaderCommit, lastLogIndex),再按序 apply 到状态机。
141141

142-
Raft 保证以下两个性质:
142+
### 4.1 日志匹配属性(Log Matching Property)
143143

144-
- 在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们一定有相同的 cmd
145-
- 在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们前面的 entry 也一定相同
144+
Raft 通过 **日志匹配属性(Log Matching Property)** 保证日志绝对不会分叉,这是 Raft 安全性的基石之一。该属性包含两个核心保证:
146145

147-
通过“仅有 Leader 可以生成 entry”来保证第一个性质,第二个性质需要一致性检查来进行保证。
146+
- **保证一**:如果两个日志在相同 index 位置的 entry 具有相同的 term,那么它们存储的 cmd 一定相同
147+
- **保证二**:如果两个日志在相同 index 位置的 entry 具有相同的 term,那么该位置之前的所有 entry 也完全相同
148148

149-
一般情况下,Leader 和 Follower 的日志保持一致,然后,Leader 的崩溃会导致日志不一样,这样一致性检查会产生失败。Leader 通过强制 Follower 复制自己的日志来处理日志的不一致。这就意味着,在 Follower 上的冲突日志会被领导者的日志覆盖。
149+
#### 归纳法证明
150+
151+
日志匹配属性通过归纳法得以保证:
152+
153+
1. **基础情况**:日志为空时,属性自然成立
154+
2. **归纳步骤**:假设日志在 index N 之前完全一致,当 Leader 尝试追加 entry N+1 时,通过 **AppendEntries RPC 的一致性检查** 确保:
155+
156+
```
157+
AppendEntries RPC 参数:
158+
- prevLogIndex:前一条日志的索引(Leader 认为与 Follower 对齐的位置)
159+
- prevLogTerm:前一条日志的任期
160+
- entries[]:待追加的新日志条目
161+
```
162+
163+
**一致性检查逻辑**
164+
165+
- Follower 收到 AppendEntries 请求后,检查本地日志中 index = prevLogIndex 的位置
166+
- 如果该位置的 entry.term == prevLogTerm,说明Leader和Follower在prevLogIndex之前的日志完全一致,通过检查
167+
- 如果不存在或 term 不匹配,拒绝追加,返回失败
168+
169+
**关键点**:通过检查 prevLogIndex 和 prevLogTerm 的配对,Leader 和 Follower 能够**数学上确保**它们对日志历史达成一致。只有当"最后一个已知一致点"确实一致时,才会追加新日志。这形成了归纳证明的传递链条:
170+
171+
```
172+
entry[0] 一致 → entry[1] 一致 → entry[2] 一致 → ... → entry[N] 一致
173+
↑_____________通过 prevLogIndex/prevLogTerm 递归验证_____________↑
174+
```
175+
176+
因此,日志绝不会出现两个不同的值在同一 index 位置被"提交"的情况——即日志不分叉。
177+
178+
#### 工程实现优化
179+
180+
在实际生产实现(如 etcd 3.5.x)中,除了上述基础的一致性检查外,还包含多项工程优化:
181+
182+
- **快速回退(Fast Backup)**:当 AppendEntries 一致性检查失败时,Follower 返回冲突日志对应的 term 及其边界索引(该 term 的第一条和最后一条 index),Leader 据此一次性跳过整段冲突区间,而非逐条递减 nextIndex 重试。
183+
184+
- **重试风暴防护**:高负载下可能出现大量 AppendEntries 失败重试,实现中通常会加入:
185+
- **Jitter 退避**:重试间隔加入随机抖动,避免多个 Follower 同时重试
186+
- **背压机制**:限制单个 Follower 的重试速率,防止占用过多网络带宽
187+
188+
这些优化不影响日志匹配属性的理论正确性,而是提升了系统在异常场景下的恢复效率。
189+
190+
### 4.2 日志不一致的恢复
191+
192+
一般情况下,Leader 和 Follower 的日志保持一致,但 Leader 的崩溃会导致日志出现差异。此时 AppendEntries 的一致性检查会失败,Leader 通过强制 Follower 复制自己的日志来处理日志的不一致。这就意味着,在 Follower 上的冲突日志会被领导者的日志覆盖。
150193

151194
为了使得 Follower 的日志和自己的日志一致,Leader 需要找到 Follower 与它日志一致的地方,然后删除 Follower 在该位置之后的日志,接着把这之后的日志发送给 Follower。
152195

153196
`Leader` 给每一个`Follower` 维护了一个 `nextIndex`,它表示 `Leader` 将要发送给该追随者的下一条日志条目的索引。当一个 `Leader` 开始掌权时,它会将 `nextIndex` 初始化为它的最新的日志条目索引数+1。如果一个 `Follower` 的日志和 `Leader` 的不一致,`AppendEntries` 一致性检查会在下一次 `AppendEntries RPC` 时返回失败。
154197

155-
(朴素实现)在失败之后,`Leader` 会将 `nextIndex` 递减然后重试 `AppendEntries RPC`,直到找到 Leader 与 Follower 日志一致的位置。
198+
**(朴素实现)**在失败之后,`Leader` 会将 `nextIndex` 递减然后重试 `AppendEntries RPC`,直到找到 Leader 与 Follower 日志一致的位置。
156199

157-
(工程优化)实际生产实现通常会加入快速回退(Fast Backup):Follower 在拒绝 AppendEntries 时返回冲突日志对应的任期(term)以及该任期的边界索引,Leader 据此一次性跳过整段冲突区间,显著减少重试次数。
200+
**(工程优化)**实际生产实现通常会加入快速回退(Fast Backup):Follower 在拒绝 AppendEntries 时返回冲突日志对应的任期(term)以及该任期的边界索引,Leader 据此一次性跳过整段冲突区间,显著减少重试次数。
158201

159202
最终 `nextIndex` 会达到一个 `Leader``Follower` 日志一致的地方。这时,`AppendEntries` 会返回成功,`Follower` 中冲突的日志条目都被移除了,并且添加所缺少的上了 `Leader` 的日志条目。一旦 `AppendEntries` 返回成功,`Follower``Leader` 的日志就一致了,这样的状态会保持到该任期结束。
160203

@@ -172,11 +215,64 @@ Leader 需要保证自己存储全部已经提交的日志条目。这样才可
172215

173216
Leader 推进 commitIndex 时,需要满足"当前任期产生的某条日志已复制到多数派"这一条件。对于旧任期遗留的日志,即使它们已经复制到多数派,Leader 也不应仅凭此直接提交;通常通过提交当前任期的一条新日志(常见为 no-op)来间接提交历史日志。这一限制用于避免 Leader 频繁切换时出现已提交日志被覆盖的安全性问题。
174217

175-
### 5.3 节点崩溃
218+
### 5.3 节点崩溃与网络分区
219+
220+
如果 Follower 和 Candidate 崩溃,处理方式会简单很多。之后发送给它的 RequestVote RPC 和 AppendEntries RPC 会失败。由于 Raft 的所有请求都是幂等的,所以失败的话会无限的重试。如果崩溃恢复后,就可以收到新的请求,然后选择追加或者拒绝 entry。
176221

177222
如果 Leader 崩溃,节点在 electionTimeout 内收不到心跳会触发新一轮选主;在选主完成前,系统通常无法对外提供线性一致的写入(以及线性一致读),表现为一段不可用窗口。
178223

179-
如果 Follower 和 Candidate 崩溃,处理方式会简单很多。之后发送给它的 RequestVote RPC 和 AppendEntries RPC 会失败。由于 Raft 的所有请求都是幂等的,所以失败的话会无限的重试。如果崩溃恢复后,就可以收到新的请求,然后选择追加或者拒绝 entry。
224+
**量化分析**:在 5 节点集群中,Leader 崩溃后的不可用窗口通常小于 1 秒(P99 < 500ms 选举超时 + 一轮选举时间)。这是 **PACELC 定理**的体现:发生分区(P)时,系统选择牺牲可用性(A)以保证一致性(C)。幂等重试机制确保节点恢复后能安全追赶数据状态。
225+
226+
#### 单节点隔离与 Term 暴增问题
227+
228+
在标准 Raft 算法中,**单节点网络隔离**可能导致 **Term 暴增(Term Inflation)** 问题,造成"劣币驱逐良币"——一个被隔离的少数派节点在恢复后破坏健康集群的稳定性。
229+
230+
**场景推演**
231+
232+
假设一个 5 节点集群,Leader 为节点 A,Follower 为 B、C、D、E。此时节点 E 发生网络分区,被彻底隔离:
233+
234+
```
235+
正常区域:{A, B, C, D} (Leader A + 多数派,可正常服务)
236+
隔离区域:{E} (单节点隔离,无法收到心跳)
237+
```
238+
239+
| 时间线 | 正常区域 {A, B, C, D} | 隔离区域 {E} |
240+
| ------ | ------------------------------------------------- | ---------------------------------------------- |
241+
| T0 | Leader A 正常服务,Term = 5 | E 收不到心跳,选举超时 |
242+
| T1 | 集群继续正常工作 | E 自增 Term 发起选举(Term 6),但无响应 |
243+
| T2 | ... | E 继续自增(Term 7, 8, ...),假设涨到 Term 99 |
244+
| T3 | 网络恢复,E 带着 Term 99 接入集群 | E 向 {A, B, C, D} 广播 RequestVote (Term 99) |
245+
| T4 | 节点 A 收到 Term 99 > 自己的 Term 5,**被迫退位** | E 的"高 Term"破坏了健康集群 |
246+
247+
**问题分析**
248+
249+
- {A, B, C, D} 是**合法的多数派**(4/5),系统本应继续正常工作
250+
- 节点 E 是**少数派**(1/5),它的隔离不应影响集群整体
251+
- **关键问题**:E 的 Term 暴涨导致健康的 Leader A 被迫下线
252+
- **后果**:整个集群需要重新选举,造成不必要的写入中断
253+
254+
这是标准 Raft 的一个已知边界问题:少数派节点的"疯狂选举"会干扰多数派的正常运行。
255+
256+
#### Pre-Vote 机制
257+
258+
为了解决上述问题,Raft 的扩展方案 **Pre-Vote** 被提出。Pre-Vote 要求节点在真正发起选举前,先进行一次"预投票":
259+
260+
1. **预投票阶段**:Candidate 向其他节点发送 PreVoteRequest,携带自己的日志信息
261+
2. **预投票条件**
262+
- 候选人的日志至少与接收者一样新(选举限制)
263+
- **接收者确认自己与 Leader 的连接已断开**(超过 electionTimeout 未收到心跳)
264+
3. **正式选举**:只有收到多数节点的 PreVote 响应后,才真正增加 term 并发起 RequestVote
265+
266+
**Pre-Vote 如何防止 Term 暴增**
267+
268+
- 在上述单节点隔离场景中,E 在隔离期间发起 Pre-Vote 时,**其他节点仍能收到 Leader A 的心跳**
269+
- 因此其他节点会**拒绝 E 的 PreVote 请求**(因为与 Leader 连接正常)
270+
- E 无法获得多数 PreVote 响应,**不会真正增加 Term**
271+
- 网络恢复后,E 的 Term 仍然较低,不会干扰健康的 Leader A
272+
273+
**核心思想**:只有确认自己与 Leader 失去连接后,节点才开始真正增加 Term。这有效防止了少数派节点的 Term 暴涨干扰多数派。
274+
275+
Pre-Vote 机制已广泛应用于 etcd、TiKV、Consul 等生产级 Raft 实现。
180276

181277
### 5.4 时间与可用性
182278

0 commit comments

Comments
 (0)