Raft 笔记
概览
Raft is a consensus algorithm for managing a replicated log:管理复制日志的一致性算法。
Consensus:[kənˈsensəs] 一致性,共识
In designing Raft we applied specific techniques(特别的技巧) to improve understandability, including decomposition (Raft separates leader election, log replication, and safety) and state space reduction (relative to Paxos, Raft reduces the degree of nondeterminism and the ways servers can be inconsistent with each other).
Decomposition:[ˌdiːˌkɒmpəˈzɪʃn] 分解,腐烂;变质(compose、composition)
Raft 使用一些特别的技巧来提升它的可理解性,包括以下两部分:
算法分解(主要分解为领导选举、日志复制、安全性和成员变更核心模块)
减少状态机的状态
下面的篇幅也会围绕这两部分进行讲解
Raft 和市面上其他的一致性算法很类似,但有一些独特的 features:
- Strong leader:和其他一致性算法相比,Raft 使用一种更强的领导能力形式。比如,日志条目只从领导人发送给其他的服务器。这种方式简化了对复制日志的管理并且使得 Raft 算法更加易于理解。
- Leader election:Raft 算法使用一个随机计时器来选举领导人。这种方式只是在任何一致性算法都必须实现的心跳机制上增加了一点机制。在解决冲突的时候会更加简单快捷。
- Membership changes:Raft 使用一种共同一致的方法来处理集群成员变换的问题,在这种方法下,处于调整过程中的两种不同的配置集群中大多数机器会有重叠,这就使得集群在成员变换的时候依然可以继续工作。
Raft 算法
Raft 通过选举一个杰出的领导人,然后给予他全部的管理复制日志的责任来实现一致性。
Raft implements consensus by first electing a distinguished leader, then giving the leader complete responsibility for managing the replicated log. The leader accepts log entries from clients[1], replicates them on other servers, and tells servers when it is safe to apply log entries to their state machines.
Having a leader simplifies the management of the replicated log. For example, the leader can decide where to place new entries in the log without consulting other servers, and data flows in a simple fashion from the leader to other servers. A leader can fail or become disconnected from the other servers, in which case a new leader is elected.
- 领导人处理所有的客户端请求(如果一个客户端和跟随者联系,那么跟随者会把请求重定向给领导人)
distinguished:[dɪˈstɪŋɡwɪʃt] adj. 卓越的,杰出的;高贵的,尊贵的
通过领导人的方式,Raft 将一致性问题分解成了三个相对独立的子问题,这些问题会在接下来的子章节中进行讨论:
- 领导选举:当现存的领导人发生故障的时候, 一个新的领导人需要被选举出来
- 日志复制:领导人必须从客户端接收日志条目(log entries)然后复制到集群中的其他节点,并强制要求其他节点的日志和自己保持一致。
- 安全性:在 Raft 中安全性的关键是状态机安全:如果有任何的服务器节点已经应用了一个确定的日志条目到它的状态机中,那么其他服务器节点不能在同一个日志索引位置应用一个不同的指令。章节 5.4 阐述了 Raft 算法是如何保证这个特性的;这个解决方案涉及到选举机制上的一个额外限制。
Raft 基础
一个 Raft 集群包含若干服务器节点,它们具有以下特性:
- 在任何时刻,每一个服务器节点都处于这三个状态之一:领导人、跟随者或者候选人。
- 在通常情况下,系统中只有一个领导人并且其他的节点全部都是跟随者。
- 跟随者都是被动的:他们不会发送任何请求,只是简单的响应来自领导人或者候选人的请求。
- 领导人处理所有的客户端请求(如果一个客户端和跟随者联系,那么跟随者会把请求重定向给领导人)。
转换关系如下:
服务器状态。跟随者只响应来自其他服务器的请求。如果跟随者接收不到消息,那么他就会变成候选人并发起一次选举。获得集群中大多数选票的候选人将成为领导人。在一个任期内,领导人一直都会是领导人,直到自己宕机了。
Raft 算法中服务器节点之间通信使用远程过程调用(RPCs),并且基本的一致性算法只需要两种类型的 RPCs。
- 请求投票(RequestVote) RPCs 由候选人在选举期间发起
- 附加条目(AppendEntries)RPCs 由领导人发起,用来复制日志和提供一种心跳机制
后面还有为了在服务器之间传输快照增加了第三种 RPC。当服务器没有及时的收到 RPC 的响应时,会进行重试, 并且他们能够并行的发起 RPCs 来获得最佳的性能。
领导人选举
触发机制
Raft 使用一种心跳机制来触发领导人选举。
- 当服务器程序启动时,他们都是跟随者身份。
- 领导人周期性的向所有跟随者发送心跳包(即不包含日志项内容的附加条目(AppendEntries) RPCs)来维持自己的权威。
- 如果一个跟随者在一段时间里没有接收到任何消息,也就是选举超时,那么他就会认为系统中没有可用的领导人,并且发起选举以选出新的领导人。
总结就是任何一个跟随者只要发现在规定时间(也就是选举超时)内没有收到领导人的心跳包,那么它就发起一次选举。
选举过程
开始一次选举,有以下步骤:
- 增加自己的当前任期号
- 发起选举的跟随者增加将自己切换到候选人状态
- 并行地向集群中其他服务器节点发送请求投票的 RPCs 来给自己投票。
- 当这个候选人获得了整个集群中大多数服务器节点针对同一个任期号的选票,那么它就赢得了这次选举成为领导人。
- 成为领导人后,它就会向其他服务器发送心跳消息来建立自己的权威并且阻止发起新的选举(如果不发送心跳,别的跟随者就要选举超时了,又要开始选举了)。
在投票的过程中,有这个一个约定:
同一个任期内,一个服务器节点最多只能投出一张选票,按照先来先服务的原则。
捷足先登
选举的过程中,可能会出现这样的场景:
候选者在等待别的节点给它投票的时候,它收到了其他服务器声称是领导人的附加条目 RPCs(通俗点讲就是:我正在请求当选老大呢,有个家伙发信息过来说他是老大),这个时候分两种情况:
- 如果这个领导人的任期号(通过它发过来的附加条目 RPCs 中可以得知)>= 候选人的任期号,那么候选人就承认领导人的合法并切换回跟随者状态。(这种情况的场景就是有别的跟随者更早的发起了选举,并成功当选)
- 如果这个领导人的任期号 < 候选人的任期号,那么候选人直接拒绝这次的 RPC 并且保持候选人状态。
所以我给这种场景命名为:捷足先登(如有不当,还请见谅~~)
选票瓜分
试想这种场景:领导人节点宕机,这时候必然会有多个跟随者发起了选举,并且任期号都是一样,然后都向其他服务器节点发起投票。假如总共有 6 台服务器,3 台发起选举,但前面说过,一个任期内,一个服务器节点最多只能投一票,所以每个候选人极有可能均分了投票,每人 2 票,不符合大多数投票规则,那这个任期内没有人赢了选举,于是又会发起新一轮的选举。如果没有机制来处理了,很有可能会无限地选举下去。
Raft 算法使用随机的选举超时时间(比如 150 - 300 毫秒)来确保很少会发生选票瓜分的情况。如果加了随机的选举超时时间,再回头看上面说的场景:领导人宕机,很少可能会有多个跟随者同时发起选举,因为每个跟随者的选举超时时间(多久没收到领导人的心跳包)不一样了,必然会有最先发起选举的跟随者,即使后续再有其他跟随者发起选举,第一个发起选举的已经完成了选举,成为了领导人,后面的候选人会收到新领导人的附加条目 RPC,也就是上面捷足先登的场景了。
日志复制
一旦一个领导人被选举出来后,它就开始为客户端提供服务。客户端的每一个请求都包含一条被复制状态机执行的指令。领导人把这条指令作为一条新的日志条目附加到日志中去,然后并行地发起附加条目 RPCs 给其他的服务器,让他们复制这条日志条目。
一个日志条目包含以下三个元素:
- 任期号
- 状态机需要执行的指令
- 整数索引值来表明它在日志中的位置
日志复制的过程:
- client 向系统发送(会导向leader节点)更改消息
- leader 预修改数据,但不commit(未确认修改)
- leader 向follower,发送修改消息
- follower 回复 ack,并且预修改这个消息,但不commit
- leader收到这个ack之后,首先进行commit(确认修改),并且发给client
- leader 向 follower发送心跳包,携带leader已commit这个消息
- follower收到leader的已commit消息,知道leader已经commit了,那么自己也进行commit
一致性保证
Raft 的日志机制来维护不同服务器日志之间的高层次的一致性。这么做不仅简化了系统的行为也使其更具有可预测性,同时它也是安全性保证的一个重要组件。Raft 维护着以下的特性:
如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们存储了相同的指令。
原因如下:领导人最多在一个任期里在指定的一个日志索引位置创建一条日志条目,同时日志条目在日志中的位置也从来不会改变
如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们之前的所有日志条目也全部相同。
这个特性是由附加日志 RPC 的一个简单的一致性检查所保证。在发送附加日志 RPC 的时候,领导人会把新的日志条目前紧挨着的条目的索引位置和任期号包含在日志内。如果跟随者在它的日志中找不到包含相同索引位置和任期号的条目,那么他就会拒绝接收新的日志条目。一致性检查就像一个归纳步骤:一开始空的日志状态肯定是满足日志匹配特性的,然后一致性检查在日志扩展的时候保护了日志匹配特性。因此,每当附加日志 RPC 返回成功时,领导人就知道跟随者的日志一定是和自己相同的了。
Strong Leader
在 Raft 算法中,领导人是通过强制跟随者直接复制自己的日志来处理不一致问题的。这意味着在跟随者中的冲突的日志条目会被领导人的日志覆盖。
要使得跟随者的日志进入和自己一致的状态,领导人必须找到最后两者达成一致的地方,然后删除跟随者从那个点之后的所有日志条目,并发送自己在那个点之后的日志给跟随者。所有的这些操作都在进行附加日志 RPCs 的一致性检查时完成。领导人针对每一个跟随者维护了一个 nextIndex,这表示下一个需要发送给跟随者的日志条目的索引地址。当一个领导人刚获得权力的时候,他初始化所有的 nextIndex 值为自己的最后一条日志的 index 加 1(图 7 中的 11)。如果一个跟随者的日志和领导人不一致,那么在下一次的附加日志 RPC 时的一致性检查就会失败。在被跟随者拒绝之后,领导人就会减小 nextIndex 值并进行重试。最终 nextIndex 会在某个位置使得领导人和跟随者的日志达成一致。当这种情况发生,附加日志 RPC 就会成功,这时就会把跟随者冲突的日志条目全部删除并且加上领导人的日志。一旦附加日志 RPC 成功,那么跟随者的日志就会和领导人保持一致,并且在接下来的任期里一直继续保持。
安全性
选举限制
参考上文中捷足先登部分说明。
提交之前任期内的日志条目
Raft 永远不会通过计算副本数目的方式去提交一个之前任期内的日志条目。只有领导人当前任期里的日志条目通过计算副本数目可以被提交;一旦当前任期的日志条目以这种方式被提交,那么由于日志匹配特性,之前的日志条目也都会被间接的提交。在某些情况下,领导人可以安全的知道一个老的日志条目是否已经被提交(例如,该条目是否存储到所有服务器上),但是 Raft 为了简化问题使用一种更加保守的方法。
当领导人复制之前任期里的日志时,Raft 会为所有日志保留原始的任期号, 这在提交规则上产生了额外的复杂性。在其他的一致性算法中,如果一个新的领导人要重新复制之前的任期里的日志时,它必须使用当前新的任期号。Raft 使用的方法更加容易辨别出日志,因为它可以随着时间和日志的变化对日志维护着同一个任期编号。另外,和其他的算法相比,Raft 中的新领导人只需要发送更少日志条目(其他算法中必须在他们被提交之前发送更多的冗余日志条目来为他们重新编号)。