本文先对raft协议进行通俗的讲解,然后基于MongoDB 4.4.2 源码版本分析了MongoDB副本集协议的主要实现流程,最后指出raft核心算法在MongoDB副本集协议源码中的体现。
1.raft协议
1.1. 提出问题
在我们的生活中,很多大型基础设施例如银行系统、医疗系统、电商系统都需要提供24小时不间断服务。但在现实生活中总会遇到各种各样的问题。例如,放置服务器的地方发生了地震。为了避免这一类问题,我们能想到的一个办法就是为服务提供多个副本,然后在不同的地方放置这些副本,一但服务发生了问题,另外的副本能够马上接管当前的服务。对于无状态的服务来说,多个副本并不会带来更多需要解决的问题。但是对于有状态的服务(一个典型的例子就是数据库服务),就会带来一个问题,就是另一个副本接管了之前的服务之后,一定要保证和之前服务的数据是一致的,这个问题正是使用共识算法来解决的。
也就是说,共识算法是为了保证多个副本之间的(强)一致性而提出的一种算法,他让一个分布式系统对外看起来就像是只有一个副本一样,所有的操作都是原子的。也就是说,当外部客户端对该系统进行了一次写操作,如果系统提示写入成功,随后就一定能够读取到当前写入的值。
raft协议便是共识算法的一种,但它不是最早提出的共识算法,而是从PAXOS协议衍生而来。学习共识算法建议可以直接从raft协议开始,因为它的论文具有更加工程学的描述,这点比PAXOS算法好多了。如果你直接去看PAXOS的原始论文,可能会怀疑这个作者到底是一个科学家还是一个文学家。
1.2.共识算法模型
共识算法基于复制状态机模型,下面的图摘自raft论文:
①客户端Client提交写请求;
②共识算法模块(Consensus Module)将写入的数据提交的多个副本的Log模块;
③各个副本的Log模块将本次操作应用到状态机(State Machine);
④状态机向客户端返回结果。
该模型中我们重点要关注的步骤就是②和④,即共识算法模块是怎样将写操作复制到各个副本的,以及什么时候状态机将结果返回给客户端。
1.3.raft核心概念
一般来说,从事创造性的工作,都是从提出问题开始,然后一步步寻找问题的解决方案,最终形成理论。但对于学习者来说,顺序反过来会更加轻松且容易,我们可以先了解理论,再反过来看它解决了什么问题。所以,先来看看raft协议的总体框架,再去研究它的细节。
raft论文中将协议拆分成了三个子问题:选举、日志复制和安全性。
选举就是从raft集群中选取一个副本来承担主节点的角色。主节点的工作就是接收从客户端发来的请求并负责将操作封装成日志发送给其他节点,这一过程称作日志复制。在进行选主和日志复制的过程中有一些需要遵循的规则限制来保证系统的一致性,这一部分要讨论的内容称作安全性。接下来分别讨论这些内容。
1.4.选举
1.4.1.节点的角色
我们称raft集群内的副本为节点,节点具有三种角色:主节点(Leader)、竞选者(Candidate)、从节点(Follower)。主节点的职责是接收从客户端发来的请求并负责将操作封装成日志发送给其他节点,当没有日志可发送时,也要持续给其他节点发送心跳来维持自己的权威,这样其他节点就会知道主节点的存在。这个其他节点指的就是从节点,从节点负责将主节点发来的日志信息同步到本地。当选举发生时,发起选举的从节点就会变成竞选者的角色,竞选者会向其他节点发送RPC投票请求,如果收到了大多数节点的同意,自己就会变成主节点。raft协议中,一轮选举成功后,只允许存在一个主节点,其他节点都是从节点。
1.4.2.任期
任期(term)标志选举的轮数。每发起一次选举,任期的值都会自增加1,不管选举是否成功。raft集群内的所有节点在通信时都会带上自己所知的任期的最新的值。在现实中,网络总会出现延时或中断的情况,任期能够让相互通信的节点之间知道发来的哪些信息是过时的。比如,如果节点接收到了一个term值比自己已知的term值小的RPC投票请求,就可以果断丢弃这个信息。
1.4.3.RPC通信
节点之间通过rpc进行通信,主要是下面两类请求:
RequestVote RPCs: 用于 candidate 拉票选举。
AppendEntries RPCs: 用于 leader 向其它节点复制日志以及同步心跳。
1.4.4.节点之间的角色状态转换
follower的状态转换过程
follower只能变成candidate,不会直接变为leader。一开始的时候,集群内的所有节点都是follower角色,每个节点给自己维持一个选举超时计时器,当在超时时间内没有收到来自主节点的心跳包时,就会变成candidate参与选举,否则维持自己的follower身份。这里有一个问题,如果每个节点的选举超时时间相同的话,就会出现多个节点同时发起选举的情况,这样有可能造成选票瓜分(没有一个candidate获得足够的选票成为leader)。如下图,黄色节点在同一轮选举中同时发生了选举超时,他们都各自投了自己一票,然后前两个节点各自又收到了来自其他节点的一票,这样收到的选票情况是221,没有一个节点收到了大多数的选票(5个节点的raft集群中至少要3票才能当选),只能再发起一轮新的选举。所以,这里的解决方法是,让每个节点的选举超时时间不一样,每次都随机选择,就能避免无意义的投票轮数。
candidate的状态转换过程
candidate的角色转换分为以下三种情况:
①选举成功:candidate收到大多数节点的同意票,变为leader。
②选举失败:candidate收到其他leader发来的心跳包,退回follower。
③选举超时:candidate在选举超时时间内未收到大多数选票,重新发起选举,保持角色状态不变。
情况①:选举成功
n1的选举超时时间先到了,开始了集群内的第一轮选举。t0时刻,n1先投自己一票,然后将自己的term加1(假设term变量的初始值为0),随后广播RequestVote RPC请求。n2在t1时刻收到了该请求,便更新自己的term为1,然后如果发现term的值不小于自己当前保存的term值(初值为0)且n1满足成为主节点的条件(该条件在安全性部分讨论),就给n1投出赞成票。t2时刻,n1收到了n2发来的赞成票,再加上自己给自己投的一票,已经得到了大多数节点的投票,所以把自己变成了主节点。尽管t3时刻才收到n3的响应,但是这个响应已经不再重要。
情况②:选举失败
假设在t0时刻n1发起选举后,t1时刻n3也发生了选举超时,n3将自己的term加1,同时投自己一票,并广播自己的RPC投票请求。
t2时刻n1收到了n2的赞成票后,成为了主节点。同时n3收到了n1的RPC投票请求,但由于票在本轮中已经投给了自己,便不再投票。
n1和n2分别在t3时刻和t2时刻收到了n3的RPC投票请求,但由于票在本轮中已经投给了自己,便不再投票。
t4时刻n1开始向其他节点发送心跳包。
t6时刻n3收到了来自主节点n1的心跳包,发现n1携带的term值不小于自己的,于是承认了n1主节点的地位,自己从candidate角色退回follower角色。这里的n3节点就是选举失败的情况。
情况③:选举超时
这个就是上文所说的几乎同时发起选举可能出现的情况。这里再举一个极端的例子,t0时刻n1,n2和n3同时发起选举,最后他们都将票投给了自己,选票被瓜分。t2时刻,n3选举超时,因此继续保持自己的candidate状态,将term加1后,再次发起选举。
leader的状态转换过程
t1时刻,作为主节点的leader宕机。
t2时刻n2发生了选举超时,将自己的term自增1,发起RPC投票请求,并投自己一票。
t3时刻n1还没有恢复,无法收到RPC投票请求;同时n3收到了投票请求,将自己的term自增1,同时给n2投出赞成票。
t4时刻n2收到了大多数选票,成为主节点;同一时刻n1恢复。
t5时刻n2开始发送心跳包。
t6时刻n1和n3都收到了n2发来的心跳包,n1发现n2的term比自己的大,于是自己从leader退回follower,同时更新自己的term为2。
1.5.日志复制
选举完成后,主节点将承担起日志复制的工作。当客户端没有操作请求时,主节点只需要发送心跳包即可,否则在将这次操作的log持久化到本地后,需要主动向其他节点发送AppendE