Paxos
Paxos是最基础的共识算法,其包含了一系列的共识算法,有着许许多多的变种,被归为Paxos 族共识算法
基本概念
每个节点(议员)都可能提出提案,Paxos用提案来推动整个算法,使系统决议出同一个提案 提案包括提案编号和提案值;各个节点需要通过消息传递不断提出提案,最终系统接受同一个提案 ,进入一个一致的状态,即对某一个提案达成共识 Paxos算法认为,如果集群中超过半数的节点同意接受该提案,那么对该提案的共识达成,称该 提案被批准 在Basic Paxos算法中,一旦某个提案被批准,提议者都必须将该值作为后续提案值 Paxos算法的角色分为:
- 客户端:像分布式系统发起请求,并等待响应
- 提议者:收到客户端的请求,提出相关提案,试图让接受者接受该提案,并在发生冲突时进行协调
- 接受者:投票接受或拒绝提议者的提案,若超过半数的接受者接受提案,则该提案被批准
- 学习者:只能学习被批准的提案,不参与决议提案
问题描述
首先,我们不能只有一个接受者,否则该接受者一旦宕机,则整个系统无法正常工作 对于多个接受者的系统,如果我们采用最直接的算法,假设每个节点只接受第一次收到的值;那么就会 出现以下的情况,节点接受了不同的写请求,但并没有产生多数派,没有一个提案被超过半数的节点接受,没有提案被批准,违反了共识算法的活性
为了解决无法达成多数派条件的问题,我们放宽限制,允许一个节点接受多个不同的值,这时候新的问题 出现了,集群中不止一个提案被批准,这就违反了安全性
Basic Paxos算法强调,一旦一个值被批准了,未来的提案就必须提议相同的提案值;Basic Paxos 算法只会批准一个提案值
基于上述情况,S3直接拒绝写请求B的值,因为S3已经接受了写请求A
不过在使用这种方式时,我们需要知道提案的先后顺序,因分布式的时钟问题,Paxos算法会给每个 提案附加一个唯一的编号,即提案编号,如<n,server_id>,其中n被称为轮次,和服务器ID server_id 一起组成提案编号 这样,就可以通过n的大小来判断提案的先后顺序;同时会在本地持久化存储提案信息,让进程在宕机恢 复后仍然知道当前的提案编号
Paxo算法的实现流程
Basic Paxos算法使系统达成共识并决议出单一的值 主要包含两个阶段,第一阶段(分为a、b两个部分),第二阶段(分为a、b两个部分);分别对于两轮RPC消息 传递,每个阶段的a、b部分分别对应RPC的请求阶段和响应阶段
第一阶段
第一阶段的RPC请求阶段被称为Prepare阶段,提议者收到来自客户端的请求后,选择一个最新的提案编号n, 向超过半数的接受者广播Prepare消息,请求接受者对提案编号进行投票 send Prepare(++n) 这里的Prepare消息不包含提案值,只发送提案编号
第一阶段的RPC响应阶段被称为Promise阶段,接受者收到Prepare请求消息后进行判断:
- 如果Prepare消息中的提案编号n大于之前接受的所有提案编号,则返回Promise消息进行响应,并后续不会 再接受比n小的提案;如果接受者之前接受了某个提案,那么Promise消息中会将前一次接受的提案编号和提案 值一起返回给提议者
- 如果提案编号n小于等于接受者之前接受的最大编号,则忽略该请求或返回拒绝响应
为了实现故障恢复,接受者需要持久化存储已接收的最大提案编号(max_n)、已接受的提案编号(accepted_N) 、已接受的提案值(accepted_VALUE)
if (n > max_N)
max_N = n // 更新见过的最大提案编号
if (proposal_accepted == true) // 是否有已接受提案
respond: Promise(n, accepted_N, accepted_VALUE)
else
respond: Promise(n)
else
respond fail
第二阶段
第二阶段的RPC请求阶段被称为Accept阶段,当提议者收到超过半数的接受者的Promise响应后,提议者向多数派的 接受者发起Accept(n, value)请求,这次会当上提案编号和提案值 如果之前接受者Promise响应有返回已接受的值accepted_VALUE,那么使用提案编号最大的已接收值作为提案值, 如果没有返回任何accepted_VALUE,那么提议者可以自由决定提案值
if receive majority Promise // 是否收到多数派接受者的响应
if receive accepted_VALUE // 是否收到的Promis消息中有已接受的提案
val = accepted_VALUE
else
val = VALUE // 提议者自由决定提案值
send Accept(n, value) to majority acceptors
提议者不一定是将Accept请求发送给之前有应答的多数派接受者,可以再选择一个多数派发送Accept请求
第二阶段的RPC响应阶段被称为Accepted阶段,接受者收到Accept请求后,在这期间如果接受者没有接受提案编号比 n大的提案,则接受该提案,更新相关信息
if (n >= max_N)
proposal_accepted = true // 接受该提案
max_N = n // 更新提案编号
accepted_N = n // 保存提案编号
accepted_VALUE = value // 保持提案值
respond: Accepted(n, value) to the proposer and all learner // 发送Accepted消息给提议者和所有学习者
else
respond fail
案例
情况1:提案已被批准 S1、S5是提议者,每个节点都是接受者;提案编号使用n.server_id表示,例如3.1表示S1发起的第3轮提案 流程:
- S1收到客户端值为x的写请求,S1向S1、S2和S3发起Prepare(3.1)请求
- 由于S1、S2、S3没有接受过任何提案,S1继续向S1、S2、S3发送Accept(3.1, x)请求,三个节点接受该提案,满足半 数机制,提案被批准
- S5收到了客户端值为y的写请求,并向S3、S4和S5发送Prepare(4.5)请求由于提案编号4大于3,且S3已经接受了提案3, 接受者S3会回复包含提案3 编号和值的Promise消息
- S5根据S3的Promise响应,将提案值y替换成x,继续向S3、S4、S5发送Accept(4.5,X)请求,提案被批准,提案值仍为x
情况2:提案被接受,提议者可见 和情况1类似,区别在于提案3还未被批准,只是被S3接受,但S3仍会回复接受的提案3的编号和值的Promise响应,所以S5 仍然会将提案值替换成x,最终所有接受者批准提案X,只是提案编号有所不同
只要一个接受者在Promise响应中返回了提案编号和提案值,提议者就需要替换提案值
情况3:提案被接受,提议者不可见 S1接受了提案,但是S3还未接受提案,因此S3的Promise消息中不会返回提案3的信息;因此S5自行决定提案值y,由于此时 S3接受的提案编号4大于3,所以S3不再接收S1后续的Accept请求;只有两个节点接受了提案值为x的提案,不满足半数机制, 最终提案值为y的提案被批准
活锁
Basic Paxos算法存在活锁问题,提议者在第一阶段发送Prepare信息,还没来得及发送第二阶段的Accept信息;紧接着第二 个提议者在第一阶段又发送了编号更大的Prepare请求,第一个提议者接着发送更大一轮的Prepare....... 不会出现第二阶段发送Accept请求,系统会一直停留在不断地Prepare请求中
解决活锁最简单的方式就是引入随机超时,某个提议者发现提案没有被成功接受,则等待一个随机时间,timeout之后,不再继续 发送Prepare消息,减少互相抢占的可能性