分布式之Raft算法
仔细思索分布式的CAP理论,就能发现P数据分区是分布式的特性,是必须满足的特点。如果一个系统没有数据分区的存在,那这样的系统就是单体,而不是分布式。CAP三个特性无法同时满足,那么所有的分布式系统实现都是在A(可用性)和C(一致性)之间权衡选择。
Raft是一个分布式的一致性协议算法,放弃了CAP的可用性,保证严格的一致性。
Raft的应用非常广泛,etcd、consul等都使用了Raft协议。
Raft是一个分布式的一致性协议算法,通过选主机制和日志状态机模型来保证分布式系统的数据强一致性,如下图的基本模型:
关于选主机制和日志状态机模型,下面会详细的论述。
Raft的选主机制
为啥要有leader
在Raft协议里,系统中的所有节点中必须选出一个Leader。
因为分布式的数据分区特性,如果没有Leader来协调数据处理,就很难保证数据是一致的了。 如果没有Leader,则系统的每次操作都要进行一次投票,开销非常大。Paxos算法也是一个分布式一致性协议,使用P2P算法来保证,但是实际理解和实现都过于复杂。如果有一个leader会让决策变得简单快速。
Raft角色和演变
节点状态变迁如下:
Raft普通节点和Leader之间都有心跳,一有异常就等着重新选举。
- 一个新的节点加入有leader的集群,先变成follower
- term(任期):表示当前节点被选为leader节点的一段时间,如下图
- S1在term=1这段时间内是leader,S2和S3是follower
- 后面S3和leader没有心跳,则S3发起term=2的选举,但未得到大多数投票(term=2的时间很短,实际不存在这段时间)
- 后面S2发起term=3的选举,得到大多数投票当选;
- ...
- term=4的这段时间,S3当选
系统参数和RPC通信
Raft系统有Leader、Follower、Candidate等角色, 每个角色都维护一些状态,遵守一些规则。
Raft系统采用RPC心跳来通信,主要有两种类型的RPC:
- AppendEntries:
- 用于Leader向其他Follower同步日志数据
- 维护和Followers的心跳
- RequestVote:Candidate调用的请求用于获取投票
选主过程
每次选主都有一个新的term,下图显示了选主的过程:
- S2的时钟先发现心跳超时,发起选举(使用RequestVote的心跳包),作为Candidate投自己一票。
- S3(使用RequestVote的心跳包)也投S2一票,则S2当选为新的Leader。
- 当S1和S2重新有心跳时,发现系统有Leader,则也成为S2的Follower。
日志状态机
分布式要保证可用性,往往意味着多副本存储,且各副本节点的数据一致;Raft维持多副本数据一致的方式是使用日志状态机。
从上图对应的初始状态开始,这个时候如果有客户端发起操作数据的请求,这个数据会被leader接收,并且leader使用AppendEntries的RPC请求到其他各个Followers。
那么,什么是日志状态机呢?
日志状态机通过二元组(index,term)来作为日志状态向量。
- index是连续递增的数字,是日志的逻辑序号,各个副本上相同的index,有相同的数据日志。
- term任期也是连续递增的数字,如上面解释的那样,一个term则表示某个节点当选的一段时间。
日志状态向量在Raft系统里面的数据复制和选主过程里发挥着重要的作用。
我们知道Raft分布式系统正常工作时就处理请求,内部复制日志,异常的时候发生新的内部选举,对外可能就出现短暂的可用。下面分这两种情况具体分析。
日志状态向量在正常复制流程是怎样工作的?
下图也整体表现了日志状态向量在正常复制流程是怎样工作的。
S1是Leader,从图上可以看出,此时的S2复制了S1的全部日志,而S3还有所落后。
放大复制的日志条目来看:
-
Leader知道大部分节点都成功之后,将这个index的日志状态设置为committed。Follower通过心跳包得知这个log(日志状态向量标记的)已经被成功复制,然后所有节点会将该日志状态设置为committed。
-
commit的日志可以应用到系统上(板上钉钉,不会回滚)。
-
S3比较慢,同步到(index=2,term=1),和其他节点的(2,1)对应的数据都相同。
-
(index=4,term=1)的数据已经有大多数统一,则状态为committed。
-
此时客户端都知道y=1,z=9,x=1。
异常时,日志状态向量在选举流程是怎样工作的?
选举的时候也会利用到这个日志状态向量(index,term)。
客户端找Leader
如下图, Raft算法规定客户端将所有请求发送给Leader。
如果Leader突然宕机,则客户端的请求不通。
客户端启动的时候,如何知道哪一个节点是Leader呢?
具体办法是客户端随机挑选一个服务器进行通信,如果客户端选的服务器不是Leader, 那么被挑选的服务器会拒绝客户端的请求, 并且提供它最近接收到的Leader的信息,即通过收到Leader发送的心跳的RPC得到Leader的网络地址。 如果LEADER已经崩溃了,那么客户端的请求就会超时,客户端之后会再次重试随机挑选服务器的过程。
对于下图这种情况,客户端发送的请求则是无效的。
在Raft系统里,特殊情况下,一部分请求可能存在短暂的不可用, 但保证了系统的严格一致性。
日志状态机帮助正确的选主
那么异常发生的时候,也就是说选举等过程是怎么利用这个日志状态向量的呢? 如下图的演变过程:
-
初始是S1是Leader,复制数据给S2和S3.
-
假设S3先发起选举Candidate(term=2), 因为S3节点最近commit的数据(index=2,term=1), 而S2最近commit的数据(index=4,term=1),已经commit的数据包含S3所没有的(index=3,term=1)、(index=4,term=1),也就是说数据比S3新,则S2不会投票给S3。
-
此后,S2会也发起选举Candidate(term=3),因为S2的最新commit的日志状态向量是(index=4,term=1), S3此时的日志状态向量(index=2,term=1),S3知道S2的数据比自己新,则同意S2的选举。
-
随后继续接收请求,复制数据。
通过上面的分析,可以看出,日志状态机使得不至于选出拥有很老数据的节点来当Leader。
Raft为何一定要设计term这个概念?
上面章节的假设稍微有一点改变,假设S3 term=2的选举也是成功的,因为客户端的请求完全相同,这两个情况下的系统最后都有完全一样的数据(1,2,3,4,5,6,7)。
两个假设的场景,系统有完全一样的数据(1,2,3,4,5,6,7),但是通过term的表示可知道系统演化的状态是有区别的。(index=2,term=1)和(index=2,term=2)是不同的日志状态向量。
从上面的分析我们知道,有了term,我们才知道这个事情发生时的状态。
Raft设计任期这个概念,主要是基于这样的一些考虑:
- 首先很形象,就像总统选举一样,每次都有一个任期号
- 每个Term都有自己的leader,而且这个Term会带到log的每个entry(AppendEntries)中去,来代表这个entry是哪个leader 的term时期写入的。
- 任期连续递增。如果在规定的时间内leader没有发送心跳,Follower就会认为leader已经挂掉,会把自己收到过的最高的Term加上1做为新的term去发起一轮选举。
如果参选人的term还没自己的高的话,follower会投反对票,保证选出来的新leader的term是最高的。
如果在time out周期内没人获得足够的选票(这是有可能的),则follower会在term上再加上1去做新的投票请求,直到选出leader为止。
最初的Raft是用C语言实现的,这个timeout时间可以设置的非常短,通常在几十ms,因此在raft协议中,leader挂掉之后基本在几十ms就能够被检测发现,故障恢复时间可以做到非常短。
新加入的日志是怎样应用压缩日志的?
- 当日志被leader做了快照并删除了的时候,leader需要把快照发送给follower
- 或者集群有新的节点加入,leader也可以直接发送快照,大量节点日志传输和回放时间。
快照只包括已提交的数据。 将已提交的数据和未提交的数据一起给follower就可以,大大节省了日志传输和回放时间。
未提交的日志在选主之后何去何从?
日志都提交的场景
这个场景,发起选举时,假设黑色节点比红色节点的index大1,且是提交的。此时黑色节点和红色节点commit的日志是(index=6,term=4), 黑色节点都有提交的数据(index=7,term=4)。
选举后:
- Follower的日志落后,从落后的地方开始复制Leader的数据。
有一个日志index未提交
下图这个场景,发起选举时,假设黑色节点比红色节点的index大1,但是这个数据的状态因为没有大多数的认同,目前还处于uncommit的状态: 此时黑色节点和红色节点commit的日志是(index=6,term=4), 黑色节点都有未提交的数据(index=7,term=4)。
两种情况:
- 黑色节点其中一个发起选举,被选为leader
- 将未提交的数据也复制给其他Follower,等这个日志提交
- 红色节点其中一个发起选举,被选为leader
- leader上没有未提交的index日志,黑色节点的数据要保持和leader的一致,则会删除 未提交的数据(index=7,term=4)
新Leader当选之后的数据复制
这个图是Raft论文里面的一个图,这是一个Raft系统某一时间的日志状态场景:
此时这个Leader节点处于任期8,准备向其他节点复制数据。
当选发生的时机是d宕机,当前节点发起TERM=8的选举,看看其他节点对选举的反应:
- f节点:Candidate的当前日志(index=10,term=6)比f节点当前日志(index=11,term=3)的term大,所以f同意
- a同意
- b,e同意
- 可是c不同意,d也不同意,不过已经有5票了,当前节点当选
- c当前日志(index=11,term=6),Candidate的当前日志(index=10,term=6),先比较term相等,但index 11>10,所以c不同意
- d当前日志(index=12,term=7),Candidate的当前日志(index=10,term=6),先比较term 7<6,所以d不同意
变为Leader后,当前节点要把数据同步给其他节点,那么对于每个节点是怎么同步的呢?
- a节点接收一个(10,6)
- b节点还有很多数据要同步,从(5,4)开始
- c节点多了一个,删除(11,6)
- d节点多了两个(12,7),(11,7),需要删除
- e节点要同步的数据也比较多,(7,5)和leader的(7,4)不一样,(6,5)和Leader的(6,4)也不一样,这两个日志先删掉,然后重新从leader同步数据过来
- f节点也是,需要删除的日志很多,然后再同步。
- 产生f节点的原因是因为它当选了term 2和 term 3的leader,但是数据都不曾同步给其他节点,如下面这张图所示
扩展问题
Raft复制和MySQL的复制有什么异同?
-
MySQL的binlog也是日志,被复制到其他的备机。
-
Raft在有异常的时候,是自动的选主、切换主备。
-
MySQL没有通过投票选主机制+复制日志的状态模式来选主,一般都是手动切换的。这是因为Raft看重分布式的一致性;而实际应用的时候,MySQL更看重可用性。
-
MySQL不支持这么复杂的分布式情况,侧重点在业务关系复杂性,保证高可用性。
什么是脑裂现象
在一个分布式系统系统,如果出现多个leader,则是脑裂。
怎么解决? 需要设置大于半数的节点同意才是主。
如上图,如果要满足必须大于半数的节点同意才是主,则选不出Leader,系统瘫痪。
但是如果使用奇数个节点,必须大于半数的节点同意才是主,这样很容易满足,如下图: