paxos是可以被证明的共识协议。
两阶段提交的主要问题是阻塞问题,即系统在有节点失败的情况下,整个系统必须等在那里,什么都不做。
为了解决这个问题,三阶段以及类似协议寻求在两阶段协议的基础上增加交互内容,或者增加节点日志内容,方便在系统阻塞的情况下,恢复协议的继续执行。
paxos也可以理解是类似的思路,它采用两阶段提交,第一阶段是投票,第二阶段是决定。
paxos协议增加了什么内容呢?
整体上增加了一个多数派的概念,在具体的协议流程上,增加一个全局唯一序列号(ID)。
多数派
二阶段产生阻塞的主要原因是,协议必须保证所有节点都同意一个提案,这是一个很强的要求。而paxos协议做了妥协,只需要系统中大多数节点,即大于一半的节点同意一个值,就认为这个提案已经被commited了。这就为解决阻塞问题减少了障碍。但是有得必有失,在二阶段协议中,客户端可以查询任何一个节点就可以知道这个提案是否已经确定,而在paxos协议中,就不能这么简单处理,必须查询到大多数节点接受的同一个提案,才能认为这个提案是被确定了(接受了)。
序列号
这个有点逻辑时钟的思想在里面,在分布式系统中,最重要的就是顺序,换句话说就是同步。这个序列号必须是全局唯一,越大表示发生的越靠后。大的序列号的提案可以覆盖小的序列号的提案,但是增加了一个约束,即如果一个提案已经被某些节点接受了,那么大序列号提案的提议的值要使用这个已经被接受的值,保证协议能够收敛。
具体流程
// the proposer
var value = from client request
var id = 0
id++
promises = sendProposeToAllAcceptors(id)
if promises.length > half of node number{
for any promise in promises if promise.acceptedValue != nil{
accepts = sendAcceptToAllAcceptors(id, promise.acceptedValue)
}
else{
accepts = sendAcceptToAllAcceptors(id, value)
}
if accepts > half of the node number{
value comfirmed!
}else{
id++
run the protocol again
}
}else{
id++
run the protocol again
}
// the acceptor
var maxId = 0, acceptedValue = nil
on propose(id){
if id > maxId {
maxId = id
return promise(acceptedValue)
}else{
not respond
}
}
on accept(id, value){
if id >= maxId{
acceptedValue = value
return accepted(acceptedValue)
}else{
not respond
}
}
活锁
在一种情况下会出现活锁:
- A使用一个序列号提交一个提议,获得了大多数节点的承诺;
- B使用一个更大的序列号提交一个提议,获得了大多数节点的承诺;
- A发送接受请求,没有获得大多数的接受恢复,因为在第二步中,大多数已经承诺了B;
- A选择一个更大的序列号提交一个提议,获得了大多数节点的承诺;
- B发送接受请求,没有获得大多数的接受恢复,因为在第四步中,大多数已经承诺了A;
如此往复。这种情况破坏了协议的活性。对应的解决办法是,在提案没有获得大多数同意,不管是提议还是接受阶段,都随机等待一定时间在开始增加序列号,重新开始协议。这样大概率能够打破上述循环。但是理论上这个协议还是无法证明其满足活性的特性。
multiple paxos
在工程的实现,一般会用multiple paxos这种方式。把协议的第一阶段当成一个选主的阶段,第二个阶段是日志复制阶段。第一个阶段当一个提议者获得了大多数接受者的同意之后,就是这个集群的领导,它可以一直调用第二阶段的accept请求直接进行提议的提交。当然,在这个过程中,其他的任何提议者,只要他们愿意,也可以发送一轮第一阶段的流程来称为领导者,一旦大多数接受者承诺了这个新的领导者(因为它有更高的序列号),协议将会自动阻止原来的领导者提交决议(因为accept请求,将得不到大多数的回复)。当然这样会引起竞争,降低效率,所以一般我们尽量保证一个集群中只有一个领导者,除非这个领导者挂了(通过心跳检测)。