论文
Github 上有大佬们的翻译,对习惯中文的朋友会友好一些:
详细的内容请参考上面仓库,下面的内容仅涉及大体流程和部分细节。
流程讲解可以参考(上面的论文可以最后看,但是在看后面的内后之前至少看看这两篇文章):
可能有疑问的地方(Figure 8)
了解 raft 的局限(也是 Paxos 等的问题)(无法完全解决拜占庭问题,无法解决出现“叛徒”的问题):
https://zh.m.wikipedia.org/wiki/%E6%8B%9C%E5%8D%A0%E5%BA%AD%E5%B0%86%E5%86%9B%E9%97%AE%E9%A2%98
如果你有兴趣可以看看一些分布式理论:
数据格式
节点状态
所有节点的持久性状态
(在响应 RPC 请求之前,已经更新到了稳定的存储设备)
参数 | 解释 |
---|---|
currentTerm | 服务器已知最新的任期(在服务器首次启动时初始化为0,单调递增) |
votedFor | 当前任期内收到选票的 candidateId,如果没有投给任何候选人 则为空 |
log[] | 日志条目;每个条目包含了用于状态机的命令,以及领导人接收到该条目时的任期(初始索引为1) |
raft 的最终目标即是使集群中超过半数节点的的 log[] 相同
所有节点的易失性状态
所有服务器上的易失性状态
参数 | 解释 |
---|---|
commitIndex | 已知已提交的最高的日志条目的索引(初始值为0,单调递增) |
lastApplied | 已经被应用到状态机的最高的日志条目的索引(初始值为0,单调递增) |
和状态机的关系请移步:分布式共识算法 (kicey.site)
主节点的易失性状态
(选举后已经重新初始化)
参数 | 解释 |
---|---|
nextIndex[] | 对于每一台服务器,发送到该服务器的下一个日志条目的索引(初始值为领导人最后的日志条目的索引+1) |
matchIndex[] | 对于每一台服务器,已知的已经复制到该服务器的最高日志条目的索引(初始值为0,单调递增) |
主节点需要了解其他节点的日志情况。
追加条目的 rpc 调用
由领导人调用,用于日志条目的复制,同时也被当做心跳使用
参数 | 解释 |
---|---|
term | 领导人的任期 |
leaderId | 领导人 ID 因此跟随者可以对客户端进行重定向(译者注:跟随者根据领导人 ID 把客户端的请求重定向到领导人,比如有时客户端把请求发给了跟随者而不是领导人) |
prevLogIndex | 紧邻新日志条目之前的那个日志条目的索引 |
prevLogTerm | 紧邻新日志条目之前的那个日志条目的任期 |
entries[] | 需要被保存的日志条目(被当做心跳使用时,则日志条目内容为空;为了提高效率可能一次性发送多个) |
leaderCommit | 领导人的已知已提交的最高的日志条目的索引 |
(接收者)其他节点响应内容
返回值 | 解释 |
---|---|
term | 当前任期,对于领导人而言 它会更新自己的任期 |
success | 如果跟随者所含有的条目和 prevLogIndex 以及 prevLogTerm 匹配上了,则为 true |
entrids[] 是多个日志记录,每次可以追加不知一条记录
leaderCommit 用于通知跟随节点主节点(代表集群状态)当前已经提交的状态(主节点没有一个请求单独通知跟随节点)
对于追加条目,接收者的行为遵守
接收者的实现:
- 返回假 如果领导人的任期小于接收者的当前任期(译者注:这里的接收者是指跟随者或者候选人)
- 返回假 如果接收者日志中没有包含这样一个条目:该条目的任期在 prevLogIndex 上能和 prevLogTerm 匹配上
(译者注:在接收者日志中 如果能找到一个和 prevLogIndex 以及 prevLogTerm 一样的索引和任期的日志条目,则继续执行下面的步骤 否则返回假) - 如果一个已经存在的条目和新条目(译者注:即刚刚接收到的日志条目)发生了冲突(因为索引相同,任期不同),那么就删除这个已经存在的条目以及它之后的所有条目(发生的原因是当前节点追加了某个已经下线的 learder 的某些没有追加到超过半数的节点的日志记录,这些未提交的日志记录是无效的,需要被覆盖)
- 追加日志中尚未存在的任何新条目(执行日志追加的请求)
- 如果领导人的已知已提交的最高日志条目的索引大于接收者的已知已提交最高日志条目的索引(
leaderCommit > commitIndex
),则把接收者的已知已经提交的最高的日志条目的索引 commitIndex 重置为领导人的已知已经提交的最高的日志条目的索引 leaderCommit 或者是“上一个新条目的索引,即请求中 entries 中最后一条的索引”(取两者的最小值)(原文:
If leaderCommit > commitIndex, set commitIndex =
min(leaderCommit, index of last new entry)
)
可能会对第 5 条产生疑问:
首先要搞清楚
第 5 条是在干什么:决定哪些日志可以被应用到当前节点的状态机中。
可能存在的问题:当前节点接收了其他 leader 的未提交日志,并且这些日志的超过了本次 entries 覆盖的范围,那么 entries 之后的还是之前的错误日志,如果当前 commitIndex 超过了 entries 的索引,那么直接应用就会将错误的状态应用到状态机中,所有需要取二者中的最小值。
请求投票的 rpc 调用
由候选人负责调用用来征集选票
参数 | 解释 |
---|---|
term | 候选人的任期号 |
candidateId | 请求选票的候选人的 ID |
lastLogIndex | 候选人的最后日志条目的索引值 |
lastLogTerm | 候选人最后日志条目的任期号 |
其他节点响应
返回值 | 解释 |
---|---|
term | 当前任期号,以便于候选人去更新自己的任期号 |
voteGranted | 候选人赢得了此张选票时为真 |
接收者实现:
- 如果
term < currentTerm
返回 false - 如果 votedFor 为空或者为 candidateId,并且候选人的日志至少和自己一样新(候选人的 lastLogIndex >= 当前节点的 lastLogIndex),那么就投票给他
节点遵守的规则
所有节点
- 在接收到合法追加日志,更新当前节点的状态机:
如果commitIndex > lastApplied
,则 lastApplied 递增,并将log[lastApplied]
应用到状态机中 - 跟随最新的 leader:
如果接收到的 RPC 请求或响应中,任期号T > currentTerm
,则令currentTerm = T
,并切换为跟随者状态(跟随最新的主节点)
跟随者
- 响应来自候选人和领导人的请求
- 如果在超过选举超时时间的情况之前没有收到当前领导人(即该领导人的任期需与这个跟随者的当前任期相同)的心跳/附加日志(主节点下线),或者是给某个候选人投了票,就自己变成候选人
候选人
在转变成候选人后就立即开始选举过程
- 自增当前的任期号(currentTerm)
- 给自己投票
- 重置选举超时计时器
- 发送请求投票的 RPC 给其他所有服务器
- 如果接收到大多数服务器的选票,那么就变成领导人
- 如果接收到来自新的领导人的附加日志(AppendEntries)RPC(新的主节点已经选举得到),则转变成跟随者
- 如果选举过程超时,则再次发起一轮选举
主节点
- 一旦成为领导人:发送空的附加日志(AppendEntries)RPC(心跳)给其他所有的服务器;在一定的空余时间之后不停的重复发送,以防止跟随者超时而变为候选人发起选举
- 如果接收到来自客户端的请求:附加条目到本地日志中,在条目被应用到状态机后(此前需要复制到超过半数的跟随者上)响应客户端
- 如果对于一个跟随者,最后日志条目的索引值大于等于 nextIndex[i](
lastLogIndex ≥ nextIndex[i]
)(会出现这样的情况是因为之前对这个跟随者的追加日志请求失败(包括丢失等情况)),则发送从 nextIndex 开始的所有日志条目:
* 如果成功:更新相应跟随者的 nextIndex 和 matchIndex
* 如果因为日志不一致而失败:则 nextIndex 递减并重试 - 假设存在 N 满足
N > commitIndex
,使得大多数的matchIndex[i] ≥ N
以及log[N].term == currentTerm
成立,则令commitIndex = N
(即在任期范围内,一个追加条目被大多数节点接受,那么认为该条目提交成功,便于在合法的情况下快速的更新主节点的状态)
raft 集群特性
特性 | 解释 |
---|---|
选举安全特性 | 对于一个给定的任期号,最多只会有一个领导人被选举出来 |
领导人只附加原则 | 领导人绝对不会删除或者覆盖自己的日志,只会增加 |
日志匹配原则 | 如果两个日志在某一相同索引位置日志条目的任期号相同,那么我们就认为这两个日志从头到该索引位置之间的内容完全一致 |
领导人完全特性 | 如果某个日志条目在某个任期号中已经被提交,那么这个条目必然出现在更大任期号的所有领导人中 |
状态机安全特性 | 如果某一服务器已将给定索引位置的日志条目应用至其状态机中,则其他任何服务器在该索引位置不会应用不同的日志条目 |