共识算法-Paxos

感谢 参考资料

场景

有一个变量v,分布在N个进程中,每个进程都尝试修改自身v的值,它们的企图可能各不相同,例如进程A尝试另v=a,进程B尝试另v=b,但最终所有的进程会对v就某个值达成一致,即上述例子中如果v=a是v达成一致时的值,那么B上,最终v也会为a。

需要注意的是某个时刻达成一致并不等价于该时刻所有进程的本地的v值都相同,有一个原因是进程可能挂掉,你不能要求挂掉的进程任何事;

更像是最终所有存活的进程本地v的值都会相同。

一致性的三个要求

  • v达成一致时的值是由某个进程提出的。这是为了防止像这样的作弊方式:无论如何,最终都令每个进程的v为同一个预先设置好的值,例如都令v=2,那么这样的一致也太容易了,也没有任何实际意义。
  • 一旦v就某个值达成了一致,那么v不能对另一个值再次达成一致。这个要求称为安全性。
  • 一致总是能够达成,即v总会被决定为某个值。这是因为不想无休止的等待,这个要求也称为活性。

Paxos和分布式存储系统

Paxos用来确定一个不可变变量的取值

  • 取值可以是任意二进制数据
  • 一旦确定将不再更改,并且可以被获取到(不可变性 & 可读性

在分布式存储系统中应用Paxos

  • 数据本身可变,采用多副本进行存储
  • 需要确保多个副本的更新操作序列[Op1, Op2, …, Opn]是相同的 & 不变的
  • 用Paxos依次来确定不可变变量Opi的取值(即第i个操作时什么)
  • 每确定完Opi之后,让每个数据副本执行Opi,依次类推

Google的Chubby & Megastore & Spanner都采用了Paxos来对数据副本的更新序列达成一致

Paxos希望解决的一致性问题

设计一个系统,来存储名称为var的变量

  • 系统内部由多个Acceptor组成,负责存储和管理var变量。
  • 外部有多个proposer机器任意并发调用API,向系统提交不同的var取值
  • var的取值可以时任意二进制数据
  • 系统对外的API库接口为:propose(var, V) => <ok, f> or <error>

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

  • 如果var的取值没有确定,则var的取值为null
  • 一旦var的取值被确定,则不可被更改。并且可以一直获取到这个值。

系统需要满足容错特性

  • 可以容忍任意proposer机器出现故障
  • 可以容忍少数Acceptor故障(半数以下)

其他考虑

  • 网络分化
  • acceptor故障会丢失var的信息

确定一个不可变变量的难点

  1. 管理多个Proposer的并发执行
  2. 保证var变量的不可变性
  3. 容忍任意Proposer机器故障
  4. 容忍半数以下Acceptor机器故障

推导

方案一 (确定一个不可变变量的取值)

先考虑系统由单个Acceptor组成。通过类似互斥锁机制,来管理并发的proposer运行

  1. Proposer首先向acceptor申请acceptor的互斥访问权,然后才能请求Acceptor接受自己的取值。
  2. Acceptor给proposer发放互斥访问权,谁申请到互斥访问权,就接收谁提交的取值。
  3. 让proposer按照互斥访问权的顺序依次访问acceptor
  4. 一旦Acceptor接收了某个Proposer的取值,则认为var取值被确定,其他Proposser不再更改

基于互斥访问权的Acceptor的实现

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

Acceptor::prepare():
加互斥锁,给予var的互斥访问权,并返回var当前的取值f

Acceptor::release():
1. 如果已经加锁,并且var没有取值,则设置var为V
2. 释放锁

Acceptor::release():
解互斥锁,收回var的互斥访问权

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

第一阶段

Proposer通过Acceptor::prepare获取互斥访问权和当前var的取值。如果不能,返回 (锁被别人占用)

第二阶段

根据当前var的取值f,选择执行。

  1. 如果f为null,则通过Acceptor::accept(var, V)提交数据V
  2. 如果f不为空,则通过Acceptor::release()释放访问权,返回

分析

通过Acceptor互斥访问权让Proposer序列运行,可以简单地实现var取值的一致性。

Proposer在释放互斥访问权之前发生故障,会导致系统陷入死锁。因此,此防范不能容忍任意Proposer机器故障。

方案二 (确定一个不可变变量的取值)

为了解决方案一的问题,我们引入了抢占式访问权

acceptor可以让某个proposer获取到的访问权失效,不再接收它的访问
之后,可以让访问权发放给其他proposer,让其他proposer访问acceptor

抢占规则

  1. Proposer向Acceptor申请访问权时指定编号epoch(越大的epoch越新),获取到访问权之后,才能向acceptor提交取值。
  2. Acceptor采用喜新厌旧的原则
    • 一旦接收到更大的新epoch的申请,马上让旧epoch的访问权失效,不再接收他们提交的取值
    • 然后给新epoch发放访问权,只接收新epoch提交的取值

由抢占规则引申出来的问题

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

因此,为了保持一致性,不同epoch的proposer之间采用后者认同前者的原则

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

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

Acceptor保存的状态
1. 当前var的取值<accepted_epoch, accepted_value>
2. 最新发放访问权的epoch(latest_prepared_epoch)
注意:accepted_epoch不需要与latest_prepared_epoch相等

Acceptor::prepare(epoch):
1. 只接收比latest_prepared_epoch更大的epoch,并给予访问权
2. 记录latest_prepared_epoch = epoch 并 返回当前var的取值

Acceptor::accept(var, prepared_epoch, V):
1. 验证latest_prepared_epoch == prepared_epoch,
2. 若验证通过,设置var的取值<accepted_epoch, accepted_value> = <prepared_epoch, v>

思考

如果var取值存在,则proposer就直接返回,Proposer时不需要通过Acceptor::accept(var, epoch, V)提交数据V。

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

第一阶段

Proposer获取epoch轮次的访问权和当前var的取值

  1. 简单选取当前时间戳为epoch,通过Acceptor::prepare(epoch),获取epoch轮次的访问权和当前var的取值
  2. 如果不能获取,返回

第二阶段

采用“后者认同前者”的原则执行

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

流程:
1. 如果var的取值为空,则肯定旧epoch无法生成确定性取值,则通过Acceptor::accept(var, epoch, V)提交数据V:
1. 成功后Acceptor返回

分析

基于抢占式访问权的核心思想(让Proposer将按照epoch递增的顺序抢占式的依次运行,后者会认同前者),可以避免proposer机器故障带来的死锁问题,并且仍可以保证var取值的一致性。

注意:仍需要引入多acceptor,因为单机模块Acceptor是故障导致整个系统宕机,无法提供服务。

Paxos

Paxos在方案2的基础上引入多Acceptor,即Acceptor的实现保持不变。仍采用“喜新厌旧”的原则运行。

因为引入了多Acceptor,Paxos采用“少数服从多数”的思路。

容错率50%的由来

一旦某epoch的取值v被半数以上acceptor接收(落盘了),则认为此var取值被确定为v,不再更改。

为什么是半数以上,因为这样能保证,Proposer在propose阶段总能从n/2+1个Acceptor中的至少某一个Acceptor得知

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

第一阶段

选定epoch,获取epoch访问权和对应的var取值

具体:

选定epoch,获取**半数以上**acceptor的访问权和对应的一组var取值[

第二阶段

采用“后者认同前者”的原则执行:
1. 在肯定旧epoch无法生成确定性取值时,新的epoch会提交自己的取值,不会冲突
2. 一旦旧epoch形成确定性取值,新的epoch肯定可以获取到此取值,并且会认同此取值,不会破坏。

具体(前提上获得半数以上的访问权):

  1. 如果获取的var取值都为空,则旧epoch无法形成确定性取值。此时努力使

为什么选择最大accepted_epoch对应的取值V

2.2情况中,有两种可能,V已经被确定了 & V还没被确定

a) 对于情况V还没被确定,我们其实不需要选择最大accepted_epoch对应的取值V,我们是可以按自己的想法干的
b) 对于情况V已经被确定下来了,我们考虑最差的情况,n/2+1个Acceptor中,假设n为奇数,n=5,此时V的取值已经确定下来了,然而,宕机了,留下了最差的局面:
1 : V ; 2 : V’ ; 3 : V’
怎么识别出V才是被确定下来的值呢?我们可以知道V肯定会比V’有更新的epoch。用反证法可以证明。

从查询者的角度看到,(<epoch_3, V>, <epoch_2, V'>, <epoch_1, V'>),他不能区分情况a), b),V具有最新的epoch_3也不意味着它已经被确定,所以查询者要向1, 2, 3提出accept请求,请求值为V。这样
1. 如果V未被确认,那么走确认流程
2. 如果V已经被确认了,走多一次确认流程,V值不会被改变

Paxos算法

Paxos算法的核心思想

  • 在抢占式访问权的基础上引入多acceptor
  • 保证一个epoch,只有一个proposer运行,proposer按照epoch递增的顺序依次运行
  • 新的epoch的proposer采用“后者认同前者”的思路进行
    • 在肯定旧epoch无法生成确定性取值时,新的epoch会提交自己的取值,不会冲突
    • 一旦旧epoch形成确定性取值,新的epoch肯定可以获取到此取值,并且会认同此取值,不会破坏

Paxos算法可以满足容错要求

  • 半数以下acceptor出现故障时,存活的acceptor仍然可以生成var的确定性取值
  • 一旦var取值被确定(不是指存在),即使出现半数以下acceptor故障,此取值可以被获取,并且将不再被更改

Paxos算法的Liveness问题

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

更多

  1. Paxos在Google的Chubby/Megastore和Spanner中的应用

猜你喜欢

转载自blog.csdn.net/jason_cuijiahui/article/details/81108871