分布式一致性算法

235 阅读13分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

背景

本文主要讲解分布式一致性算法的一些基本原理和关键步骤,不涉及算法本身的底层逻辑,文章主要也是作为对于自己了解这部分内容的一次记录,主要以讲述Paxos和Raft的区别,以及各自如何保证一致性来解释分布式一致性。

首先从一个大的方面来看Paxos(basic,multi)和Raft是否都有leader的层面来看下这三个算法是否都有leader

名称是否有leader特点
basic paxos每个节点都可以处理客户端请求,每次请求都要经过两个阶段(不够高效),任何一个节点都可以发起提案
multi paxos通过leader处理客户端请求,不需要准备阶段,效率高于basic paxos
raft通过leader处理客户端请求,不需要准备阶段,效率高于basic paxos

Paxos的一些术语

  • proposer: 提案发起者处理客户端请求,将客户端请求发送到集群中;
  • acceptor: 提案处理者,负责处理接收到的提案,会记录存储一些信息来决定是否接收这个提案;
  • proposal: 提案包含一个proposal number和一个value;
  • round: 一次提案的过程,包含准备阶段和接受阶段,round并不完全决定一个提案是否被通过;
  • paxos instance: 多个节点中对于某一个proposal达成一致的过程,可能包含多个round,一个instance对应一个logIndex;
  • acceptedProposal: 在一个paxos instance中已经接受过的提案;
  • acceptedValue: 在一个paxos instance中已经接受过的提案对应的值;
  • minProposal: 在一个paxos instance中,当前接受的最小提案值

Basic Paxos

协议流程

paxos.png

  1. proposer向集群所有节点查询当前最大logId,从大多数回答结果中选取最大的logId,然后在基础上+1作为本次的proposalId;
  2. proposer向所有节点广播准备请求Prepare(n);
  3. acceptor比较n和minProposal,如果n>minProposal那么接受新提案,进行minProposal=n操作;否则返回Return(acceptedProposal,acceptedValue);
  4. proposer接收到大多数请求后,如果发现有(acceptedProposal,acceptedValue)返回,表示有更新的提案保存acceptedValue到本地,然后从新开始步骤1,生成更高的提案;
  5. proposer如果发现在当前paxos instance中没有优先级更高的请求,那么进入到第二阶段,广播accept(n,value)到所有节点;
  6. acceptor比较n和minProposal,如果n>=minProposal,则进行acceptedProposal=minProposal=n,acceptedValue=value本地持久化操作,否则返回minProposal;proposer收到大多数请求后,如果发现有返回值>n,表示有新提案,然后重新开始步骤1;否则就视为提案accpet(n,value)达成一致。

Basic Paxos实现一致性主要分为两个阶段:

  • 准备阶段

集群内的任何一个节点(proposer)都可能发起一个提案n给acceptor,acceptor接收到proposer的提案,如果提案编号n大于acceptor处理过的所有提案,那么acceptor将自己上次处理的提案编号返回给proposer并且表示不再处理小于n的提案。

  • 接受阶段

当poposer收到多个acceptor的成功回复后就进入到接受阶段,它要向回复请求的acceptor发送接受提案请求,包括提案编号n和value,acceptor接受到accept请求即接受请求。

简单来说就是分为三个步骤,第一步向所有节点发送请求查询最大的logId然后在基础上+1作为本次的请求Id,第二个步发起准备阶段请求到集群中,然后集群节点分别进行回应,只要有大于集群一半的节点有成功返回,那么就可以进行第三步阶段,proposer发送接受请求到acceptor,acceptor接受处理proposer提案。缺点就是每次请求都要经过准备阶段,每次请求在集群内部至少要经过三次网络IO,效率会有一定损耗

另外如果客户端请求很多,那么集群中处理起来每一个请求都要经过准备和接受阶段,势必会造成很大的不必要影响,因此basic paxos虽然解决了分布式一致性的问题,但是因为不够高效不会作为首选方案。 活锁问题

Multi Paxos

在上面提到了basic paxos对于请求处理的效率问题,为了解决这个问题在multi Paxos中引入了leader角色来处理客户端请求,也就是说集群内的其他节点不会再发起proposer请求,所有的客户端请求都交给leader来处理,这样一来集群内的提案都统一由leader生成,提出提案后就直接进入到接受阶段。multi paxos引入的leader也很好的解决了basic paxos的活锁问题。

新主节点恢复流程

对于集群来说任何节点都可以参与选举并且最终成为leader,又因为paxos中总是大多数节点通过accpet就会完成提案,因此当原先的leader节点离开集群,就无法保证选举出的新leader包含了之前的所有提案,可能存在空洞,所以在真正提供服务之前需要有一个提案恢复过程。

新主向所有节点发起查询最大logId请求,收到大多数节点响应后选择最大的logId作为日志恢复结束节点,然后从头开始对每个logId逐条进行paxos协议,在这个过程中新主是无法对外提供服务的。如果逐个日志进行恢复,并且在恢复期间无法对外提供服务,因此对于可用性会造成很大程度的影响。所以multi paxos中引入了confirm机制。

confirm机制即leader持久化一条日志,得到多数派accept后,就再本地写一条针对该日志的confirm日志,表示这条日志已经确认得到多数派备份。因此在leader重启后扫描本地日志,对于已经拥有confirm日志的log就不会再发起paxos。通常confirm日志是先写入本地,然后再延迟批量同步出去。

Raft

复制状态机

在了解raft算法以前我们先看下复制状态机的关键理念 statue-mashine.png

状态机的一个核心思想就是: 相同的初始状态+相同的输入=相同的结束状态。那么换句话说就是在多个机器上,只要初始状态相同,那么执行相同的操作后,最终的状态所有机器都是一致的。而raft算法就是基于多副本状态机的角度出发来考虑解决一致性问题的。

raft算法对于一致性问题分成几个大的方面来处理:

  • 选举
  • 日志复制
  • 安全性
  • 日志压缩
  • 成员变更

核心概念

Raft在线演示动画

  • term: raft将时间分为大小不同的时间段,每个时间段就是一个任期(term);
  • leader: 接受客户请求,并向follower同步请求日志,当日志到达大多数节点后,通知follower提交日志;
  • follower: 接受并持久化leader同步的日志,在收到leader提交日志的请求后,提交日志
  • candidate: leader选举过程中的临时角色;
  • requestVote RPC: candidate在选举起发起的请求投票信息;
  • appendEntries RPC: leader的心跳机制,复制日志也在appendEntries中完成;
  • installSnapshot RPC: leader发送快照给日志落后太多的follower;

leader选举

  1. 在集群初始化之后,所有节点都是follower此时集群中没有leader,follower无法与leader保持心跳,因此follower认为leader已经下线,然后follower会在随机事件后然后转变成candidate状态发起选举;
  2. candidate将自己的任期号+1,并且给自己投1票,同时发送请求(requestVote)到其他服务器,也会将本地计时器重置,开始等待下一次超时;
  3. 如果candidate收到集群过半投票,那么candidate会被选举为leader和其他节点通信,其他节点都转为follower;
  4. 被选举的leader会周期性(term)的发送心跳到follower,如果follower在term时间内没有收到心跳就会转为candidate发起新的选举;

选举逻辑

  1. 每个服务器在一个任期内只能给自己投1票,并且只能投给先到者也就是到达自己的第一个请求;
  2. 请求投票的信息中要包含请求者所在的当前任期号;
  3. 必须包含最新、最全日志的节点才能被选为leader

日志复制

leader接收到客户端请求,然后将请求作为log entry加入到它的日志中,并行的向其他follower发起AppendEntries,当这条日志被复制大多数服务器上,leader再将它应用到状态机并向客户端返回执行结果。

  1. leader收到来自客户端的请求,将请求以log entry的形式追加到自己的日志中;
  2. leader并行的向其他节点发送日志复制信息(appendEntries RPC),并且leader在发送RPC日志的时候会附带这条日志之前日志的索引和任期号发送给follower
  3. follower接收到日志确认没有问题后(follower会结合leader发送的日志前一个日志索引和任期号来进行一致性检查),将log entry加入到自己的日志中,并向leader返回接受成功ACK;
  4. leader在随机的超时时间内收到大多数节点的ACK,然后将这条日志应用到它的状态机并向客户端返回结果;
  5. 如果有follower没有成功复制日志,leader会无限重试直到所有follower最终存储了所有的log entry;

示例

  1. 客户端发起一个请求save a此时日志还没有被其他节点接受,状态为uncommitted;
  2. leader将这个日志通过心跳发送给其他follower节点;
  3. 一旦大多数节点都写入了这条日志,那么leader将日志状态更新为committed状态并且更新值为a
  4. leader节点通知其他节点将值更新为a,那么此时集群中所有状态是一致的;

raft-log.png

以上这个过程就是日志复制。

日志组成

  1. 日志是由有序的编号log index条目组成;
  2. 每个日志包含了它被创建时的任期号term和用于状态机执行的命令;
  3. 日志如果被复制到大多数服务器上,就被认为可以commit了 raft-logentry.png
日志复制常见问题
  1. 一般情况下leader和follower的日志是保持一致的,因此follower的一致性检查一般不会失败;
  2. 如果发生leader切换之后,有可能会出现leader和follower日志不一致,但是最终follower上的日志会被leader的日志覆盖,这是因为leader为了确保follower日志同自己保持一致,会找到follower同leader日志不一样的地方,然后覆盖follower在该位置之后的日志条目保持一致;

安全性

通过上面知道了raft通过leader选举和日志复制来保障集群中各个节点的数据一致性,但是如果是理想情况下那么按照以上两点就可以保证集群中的数据一致,但是实际往往会更复杂。例如:

  1. leader将日志复制到大多数节点之后,进行commit时发生了宕机
  2. 某些follower没有复制到有些日志,然后参与选举成为leader

实际的情况多种多样,raft在leader选举+日志复制的基础上,进行了一些安全性的限制来保障状态机的安全和raft的正确性。

安全性限制

  • 只有拥有最新已经提交的log entry的follower才有资格成为leader,这点要求candidate在发送requestVote的时候要携带上自己最后一条日志的term和log index,其他节点在收到请求后会进行比较,如果发现自己的日志比请求的新,则拒绝投票(follower投票比较规则:先比较term,如果term相同,再比较log index,数值更大的表示更新)。
  • leader只能推进commit index来提交当前term的已经复制到大多数服务器上的日志,旧term日志的提交要等到提交当前leader所在term的日志来间接提交,这个限制主要是为了解决下面的场景: raft-safty.jpeg
  1. 阶段a term是2,s1是leader这时候s1写入日志(term,index)为(2,2)的日志,然后同步到s2
  2. 阶段b term是3,s1离线发生新的选举,s5成为leader写入term,index为(3,2)的日志;
  3. 阶段c term是4,s5离线发生新的选举,s1再次成为leader这时候s1同步日志(2,2)到s3,这时候由于日志已经被同步到大多数节点,此时可以提交(2,2)日志;
  4. 阶段d term是5,s1离线发生新的选举,s5再次成为leader(因为s5的日志(3,2)比大多数的节点日志(2,2)都新),于是s2,s3中已经被提交的日志(2,2)被覆盖了。

但是添加了限制之后,在阶段c即使日志(2,2)被大多数节点确认后,也不能被commit因为虽然leader还是s1但是term不同,此时的s1是term4的leader,term2的日志(2,2)需要等到term4的日志(4,3)被大多数follower确认后,随着(4,3)提交而提交;但是假如提交(4,3)的时候s1再次离线,那么也没问题,因为s5已经不满足成为leader的条件了,集群中大多数节点都是(4,3),s5的最新日志没有(4,3)新

日志压缩

日志压缩很容易理解了,就是为了节省磁盘空间,并且限制日志大小减少系统重启之后数据恢复的时间,从而影响性能。raft也是对系统进行快照来解决,snapshot之前的日志都可以丢弃,snapshot之前的日志一般都大多都落后leader很多,如果leader发现日志落后太多的follower,或者有新机器加入集群,leader通过发送installSnapshot RPC发送snapshot到follower。 snapshot包含:

  • 日志元数据最后一条已提交的log entry的log index和term,这两个值再snapshot之后的第一条log entry的appendEntries RPC的一致性检查会被用到;
  • 当前系统的状态

成员变更

参考链接

blog.51cto.com/liuminkun/2…
blog.csdn.net/zhengchao19…
baijiahao.baidu.com/s?id=173956…
blog.csdn.net/zhou9207863…
t.zoukankan.com/lanyangsh-p…
juejin.cn/post/690124…
www.dandelioncloud.cn/article/det…
blog.csdn.net/yzf27953310…
zhuanlan.zhihu.com/p/32052223