1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
//! 实现 Raft 分布式共识协议。
//!
//! 细节参见 Diego Ongaro 的原始文献:
//!
//! * Raft 论文:<https://raft.github.io/raft.pdf>
//! * Raft 学位论文:<https://web.stanford.edu/~ouster/cgi-bin/papers/OngaroPhD.pdf>
//! * Raft 网站:<https://raft.github.io>
//!
//! Raft 是一组计算机就某些数据达成一致的协议——更简单地说,就是复制数据。
//! 它与 [Paxos] 和 [Viewstamped Replication] 大致等价,但规定更明确、更易理解。
//!
//! Raft 有三个主要性质:
//!
//! * 容错:只要多数节点(>50%)仍在运行,系统就能容忍节点故障。
//!
//! * 线性一致性(强一致性):客户端写一旦被接受,对所有客户端可见——他们不会看到过期数据。
//!
//! * 持久性:只要多数节点仍在,写就不会丢失。
//!
//! 做法是选出单一领导者节点,由它服务客户端请求并向其它节点复制写操作。
//! 请求在被严格多数(quorum)确认后执行。若领导者失败,则选出新领导者。
//! 集群通常有 3 个及以上节点,因为两节点集群无法容忍故障(1/2 不是多数,会导致脑裂)。
//!
//! 值得注意的是,Raft 不提供水平扩展。客户端请求由单一领导者处理,容易成为瓶颈,
//! 且每个节点保存完整数据副本。系统通常通过把数据分片到多个 Raft 集群,并在其间
//! 使用分布式事务协议来解决,但这超出了本文范围。
//!
//! 本实现基本遵循 Raft 论文,但与多数实现一样做了一些小的取舍。
//!
//! [Paxos]: https://www.microsoft.com/en-us/research/uploads/prod/2016/12/paxos-simple-Copy.pdf
//! [Viewstamped Replication]: https://pmg.csail.mit.edu/papers/vr-revisited.pdf
//!
//! Raft 日志与状态机
//! =================
//!
//! Raft 维护一条有序命令日志,包含客户端提交的任意写命令。它通过把日志复制到
//! 多数节点来尝试达成共识。成功后,日志到该点为止视为已提交且不可变。
//!
//! 一旦提交,日志中的命令会在每个节点上顺序应用到本地状态机。Raft 本身不关心
//! 状态机与命令是什么——可以是 SQL 数据库,也可以是任何东西。Raft 只是把不透明
//! 命令交给不透明状态机。
//!
//! 每条日志含索引、领导者任期(见下节)与命令。例如:
//!
//! Index | Term | Command
//! ------|------|------------------------------------------------------
//! 1 | 1 | CREATE TABLE table (id INT PRIMARY KEY, value STRING)
//! 2 | 1 | INSERT INTO table VALUES (1, 'foo')
//! 3 | 2 | UPDATE table SET value = 'bar' WHERE id = 1
//! 4 | 2 | DELETE FROM table WHERE id = 1
//!
//! 状态机必须是确定性的,使所有节点达到相同状态。Raft 会在所有节点上以相同顺序
//! 应用相同命令;若命令有非确定行为(随机数、外部通信等),会导致状态分叉与结果不一致。
//!
//! 本库中,Raft 日志由 `Log` 管理,本地保存在 `storage::Engine` 中。
//! 状态机接口是 `State` trait。详见各自文档。
//!
//! 领导者选举
//! ==========
//!
//! Raft 节点可处于三种状态(角色):跟随者、候选人、领导者。
//! 本库建模为 `Node::Follower`、`Node::Candidate`、`Node::Leader`。
//!
//! * 跟随者:从领导者复制日志。可能尚不知道领导者。
//! * 候选人:在选举中竞选领导者。
//! * 领导者:处理客户端请求并向跟随者复制写操作。
//!
//! Raft 的根本保证是:任意时刻至多有一个**有效**领导者(旧的、已被替换的领导者
//! 可能仍以为自己是领导者,例如在网络分区时,但它们做不了什么)。
//! 该保证通过领导者选举协议强制执行。
//!
//! Raft 把时间划分为任期(term),任期是单调递增的数字。更高任期总是优先于更低任期。
//! 一个任期内至多一个领导者,且不能更换。节点跟踪其已知最后任期并落盘
//! (见 `Log.set_term_vote()`)。节点间消息带有当前任期(`Envelope.term`)——
//! 旧任期被忽略,未来任期会使节点成为该任期的跟随者。
//!
//! 节点以无领导者的跟随者起步。若收到领导者消息(当前或未来任期),则跟随它。
//! 否则等待选举超时(数秒),成为候选人并发起选举。
//!
//! 候选人把任期加 1,并向所有节点发送 `Message::Campaign` 请求投票。
//! 节点以 `Message::CampaignResponse` 回复是否授予选票。一个任期内节点只能投一票
//! (经 `Log.set_term_vote()` 落盘),先到先得;候选人隐式投自己。
//!
//! 候选人获得多数票(>50%)后成为领导者。它向所有节点发送 `Message::Heartbeat`
//! 声明领导权,收到的节点都成为跟随者(不论原先投给谁)。领导者约每秒发送一次心跳。
//! 新领导者还会向日志追加一条空条目,以便安全提交此前任期的条目(论文 5.4.2 节)。
//!
//! 新领导者日志中必须包含所有已提交条目(否则集群会丢数据)。为此,授予选票还有
//! 一个条件:候选人的日志至少与投票者一样新。因为条目必须复制到多数才提交,
//! 这保证只有日志已包含全部已提交条目的候选人才能赢得多数票(论文 5.4.1 节)。
//!
//! 也可能没有候选人获胜,例如平票或多数节点离线。选举超时后,候选人再次提升任期
//! 并发起新选举,直到选出领导者。为避免频繁平票,节点使用不同的随机选举超时
//! (论文 5.2 节)。
//!
//! 类似地,若跟随者在一个选举超时内未收到领导者消息,会成为候选人并发起选举。
//! 只要领导者在运行且连通,周期性心跳会阻止这种情况。与领导者断连的节点会不断
//! 独自发起选举,直到网络恢复,届时会在其任期举行新选举(打断当前领导者)。
//!
//! 复制与共识
//! ==========
//!
//! 领导者收到客户端写请求时,经 `Log.append()` 追加到本地日志,并以
//! `Message::Append` 发送给所有同伴。跟随者尝试持久追加到本地日志,并以
//! `Message::AppendResponse` 响应。
//!
//! 一旦多数确认追加,领导者经 `Log.commit()` 提交该条目,应用到本地状态机,
//! 并把结果返回客户端。它会在下一次心跳的 `Message::Heartbeat.commit_index` 中
//! 通知跟随者,以便它们也应用——但这不是正确性所必需的(它们成为领导者时会提交并应用;
//! 否则不必应用)。
//!
//! 跟随者可能无法追加:不可达、落后,或日志分叉(论文 5.3 节)。
//! `Append` 含被复制条目前一条的索引与任期,即 `base_index` 与 `base_term`。
//! 索引/任期对唯一标识命令;若两份日志有相同索引/任期对,则到该条目为止日志相同
//! (论文 5.3 节)。若 base 匹配跟随者日志,则追加(可能替换冲突条目),否则拒绝。
//!
//! 跟随者拒绝 append 时,领导者必须找到双方日志中的公共条目以恢复复制。
//! 做法是发送仅含 base 索引/任期、不含条目的 `Message::Append` 探测——
//! 逐个递减索引探测,直到跟随者响应匹配,再发送含缺失条目的 `Append`(论文 5.3 节)。
//! 用 `Progress` 结构跟踪每个跟随者的 `match_index` 与 `next_index`。
//!
//! 若 `Append` 消息或响应丢失,领导者还会在每次 `Heartbeat` 中发送 `last_index` 与任期。
//! 若跟随者日志中没有该索引/任期对,会在 `HeartbeatResponse` 中说明,
//! 领导者可像 append 被拒时一样开始探测其日志。
//!
//! 客户端请求
//! ==========
//!
//! 客户端请求以 `Message::ClientRequest` 提交给本地 Raft 节点。只在领导者上处理,
//! 但跟随者会代理到领导者(学位论文 6.2 节)。为避免消息重放的复杂问题
//! (学位论文 6.3 节),内部不重试请求,并在领导者/任期变更以及选举时以
//! `Error::Abort` 明确中止。
//!
//! 写请求 `Request::Write` 追加到 Raft 日志并复制。领导者用 `Write` 结构跟踪请求
//! 及其日志索引。命令提交并应用到本地状态机后,领导者按日志索引查找写请求并把结果
//! 发给客户端。确定性错误(如外键冲突)也返回客户端;非确定性错误(如 IO 错误)
//! 必须让节点 panic,以免状态分叉。
//!
//! 读请求 `Request::Read` 只在领导者执行,不必经 Raft 日志复制。但为确保线性一致性,
//! 领导者必须与多数确认自己仍是领导者。否则别处可能已选出新领导者并执行了写操作。
//! 做法是为每次读分配递增序列号,用 `Read` 结构跟踪请求,并立即发送带最新序列号的
//! `Read` 消息。跟随者用序列号响应,多数确认后执行读并把结果返回客户端。
//!
//! 实现取舍
//! ========
//!
//! 为简单起见,本实现只覆盖正确可用的最小 Raft,省略了生产系统需要的若干高级机制:
//!
//! * 无租约:为保证线性一致性,每次读都要求领导者与跟随者确认自己仍是领导者。
//! 可用预定义时间间隔的领导者租约避免(论文 8 节、学位论文 6.3 节)。
//!
//! * 已实现联合共识成员变更(论文 6 节):`Request::ChangeMembership`。
//!
//! * 已实现 Pre-vote 与 CheckQuorum(可通过 `Options` 关闭)。
//!
//! * 无快照:新节点或落后节点须通过复制并重放整份日志追赶,而不能发送状态机快照
//! (论文 7 节)。
//!
//! * 无日志截断:因不支持快照,整份 Raft 日志须永久保留以追赶新/落后节点,
//! 导致存储占用过大(论文 7 节)。
//!
//! * 无请求重试:领导者变更或消息丢失时不重试客户端请求,并积极中止,
//! 以规避消息重放问题(学位论文 6.3 节)。
//!
//! * 无拒绝提示:若跟随者日志分叉,领导者逐条探测直到找到匹配。
//! 复制协议可用拒绝提示扩展(论文 5.3 节)。
// 演示用 KV 状态机
// Raft 持久化日志(crate 内私有)
// 联合共识成员配置
// 节点间消息与客户端请求类型
// 角色状态机与 Node 驱动
// 客户端 session 写去重
// 应用状态机 trait
// 选举超时默认区间类型
use Range;
// tick 墙钟间隔
use Duration;
// 导出日志条目与索引类型
pub use ;
// 导出成员配置类型
pub use ;
// 导出协议消息与状态查询类型
pub use ;
// 导出节点与运行参数
pub use ;
// 导出会话包装状态机与编码辅助
pub use ;
// 导出状态机 trait
pub use State;
/// Raft tick 的时间间隔,即 Raft 的时间单位。
// 约 100ms 一拍,心跳/选举超时以 tick 计
pub const TICK_INTERVAL: Duration = from_millis;
/// 领导者心跳间隔(以 tick 计)。
// 模块内默认;可被 Options 覆盖
const HEARTBEAT_INTERVAL: Ticks = 4;
/// 默认选举超时范围(以 tick 计)。为避免选举平票,节点在此区间内随机取值。
// 半开区间 [10, 20)
const ELECTION_TIMEOUT_RANGE: = 10..20;
/// 单条 Append 消息中最多发送的日志条目数。
// 限制单次 RPC 体积,避免大包阻塞
const MAX_APPEND_ENTRIES: usize = 100;