RAFT算法深入(上)

Posted 双水村的男人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RAFT算法深入(上)相关的知识,希望对你有一定的参考价值。

        前面曾大概介绍过raft共识算法,今天把etcd源码中raft部分拿来分析一下,不涉及代码。首先介绍下etcd,作为一个实现了高可用强一致性的库,被广泛应用于大型开源项目,如Kubernetes、openstack等。而etcd中最为核心的算法就是raft了,那么etcd中是怎么实现raft算法的呢?


通用实现词汇表(与etcd的实现对应)

  • 消息:传输层传输信息所使用的最小单位

  • MsgApp:leader向follower发送的AppendRequset消息

  • MsgSnap:leader向follower发送的snapshot消息

  • MsgAppResp: follower接收到MsgApp或MsgSnap之后回复给leader的消息

  • Reject: follower告诉leader自己接收这条消息后,是否与自己的raftlog冲突(落后太多)

  • MsgProp: 要求共识的消息

  • MsgReadIndex:一致性读请求消息

  • raftlog:即Log Replication中使用的一系列有序的记录的集合


一、原则

1. Election Safety(选举安全)

At most one leader can be elected in a given term

2. Leader Append-Only(leader只增)

A leader never overwrites or deletes entries in its log; it only appends new entries

3. Log Matching(log向前一致)

If two logs contain an entry with the same index and term, then the logs are identical in all entries up through the given index

4. Leader Completeness(leader完整性)

If a log entry is committed in a given term, then that entry will be present in the logs of the leaders for all higher-numbered terms

5. State Machine Safety(状态机安全)

If a server has applied a log entry at a given index to its state machine, no other server will ever apply a different log entry for the same index


必要条件

raft共识能够顺利进行的一些必要条件:

  • broadcastTime(网络传输速度) << heartbeatTimeout < electionTimeout << MTBF(节点掉线重连时间)

  • append请求发送到follower之后能够快速转发到leader上


二、选举

基本流程

        tickElection结束 -> 变为Candiate -> 发送MsgVote -> 记录投票结果 -> 变为leader -> tickHeartBeat


(PreVote的情况参考下方PreVote机制)

        集群启动之后,所有的节点都为follower状态(此时term为1)并且开始tickElection(一般tickElection的时间为tickHeartBeat的10倍)。tickElection倒计时结束之后节点会变为{Pre,}Candidate状态,并向其他节点发送{Pre,}Vote消息(如果没有PreVote,term会+1),直到收到到多数节点的同意投票,变为leader发送心跳。


        选举是抢占式的,一个follower可能同时收到多个Candiate的MsgVote但是只能给一个Candiate投票:


  1. Candidate会发送它的raftlog的last index以及term给其他节点,如果自己的log不够新,则节点不会给这个candidate投票(保证原则2)

  2.  follower给Candidate投票遵循“先来先到”原则,给第一个Candidate投票,不会响应之后的MsgVote消息。(保证原则1)

  3.  如果一个Candidate收到一条更高term的MsgVote消息,则会变回follower。如果收到一条相同term的MsgVote消息,由于已经投票给自己因此不会响应(leader transfer时同理)

  4. 如果在一次tickElection的时间内没有选出leader,则会重新开始一次选举,此时term会增加并重新开始tickElection倒计时。tickElection的时间在实现上会做随机处理(一般为150ms - 300ms),防止多个Candidate分票之后没有选出leader,减少选举死锁出现的可能性。(保证原则1)

  5. 变为leader之后会马上进行一次空的log replication


PreVote机制

        如果一个follower突然和集群大多数节点断开,没收到leader的心跳会自己变成Candidate,然后发起选举(当然不可能选举成功), 自己的term就会快速递增。网络恢复之后它重新加入集群,如果收到新的MsgApp信息,在返回MsgAppResp时会带上自己的Term


(除了MsgProp和MsgReadIndex,其他信息都会带上自己的term。MsgProp这时不需要term的信息,只需要发给leader。而MsgReadIndex不会改变状态机状态,因此对多leader不敏感,只要这条消息被commit了即可)


        leader收到这个信息发现term比自己大很多,就会直接变为follower,集群失去leader,于是触发重新选举,扰乱了原本稳定的raft集群


(收到高term信息变为follower是很正常的,因为如果网络出现partition,自己在较小一方变成leader而真leader是在多数一方并且多数一方的term可能更大(可能进行了节点加入),网络恢复之后保证term最高的那个为leader,保证term的递增性)


       为了防止这种情况,使用一种PreVote机制,在选举流程中添加一个PreVote阶段,节点变成Candidate之后,不马上发送投票询问,而是先询问所有节点是否愿意给他投票成为leader,如果不能得到大多数相应,则不发送投票请求,这时候并不会增加term。


其他节点若是接收到更高term的PreVote消息,则根据以下情况返回:


  1. 如果自己已经有leader,且定期收到过心跳,则不发送同意投票

  2. 如果上述条件通过,且自己本地log足够新,则发送同意投票


        如果节点发起PreVote但是没有得到大多数同意,则会降为follower,同时不增加自己的term。这样当网络恢复它重新加入集群时就不会引起重新选举,可以保证集群正常工作。

        如果节点发起PreVote得到大多数同意,则会进入真正投票阶段,阶段会发送MsgVote给所有节点。


Prevote是一个典型的2PC协议,第一阶段先征求其他节点是否同意选举,如果同意选举则发起真正的选举操作,否则降为Follower角色。这样就避免了网络分区节点重新加入集群,触发不必要的选举操作。


leader transfer机制

有时候我们可能需要手动将集群的其中一个follower替代为集群的新leader:


  1. leader节点需要进行硬件维护,同时我们不想当leader节点关闭后,集群需要重新选举,造成选举过程中无法接收新的log

  2. 集群中有更合适的节点来作为leader,如果一个节点它的负载非常大就不适合做leader等等


leader transfer的步骤如下:


  1. leader接收到MsgTransferLeader的消息,如果之前有正在进行的leader转移过程,则停止

  2. 判断转移目标节点的raftlog是不是和自己同步(last index与progress的match index相同),如果不同步则进行消息同步的流程,等到同步之后(这之间可能出现新的leader transfer请求)发送MsgTimeout请求,并且要求leader transfer在一个election周期内完成(接收到MsgVote投票时间 - MsgTimeout发送时间 < election周期),否则终止转移,leader不变

  3. 目标节点收到MsgTimeout请求之后变为Candidate进行选举流程


     在leader transfer期间如果收到新的proposal请求,则会拒绝该请求(当然如果是follower转发来的,follower本身是不会知道这个拒绝的结果的)


三、心跳

        leader向每一个follower发送心跳时会发送以下内容:


  1. follower progress的match与自己raftlog commit index中较小的一个,用来更新目标follower的commit index

  2. ReadIndex的上下文([]byte),用来执行一致性读操作


        follower收到心跳之后会更新自己的raftlog的commit index,这个更新不像接收Append消息的时候要检查term是否有效,是无视term的。之后回复leader信息。leader收到follower心跳回复后(MsgHeartbeatResp),检查一下progress的match,如果落后自己就进行一次Log Replication(这样不必等到有proposal发过来的时候再向follower同步,保证follower同步的进度)。如果有通过heartbeat来传递的读请求(ReadIndexSafe),那么会发送MsgReadIndexResp给对应发生读请求的follower


Check Quorum机制

        在tickHeartbeat时leader也会检查自己是否依然是集群的leader,通过发送一条MsgCheckQuorum消息给自己,确定最近(一个心跳周期内)有接收到过大多数节点的心跳。如果不足大多数,则变为follower,集群会进行重新选举,有点挥刀自尽的感觉。

以上是关于RAFT算法深入(上)的主要内容,如果未能解决你的问题,请参考以下文章

Raft 算法浅析

深入解析Raft模块在ZNBase中的优化改造(上)

深入剖析区块链的共识算法 Raft & PBFT

深入浅出etcd之raft实现

Raft算法国际论文全翻译

深入解析Raft模块在ZNBase中的优化改造(下)