Etcd raft源码阅读

前阵子用到了etcd,出于兴趣,把etcd的raft库代码研读了几遍,感觉受益匪浅,特记录在此,待日后时时翻阅。

etcd是一个强一致性的kv存储数据库,就CAP这个概念来讲,是满足CP两点的,如何理解CAP呢,作为分布式数据库,P肯定要满足的,为了满足C,我们一般采用quorum机制,就是说需要大多数节点同意,假设我们有三个节点1,2,3,如果3被分区了,如果我们允许3被更新,就丧失了C,如果不允许3更新,就丧失了A,etcd使用了raft协议,满足了C放弃了A。

raft并不是第一个分布式一致性协议,之前就有Leslie Lamport大神提出的paxos协议,paxos协议是用来确定一个值的,把所有节点分成三种,proposer,accepter,learner,一次决议分成三个阶段,proposer发起一个提案编号,其他accepter发现这个这个编号比之前自己接受过的编号大就同意,proposer发现大多数节点同意就会带上这个编号发起一次决议,大多数同意这个决议就让所有人learn,一次决议结束。后来为了决议一串数据,又产生了multi-paxos,引入了leader的概念,减少了冲突的可能性。

这个只是大致流程,实现中却很复杂。所以raft协议应运而生,raft协议相比paxos协议简单易懂得多,按照raft作者的论文,基本可以参照来实现一遍,大大降低了一致性协议的入门门槛。raft协议使用了日志复制,当所有节点的日志都一样的时候,只要状态机按照日志来执行一次,那么各节点的数据肯定是一样的。

raft跟paxos一样,也需要大多数节点同意才算决议成功。但他把整个协议分成了三部分来简化流程,1.领导人选举,2.日志复制,3.节点增减。

其实领导人选举也是基于日志的,一个节点的日志比大部分人新才允许被选为领导人。节点增减也需要通过日志复制来进行。

每条日志都会有两个属性,term和index,term是任期,index是日志序号,随着日志增加,term不能下降,index每次单调加1,不允许日志之间有空洞。

每个节点一定是三种角色之一,leader,follower,candidate。

A)选举

一启动的时候每个节点会设置自己的状态为follower,term为1,如果在一个election timeout之内收到了其他节点的append、heartbeat、snapshot消息,就知道对方是leader,从而把自己设为follower;如果收到了vote,那就判断对方的日志是否比自己的新(term更大或者term一样但是index更大),新的话就同意对方的vote请求;如果election timeout过了还是没收到相应的消息,就会设置自己的状态为candidate,把自身的term加1,然后向其他节点发送vote请求。所有节点在一个term中只会同意一次vote请求,只有得到多数节点的vote同意才会把自己标记为leader,然后向所有节点发送append来声明自己的leader地位;如果过了一段时间没收到多数同意,那么在等待一段随机事件之后会增加自己的term,并再次发送vote请求。

在收到多数节点同意的vote resp之后,按理说leader可以把之前term未提交的日志进行决议提交,但事实上这是有问题的,具体原因如下:

raft有个安全截断机制,如果follower发现leader发过来的日志跟自己本身的日志index一样,但是term不一样,那就把index及其之后的日志删除掉,并把leader的日志复制到后面,前提是要删掉的日志还没有commit,不然就是bug了。


这是一个corner case,流程步骤如下:

a.S1是leader,他在term1把index为1的日志条目复制到其他节点并提交,并把index为2的条目复制给了S2

b.S1崩溃,S5被S3-S4加上自己选举为leader,并提议了一条term为2,index为3的日志,但是没有复制到其他节点

c.S5崩溃,S1被S1-S4选举为主,并把之前term为2,index为2的日志复制给了3,然后进行了提交,并提议了一条index为4的日志

d.S1崩溃,S5再次被选举为主,然后把其他节点的日志给安全截断了

这里可以看到问题了,index为2的日志被提交之后回滚了,这是不行的,一旦commit就不能回滚。所以我们看e是怎么解决的

e.如果我们在c的时候不急着提交index为2的日志,而是在选为主之后,马上提议一条term为3,index为4的日志,只有日志4被大多数节点同意,我们commit4,进而把2也commit了。看到关键点在哪了吗,如果4在被commit之后崩溃了,即使S5被选为主,进而把index为2,4的日志冲掉也没事,反正没提交,如果4被提交(至少被多数同意)然后S1崩溃,S5也没法被选为主,也就不存在冲掉日志的可能了。

这里总结出一条:只有在本term决议的日志才能在多数节点同意之后进行commit,不然需要在本term提交一条日志,顺带把之前的日志给commit。所以一旦有节点被选为主,马上进行一条空日志的决议。

B)日志复制

每一条决议都是以日志的形式存储,每个节点按照日志从头开始apply,那只要日志一样,那么每个节点的状态机,或者说数据库就是一样的。

每条日志都有term和index两个字段,index是+1递增的,不允许有空洞,而paxos是允许有空洞的,所以paxos选主之后需要补全日志,比较麻烦。而paxos选主之后,主就有最新的日志,follower只要同步leader的日志即可。如果leader发现follower需要的日志已经被leader删除了,那么需要发送一个快照给follower,然后再发送快照之后的日志。

C) 节点增减

节点增减也是通过日志来实行,但是跟其他日志不一样的是,节点不需要这条日志commit就要apply,不然在极端情况下会出现问题。//TODO补全

不过一般使用一种更简单的方法来处理,每次只处理一个几点的增减,但是需要日志commit之后再apply。譬如我们从三个节点a,b,c加到四个节点a,b,c,d,在过程中有的节点知道有3个几点,有的节点知道有4个节点,即使分区也没关系,因为三个节点的多数是2,四个节点的多数是3,2+3>4,所以肯定有一个节点要被两边争取,所以分区也不会出现两边都有主的情况。

编辑于 2018-09-01