零、基本认识
1. Raft是什么?
是一种算法,可以解决一定互信环境下的分布式的共识问题。
2. 如何解决的?
让一个班级知道作业,要同学们先选出一个班长,老师告诉班长作业,班长再告诉同学。对一个集群而言也是这样,让客户端只与集群自发选出的leader通信,leader再把消息复制给follower。
3. 需要明确的是:
-
集群一般包含多个服务器节点,2N+1。
-
每个结点同一时刻只能处于三种状态中的一种:leader、follower、candidate;
-
leader处理所有的客户端请求(如果一个客户端和follower通信,follower会返回leader的IP,和candidate通信则会请求超时);
-
每任leader的任期都是一个term,term单调递增。选举失败,term也会+1;
-
服务器之间通过RPC通信,基本的RPC有两种,一种是用来发送复制日志内容(Log entry)或心跳信号的AppendEntries RPC,一种是candidate发起投票用的RequestVote RPC。
那么在这样的场景下,就会被分成了三个小问题:选举leader,日志复制,保证安全
一、选举
Raft通过心跳机制来选举。服务器启动时都是follower。leader会周期性的发送心跳信号(不包含Log entry的AppendEntries RPC)。如果一个服务器如果能收到有效RPC,就维持follower状态。如果follower在选举超时时间内没接收到消息,那么就开始发起新一轮的选举。
1. 一轮选举
follower变成candidate,并且term+1,给自己投票并且发送RequestVote RPC,直到:
a. 收到半数以上投票
b. 其他服务器成为leader
c. 一段时间后没有获胜者
2. 情况a
每个任期一个服务器只会投给一个candidate,先来先得。半数以上票选成功保证了安全性(即leader唯一性)。成为leader后,会发送心跳信号,确定地位,阻止新的选举。
3. 情况b
在等待投票期间,因为网络延迟等因素,使得大多数服务器已经接收到了别的candidate发起的RequestVote RPC,且选举成功,随后新的leader向candidate发起AppendEntries RPC,如果这个RPC的term不小于当前candidate 的term,则candidate回到follower。不然则会保持candidate状态。如果网络出现了问题,那么RPC超时抵达,那么是否会导致一个网络环境差的candidate成为leader?
4. 情况c
在投票时出现了平票,则会candidate选举超时,重新选举。在超时时间都一样的情况下,可能所有candidate同时超时,又同时发起投票(极端情况下)。为了解决这个问题,raft引入了随机选举超时,将超时时间限制在150ms-300ms。每次发起选举之前,candidate会随机生成一个超时时间。
二、日志复制
1. 如何复制
Log entry:存储了状态机要执行的指令以及leader收到消息时候的任期号等信息。
客户端的每一个请求都包含了一个指令,要被复制状态机执行(即每台服务器上执行命令的机器)。leader把该指令放到一个log entry中,并行的发起AppendEntries RPC,包含了Entry数组。安全的复制后,leader会把该指令应用到自己的状态机里,并且回复客户端。
如果还有follower没有存储所有的日志,leader会不断的重复AppendEntries RPC。
当一个日志已提交(即从Leader复制到超过半数的服务器上),把日志应用到状态机中就是安全的。Raft 算法保证所有已提交的日志条目都是持久化的并且最终会被所有可用的状态机执行。如果一个日志被提交了,说明在它前面的一个日志已经在entry数组内了,所以也会被提交。Leader保存将会被提交的日志条目的最大索引,AppendEntries RPC也会传递这个值,这样其他的服务器才能最终知道哪些日志条目需要被提交。Follower 一旦知道某个日志条目已经被提交就会将该日志条目应用到自己的本地状态机中(按照日志的顺序)。
2. Raft的保证
raft保证:
- 如果不同日志中的两个条目拥有相同的索引和任期号,那么他们存储了相同的指令。
- 如果不同日志中的两个条目拥有相同的索引和任期号,那么他们之前的所有日志条目也都相同
每个服务器上的log entries都是有序的,日志索引与日志一一对应。在日志索引的位置,所有服务器上该索引对应的entry一致or不存在(还没传递过来)。AppendEntries RPC的名字也暗示了,日志只会不断增加,不会删除和修改。
第二个特性是由 AppendEntries RPC 执行一个简单的一致性检查所保证的。AppendEntries RPC 包含了前一个日志条目的索引位置和term。如果 follower 在它的日志中找不到包含相同索引位置和任期号的条目,那么他就会拒绝该新的日志条目。那么如何知道这个follower需要什么条目?Leader 针对每一个 follower 都维护了一个 nextIndex,表示要发送的下一个entry索引。
为什么要上面这两种废话保证?
因为leader会崩溃,选出来一个没复制完uncommitted的log entry的新leader。这样的话,leader的选举还要保证是最新的committed。raft会让leader强制覆盖follower的log entries中,与leader不一致的部分。为此,需要找到从零开始的最长一致部分。这些操作都发生在AppendEntries RPC中。新leader选举后,该 leader 将所有 nextIndex 的值都初始化为自己最后一个日志条目的 index 加1。
如果出现follower和leader日志不一致,如nextIndex 被初始化成lastLogIndex + 1而实际的nextIndex是另一个值,AppendEntries RPC就会失败。在不断失败的过程中,nextIndex不断-1,最终nextIndex 会在某个位置使得 leader 和 follower 的日志达成一致。此时,AppendEntries RPC 就会成功,将 follower 中跟 leader 冲突的日志条目全部删除然后追加 leader 中的日志条目(如果有需要追加的日志条目的话)。一旦 AppendEntries RPC 成功,follower 的日志就和 leader 一致,并且在该任期接下来的时间里保持一致。
3. 减少AppendEntries RPC失败次数的优化方案
当拒绝一个 AppendEntries RPC 的请求的时候,follower 可以包含冲突条目的任期号和自己存储的那个任期的第一个 index 。借助这些信息,leader 可以跳过那个任期内所有冲突的日志条目来减小 nextIndex;这样就变成每个有冲突日志条目的任期需要一个 AppendEntries RPC 而不是每个条目。在实践中,这种优化是没有必要的,因为失败不经常发生并且也不可能有很多不一致的日志条目。
三、安全性
如何保证选出来的leader有最大的committed Index?即保证客户端提交成功的信息不丢失。
1. 选举限制
candidate包含所有的已提交日志。通过投票的规则来限制,当一个服务器收到来自candidate的RequestVote RPC时:
- 如果任期内投过票,则反对,不然则下一步
- 如果本服务器的term更大,则反对,大于则同意,等于则下一步
- 本服务器的的日志更长则反对,不然则同意。
2. leader复制了之前term的log entry
raft无法判断之前任期的日志是否被提交,哪怕已经在大多数机器上复制了。超过半数复制了一个log entry但那么这个log entry可能被一个拥有更远term且没有该entry的leader覆盖。
leader只通过超过半数来判断当前term内的log entry是否被提交。一旦当前term的某个log entry以这种方式被提交,那么由于日志匹配特性,之前的所有日志条目也都会被提交。
3. follower和candidate崩溃
leader无限发起AppendEntries RPC
4. 持久化与服务器重启
每个服务器保存当前term、和投票信息。每个服务器还会持久化已提交的log entry,这样可以防止已提交的log entry丢失或者在服务器重启时导致其变成未提交。
然而,重启后commitIndex被设置为0,虽然暂时落后于真实value,但是在leader被选举出来并且commit 新的log entry后,很快会通过AppendEntries RPC恢复到正常值。
状态机是易失的也是可以持久化的,可以通过重放 log entries来恢复。如果在重启前,apply了部分log entries,为了防止重放,就要维持一个apply的log entry index。
如果一个服务器失去了其持久化的状态,则需要以新的身份加入集群(成员变更)
(大部分服务器丢失了log entry,也能继续运行,只不过需要接受丢失信息的现实)
5. 定时与可用
定时与可用性的权衡在于:定时会让某些事件慢一点可能导致错误的结果,可用性又要依赖定时
所以在这两者中间,要做一个权衡,在下面这种情况下raft可以选举并且维持一个稳定的leader
广播时间(broadcastTime) << 选举超时时间(electionTimeout) << 平均故障间隔时间(MTBF)
广播时间:一个服务器并行地发送 RPCs 给集群中所有的其他服务器并接收到响应的平均时间;
选举超时时间:150ms-300ms
平均故障间隔时间:单个服务器两次故障间隔时间的平均值。广播时间必须比选举超时时间小一个量级,这样 leader 才能够可靠地发送心跳消息来阻止 follower 开始进入选举状态;再加上随机化选举超时时间的方法,这个不等式也使得选票瓜分的情况变得不可能。选举超时时间需要比平均故障间隔时间小上几个数量级,这样整个系统才能稳定地运行。当 leader 崩溃后,整个系统会有大约选举超时时间不可用;我们希望该情况在整个时间里只占一小部分。
前后两个由系统决定,选举超时由我们决定。广播时间大约是 0.5 毫秒到 20 毫秒之间,取决于存储的技术。选举超时时间可能需要在 10 毫秒到 500 毫秒之间。大多数的服务器的平均故障间隔时间都在几个月甚至更长,很容易满足时间的要求。
6. leader transfer extension
如果leader在已知将要下线,则可以通过leader transfer来规避掉leader election选举时的不可用时间。
如果一个follower在定期检查中被发现更适合做leader,那么可以在保证有足够的新log entry,就能变成新的leader。
raft中,先由leader发送log entries到target server,target server不等待超时就发起选举。
- 当前leader停止接收客户端请求
- 当前leader保证target server完全复制了自己的log entries
- 当前Leader发送一个TimeoutNow请求给target server。让target server进入candidate状态,发起选举,选举成功则成为leader,transfer完成。
如果transfer不能在选举超时时间内完成,leader就会终止转换,并且重新接受客户端请求。
四、成员变更
如果扩容过程中,老leader获得了老follower的过半投票,新leader获得了新leader的过半投票,则会出现双leader。
在 Raft 中,集群先切换到一个过渡的配置,即联合一致(joint consensus);一旦联合一致已经被提交了,那么系统就切换到新的配置上。联合一致结合了老配置和新配置:
- 日志条目被复制给集群中新、老配置的所有服务器。
- 新、旧配置的服务器都可以成为 leader 。
- 达成一致(针对选举和提交)需要分别在两种配置上获得过半的支持。
五、日志压缩
日志在正常操作中会不断增长,但在实际系统中,日志不能无限制增长,会占用越来越多的空间以及需要花更多时间回放。所以需要压缩技术。
快照技术,最简单方法。当前系统整个状态都以快照的形式持久化存储,并且将该时间节点前的日志全部丢弃。(zk就使用了这个技术)
增量压缩方法,日志清理 OR 日志结构合并树,每次只对小部分数据操作,以分散压缩的负载压力。How?选择一个有大量被覆盖或者被删除的对象的数据区域。重写活着的对象,释放该区域。
与快照技术相比,需要大量的extre 机制。树可以使用和LSM树相同的interface,但是日志清除就较为复杂。
快照技术:每个服务器用快照替代日志中已经提交了的条目。只存储当前状态,不存储过程。包含了last index和last term。而当leader需要向follower发送leader已经丢弃的log entry时,就要向follower发送快照。并且直接日志压缩。
六、客户端交互
1.随机挑选服务器通信
2.如果连接到follower,则follower拒绝客户端且返回leader的IP。如果连接到的leader挂了,客户端请求超时,重新随机挑选通讯服务器。
3.写时,指令对应一个唯一的序列号,如果在leader在响应客户端前崩溃,防止让客户端再次发送同一个指令并且被二次执行。
3.读时,如果直接返回,可能会返回一个stale data,比如新的leader产生了,旧的leader还没收到通知。——如何解决呢?leader维护commit index。leader虽然拥有所有已经提交了的日志,但是不知道哪些是已经被提交了的,所以raft通过让leader提交一个空操作log entry,来校对commit index。并且leader在处理只读请求之前必须通过半数的心跳信号检查自己是否被取代。