hashicorp raft 代码分析1 - 选举leader
Raft介绍
Raft是一种一致性算法,一致性算法的作用是将几个机器看成一个整体,即使其中的某台或者某几台机器发生故障,整个系统仍能正常工作。在Raft出现之前Paxos几乎是一致性算法的代名词。Ceph就使用Paxos算法同步各个节点的状态。虽然Raft和Paxos的作用相同,但是Raft算法更容易理解和实现,可以给学习一致性算法带来极大的方便。hashicorp的raft实现在各个开源项目中有着广泛应用,比如influxdb,consul。这里分析的代码是基于commit id: 9c733b2b
。
Raft只有三种节点:follower,candidate和leader。leader节点接收来自client的请求,接收到的请求以Log形式会被储存在本节点并分发到follower节点,如果整个集群中有半数以上节点已经存储了这个Log Entry,那么这个Log Entry就算提交(commit)成功了。Commit成功的Log Entry会被Apply到一个FSM(有限状态机)。FSM的任何一个状态都能通过Apply之前所有的Log Entry而获得,所以即便有节点因为故障而离开集群一段时间,在这个节点恢复时可以Apply它缺失的Log Entry而重新与其他节点保持一致。如果无限制保存Log Entry会需要无限大的磁盘空间,Raft提供Snapshot机制解决这个问题。Snapshot将当前FSM中需要保护的状态保存下来,然后删除这个状态之前的Log Entry,如果出现follower短暂故障而恢复,leader只需要先让follower install leader的最新snapshot,然后再将这个snapshot之后的Log Entry发送给这个follower就可以使其状态与leader保持一致。
完整的Raft协议可以查看这里。
选举Leader过程
Raft集群中leader负责接收客户端发来的请求并把这些请求保存和分发到follower,作用至关重要,是集群和外界通信的唯一节点。各个Raft节点通过投票选出leader,每个节点在一个Term中只能投一票,获得半数以上票数的node胜出。这也意味着一个集群中最好有奇数个node。下图为集群启动后的选举过程。
+----1----+ +-----2-----+ +----3-----+
| Follower| | Follower | | Follower |
+---------+ +-----------+ +----------+
| (A)
+----1----+ +-----2-----+ +----3-----+
| |<-(B)-| |-(B)->| |
| Follower| | Candidate | | Follower |
| |-(C)->| |<-(C)-| |
+---------+ +-----------+ +----------+
|(D)
+----1----+ +----2-----+ +----3-----+
| |<-(E)--| |-(E)->| |
| Follower| | Leader | | Follower |
| |-(F)-->| |<-(F)-| |
+---------+ +----------+ +----------+
- 系统启动时node 1,2,3都是follower。
- A: node 2的计时器第一个timeout,因为系统现在没有leader所以node 2在设置时间内没有跟其他节点通讯过。node 2将自己的角色变成Candidate。
- B: node 2首先将自己的Term加1,并且给自己投上一票,然后将这个Term 放到RequestVote的RPC请求中,发送给node 1和node 3。
- C: node 1和node 3收到RPC请求后,检查以下几个条件是否都成立,如果是那么他们就可以把自己的一票投给node 2,并且更新自己的Term为最新值。
- C.1 自己没有leader。
- C.2 自己当前Term小于收到的RPC Term。
- C.3 在这个Term中没有把票投给其他node。
- C.4 自己的Index小于RPC请求中的Index。
- D: node 2根据步骤C中的response检查自己的得票总数,如果超过半数就把自己的角色变成Leader。(如果在规定时间内没有得到足够票数的话,则启动新一轮投票。)
- E: node 2成为Leader后开始发送AppendEtries RPC给node 1和node 3。
- F: node 1和node 3接收到AppendEtries后记录node 2为最新的leader,返回RPC response给node 2。
这个过程中大家可能会有疑问**启动过程中会不会出现几个node同时变成Candidate呢?**这种情况是可能的,但是很少发生。因为从follower到candidat的时间是由两部分相加组成的,1. 一个可以设置的固定时间, 2. 一个随机时间。由于第二部部分的存在同时变成candidate的可能性很小。当然这也不代表着不可能,如果candidate在等待时间内无法得到足够的票数就会进入下一轮投票。由于candidate等待投票结果的时间也是由一个固定时间和一个随机时间相加而得,所以同一时间内进入下一轮投票的可能性也很小。这两个时间可以保证Raft集群可以很快选举出一个Leader。
选举Leader的代码流程
raft.Raft是这个库的主要数据结构记录着raft的各个节点,自己当前状态等。raft.Raft创建过程中会启动三个主要的后台go routines,分别是
- run: 根据自己的角色(Leader, Candidate, Follower)执行对应的操作。
- runFSM: 调用FSM接口
- runSnapshots: 执行快照操作
选举Leader主要集中在(*Raft).run中。三种角色分别对应函数(*Raft).runFollower,(*Raft).runCandidate,(*Raft).runLeader。
三种角色共用一个(*Raft).processRPC函数用于处理RPC,RPC就三种类型:Raft.AppendEntriesRequest, Raft.RequestVoteRequest, Raft.InstallSnapshotRequest。选举Leader过程会使用前两个RPC请求。下面简单介绍RPC的调用流程。
RPC调用流程
RPC调用的过程如下图所示。
+-------------------Node 1 (Leader)---------------+ +--------------------Node 2-------------------------+
|+--------------+ +--------------+ | | +--------------+ +--------------+ |
||Raft | |Network | | | |Network | |Raft | |
|| | |Transform | | | |Transform | | | |
|| | | | | | | +-----------| |------+ | |
|| |---RPC----->| |-----+---+->| |consumeCh |---RPC----->|rpcCh | | |
|| | | |<----+---+--| +-----------| |------+ | |
|| | | | | | | | |---------+ | |
|| |<-Respond---| | | | | |<-Respond---| | | |
|| | | | | | | | |---------+ | |
|+--------------+ +--------------+ | | +--------------+ +--------------+ |
+-------------------------------------------------+ +---------------------------------------------------+
Node1(Leader)要发送一个AppendEtries RPC给node 2需要经过如下步骤。
- Node 1的Raft构造一个raft.AppendEtriesRequest,通过Transform.AppendEntries传递给Transform。
- Transform定义了传输层的接口,比如AppendEntries就是接口的一部分。raft.NetworkTransport是基于网络传输的Transform实现。
- 传输层将RPC请求编码后通过网络将RPC请求发送给Node 2的传输层。
- Node 2的传输层将RPC请求放入consumeCh(在Raft中这个channel的变量名为rpcCh)channel中。
- Node 2的Raft从rpcCh channel中取出请求,处理后调用(*RPC).Respond将请求返回给Node 2的传输层。
- RPC的response最终返回到Node 1的raft.Raft。
总结
本文是Hashicorp raft协议实现分析的第一部分,主要分析了Raft集群中Leader的选举,还简单介绍了RPC的调用过程。