看过原文Paxos Made Simple感觉有的地方作者解释的还是不很清楚,通过查看其他的一些资料比如Paxos Made Code才慢慢有了一些理解,以下下是我对原文的理解.
1 一致性算法(The Consensus Algorithm)
1.1 选择一个值
先解释文中几个关键词:
- acceptor(裁判):是否批准议案的决策者
- Proposal(议案):由proposer提出,被aceeptor批准或否决
- Value(值):是议案的内容,一个议案就是一个{编号,值}对
- Accept(批准):表示议案被acceptor批准
- Choose(选中):表示议案“被选中”,也就是被多数acceptor批准
首先要明确,Paxos的目的是在分布式环境里解决一致性问题。文章先提出了两个约束(条件)。只要保证这两个约束就保证了问题解的正确性。但是, 因为开始的约束很难实现,于是对约束进行加强(满足了加强后的约束就一定满足原来的约束。就好比我要证明a>1只要证明a>2就可以了.
原文提出的问题是在分布式环境下如何选择一个值,可以想象成许多客户端提交了不同的命令,最终执行那个命令的问题.
最简单的情况是只有一个(裁判)accepter,accepter只须选择最先到达的提案(proposal)就可以了.但是这就会出现单点问题,如果这个(裁判)accepter挂了,后面就进行不下去了.
所以需要另外的方式选择一个值,就是需要多个裁判(accepter),当有大多数(半数以上)的裁判(accepter)批准(accept)了提议人(proposer)提出的提案(proposal),我们说该提案(proposal)被选中(chosen)了.假设f“一个裁判(accepter)只允许批准(accept)一个提案(proposal)”,那么最终就有且只有一个提案(proposal)被选中(chosen),因为,假如有两个的proposal被选中了,也就是有两个半数以上的裁判(accepter)分别批准(accept)了两个提案(proposal),那么必然会有至少一个裁判(accepter)批准了两个以上的提案(proposal),与假设f相违背,证明如果的假设f成立就是可行的.
我们希望即使只有一个提案(proposal)被proposer提出,这个提案也可以被选中.于是提出下面的要求:
P1. 一个accepter必须批准它接收到的第一提案(proposal)
但是这就会引起一个问题,如果有多个议案(proposal)被多个提议人(proposer)提出可能没有一个议案(proposal)被大多数裁判accepter批准,就不会有议案(proposal)被选中.即使只有两个议案(proposal),假如有5个裁判(accepter)需要对这两个议案(proposal)进行批准,恰好有一个裁判(accepter)挂了,两个议案proposal各被剩下4个accepter中的一半批准了,就无法判断那个议案proposal可以被选中.
P1和一个议案(proposal)只有在大多数裁判(accepter)都批准的情况下才会被选中的要求暗示我们一个裁判accepter必须可以批准一个以上的议案proposal(也就是说前面的“假设f”不可行).当然这样就会有多个议案(proposal)被选中.我们给每个提议人(proposer)提出的可能被裁判accepter批准的议案(proposal)编号,以方便追踪.现在议案(proposal)就有了两个属性编号(number)和值(value).我们允许有多个议案proposal被选中,但是要求被选中的议案proposal的值(value)必须相同.通过归纳法可以证明,只要满足下面的条件就可以满足这个要求.
P2. 一个值为V的提案被选中了,那么编号比这个提案大的提案选中的条件是提案的值是V
P2是不容易实现的,所以要对P2做加强约束.因为一个提案要被选中必须先要被批准,所以只要满足下面的条件就可以满足P2.
P2a. 一个值为V的提案被选中了,那么编号比这个提案大的提案被批准的条件是提案的值是V。
因为消息的传输是异步的,一个accepter C可能没有接收到过任何提案,这就引出一个问题,假设一个新的proposer发送了一个更高编号的单值不同的提案给C,根据“P1”C要接受这个提案,但是这却违背了P2a.为了同时满足P2a和P1,需要加强对P2a的约束.
P2b. 一个值为V的提案被选中了,那么编号比这个提案大的提案被提出的条件是提案的值是V。
一个提案在被裁判(accepter)接受之前必须先要被提出,所以P2b满足了P2a,同时也就满足了P2.
现在P2b要求“假设编号是m值是v的提案被选中了,那么提出的编号是n(n>m)的提案的值也是v”.通过归纳法就可以证明只要下面这个条件成立“R1.假设编号m值v的提案被选中了,并且任何提出编号在m..(n−1)中的提案的值都是v,那么提出的编号是n(n>m)的提案的值也是v”,那么P2b就是立的住的.下面就要分析什么样的约束可以保证R1条件成立.因为提案m被选中了,所以存在这样一个集合C,C中包含的是占一半数量以上的批准了提案m的裁判(accepter).结合R1中的另一条假设可以总结出下面的结论:
任何一个C中的裁判都批准了一个编号是m的提案,任何一个编号在m..(n−1)的被批准的提案的值都是v
任何一个包含超过一半数量裁判的集合S,都至少包含一个C中的成员,也就是说S中至少有一个裁判批准了编号是m的提案.所以只要保证下面的条件成立就能保证R1成立.
P2c. 对于任何的值v和编号n,一个拥有值v和编号n的提案被提出的前提条件是:有这样一个包含了半数以上的裁判的集合S,要么(a)集合S中没有任何一个裁判批准过小于编号n的提案,要么(b)新提出的提案的值v是集合S中的裁判已批准的编号小于n的提案中编号最大的提案的值.
再详细证明一下P2c是如何保证R1成立的,通过前面的分析可以知道S中至少有一个裁判批准了编号是m的提案,根据P2c,新提出编号n(n>m)的提案的值必然是集合S中小于编号n且大于等于编号m的提案的值,编号m..(n−1)的提案的值都是v,所以编号n的提案的值等于v.
保证p2c的关键问题,其实是要批准者做出不批准某种类型提案的承诺。所以:提案者提出一个提案前,首先要和足以形成多数派的批准者进行通信,获得他们进行的最近一次批准活动的编号,并且得到他们不批准比当前提案编号小的提案的承诺(prepare过程),之后根据回收的信息决定这次提案的 value,形成提案开始投票。当获得多数批准者批准后,提案获得通过。这个简略的过程经过进一步细化后就形成了 Paxos 算法。
如果一个批准者在prepare过程中回答了一个编号为n的提案,但是在开始对 n 进行投票前,又批准另一个提案编号小于n的提案,如果两个提案具有不同的 value,这个批准就会违背 P2c。因此在 prepare 过程中,批准者对于”不再批准编号小于n的提案”的承诺是保证算法正确的基础。所以又有一个对 P1 的加强:
P1a. 当且仅当批准者没有收到编号大于n的提案请求时,批准者批准编号为 n 的提案。
现在已经可以提出完整的算法了。 通过一个决议分为两个阶段:
- prepare 阶段:
(a) 提议人选择一个提案编号n并将prepare请求发送给裁判中的一个多数派;
(b) 裁判收到 prepare消息后,如果提案的编号大于它已经回复的所有prepare消息,则批准者将自己上次的批准(如果有的话)回复给提案者,并承诺不再批准小于 n 的提案; - 批准阶段:
(a)当一个提议人收到了多数裁判对prepare的回复后,就进入批准阶段。它要向回复 prepare 请求的裁判发送批准的请求,包括编号 n 和根据 P2c 决定的 value(如果根据 P2c 没有决定 value,那么它可以自由决定 value)。
(b)在不违背自己向其他提议人的承诺的前提下,裁判收到批准请求后即批准这个请求。
这个过程在任何时候中断都可以保证正确性。
1.2 找出最终选中的值(Learning a Chosen Value)
accepter批准提案后需要把该提案提交给leaner.leaner发现一个提案被accepter集合中的多数派批准了,那么该提案就被选中了,这个instance就可以结束了.这里有个问题,leaner也是一个集合,accepter每批准一个提案就通知每个leaner,还是先通知一个主leaner,该leaner选中一个提案后,再通知其他leaner,前者需要a*b次通信而后者只需要a+b次通信.但是后者会有单点问题.所以可以用一个折中的方案,就是选出几个leaner成为一个主leaner集
1.3 Progress
progress就是程序不会出现死锁/假死等现象.比如:proposer p1用提案号n1完成了阶段1的请求,proposer p2用提案号n2>n1也完成了阶段1的请求.p1又进行阶段2的请求,因为accepter已经许诺不会批准小于n2的请求,p1被拒,返回阶段一,用新的提案号n3>n2重新开始并完成了阶段1. 这是p2开始阶段2的请求,和p1同样的原因被拒,并重复p1被拒后的相同过程.如此反复,出现假死现象.
要解决这个问题就要提出一个主proposer,每个提案都又主proposer提出.其他的proposer只是在主proposer挂了后,作为备用.
1.4 实现
在实现上一个进程可以扮演proposer, acceptor, learner等多个角色. 另外还有选出一个leader作为主proposer和主learner.未来让进程在失败后可以恢复,accepter需要保存在阶段1中接受到的最高编号,阶段2中批准的最大编号和对应值,还需要在响应之前保存响应信息.
剩下的就是各个proposer的编号不能重复,可以采用n+100*i编号(100是容纳的最大proposer数量,你是进程号,i递增号)
2 状态机实现
状态机就是维护一系列数据的有续集合.状态机的初始状态是已定的,那么根据后面执行的一系列的命令(command)就可以确定最终状态.如果是多台服务器只要保证这一系列的命令的内容和顺序都是一致的,就能保证执行完这些命令后每台服务器的状态都是一致的.
执行paxos算法可以最终选出有且只要一个值,我们把执行完这样一个选举的过程叫instance.每个instance独立执行,互不影响.这样就可以有多个instance同时执行,把每个instance编号i,第i个instance选举出的命令(command)的顺序号是i.这样就保证了命令序列的一致性.
一个instance的执行过程如下:
Client Proposer Acceptor Learner
| | | | | | | --- First Request ---
X-------->| | | | | | Request
| X--------->|->|->| | | Prepare(N,I)
| |<---------X--X--X | | Promise(N,I,{Va,Vb,Vc})
| X--------->|->|->| | | Accept!(N,I,Vm)
| |<---------X--X--X------>|->| Accepted(N,I,Vm)
|<---------------------------------X--X Response
| | | | | | |
Vm = last of (Va, Vb, Vc)
N 是Proposal编号
I 是Instance编号
所有的instance可以使用相同的议案编号N,leader可以一次选取α个客户端命令给α个Instance.每当第i个状态机命令执行完成之后,就执行第i..i + α个instance.这样当加入一台新的服务器的时候可以复制第i个状态机命令执行完成之后的状态,然后开始执行第i..i+α个命令.这中间任何一个Instance的选举失败都会形成一个命令序列的空缺,导致命令序列不能执行.假如第i个Instance选举失败,但是有accepter已经批准了提案,那么proposer就需要用提案号N+1重新选举,假如成功了就可以用原来批准过的命令补齐命令序列中的第i个空缺,原文中的135和140就是这样的情况.假如第i个Instance选举失败,并且还没有accepter批准过提案,proposer可以向客户端取新的命令并重新选举.但是为了快速补缺,可以用“no-op”命令(该命令不改变任何状态)替代.