最简单的分布式存储系统如上所述,有一个master节点和slave节点,client的写入操作全部到master,读取操作可根据需求选择master和slave节点。该方案能够较好的解决数据一致性问题,但是引入了另一个问题,当master挂掉时,存储系统将不可用。
Raft是一个共识算法(ConsensusAlgorithm),它保证当集群中小于一半的节点挂掉时,存储系统仍然可用。单个Raft节点由三部分组成:
Raft算法将共识问题分解成以下三个子问题:
每个raft节点的状态会在follower、candidate、leader之间转变。
这里又引入了一个term的概念。raft算法保证,同一个term最多只可能存在一个leader,这是由Raft的选主机制决定的,下一节会细讲。
为了说明选主过程,我们需要引入两个rpc方法。
一个用于竞选,叫做RequestVote
messageRequestVoteRequest{int64term=1;int64candidateId=2;int64lastLogIndex=3;int64lastLogTerm=4;}messageRequestVoteReply{int64term=1;boolvoteGranted=2;}一个用于心跳(以及后续会讲到的日志复制),叫做AppendEntries
messageAppendEntriesRequest{int64term=1;int64leaderID=2;int64prevLogIndex=3;int64prevLogTerm=4;repeatedEntryentries=5;int64leaderCommit=6;}messageAppendEntriesReply{int64term=1;boolsuccess=2;}messageEntry{int64operation=1;stringkey=2;stringvalue=3;int64index}选主的过程以五个节点的集群为例:
2)有一个节点先于其他节点成为candidate,向剩余节点发送RequestVote,它需要得到多数票才能成为Leader。另外一个follower在一个term里只能投票给一个candidate,由此可以保证一个集群在一个term里只有一个leader。
4)如果其他pod收不到心跳,则会认为leader挂了,则会term+1并转变为candidate参与竞选。图中2号leader挂掉,3号成为新leader。
5)此时如果2号leader又活过来了,它会收到来自其他leader的term更大的心跳,转变为follower。
竞选成功后,leader需要承担起日志复制的责任。
1)leader收到请求,写入自身日志,然后使用AppendEntries命令发送给followers。这条日志目前处于uncommitted状态,图中用虚线表示。这里注意,每条日志都会带上term和index信息,确保唯一性。
2)follower收到请求,反馈给leader成功。当leader确认大于一半的节点成功复制日志后,这条日志就转变成committed状态,可以提交到状态机里了,图中用实线表示。
3)在下一次心跳时,leader把commited信息告知各节点。
1)Raft保证:如果不同的节点日志集合中的两个日志条目拥有相同的term和index,那么它们一定存储了相同的指令。这是因为:每一个term最多只会选出一个leader,leader同一个index只会写一条日志,且leader不会修改日志。
2)同时Raft也保证:如果不同的节点日志集合中的两个日志条目拥有相同的term和index,那么它们之前的所有日志条目也全部相同。这是因为:leader发出的AppendEntries请求中会额外携带上一条日志term和index,如果follower在本地找不到相同的日志,则拒绝接收这次新的日志。
3)所以,只要follower持续正常地接收来自leader的日志,那么就可以通过归纳法验证follower和leader拥有相同的日志序列。
分布式系统非常复杂,当leader上任时,有可能会遇到a-f的各种情况:
这里需要注意,raft的机制(在下一节会细讲)保证选上的leader拥有所有committed的日志,follower比leader多出的日志全部都是uncommitted的。
下面讲这些情况发生的原因:
至于leader处理不一致的方法很简单:向前回溯follower跟自身的分歧点,然后从分歧点开始修复覆盖follower的日志。
到目前为止的这套机制还不能100%保证一致性,想象以下情况:
因此仍然需要增加一些选主和日志commit的限制,才能保证一致性
回过头来看选举的RPC请求,candidate竞选时会带上logindex和term,follower只会给日志比自己新的candidate投票。因为leader当选必须获取超过一半的选票,所以能当选的leader一定比超过一半的节点拥有更新的日志。又由于一条日志想要commit,必须复制到超过一半的节点上,那么leader一定拥有所有已经committed的日志。
messageRequestVoteRequest{int64term=1;int64candidateId=2;int64lastLogIndex=3;int64lastLogTerm=4;}messageRequestVoteReply{int64term=1;boolvoteGranted=2;}日志commit限制复习一下commit是什么:一条日志确认生效,应用到状态机中。一般情况下,leader将日志复制到超过一半的节点后,即可commit日志,但是这么做在以下case时会出现问题:
a)term2的leaders1将日志复制到s2节点后挂掉了b)s5节点在term3选为leader,日志3还没来的急复制就挂掉了c)s1节点在term4重新选为leader,将日志2复制到s3后挂掉,此时日志2已经复制到超过一半的节点了,符合commit条件d)因为此时s2、s3、s4的最新日志都是2,不比s5新,s5可以获得多数票;s5再次成为leader时,把日志3复制给s1-s4,覆盖了已经提交的日志2e)位了避免这种情况,引入一条日志commit限制:leader只能commit当前term内的日志,之前的日志只能顺带同时提交;图中s1成为leader,把日志2复制出去后,没有commit,等到日志4复制出去后,才顺带commit,这样s1-s3的日志都比s5新,s5无法成为leader