Paxos 算法
Paxos 问题是指分布式的系统中存在故障(crash fault),但不存在恶意(corrupt)节点的场景(即可能消息丢失或重复,但无错误消息)下的共识达成问题。这也是分布式共识领域最为常见的问题。因为最早是 Leslie Lamport 用 Paxon 岛的故事模型来进行描述,而得以命名。解决 Paxos 问题的算法主要有 Paxos 系列算法和 Raft 算法。
作为后来很多共识算法(如 Raft、ZAB 等)的基础,Paxos 算法基本思想并不复杂。
基本原理
算法中存在三种逻辑角色的节点,在实现中同一节点可以担任多个角色。
- 提案者(Proposer):提出一个提案,等待大家批准(Chosen)为结案(Value)。系统中提案都拥有一个自增的唯一提案号。往往由客户端担任该角色。
- 接受者(Acceptor):负责对提案进行投票,接受(Accept)提案。往往由服务端担任该角色。
- 学习者(Learner):获取批准结果,并帮忙传播,不参与投票过程。可为客户端或服务端。
算法需要满足安全性(Safety) 和存活性(Liveness)两方面的约束要求。实际上这两个基础属性也是大部分分布式算法都该考虑的。
- Safety:保证决议(Value)结果是对的,无歧义的,不会出现错误情况。
只有是被提案者提出的提案才可能被最终批准;
在一次执行中,只批准(chosen)一个最终决议。被多数接受(accept)的结果成为决议; - Liveness:保证决议过程能在有限时间内完成。
决议总会产生,并且学习者能获得被批准的决议。
基本思路类似两阶段提交:多个提案者先要争取到提案的权利(得到大多数接受者的支持);成功的提案者发送提案给所有人进行确认,得到大部分人确认的提案成为批准的结案。
实现 (Paxos 算法)
Paxos 算法就是通过两个阶段确定一个决议:
- Phase1:确定谁的编号最高,只有编号最高者才有权利提交 Proposal(提议:给定的具体值);
- Phase2:编号最高者提交 Proposal,如果没有其他节点提出更高编号的 Proposal,则该提案会被顺利通过,否则整个过程就会重来。
结论就是这个结论,至于整个过程的推导,就不在这里展开细说了。但是有一点需要注意的是,在过程第一阶段,可能会出现活锁。你编号高,我比你更高,反复如此,算法永远无法结束。可使用一个“Leader”来解决问题,这个 Leader 并非我们刻意去选出来一个,而是自然形成出来的。同样再次也不展开讨论了,本篇主要是以 Code 为主的哈!
Phase1
func (px *Paxos)Prepare(args *PrepareArgs, reply *PrepareReply) error {
px.mu.Lock()
defer px.mu.Unlock()
round, exist := px.rounds[args.Seq]
if !exist {
//new seq of commit,so need new
px.rounds[args.Seq] = px.newInstance()
round, _ = px.rounds[args.Seq]
reply.Err = OK
}else {
if args.PNum > round.proposeNumber {
reply.Err = OK
}else {
reply.Err = Reject
}
}
if reply.Err == OK {
reply.AcceptPnum = round.acceptorNumber
reply.AcceptValue = round.acceptValue
px.rounds[args.Seq].proposeNumber = args.PNum
}else {
//reject
}
return nil
}
在 Prepare 阶段,主要是通过 RPC 调用,询问每一台机器,当前的这个提议能不能通过,判断的条件就是,当前提交的编号大于之前的其他机器 Prepare 的编号,代码 if args.PNum > round.proposeNumber
的判断。还有一个就是,如果之前一台机器都没有通过,即使当前是第一个提交 Prepare 的机器,那就直接同意通过了。代码片段:
round, exist := px.rounds[args.Seq]
if !exist {
//new seq of commit,so need new
px.rounds[args.Seq] = px.newInstance()
round, _ = px.rounds[args.Seq]
reply.Err = OK
}
在完成逻辑判断过后,如果本次提议是通过的,那么还需返回给提议者,已经通过提议和确定的值。代码片段:
if reply.Err == OK {
reply.AcceptPnum = round.acceptorNumber
reply.AcceptValue = round.acceptValue
px.rounds[args.Seq].proposeNumber = args.PNum
}
Phase2
func (px Paxos)Accept(args *AcceptArgs, reply *AcceptReply) error {
px.mu.Lock()
defer px.mu.Unlock()
round, exist := px.rounds[args.Seq]
if !exist {
px.rounds[args.Seq] = px.newInstance()
reply.Err = OK
}else {
if args.PNum >= round.proposeNumber {
reply.Err = OK
}else {
reply.Err = Reject
}
}
if reply.Err == OK {
px.rounds[args.Seq].acceptorNumber = args.PNum
px.rounds[args.Seq].proposeNumber = args.PNum
px.rounds[args.Seq].acceptValue = args.Value
}else {
//reject
}
return nil
}
在 Accept 阶段基本和 Prepare 阶段如出一辙咯。判断当前的提议是否存在,如果不纯在表明是新的,那就直接返回 OK 咯!
round, exist := px.rounds[args.Seq]
if !exist {
px.rounds[args.Seq] = px.newInstance()
reply.Err = OK
}
然后同样判断提议号是否大于等于当前的提议编号,如果是,那同样也返回 OK 咯,否者就拒绝。
if args.PNum >= round.proposeNumber {
reply.Err = OK
}else {
reply.Err = Reject
}
与此重要的一点就是,如果提议通过,那么就需设置当轮的提议编号和提议的值。
if reply.Err == OK {
px.rounds[args.Seq].acceptorNumber = args.PNum
px.rounds[args.Seq].proposeNumber = args.PNum
px.rounds[args.Seq].acceptValue = args.Value
}
整个使用过程中使用了 Map 和数组来存储一些辅助信息,Map 主要存储的是,每一轮的投票被确定的结果,Key 表示每一轮的投票编号,Round 表示存储已经接受的值。Completes 数组主要是用于存储在使用的过程中,已经确定完成了的最小的编号。
rounds map[int]*Round //cache each round paxos result key is seq value is value
completes [] int //maintain peer min seq of completed
func (px *Paxos)Decide(args *DecideArgs, reply *DecideReply) error {
px.mu.Lock()
defer px.mu.Unlock()
_, exist := px.rounds[args.Seq]
if !exist {
px.rounds[args.Seq] = px.newInstance()
}
px.rounds[args.Seq].acceptorNumber = args.PNum
px.rounds[args.Seq].acceptValue = args.Value
px.rounds[args.Seq].proposeNumber = args.PNum
px.rounds[args.Seq].state = Decided
px.completes[args.Me] = args.Done
return nil
}
同时 Decide 方法,用于提议者来确定某个值,这个映射到分布式里面的状态机的应用。
客户段通过提交指令给服务器,服务器通过 Paxos 算法是现在多台机器上面,所有的服务器按照顺序执行相同的指令,然后状态机对指令进行执行最后每台机器的结果都是一样的。
故障恢复
在分布式环境之中,网络故障宕机属于正常现象。如果一台机器宕机了,过了一段时间又恢复了,那么他宕机的时间之中,怎么将之前的指令恢复回来?当他提交一个 jmp 指令的时候,索引 1、2 都是已经确定了的指令,所以可以直接从索引 3 开始,当他提交 Propser(jmp)的时候,会收到 s1、s3 的返回值(cmp),根据 Paxos 算法后者认同前者的原则,所以他会在 Phase2 阶段提交一个值为 cmp accept 的请求,最后索引为 3 的就变成了 cmp,如果说在这个阶段没有返回值,那么就选择客户端的返回值就可以了,最后就达成了一致。