首先我们这个平台是ES专题技术的分享平台,众所周知,ES是一个典型的分布式系统。在工作和学习中,我们可能都已经接触和学习过多种不同的分布式系统了,各有各的区别,但也有很多共性。不知道大家在接触过这么多分布式系统后,会不会有下面这些疑问和困惑:
1.不同的分布式系统采用了不同的架构模式,比如主备模式(primary-backup)、领导者跟随者模式(leader-follower)、主仆模式(master-slaver)等等,这些模式有什么区别,应用场景是什么?
2.我们了解ES的分片机制,主分片数量一旦确认后不允许修改,我们知道是因为数据的路由机制和主分片数量直接关联导致的,所以不允许修改;但是同样是分布式的Mongodb却可以修改动态修改分片数量,凭什么,是ES比Mongo菜吗,除了物理文件的结构限制外,它们分布式实现机制又有什么不同约束?
3.“死锁”的概念我们大家都知道,在分布式算法中,还有一个“活锁”的概念,这个又是怎么回事儿呢,在paxos和raft中各自出现的场景是什么,各自又是怎么规避和削弱这种现象的?
4.“空洞”这个概念在分布式系统中是什么意思呢,“空洞”出现意味着数据丢失吗,各种算法是怎么规避和处理的?
......
这样的问题还有很多,我这边就试图从分布式算法和原理的角度去抽丝剥茧,理顺这些相似但是又各自区别的概念和问题。
所以,这篇文章不会以ES为主线和大家分享ES技术,而是会一个系列文章的方式将我们常见的分布式系统的一些问题逐一分解,这里我们只抓分布式这一个切面,和大家一起尝试从实现的角度从底往上的角度看看它们是怎么做的。系列文章会比较枯燥,篇幅也会很长,计划将系列文章拆分成
架构模式
分布式算法
常见分布式系统纵向解析
分布式系统横向对比
这四个部分,逐个展开,系列文章目录大体如下安排,这只是初步规划,在后续行文过程中也会考虑到篇幅等的原因,可能会再拆分出更细粒度的章节:
1.基础架构模式
2.分布式算法-Paxos
3.分布式算法-Raft
4.分布式算法-Zab
5.HDFS/GFS的分布式实现
6.HBASE/BigTable的分布式实现
7.TiDB的分布式实现
8.Kafka/RabbitMQ的分布式实现
9.Zookeeper的分布式实现
10.Etcd的分布式实现
11.MongoDB的分布式实现
12.ES的分布式实现
13.横向对比总结
分布式是一个很大的话题,也是很有争议性的话题,这里面没有一个概念是笔者自己创造的,都是前人的智慧结晶,为了保障这个系列文章的质量和体系化,笔者也参考了大量博客、文章,当然也包括原作者论文,里面没有无脑的复制粘贴,但也参阅了部分其他人的说法和描述,同时也修正了网上部分博客错误解读的内容(错误的部分我会在后续具体细节地方逐个说明),甚至原论文中也有笔误的疏漏(到了具体位置我也一一指出来,这些内容也有业界大佬同原作者核实过,原作者也回复确实是疏漏),只是希望能够通过简单易懂的形式将这些深奥的问题说得大家能看明白是怎么回事儿,算是扫盲吧,有错误或理解不准确的地方,也请各位指正。
另外我们是一个ES技术分享平台,最终我们的落脚点还是ES,所以在其他各个阶段,会时不时把ES拿出来做一个简单对比,而最后两章,也会回归到ES技术上来。我们开始吧。
架构模式放在第一部分,是因为这是一个相对比较容易理解的部分,同时了解这些基础模式,也便于后续章节算法中各类角色理解。
1.首备模式(primary-backup): 顾名思义,有两类角色,primary和backup。primary工作,处理客户端请求;backup不工作,不处理客户端请求,它的存在仅仅是作为primary的备份,以及在primary挂了的时候顶替上去。2.领导者跟随者模式(leader-follower): leader工作,处理客户端请求(写+读 or 写);follower也工作,仅负责读请求。同时,leader和follower之间,或拉或推的方式,follower复制leader上的数据。3.主仆模式(master-slaver): master不工作,仅给slaver分配任务;slaver工作,具体执行master分配的任务。
常见的架构模式很多,这里只是从角色分工的角度列举了三个基本的模式(模式的名称在不同的文章里面叫法不同,这个不是特别重要,大家能看明白意思和区别就好),还有其他很多模式很大程度上都是在这三种模式上的变种和扩展。在一个具体的分布式系统中,也往往是多个模式的组合和嵌套。比如zookeeper里面,就同时使用了首备模式和领导者跟随者模式;Hbase里面,使用了主仆模式;而ES里面的分片,也是首备模式与领导者跟随者模式的组合变种。
另外,在一些质量不是很高的博文中,会有拉郎配的描述,比如把master和follower搞一块儿,或者把leader和backup扯在一起,说这些并不是我们吹毛求疵,因为即使是上述三种模式的衍生模式,命名用词也会很准确的,翻译过来的中文名我们不谈,但是英文名primary-backup、leader-follower、master-slaver是固定搭配,这是根据角色关系和功能来命名的。如果我们看到有搭配不一致的文章,我们应该保持一个谨慎的态度,很有可能博文作者在书写博文的时候对这些关系的理解是混乱的,后续文章的阅读要更加小心些。
众所周知,分布式算法中,paxos名声最响,也是被公认为是一种“科学”,它是经过数学严密证明的算法。这里我们就不去证明各个算法的科学性,就说说各个算法流程。坊间评论,从理解性的角度来说,paxos最难,zab次之,raft最简单。我们这里先从paxos开始,整完paxos后,再去看raft和zab,虽说没有像喝水一样简单那么夸张,但是也确实轻松很多。
说算法之前,先列举一些的具体例子,像Google的BigTable、Spanner,阿里的PolarDB,以及大名鼎鼎的TiDB都是这个算法具体实现的成功产品。另外,我们ES的分布式算法中,也有paxos算法的乞丐版实现,到了后续ES章节,我再详细展开描述。
Paxos是Lamport提出的算法,算法包含两部分,第一部分是核心算法(Lamport论文中叫“Paxos Consensus Algorithm”, 国内翻译为“共识算法”,行业内多叫“Basic Paxos”),另一部分是基于核心算法的扩展算法(Paxos的完整算法,论文中叫“Paxos Implementing a State Machine”, 国内翻译为“状态机算法”,行业内多叫“Multi-Paxos”)。不论哪种算法,Lamport都只是提供了思想,并没有给出具体实现细节,而我们现在看到的各种Paxos算法,都是大家基于这个论文的理解各自的实现,版本很多,这也是这个算法不好理解的原因之一。
打个比方,Lamport之于Paxos算法的描述,可以理解为赵本山对于“怎么把大象放到冰箱里面去”的回答一样,第一步打开冰箱,第二步把大象放进去,第三步把门关上,他只告诉我们步骤,至于每个步骤具体怎么干,他并没有提及,这个由实现者自己去实现。
在描述算法之前,这里先解释几个概念,以免我们理解出现分歧:
1.大多数:指的是 acceptor,如果共有2n+1个或者2n个 acceptor,那大多数就是n+1个。一般 acceptor 都是奇数个。
2.提议:实际是一个键值对,key是提议编号,value是proposer 提供给 acceptor的一个值,如果我们用n表示编号,v表示提议的值,后续一个提议我们都用 {n,v}这种结构表示
3.提议编号:每一个提议都有一个全局唯一的编号,这个编号由proposer 生成,由一个序号和进程号组成:n = serialNum + pid(注意,序号占高位,进程号占低位), 这个编号全局单调递增。
这个算法的描述很简单,只有一百来个单词,这也是Lamport的风格,他擅长用极简的方式完整准确地描述复杂的问题,就像Paxos算法一样,去掉一个单词或者颠倒个位置,意思就不对。下面,我先将算法中最核心的原文摘抄出来,供大家参考,后续的解读,也都是基于论文的描述,阅读中途如有什么困惑,可随时翻回到这个位置来对照,如果有错误,大家也容易核对:
Phase 1:
(a) A proposer selects a proposal number n and sends a prepare request with number n to a majority of acceptors.
(b) If an acceptor receives a prepare request with number n greater than that of any prepare request to which it has already responded, then it responds to the request a promise not to accept any more proposals numbered less than n and with the highest-numbered proposal (if any) that it has accepted.
Phase 2:
(a) If the proposer receives a response to its prepare requests (numbered n) from a majority of acceptors, then it sends an accept request to each of those acceptors for a proposal numbered n with a value v, where v is the value of the highest-numbered proposal among the responses, or is any value if the responses reported no proposals.
(b) If an acceptor receives an accept request for a proposal numbered n, it accepts the proposal unless it has already responded to a prepare request having a number greater than n.
注:文中有几处加红的地方,主要是为了区别请求的类型,为了后面讲解算法过程方便描述,我们这里姑且将不同角色不同阶段的请求做一个简单命名(只是我们这里方便描述暂时命的名)。
1.将 proposer(我们简称为P) 第一次发送给 acceptor **(我们简称为A**)的请求称为 prepare request2.将 A 返回给 P 的请求称为 promise request3.将 P 第二次发给 A 的请求称为 accept request (也叫“提议”)
下面,我提供一份简单的翻译和说明:
阶段一:
(a)一个 proposer 向大多数 acceptor 发送一个 携带编号为n的 prepare request
(b)如果 acceptor 收到一个编号为n的 prepare request ,则检查之前是否收到比编号n更大的prepare request 。如果收到过比n大的编号,则拒绝处理这个请求;否则回复 proposer 一个 promise request , 这个请求编号仍然为n,并承诺不再接受比编号n还小的 prepare request 了,如果在此之前还收到过其他的提议(这里指的其实就是下一阶段提到的 accept request ),同时在返回的 promise request 请求中,还携带有在此之前收到过的编号最大的那个提议中的value值(就是 accept request 的值)。
阶段二:
(a)如果 proposer 收到大多数 acceptor 返回的编号为n的 promise request ,则从 promise request 中取出携带的值value,如果value为空,可以自己决定生成一个值赋给value;同时向所有回复自己的 acceptor 发送一个 accept request,这个请求编号仍然为n,请求中还携带一个前面取到的value。这个 accept request 在算法中有个专门的名称,叫作“提议”,前面第一阶段(b)环节中提到过这个概念。
(b)如果一个 acceptor 收到一个编号为n的 accept request (提议),并且它没有再收到了一个比编号n还大的 prepare request ,它就应该接受这个编号为n的 accept request(提议)。
显然,这是一个两阶段算法,下面就细节做简要说明:
第一阶段,proposer 生成 提议编号,并通过 prepare request 的方式将编号告诉 acceptor ;如果 acceptor没见过这么大的编号,就返回一个promise request 给 proposer,并且捎带手,还把自己这边编号最大的提议的值告诉给proposer(这个值可能为空,比如在初始化的时候,它这边是没有收到过提议的);并向 proposer 承诺,我不会再接受比你编号小的 prepare request 和 accept request了,言外之意如果比你编号大的请求,我还是会接受的,所以说,proposer收到承诺后,需要在其他线程再给acceptor请求前,赶紧把第二阶段的活干完,不然前面就都白忙活了。
第二阶段,proposer 收到承诺后,赶紧看看 acceptor 有没有返回值,如果返回了值就用它返回的值,这里需要选取一个编号最大的值,因为之前是发给了多个 acceptor,每一个 acceptor 带回来的值的编号可能不一样,如果没有值,自己就生成一个值,并把这个值放到 accept request(提议)中,发给 acceptor ;acceptor 收到 accept request(提议)后,会再次检查请求编号,看是不是它迄今为止收到的最大的一个,是最大的才会接受。
为了加深理解,这里画一个简单流程图,因为这里还没有开始涉及复杂的部分,就弄个简单的示意一下,大家明白意思就好:
做一个形象的比喻:好比一个男的要找个女朋友,他会先向多个女生表明心迹,展示自己的实力(编号),女生看到编号后,会对比她之前遇到的男生,她会选取实力最好的那一个男生并回复,在回复里面也会夹带她之前遇到的哪个男生如何优秀之类的信息;男生收到回复后,会看看女生有没有夹带私货,如果有,就把私货拿出来,并告诉女生那些优点我也有,要是女生没有夹带私货,就自己瞎编一个,当信息再发回女生赶紧确定关系;女生收到回复后,会再次检查这个男生是不是最优秀的,如果是就接受这个男生并接受他带过来的信息,否则就不搭理他。当然这只是一轮流程,即使接受后,后续这个流程还会不断循环往复下去。
建议看到这里的读者,再回头看一下上面论文的英文叙述,相信再次回头读那段描述我们会比第一次读到原文时的感觉好大不相同。也许有人会疑惑,这这不是挺简单的吗,就是挑个儿大的,很容易理解,确实是这样子的,上面部分是算法最核心的部分。Paxos的核心在于共识算法,而共识算法的核心在于值的选取,上面的过程就是值选取的过程,也是核心中的核心。因为核心算法很简单,所以我只把这一段放在简介部分,下面我再开始一致性方面的解读,我们把上面正常的环节一点点去打破,而在正常流程被破坏掉时,系统还需要保持一致性,复杂的环节慢慢开始。
简介部分描述的是算法的核心部分,就是共识算法中选择值的那一部分,实际的共识算法包含选择值和学习值两个部分,用一个伪代码把整个共识算法表示一下:
// 完整的共识算法,x代表拟提议的一个值,并返回一个算法最终确认的值
public int consensus(int x) {
// 这个动作由proposer发起,acceptor接受
// 选择一个值,就是将提议发给大多数acceptor,并将选择的t返回回来
// 这个算法要保证幂等,不论执行多少次,返回结果应该是一样的
int t = proposer.select(x);
// 这个动作由acceptor内部发起,在内部达成一致
// 将上述选择的值在真个集群范围内广播,使每一个acceptor都接受t
acceptor.learn(t);
// 共识算法完成,返回t
return t;
}
这部分讲解这一段:
int t = proposer.select(x);
上面我们简单介绍了算法的选择值部分,是用一个简单的模型描述的,只有一个proposer和一个acceptor进程,情形比较好理解,实际上完整算法都是多个proposer和多个acceptor的。我们先用一个简单例子把多proposer、acceptor场景流程捋一遍。
为了方便描述,我们作以下约定:
prepare request 简写为 PR,PR(n)表示proposer向acceptor发送一个提议编号为n的 prepare request 。
promise request 简写为 PM, PM(n,{m,v})表示acceptor向proposer回复一个promise request ,n代表当前提议编号,m代表acceptor在此之前收到最大编号的提议中的编号,v为最大编号的提议中的值。
accept request 简写为 AC, AC(n,v)表示proposer向acceptor发送一个提议编号为n的accept request ,v代表要提议的值。
整个流程执行如下:
1.proposer1 想要提议x1,于是向大多数acceptor(acceptor1、acceptor2、acceptor3)发送一个编号为1.1的PR请求2.acceptor1、acceptor2、acceptor3收到PR请求后,都发现没有收到过比1.1还大的请求,于是向proposer1回复一个编号为1.1的PM请求,由于之前没有收到请求,所以值都是null3.proposer1收到PM请求后,发现所有回复里面均没有值,所以将自己想要提议的x1放到AC请求中,仍然以编号1.1发送给acceptor1、acceptor2、acceptor34.acceptor1、acceptor2、acceptor3收到AC请求后,都发现没有收到过比1.1还大的请求,于是接受请求中的值{1.1, x1}5.proposer2 想要提议y1,于是向大多数acceptor(acceptor3、acceptor4、acceptor5)发送一个编号为2.2的PR请求6.acceptor3、acceptor4、acceptor5收到PR请求后,都发现没有收到过比2.2还大的请求,于是向proposer1回复一个编号为2.2的PM请求;由于acceptor4、acceptor5之前没有收到请求,所以值都是null;acceptor3之前收到过{1.1, x1},所以返回值中带有{1.1, x1}7.proposer2收到PM请求后,发现acceptor3返回的值编号最大(acceptor4、acceptor5的值为null),于是放弃自己想要提议的y1,采用{1.1, x1}中的值x1,向acceptor3、acceptor4、acceptor5发送编号2.2的AC请求8.acceptor3、acceptor4、acceptor5收到AC请求后,都发现没有收到过比2.2还大的请求,于是接受请求中的值{2.2, x1}9.至此,两轮流程执行完毕,集群中所有acceptor接受的值一致,均为x1
完整看完一个流程后,我们需要总结一下集群中proposer和acceptor在算法中需要持久化保存的一些信息,这些信息是算法执行的基础数据:
proposer :需要持久化一个自己上次使用过的编号值,我们姑且叫“sended serial no”,简称p.sn
acceptor :需要持久化一个自己承诺过的编号值,我们姑且叫“promised serial no”,简称a.pn
acceptor:需要持久化一个自己接受过的值和它对应的编号,我们就叫“accepted value”, 简称{a.an, a.av}
在具体执行过程中,这些值的变化规则如下:
阶段一:
(a) proposer生成一个新的编号n,满足 n > p.sn,发送PR(n)给大多数acceptor
(b) acceptor收到PR(n),并且 n > a.pn,则令 a.pn = n,如果收到过{a.an, a.av},则回复PM(n, {a.an, a.av}), 如果没有收到过{a.an, a.av},则回复PM(n, null)
阶段二:
(a) proposer收到PM,从消息中取出返回的提议值,并选取编号最大的那个值,如果PM并没有返回提议值,则使用自己要提议的值,发送AC(n, v)
(b) acceptor收到AC(n, v),并且 n >= a.pn ,则令{a.an, a.av}中的a.an = n,a.av = v
注:在上面阶段二(b)中我标红了一处“ n >= a.pn ”,这处标红目的是因为在很多博文中这里的条件写的是 “n > a.pn”,并没有“=”,甚至在一些BAT大佬写的书中也是没有等于号的。但是我认为这里是不对的,这里需要包含等于的场景,因为在阶段一(b)中,已经有了“令a.pn = n”的动作,所以这里的a.pn不再是原来的a.pn了,如果没有等于号,正常情况下算法在这里是选不出值来的。另外论文原文的描述是“it accepts the proposal unless it has already responded to a prepare request having a number greater than n”,除非它在此之前接受了另外一个比n大的prepare request,所以它就应该接受当前的值,换句话说如果当前的编号等于n,它是应该接受的。论文的描述有些绕弯儿,所以这里可能会被解读错误。当然那些写书的大佬水平肯定比我高,我也是看着他们的文章在学习,只是看到这个逻辑不太对劲儿,深究了一下,也许是我自己理解的不对,读者在这里也可以推演一下,我们可以一起交流学习。
正常的流程是基于proposer和acceptor的请求是有序的,一个接一个的,进程请求顺序没有并发和错乱。但是实际情况没有那么理想,往往复杂的情形会更多。下面我们就一起来分析一下复杂场景下paxos算法的表现。
为了模型简单,我们先控制住acceptor变量,同时也只有两个proposer进程参与,我列举以下四种典型场景:
情形一:proposer1发给大多数的acceptor的AC请求被打乱,中间插入了proposer2的PR/AC请求
情形二:proposer1发给大多数的acceptor的PR请求被打乱,中间插入了proposer2的PR/AC请求
情形三:proposer1发给大多数的acceptor的AC和PR请求被打乱,中间插入了proposer2的PR/AC请求
情形四:proposer1和proposer2各自发给大多数的acceptor的AC和PR请求相互嵌套
情形一
proposer1发往acceptor1、acceptor2、acceptor3的AC请求被proposer2打断,其中发给acceptor3的请求正常,但是给acceptor2的AC请求前面被proposer2的PR请求插队了,给acceptor1的AC请求前面又被proposer2的AC请求插队了,幸运的是acceptor1和acceptor2都同proposer2没有交互,所以最终集群还能保持一致性,均为x1。
情形二
proposer1发往acceptor1、acceptor2的PR请求被打乱,中间插入了proposer2的PR/AC请求,acceptor1由于没有和proposer2交互,不会受影响,proposer1发给acceptor2的PR由于落后于proposer2发给acceptor2的PR,所以会被拒绝。最后一致性被破坏,acceptor1的值为x1,acceptor2、acceptor3的值为y1。该如何补救呢,看下面图中红色部分:
出现不一致后,proposer1在下一次提交提议时,只要不被再次打断,经过一个完整的流程后,会纠正回来,最终整个集群数据一致,均为y1。
情形三
proposer1发给acceptor3的PR和AC请求被proposer2插队,acceptor3接收了proposer1的PR后,令a3.pn=1.1,紧接着接收了proposer2的PR后,令a3.pn=2.1,这个时候再接受到proposer1的AC请求,发现p1.n
出现不一致后,proposer1在下一次提交提议时,只要不被再次打断,经过一个完整的流程后,会纠正回来,最终整个集群数据一致,均为y1。
情形四
proposer1和proposer2发给acceptor2的AC请求,均被对方下一轮的PR请求取消了,如此循环往复,就存在永远都无法达成一致的可能,这个现象就是活锁。
以上列举了四种典型的异常流程,其实还有很多不同的排列组合,如果再放开对acceptor的限制,情况会非常复杂,大家有兴趣可以试试,但是基本都能归为这四类。我们先作一个异常流程的总结:
情形一:问题不大,不会有一致性影响
情形二和情形三:都存在短暂不一致的情况,但是在后一轮算法正常执行后,系统会恢复一致性,这两种本质是一种情形,为了逻辑清晰,我拆成了两个独立的情形分别画图描述。在算法的某一个阶段,集群中部分编号较小的AC并没有被大多数acceptor都接受(它们的整个生命周期中都没有在整个集群中被认可),它们的命运注定被后面编号更大的AC覆盖,针对这种短时间被少部分acceptor接受而后面被覆盖的数据,称为空洞(这只是一个低级别的空洞现象,后续Multi-Paxoy算法中还有一个更高级别的空洞现象),它们的数据可以认为是被遗弃丢失了。
情形四:这种情况可能永远无法达成一致,但是它的成立条件非常苛刻,即使是成立后再保持下去都是很困难的,一旦网络平衡打破,这个活锁就释放了。
不论空洞还是活锁,都不是算法本身的Bug导致的,而是复杂的网络环境造成的,只要不是Bug,我们都是可以想办法规避和削弱的,我们再看看算法是如何做的。
因为这一部分只是选择值的部分,仅这一个部分,还不能完全规避算法中的活锁和空洞问题,因为这一阶段算法还有两个本质问题没有解决:
1.数据空洞无法感知,它只是在集群部分节点有数据,其他节点并不知道;
2.站在单个acceptor的角度,它并不知道当前所持有的数据是不是整个集群的最终结果,因为每一次AC只是发给大多数节点,并且大多数节点还不一定都能成功,还需要依赖后续的AC来追平整个集群中的数据,所以任一单节点的数据都是迟疑的,甚至它自己都不晓得当前阶段它的数据是不是最新的。
为了解决这些问题,有了算法第二部分,学习值过程。
这部分讲解这一段:
acceptor.learn(t);
学习值的定义很简单,也是两个步骤:
(a) 任意一个acceptor接受了一个AC后,应该向其他acceptor广播这个值
(b)一个acceptor接受到另一个acceptor发来的广播后,应接受这个提议;如果从大多数acceptor接受到某个提议,则应该接受这个提议中的值。
我们将一个acceptor发给另一个acceptor的 learn request,简称为L,请求携带参数{n, v},完整的请求描述为L({n,v})
过程挺好理解的,就是有一个小问题,每提议一个值,L请求的数量 = 大多数acceptor节点的数量 * (所有acceptor节点的数量 - 1)。为了减少L请求的数量 ,算法指定一个acceptor 为 distingguished acceptor,以后就只由distingguished acceptor向其它acceptor广播消息,这个消息简称LL,它携带提议值给其它acceptor。注意LL请求只携带提议值,不携带标号,所以一个LL请求是这样的,LL(v)。
到这里,空洞和感知问题解决了,也许有人会疑问,proposer不是把AC请求发给大多数acceptor吗,万一distingguished acceptor不在大多数之中,岂不出问题了吗?解答这个问题之前,我们再回顾一下上面选择值部分的活锁问题,它是因为多个proposer的请求相互嵌套导致的,于是算法在这里又对proposer作了限制,选择一个proposer作为distingguished proposer,并规定只有distingguished proposer才可以发起提议,它提议的时候一定得发到distingguished acceptor上去。这样就构成了这样一个数据链路:distingguished proposer发起提议,distingguished acceptor接受AC,distingguished acceptor广播LL到其他acceptor。
上面我们的例子都是proposer和acceptor分离的,在生产实践中其实是proposer和acceptor合在同一个实例中去的,当一个distingguished proposer和一个distingguished acceptor合并到一个实例上时,称这个实例为leader。下面再用一个简图回顾一下整个流程:
这个模型看起来就很清晰简单了,Basic-Paxos描述的算法,最终可以套用这个模型来抽象:distingguished proposer和distingguished acceptor合成一个leader,proposer只有一个实例,acceptor有多个实例;这是一个典型领导者跟随者模式,proposer与acceptor合并了的实例是leader,其他部署了acceptor的实例是follower;leader负责提议和接受外部读请求,follower仅接受外部读请求;leader和follower通过LL同步数据,使整个算法数据一致;即使leader挂了,其他实例也会以合适的角色和方式顶上去,容灾性也有保证。distingguished proposer和distingguished acceptor的选择并不是Paxos的必要条件,它们只是保证算法高效运行的手段,即使没有,也不影响算法的正确性。
至此,Basic-Paxos算法(共识算法)解读完毕,当然还有好多细节我这里就不多展开了,有兴趣的朋友欢迎交流沟通。看到这里可能会有朋友要骂街了,说最后就只搞一个proposer,那上面弄那么多场景干嘛,逗大家玩儿吗?当然不是这个意思,因为这只是Basic-Paxos算法,后面还有Multi-Paxos算法,在下面的扩展算法里面情况会更加复杂。
众所周知,Paxos的核心算法很简单,真正难以理解的Multi-Paxos算法,因为论文没有具体说明怎么实现,所以也有多种模式,这里计划选取两种模式介绍一下。
由于考虑到单篇文章篇幅的问题,我看就Multi-Paxos这一个部分的篇幅就会超过这篇文章之前所有的内容了,我写得累,毕竟不是纸质书本之类的传递方式,各位看得也辛苦麻烦。所以这里暂时就先将核心概念和流程介绍一下,有过各位读者对这个话题很有兴趣,我们后面再专门抽一个章节来和大家一起交流下Multi-Paxoy的细节实现方面的问题。
Multi-Paxoy简单说就是将原来的Basic-Paxos做了包装,因为一个Basic-Paxos确认一个值,多个Basic-Paxos包在一起,出多个值,形成一个行列式。主要有两种包装方式,一种是只有一个paxos算法实例,放在循环里面逐次执行;另一种方式是多个paxos算法实例实现,但是多实例共用一个PP请求。
在Multi-Paxoy算法中会,会这涉及好多我们熟悉的话题,比如:空洞、选举算法、脑裂等等。
这里先用一个简单的伪代码描述一下:
// 这里的i表示循环的轮次,可理解为提议的轮次
for(int i = 0; i++; i < n) {
// 各实例共用PP请求
proposer.propose(i);
while(i++) {
// 选出该轮次的值
t[i] = acceptor.accept(i, x[i]);
// 广播该轮次的值
acceptor.learn(t[i]);
// 算法失败,leader宕机等等
if (failed) {
break;
}
}
}
这里先用一个简单的伪代码描述一下:
// 这里的i表示循环的轮次,可理解为提议的轮次
for (int i = 0; i++; i < n) {
// consensus()多了一个i参数,指的是提议的轮次,x[i]只该轮提议具体要提议的值
// t[i]表示第i轮返回的值
t[i] = consensus(i, x[i])
}
因为这里需要依赖Multi-Paxos部分知识,故这里仅列出目录结构,安排在后面Multi-Paxos的章节补充
这篇文章是我们这个系列的第一章,主要内容有三个方面:
1.系列文章的结构安排2.架构模式介绍3.Paxos核心算法解读
另外,考虑到篇幅问题,Multi-Paxos算法只是写了写主流程,Paxos的工业实践也仅仅放了目录结构。如果大家有兴趣一起学习交流这个话题,我们将在后面单独拿一个章节把这个话题说完。下一章嘛,我们先聊一个稍稍简单些的raft算法缓和下节奏。
长按加关注,更多干货内容不错过