Skip to main content

raft_rust/raft/
mod.rs

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