Raft 是一种可靠,但相对简单的分布式一致性算法,被应用于 Etcd、RabbitMQ 等。其设计的主要动机之一是在不影响性能和正确性的前提下增强算法的可理解性。
Raft 分离了一致性算法的关键元素:leader 节点选举、日志复制、安全性,这使得 Raft 算法更加模块化以及更易于理解;同时也方便了对算法中特定的元素进行优化以及定制化。
Raft 首先会进行 leader 节点选举,之后集群中节点之间的日志复制则完全由 leader 节点负责。所有来自客户端的请求最终都由 leader 节点进行响应(其他节点的客户端请求也会被重定向到 leader 节点)。
leader
节点负责响应所有来自客户端的请求,这可能会限制 Raft 算法的性能。因为集群中leader
节点只能有一个,当客户端请求量尤其是写操作的请求量很大时,可能会对leader
节点的负载造成较大的压力,影响吞吐。一些应用场景中可以对读操作进行负载均衡,但写操作只能请求
leader
节点,大量写操作还是会对leader
节点的负载造成压力;另外,水平扩容配合负载均衡也可以降低读操作的压力,但同时会增加节点之间通信的开销以及节点之间日志复制的延时,这也会影响写操作的性能。
⒈ 节点状态
Raft 中节点只有三种状态:leader
(领导者)、follower
(追随者)、candidate
(候选者)。任何时间,集群中节点的状态只能是上述三种状态之一。通常情况下,集群中只有一个 leader
节点,其他全部是 folower
节点。
leader
节点负责响应客户端的请求以及将日志复制到follower
节点。follower
节点只会被动响应来自leader
节点以及candidate
节点的请求- 当集群中当前
leader
节点失效时,检测到leader
节点失效的follower
节点会成为candidate
节点,准备发起投票竞选新的leader
节点。
candidate
只是节点的一个临时状态。如果一个candidate
节点获得多数节点投票,则它会成为新的leader
节点;反之,则它又会变回follower
节点。
⒉ 时间
Raft 将时间分割成了任意长度的时间间隔 term
。这些 term
以连续的单调递增的整数进行编号,每次新的 leader
节点选举标志着一个新的 term
的开始,这个新的 term
一直会持续到这个 leader
节点失效为止。
在有些情况下可能会出现多个 candidate
节点都没有得到多数节点的投票,导致本次 leader
节点选举失败。此时,系统会开始一个新的 term
重新进行 leader
节点选举。
在 Raft 算法中,term
的作用相当于一个逻辑时钟。系统在选出新的 leader
节点之后,新的 term
值也会随着节点之间的消息交互传递给其他节点。系统中的所有节点都会在本地维护一个 term
值,当一个节点发现本地维护的 term
值小于消息中的 term
值时会更新本地的 term
值。如果 leader
节点或 candidate
节点发现本地维护的 term
值已经过期,则会将节点状态变为 follower
。另外,任何 term
值过期的请求都会被节点拒绝。
⒊ 消息交互
在 Raft 算法中,系统中的节点之间通过 RPC
进行消息交互。Raft 使用两种类型的 RPC
消息进行交互:candidate
节点在发起 leader
节点选举投票时使用 RequestVote
类型的 RPC
消息;AppendEntries
类型的 RPC
消息则是在 leader
节点进行日志复制以及心跳检测时使用。
RequestVote RPC
参数:
term candidate 节点当前的 term 值
candidateId candidate 节点的 ID
lastLogIndex candidate 节点最后一条日志条目的索引
lastLogTerm candidate 节点最后一条日志条目的 term 值
返回:
term follower 节点当前的 term 值
voteGranted 如果为 TRUE 则表示 follower 节点投票给 candidate
AppendEntries RPC
参数:
term leader 节点当前的 term 值
leaderId leader 节点的 ID
prevLogIndex leader 节点日志中最后一条日志条目的索引
prevLogTerm leader 节点日志中最后一条日志条目的 term 值
entries[] 要复制的日志条目,日志条目为空表示发送的心跳检测;为了提高传输效率,可以一次传多条日志条目
leaderCommit leader 节点最新一条提交执行的日志条目的索引
返回:
term follower 节点当前的 term 值
success 如果 follower 节点中最新的日志条目与 prevLogIndex 和 prevLogTerm 所对应的 leader 节点中的日志条目匹配,则返回 TRUE
如果 follower 节点中最新的日志条目的索引小于 prevLogIndex,那么 follower 的返回中除了将 success 设为 FALSE,还可以额外将 follower 节点当前日志中最新的日志条目的索引一并返回,方便后续补齐缺少的日志条目
⒋ leader 节点选举
在 Raft 算法中,leader
节点会周期性的向所有 follower
节点发送心跳检测消息,如果有 follower
节点在一定时间(election timeout
)内没有收到心跳检测消息,则会触发新的 leader
节点选举操作。
在 follower
节点触发新的选举之前,该节点首先会将自身的状态变成 candidate
并增加节点本地维护的 term
值,另外,节点还会为自己投票并向系统中的其他节点发送 RequestVote
消息。
如果一个 candidate
节点收到了多数节点的投票,则该节点会成为新的 leader
节点,之后这个新的 leader
节点继续向系统中的其他节点周期性的发送心跳检测消息。在 candidate
节点等待其他节点投票的过程中可能会收到一个自称 leader
的节点发送的 AppendEntries
消息,如果消息中的 term
值不小于 当前 candidate
节点本地维护的 term
值,则 candidate
节点放弃本次选举并重新变成 follower
;反之,则 candidate
节点拒绝响应该消息,继续等待其他节点投票。
系统中可能会存在多个节点同时发起 leader
选举,如果这些 candidate
节点本地维护的 term
值都相同,则 follower
节点只会响应最先到达的那个 RequestVote
请求。如果这些 candidate
节点都没有得到多数节点的投票,则在请求超时之后会重新进行下一轮的 leader
选举投票(同时增加各自本地维护的 term
值)。为了避免这种情况的出现,Raft 会为每次 leader
选举随机指定一个超时时间(150 ms ~ 300 ms 之间),这样,最先超时的那个 candidate
会最先发起新的一轮的 leader
选举,最终成为新的 leader
节点。
⒌ 日志复制
日志是分布式系统中维护状态一致性的关键数据。leader
节点在接收到客户端请求后会将请求转化为日志条目(Log Entry),存储到本地日志中。日志条目中除了存储本次请求要执行的命令之外,还会存储 leader
节点当前的 term
值以及日志条目在日志中的索引位置。之后 leader
节点会通过 AppendEntries
消息将这些日志条目发送到所有的 follower
节点。
follower
节点在收到日志复制的消息后,首先检查消息中的 term
值,其值不能小于 follower
节点本地维护的 term
值。其次通过消息中的 prevLogIndex
和 prevLogTerm
确定本地存储的消息是否为最新。如果确定本地存储的消息为最新,则 follower
节点会将消息中的日志条目复制到本地;反之,则不进行日志复制,而且 follower
节点还可以将本地最新的日志条目的索引返回给 leader
节点,以便后续补齐缺少的日志信息。
如果不同节点之间的两条日志条目包含相同的索引以及
term
值,则认为这两条日志条目相同,并且这两条日志条目之前的所有日志条目都相同。
待系统中的多数节点都完成日志复制之后,leader
节点会将日志条目中的命令提交执行。之后,leader
节点会将提交执行的消息通知到所有的 follower
节点,follower
节点也会在本地将相应的命令提交执行。
⒍ 安全性
在进行 leader
节点选举时,candidate
节点发送的 RequestVote
消息中会携带节点本地维护的日志中最新的日志条目的索引以及 term
值。其他节点在收到 RequestVote
消息时首先会检查消息中携带的索引以及 term
信息,只有消息中的索引以及 term
值不小于节点本地最新的日志条目的索引以及 term
值,节点才会进行投票,否则节点拒绝投票。
leader
节点的选举机制保证了在任意时刻,系统中最多只会有一个 leader
节点。另外,leader
节点中的日志条目只能追加,既不能删除也不能覆盖。
Raft 保证了客户端请求中的命令在所有节点中提交的顺序是一致的,这保证了所有节点中数据以及状态的最终一致性。