区块链共识机制:分布式系统的Paxos协议

前言:第一次接触paxos可能很多人不理解这玩意儿有啥用,近几天一直在研究paxos,不敢说理解的多到位,但是把自己理解的记录下来,供大家参考。文章主要参考知行学社的《分布式系统与Paxos算法视频课程》和知乎话题https://zhuanlan.zhihu.com/p/29706905,希望能对大家有帮助。


一、什么是Paxos,解决什么问题?

(一)Paxos是分布式系统中,在异步通信环境下,可以容忍在只有多数派机器存活(容许半数一下节点宕机)的情况下,仍然能完成一个一致性写入的协议。

异步通信环境并非只有paxos能解决一致性问题,经典的两阶段和三阶段提交(参考:https://blog.yangx.site/2018/08/05/paxos/也能达到同样的效果。但是paxos和它们这些妖艳水货不一样,因为在分布式环境里面,除了消息网络传输的恶劣环境,还有另外一个让人痛心疾首的,就是机器的当机,甚至永久失联。在这种情况下,两阶段提交将无法完成一个一致性的写入,而paxos,只要多数派机器存活(一半以上)就能完成写入,并保证一致性。

(二)解决什么问题

paxos用来确定分布式系统中确定一个不可变变量的取值。(不理解没关系,往下看)

(1)取值可以是任意二进制数据。

(2)一旦确定将不再更改,并且可以被获取到(不可变性,可读取性

一般的分布式存储系统中

(1)数据本身可变,采用多副本进行存储。

(2)多个副本的更新操作序列【Op1,Op2,……,Opn】是相同的、不变的。

(3)用Paxos依次来确定不可变量Opi的取值,即第i个操作是什么?(重点)

(4)没确定完Opi之后,让各个数据副本执行Opi,依次类推,从而实现分布式系统的一致性。

应用案例:google的chubby、Megastore和Spanner都采用Paxos来对数据副本的更新序列达成一致。


二、paxos的实现过程和实现原理是怎样的?分为哪些步骤?

为方便理解,以下通过案例来循序渐进的讲。

(一)案例设计:(分布式系统的理解可以参考资料刘杰的《分布式系统原理介绍》

1、设计一个分布式系统,用来存储名称为var的变量。系统描述如下:

(1)系统内部有多个Acceptor组成,负责存储和管理var变量。

(2)外部有多个Proposer机器任意并发调用API,向系统提交不同的var取值。

(3)var的取值可以是任意二进制数据。

(4)系统对外的API库接口为:propse(var,V) => <ok,f> or <error>,如果var的值已经被系统确定了,则返回V,如果没被系统确定则返回error;如果系统中的某一Proposer将var的设置为V,则f就代表V,否则f为其他Proposer设置的值。

2、系统需要保证var的取值满足一致性

(1)如果var的取值没有确定,则var的取值为null;

(2)一旦var的取值被确定,则不可被更改。并且可以一直获取到这个值。

3、系统需要满足容错特性

(1)可以容忍任意的Proposer机器出现故障。

(2)可以容忍半数一下的Acceptor出现故障。

4、注意,为了容易理解,暂不考虑

(1)网络故障。

(2)Acceptor故障丢失var的信息。

5、设计这一系统的难点是什么?

(1)管理多个Proposer的并发执行

(2)保证var变量的不可变性

(3)容忍任意Proposer机器故障

(4)容忍半数一下Acceptor机器故障

(二)方案一:基于互斥访问权的实现:

1、基于互斥访问权的acceptor的实现:

(1)Acceptor保存变量var和一个互斥锁lock

(2)接口Acceptor::prepare():

加互斥锁,给予var的互斥访问权,并返回var当前的取值f。

接口Acceptor::release():

解互斥锁,收回var的互斥访问权

接口Acceptor::accept(var,V):

如果已经加锁,并且var没有取值,则设置var为V。并且释放锁。

2、propose(var,V)的两阶段实现:

第一阶段,通过Acceptor::prepare获取互斥访问权和当前var的取值,如果不能获取到,说明锁被他人所占,返回<error>;如果可以获取互斥访问权,则进入第二阶段,否则,结束。

第二阶段,根据当前var的取值f,选择执行。

如果f为null,则通过Acceptor::accept(var,V)提交数据V。

如果f不为空,则通过Acceptor::release()释放访问权,返回<ok,f>。

3、补充概念:互斥锁(英语:英语:Mutual exclusion,缩写 Mutex)是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。需要此机制的资源的例子有:旗标、队列、计数器、中断处理程序等用于在多条并行运行的代码间传递数据、同步状态等的资源。

总结:

解决了多个Proposer的并发执行问题并保证var变量的不可变性,但是不能容忍任意Proposer机器故障,若Proposer在释放互斥访问权之前发生故障,会导致系统陷入死锁。

 

方案二:引入抢占式访问权

1、特点:

(1)Acceptor可以让某个Proposer获取到的访问权失效,不再接受它的访问。之后,可以将访问权发放给其他的Proposer,让其他Proposer访问acceptor。

(2)Proposor向Acceptor申请访问权时指定标号epoch(越大的epoch越新),获取到访问权之后,才能向acceptor提交取值。

(3)Acceptor接受到更大的新epoch的申请,马上让旧的epoch的访问权失效,不再接受他们提交的取值,然后给新epoch发放访问权,只接受新epoch提交的取值。

2、为何能解决proposer机器故障

新的epoch可以抢占旧epoch,让旧epoch的访问权失效,旧epoch的proposer将无法运行,新epoch的proposer将开始运行。

为了保证一致性,不同epoch的proposer之间采用“后者认同前者”的原则:

  1. 在肯定旧epoch无法生成确定性取值时,新的epoch会提交自己的value,不会冲突。
  2. 一旦旧的epoch形成确定性取值,新的epoch肯定可以获取到此取值,并且会认同此取值,不会破坏。

3、具体实现:

基于抢占式访问权的Acceptor实现:

(1)Acceptor保存的状态

当前var的取值<accepted_epoch, accepted_value>

最新发放访问权的epoch(latest_prepared_epoch)

(2)Acceptor::prepare(epoch):

只接受比latest_prepared_epoch更大的epoch,并给予访问权,

记录latest_prepared_epoch = epoch,返回当前var的取值。

(3)Acceptor::accept(var,prepared_epoch,V):

验证latest_prepared_epoch == prepared_epoch,

设置var的取值<accepted_epoch,accepted_value> = <prepared_epoch, v>

4、Propose(var,V)的两阶段实现:

第一阶段,获取epoch轮次的访问权和当前var 的取值

简单获取当前时间戳为epoch,通过Acceptor::prepare(epoch),获取eooch轮次的访问权和当前var的取值,如果不能获取,返回<error>。

第二阶段,采用“后者确认前者”的原则执行。

(1)在肯定旧epoch无法生成确定性取值时,新的epoch会提交自己的value,不会冲突。

(2)一旦旧epoch形成确定性取值,新的epoch肯定可以获取到此取值,并且会认同此取值,不会破坏。

  1. 如果var的取值为空,则肯定旧epoch无法生成确定性取值,则通过Acceptor::accept(var,epoch,V)提交数据V,成功后返回<ok,V>;如果accept失败,返回<error>,此时Acceptor被新epoch抢占或者acceptor出现故障。
  2. 如果var取值存在,则此值肯定是确定性取值,此时认同它不再更改,直接返回<ok,accepted_value>。

 

核心思想:让Proposer将按照epoch递增的顺序抢占式的一次运行,后者会认同前者。

解决问题:可以避免Proposer机器故障带来的死锁问题,并且仍可以保证var取值的一致性。

存在问题:仍需要引入多Acceptor。单机模块Acceptor可以使故障导致整个系统宕机,无法提供服务。

 

方案三、都让开,Paxos要闪亮登场,亮瞎咱们的狗眼了,Paxos是在方案2的基础上引入多Acceptor。

1、特点

(1)Acceptor的实现保持不变。仍采用“喜新厌旧”的原则运行。

(2)Paxos采用“少数服从多数”的思路,一旦某epoch的取值f被半数以上的Acceptor接受,则认为此var取值被确定为f,不再更改。

2、Paxos的组成元素:

(1)三个参与角色:

  1. Proposer:提议发起者。Proposer 可以有多个,Proposer 提出议案(value)。所谓 value,可以是任何操作,比如“设置某个变量的值为value”。不同的 Proposer 可以提出不同的 value,例如某个Proposer 提议“将变量 X 设置为 1”,另一个 Proposer 提议“将变量 X 设置为 2”,但对同一轮 Paxos过程,最多只有一个 value 被批准。
  2. Acceptor:提议接受者;Acceptor 有 N 个,Proposer 提出的 value 必须获得超过半数(N/2+1)的 Acceptor批准后才能通过。Acceptor 之间完全对等独立。
  3. Learner:提议学习者。上面提到只要超过半数accpetor通过即可获得通过,那么learner角色的目的就是把通过的确定性取值同步给其他未确定的Acceptor。

2、Proposer的两阶段运行:(为方便理解,先不引入learner)

Proposer(var,V)第一阶段:选定epoch,获取epoch访问权和对于的var取值,需要获取半数以上的acceptor的访问权和对于的一组var取值。

第二阶段,采用“后者认同前者”的原则执行。

(1)在肯定旧epoch无法生成确定性取值时,新的 会提交自己的取值,不会冲突。

(2)一旦旧epoch形成确定性取值,新的epoch肯定可以获取到此取值,并且会认同此取值,不会破坏。

3、多Acceptor实现:

(1)如果获取的var取值都为空,则旧epoch无法形成确定性取值。此时努力使新的<epoch,V>成为确定性取值。向epoch对应的所有acceptor提交取值<epoch,V>;

如果收到半数以上成功,则返回<ok,V>;

否则,返回<error>,此时被新epoch抢占或者acceptor发送故障。

(2)如果var的取值存在(需同时询问至少半数以上Acceptor),认同最大accepted_epoch对应的取值f,帮助取值f提交确认(参考下方原理图),努力使<epoch,f>成为确定性取值。

如果f出现半数以上,则说明f已经被确定性取值,直接返回<ok,f>

否则,向epoch对应的所有acceptor提交取值<epoch,f>,按所能就接受到的最新的序号取值提交,比如接收到<#1,2>,<#2,3>和<#3,4>,则对所有的acceptor提交Accept(#3,4),最终达成一致性。

 

4、特殊情况汇总

(1)第一种情况:单proposer,三个acceptor,Proposer提议正常,未超过accpetor失败情况

问题:如果第二阶段,只有2个accpetor响应接收提议成功,另外1个没有响应怎么处理呢?

处理:proposer发现只有2个成功,已经超过半数,那么还是认为提议成功,并把消息传递给learner,由learner角色将确定的提议通知给所有accpetor,最终使最后未响应的accpetor也同步更新,通过learner角色使所有Acceptor达到最终一致性。

(2)第二种情况:单proposer,三个acceptor,Proposer提议正常,但超过accpetor失败情况

问题:假设有2个accpetor失败,又该如何处理呢?

处理:由于未达到超过半数同意条件,proposer要么直接提示失败,要么递增版本号重新发起提议,如果重新发起提议对于第一次写入成功的accpetor不会修改,另外两个accpetor会重新接受提议,达到最终成功。

(3)情况再复杂一点:还是一样有3个accpetor,但有两个proposer。

情况一:proposer1和proposer2串行执行

proposer1和最开始情况一样,把value设置为v1,并接受提议。

proposer1提议结束后,proposer2发起提议流程:

第一阶段A:proposer1发起prepare(epoch,v2)

第一阶段B:Acceptor收到proposer的消息,发现内部value已经写入确定了,返回(#1,v1)

第二阶段A:proposer收到3个Acceptor的响应,发现超过半数都是v1,说明name已经确定为v1,接受这个值,不在发起提议操作。

情况二:proposer1和proposer2交错执行

proposer1提议accpetor1成功,但写入accpetor2和accpetor3时,发现版本号已经小于accpetor内部记录的版本号(保存了proposer2的版本号),直接返回失败。

proposer2写入accpetor2和accpetor3成功,写入accpetor1失败,但最终还是超过半数写入v2成功,value变量最终确定为v2;

proposer1递增版本号再重试发现超过半数为v2,接受name变量为v2,也不再写入v1。name最终确定还是为v2

情况三:proposer1和proposer2第一次都只写成功1个Acceptor怎么办

都只写成功一个,未超过半数,那么Proposer会递增版本号重新发起提议,这里需要分多种情况:

  1. 3个Acceptor都响应提议,发现Acceptor1{#1,v1} ,Acceptor2{#2,v2},Acceptor{null,null},Processor选择最大的{v2,n2}发起第二阶段,成功后name值为v2;
  2. 2个Acceptor都响应提议,
    1. 如果是Acceptor1{#1,v1} ,Acceptor2{#2,v2},那么选择最大的{v2,n2}发起第二阶段,成功后name值为v2;
    2. 如果是Acceptor1{#1,v1} ,Acceptor3{null,null},那么选择最大的{v1,n1}发起第二阶段,成功后name值为v1;
    3. Acceptor2{#2,v2} ,Acceptor3{null,null},那么选择最大的{v2,n2}发起第二阶段,成功后name值为v2;
  3. 只有1个Acceptor响应提议,未达到半数,放弃或者递增版本号重新发起提议

可以看到,都未达到半数时,最终值是不确定的!

5、总结:

Paxos算法的核心思想:

整个paxos协议过程看似复杂难懂,但只要把握和理解这两点就基本理解了paxos的精髓

第一阶段accpetor的处理流程:如果本地已经写入了,不再接受和同意后面的所有请求,并返回本地写入的值;如果本地未写入,则本地记录该请求的版本号,并不再接受其他版本号的请求,简单来说只信任最后一次提交的版本号的请求,使其他版本号写入失效;

第二阶段proposer的处理流程:未超过半数accpetor响应,提议失败;超过半数的accpetor值都为空才提交自身要写入的值,否则选择非空值里版本号最大的值提交,最大的区别在于是提交的值是自身的还是使用以前提交的。

特点

(1)在抢占式访问权的基础上引入多acceptor

(2)保证一个epoch,只有一个proposer运行,proposer按照epoch递增的顺序依次执行。

(3)新epoch的proposer采用“后者认同前者”的思路进行。

  1. 在肯定旧epoch无法生成确定性取值时,新的epoch会提交自己的取值,不会冲突。
  2. 一旦旧epoch形成确定性取值,新的epoch肯定可以获取到此取值,并且会认同此取值,不会破坏。

Paxos算法可以满足容错要求

  1. 半数半数的acceptor出现故障时,存活的acceptor仍然可以生产var的确定性取值。
  2. 一旦var取值被确定,即使出现半数以下acceptor故障,此取值可以被获取,并且将不再被更改。

Paxos算法的Liveness问题

新轮次的抢占会让旧轮次停止运行,如果每一轮次在第二阶段执行成功之前都被新一轮抢占,则导致活锁,如何解决??

答:

引入leader,也就是multi paxos的情形。先选举leader,leader任期内不需要执行prepare,直接accept即可,在同一个leader任期内只有leader发起proposal,《Multi-Paxos: An Implementation and Evaluation》,不再展开。

思考题:

  1. 什么情况下可以认为var的取值被确定,不再更改?
  2. Paxos两阶段分别在做什么?
  3. 一个epoch是够会有多个proposer进入第二阶段运行?
  4. 什么情况下,proposer可以将var的取值确定为自己提交的取值?
  5. 在第二阶段,如果获取的var的取值都为空,为什么可以保证旧epoch无法过程确定性取值?
  6. 新epoch抢占成功之后,旧epoch的proposer将如何运行?
  7. 如何保证新epoch不会破坏已经达成的确定性取值?
  8. 为什么在第二阶段存在var取值时,只需要考虑accepted_epoch最大的取值f?
  9. 在形成确定性取值之后出现任意半数一下的acceptor故障,为何确定性取值不会被更改?
  10. 如果proposer在运行过程中,任意半数以下的acceptor出现故障,此时将如何运行?
  11. 正在运行的proposer和任意半数以下acceptor都出现故障时,var的取值可能是什么情况?为何之后新的proposer可以形成确定性取值?

 

参考答案:

  1. 半数以上Acceptor写入var值就被确认,不再更改。
  2. 第一阶段请求访问权和存储的值,第二阶段空时提交;
  3. 不会,一个epoch只有一个proposer执行,其他的proposer递增epoch顺序。
  4. 自己提交时无确定的值,且自己的提交的值被确认的acceptor数量达到一半以上。
  5. Acceptor采用喜新厌旧的做法,而且现在至少有一半的acceptor接受提交新epoch,所以有旧epoch提交时会自动拒绝,这样至少一半以上的epoch会拒绝旧epoch,旧epoch永远无法确认。
  6. 一旦被新epoch抢占成功,旧epoch的访问权失效,旧epoch的proposer将无法运行。
  7. 已经达成确认的取值,节点数大于1/2,新epoch请求时,发现已确认了,半数以上节点都是这个值,不再提议,弱发现确认的节点数不到1/2,只要不全是空就会递增版本号同样提交取到的这个值,不会赋予新值(后者确认前者原则)。
  8. 喜新厌旧,存在新epoch的var值时,旧epoch会被acceptor自动拒绝。
  9. 确定性取值需要半数以上确认。
  10. 正常运行,容错率达到了半数。
  11. 因为新的proposer仍然可以达到一半以上的acceptor确认。

推荐参考资料:

(下载地址:https://pan.baidu.com/s/1AwkZ8OHnEn_9I8GzBnj97g,包含以下1、4、5、8、9

参考资料:

1、知行学社的《分布式系统与Paxos算法视频课程》

2、分布式系统一致性及Paxos详解https://blog.yangx.site/2018/08/05/paxos/

3、知乎话题https://zhuanlan.zhihu.com/p/29706905

4、刘杰的《分布式系统原理介绍》 ,里面有关于paxos的详细介绍,例子非常多,也有包括paxos协议的证明过程,大而全,质量相当高的一份学习资料!

5、ppt《可靠分布式系统基础 Paxos 的直观解释》,虽然是只是一份ppt没有讲解视频,但看ppt也能理解整个的paxos介绍和推导过程,写的很具体,配图很清晰明了;

6、微信的几篇公众号文章:《微信PaxosStore:深入浅出Paxos算法协议》https://mp.weixin.qq.com/s/aJoXSQo9-zmukN2RsiZ3_g(微信PaxosStore:深入浅出Paxos算法协议 )、《微信开源:生产级paxos类库PhxPaxos实现原理介绍》https://mp.weixin.qq.com/s/6VWUA5EDV2UIq4NqmQYWUA(微信自研生产级paxos类库PhxPaxos实现原理介绍 ),文章写的都挺好,不适合入门,需要有一定基础才好理解;

7、源代码:微信开源的phxpaxos:https://github.com/tencent-wechat/phxpaxos,结合代码对协议理解更深。

8、paxos作者Lamport《paxos made simple》的论文。

9、《Multi-Paxos: An Implementation and Evaluation》论文。


以上均为个人结合参考资料理解,如有错误,恳请大家批评指正!

猜你喜欢

转载自blog.csdn.net/yangwei256/article/details/83337563