首先Paxos算法是分布式共识算法,分布式共识算法是为了解决分布式系统中什么问题?
分布式系统的设计目标之一是高可靠性,即在一堆不可靠的硬件上构建一个可靠的系统,保证高可靠的一个最常用手段是多副本策略,即同一份数据存储在多个副本上,这样即使一定数量的副本挂掉了,不会影响系统的整体运行,多副本解决了可靠性问题,但同时又带来另外的问题:一致性问题,即多个副本上数据必须是一致的,这个问题就需要共识算法解决。
Paxos算法作为分布式共识算法的一种,是要解决这么一个问题:在分布式系统中,在非拜占庭异步网络下,多个节点对同一个value赋值,Paxos算法保证这些发起赋值的多个请求中,只有一个能赋值成功,并且所有节点对这次成功的赋值达成一致。
有关Paxos算法的论文:
The Part-Time Parliament,中文翻译 : lamport最初在这篇论文中提出了Paxos算法,通过一个小岛上议员对法律达成一致这么一个故事,描述解释了Paxos算法的思想,但是由于其中的数学证明晦涩难懂,lamport老爷子后来又发表了一篇论文:Paxos Made Simple,中文版,进一步抽象了Paxos算法的过程,这部分也称为Basic Paxos,但是这篇论文的思想距离工程实践还是差距太远,为了提高算法效率以及更好的工程实现,演化出了Multi Paxos,但是Multi Paxos算法在很多细节上lamport没有阐述,导致各家在实现Paxos时,各有不同,其中比较著名的两种实现:Chubby和ZooKeeper,google在实现Paxos时发表了Paxos Made Live - An Engineering Perspective,中文版,阐述了在工程实现Paxos算法过程中的经验教训,最后lamport为了进一步提高Paxos算法的效率,发表了Fast Paxos。
可以将一个有状态的系统看成状态机,在接受一组命令集合时,状态机根据自身状态以及对应的输入产生确定的输出;
非拜占庭异步网络:
1、节点之间通过网络rpc通信
2、rpc消息可能丢失、延时、重传,但是节点不会恶意干扰,篡改、伪造消息
3、节点之间可能随时失联,也可能随时重新建立联系,同时可能有新节点加入集群或者老节点离开集群
如果在非拜占庭异步网络中,各个副本间的输入集合完全一致,即应用到各自状态机的指令顺序相同,那么各个副本间就可以实现一致。Paxos算法来保证在各个副本间的输入集合一致。
在没有Paxos算法之前,要想实现多副本间的一致性,主要有以下几种算法,以下算法在3个节点上实现节点间的一致性:
主从异步复制比较简单,一个master,二个slave,主节点收到client请求,应用到自己状态机,即可给client响应,然后异步发送给2个slave。
在master异步复制给slave之前,如果slave失联,主从便不一致。
使用这种方式的有Redis、Mysql
主节点收到client请求时,应用到自己状态机,然后同步复制给两个slave,等待收到slave成功的响应时,再回复client。
这种复制方式能够严格保证一致性,但是可靠性大大降低,如果一旦有一个slave不可用,整个系统及不可用。
主节点收到client请求时,应用到自己状态机,然后同步复制给足够多的slave,等到半数以上的slave成功的响应时,再回复给client。
这种方式保证了可用性,允许一部分节点不可用,同时也保证最终一致性。但是当master不可用时,剩下的slave可能出现数据不一致,从而数据丢失。
有N个节点,每次写入至少W个节点成功,每次读时读出R个节点,要满足条件:W+R > N时,能够保证读到最新写的数据,并且可以容忍(N-1)/2个节点不可用。
但是这种方式没有逻辑时序,容易出现读冲突,比如:ABC三个节点,第一次写x写入A、B两个节点,第二次写y到A、C两个节点,读请求只与B、C两个节点联通,读出来x、y,产生冲突
为了解决读冲突的问题,可以给每次写带上一个逻辑次序,最简单的带上时间戳,读时,谁的时间戳大,谁的值最新,就以谁为准。
使用这种方式的有Dynamo、Cassandra。
但是这样仍然不能保证数据一致性,比如第一次向A、B写入x,第二次向A、C写入y,但是此时client只写成功了C,没有写成功A,就挂了,当有别的client来读取时,如果读A、B得到x,如果读A、C得到y,仍然会有读冲突,本质原因还是quorum读写无法对某次失败的写做处理。
一次写入分为两个阶段,prepare和sync阶段,master接受到client写请求时,先发送一个prepare预写请求,带上本次的时序,等到所有的slave给master回复可写时,master在进行sync阶段,真正的写入数据。prepare相当于给slave加锁,只能本次写入,不允许其他时序的写入。
两阶段提交是实现分布式事务的方式,能够严格的保证一致性,原子写,但是可靠性不高,两个阶段中任何阶段slave没有相应,master就只能不停的重试。
两阶段提交可以保证一致性,quorum读写可以提高可用性,那么两阶段提交+quorum读写是不是就可以在保证一致性的同时,提高可靠性呢?基于这个思想,Paxos算法基本就呼之欲出了。
确定一个值仍为两个阶段:propose和accept阶段。propose阶段需要多数派响应才算成功,这样就保证了任一逻辑时刻不可能同意两个值写入,相当于把多数派锁住,保证原子性;accept阶段也要多数派响应才算成功,这样无论读写,多数派中必然包含最新写入的值。
上述措施能够保证在一个逻辑时刻,系统只能够唯一的确定一个值,然而这还不够,考虑这样一个场景:如果每次写请求都在accept阶段失败,这个值在整个系统看来是失败的,但是某些节点是成功的,如果这个值是符合因果律的,比如incr命令,系统中的某一个节点的值一直增加,其他节点的值不变,但是这个值是无效的,这种情况在故障的随机性会恶化到集群所有节点,这时候需要去修复这个节点的值,那么谁去修复,何时修复?propose的节点在propose时候去修复。
Paxos算法的完整介绍:Basic Paxos
Paxos算法中有三种角色:Proposer、Acceptor、Learner
Proposer:接受client请求,然后向Acceptor发起对值的赋值,即propose提案
Acceptor:提案发送给Acceptor,Acceptor对提案处理,同时还复制对值存储
Learner: 当系统中一个值被确定后,异步获取这个值,并存储,是一个可选角色
另外还有两个概念:quorum多数派,只有多数派确认,本次请求才成功;round汇合,系统中每次状态变更就是一个round,是一个逻辑时序,不减即可,不要求连续。
下面是Basic Paxos算法的具体过程:
phase1:prepare阶段:
phase2:
Proposer开始处理phase1的结果:
a、如果没有收到多数派的promise(包括超时和reject),则重新选择一个提案编号,这个编号要比所有reject(k)中最大的k要大,对phase1重试。
b、如果收到了多数派的promise(k,v),更新自己的value,设置为所有promise(k,v)中提案轮次k最大的v,如果没有,则保持不变。然后向所有Acceptor发送accept(n,v)请求。
Acceptor:收到accept(n,v)请求之后,如果没有响应或者接受过比n大的accept请求,则接受accept(n,v),并设置自己的value为v;否则回复reject(k),其中k是已经响应的最大提案编号。
当确定一个值后,除了确定这个值的Proposer之外,其他的Proposer和Acceptor都不知道确定的value是多少,想要读取这个值,就要进行一次多数派读,重新走一遍prepare过程,也就是phase1过程,如果得到多数派的应答,则拿到提案序号最大的value,就是要读取的value。
最后一个角色Learner:用来存储已经被确定的value,当Acceptor决定接受这个值时,就把这个值发送给Learner,当一个Learner收到多数派的value时,就认为这个值已经被确定了,存储下来,但是这样做传递的rpc消息数量是Acceptor个数*Learner个数。一个简化方法是选出一个"高级"Learner,Acceptor把确定的值发送给"高级"Learner,由"高级"Learner传递给其他Learner,但是这样会有单点问题。最后可以找一组"高级"Learner,由这一组"高级"Learner通知其他Learner。
基于brpc实现的Basic Paxos算法:Basic Paxos