博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

[zz]paxos算法介绍

Posted on 2011-09-25 01:08  wangwangkunkun  阅读(193)  评论(0编辑  收藏  举报

http://rdc.taobao.com/blog/cs/?p=160

    本介绍是 Paxos Made Simple 的中文说明。会摘录一些原来的段落讲解。如果大家发现有问题的地方,参照原文。说明中部分内容摘自维基百科。
    Paxos Made Simple 是因为最初的论文比较难懂,作者又写了这篇比较形象好懂的介绍。首先要明确的是 Paxos 算法,是为了解决分布式环境下一致性的问题而提出的算法。这里将牺牲一些严格性,采用比较好懂的具体例子对文章加以解读。

    注意:对这个说明进行理解的时候要先搞清这么一种思路:Paxos的目的是在分布式环境里解决一致性问题。文章先提出了两个约束(条件)。只要保证这两个约束就保证了问题解的正确性。但是, 因为开始的约束很难实现,于是对约束进行加强(满足了加强后的约束就一定满足原来的约束。比如, 我对自己提一个约束:明天的约会对象必须是个人,然后对这个约束做了一次加强,加强成明天的约会对象必须是个女人. 那么只要后一个得到满足, 则加强前的一个一定得到满足.),最终找到一些更强的约束, 而这些约束可以实现,  于是也就找到了实现最初约束的办法. 下面按照这种思路, 我们来看看 Paxos Made Simple 是怎么解决分布式环境一致性的问题的.

    原文提出的问题是在分布式环境下如何选择一个值。我把这个问题形象化成一个更实际的问题,如何确定一把锁的归属。假设一个场景,一个唯一资源被一把锁保护着,现在又很多分布在不同主机上的进程开始争用这把锁。不妨让争用锁的进程分别是S1,S2,S3……。 锁名称L1. 那么回归到原文的例子上,就是对变量L1选择一个值,可能值为(S1,S2,S3….). 当然,最简单的办法是用一个单一进程来做为server,S1, S2, S3 …..向其申请锁,谁先到归谁。这样的结果是,系统产生了一个单点。如果这个唯一的server宕机, 则整个系统不可用. 为了提高其可用性, 我们考虑用一个小集群来代替这个唯一的server. 这个小集群有下面的特点:其中任何一台机器可能失效,但是这台机器恢复回来的时候,应该是有记忆的,就是说,能找到其失效前的一些必要数据。
    在介绍算法的过程中, 我们会遇到几种角色, 先来看我们马上要接触的两种,提案者和批准人。提案者是提出议案的,批准人进行批准。注意,本文只介绍这两者之间的交互关系,不介绍 其他角色的参与(learners)。角色代表了一定的权利和义务, 就是说能干什么以及必须干什么, 一个进程是可以身兼多种角色的。
    现在我们暂时先把那个替代唯一server的小集群看成一批批准人, 并且假设这个集群有5个节点, 也就是有5个批准人(A1-A5)。S系列的进程是提案者, 他们希望的提案就是自己占有锁资源。比如S1的提案是L1 = S1。 S2的提案是 L1 = S2 …… 。实际上就是大家都想获得锁资源. 锁只有一把, 显然,只有一个提案可以获得通过,那么5个批准人的集群如何确定那个提案获得通过呢?简单来说,少数服从多数。就是获得超过半数批准人支持的提案获胜。显然, 当每个批准人只能批准一个提案的时候, 不肯能出现两个都获得超过半数批准的提案. 我们称超过半数的一群批准者为一个多数派。所以只要我们约定,一个批准者只能批准一个提案, 只有获得多数派批准的提案才生效。有了这两个约定, 那么生效的提案一定是唯一的. 也就是说L1的取值唯一。当然, 如果只有一个提案者做出了提案, 则提案应该被接受。就是说,S系列进程只有一个进程要求得到锁资源,这个要求应该被满足。我们提出如下要求:
    P1 一个批准者必须接受他收到的第一个提案。
    这一个简单的要求能解决上面的问题吗? 显然不能. 当几个S进程一起要求锁的时候,可能每个批准者批准了一个。从而无法形成多数派。即便只有两个S进程提出锁要求,假设我们的5人集团当掉一个,剩下四个有两个同意S1,有两个同意S2,还是形不成多数派。为了解决此问题, 我们允许一个批准者可以批准超过一个的提案。可是如此一来,就会有多个提案获得多数派的批准了啊, 所以还要有限制, 为了方便,让我们把每个提案者提出的提案进行编号。每个提案者的编号不同,而且编号是偏序的(做到这一点很容易, 比如提案的时候, 把提案号带上主机的ip等)。在我们的例子里,就先用机器名加变量名S1_L1,S2_L1来作为提案的编号。当多数派批准一个提案比如S1_L1(提案 S1_L1在我们的例子中应该是L1 = S1)的时候,值被选定,提案被通过(注意“通过”这个词,后续中,对于经过多数派批准的生效的提案,我用“通过”这个词)。我们允许多个提案被通过,只要他们提案的值是一样的。比如S1_L1提案(S1_L1:L1=S1)被通过之后,如果机器S2提出提案S2_L1,要求把锁给 S1(S2_L1:L1=S1)那么这个提案应该也被通过。当然我们的例子里,S2是不会这么干的。我们把这个规则表述成如下的形式
    P2 一个值为V的提案被通过了,那么编号比这个提案大的提案通过的条件是提案的值是V。
    因为一个提案被通过,那么至少要被一个批准者批准。所以我们可以对P2做加强。
    p2a 一个值为V的提案被通过了,那么编号比这个提案大的提案被批准的条件是提案的值是V。
    请注意p2和p2a的区别,别糊涂了。(区别是通过和批准这两个词, 说p2a是p2的加强是因为保证了p2a就保证p2。)
    注意,这里又有一个问题,假设S1_L1提案被A1,A2,A3批准,从而获得了通过。现在S2提出提案S2_L2,提给了A5.A5跟据P1的要求,应该批准该提案,但是这是违反p2a的。我们再对p2a做一次加强
    p2b 一个值为V的提案被通过了,那么编号比这个提案大的提案的值应该是V。

    想想为什么p2b是p2a的加强, 应该很容易想明白吧.

    只有被提出的提案才可能被批准,我们规定了提出的提案的值应该是V,自然保证了被批准以及被通过的值。所以保证了p2b 就一定保证了 p2a, 就一定保证了p2.那我们的例子来说,一旦L1的值被确定了,后续的提案其值只能遵循已经通过的值。
    p2b 是难以实现的。这里我们引入另一个约束
    p2c 提出一个编号为n具有值v的提案的前提是:存在一个多数派,要么他们中没有人批准过编号小于n的任何提案,要么他们批准的提案中编号小于n的最大的提案值是v。
  下面通过数学归纳法证明p2c蕴含了p2b:
    假设具有值 v 的提案 m 获得通过,当 n=m+1 时,根据 P2c,由于任何一个多数派中至少有一个批准了 m,因此提案n具有值 v;若 (m+1)..(n-1) 所有提案都具有值v,根据 P2c,若反设新提案 n 不具有值v 则存在一个多数派,他们没有批准过 m..(n-1) 中的任何提案。但是我们知道,他们中至少有一个人批准了 m。于是我们导出了矛盾,获得了证明。也就是说,只要满足p2c,那么p2b就得到满足。
    以上这么啰嗦的一堆,其实是证明了,只要保证了p2c,我们就保证了p2, 保证了p2, 则选出的值一定是唯一值。那么下面看如何设计算法保证p2c。

    保证p2c的关键问题,其实是要批准者做出不批准某种类型提案的承诺。所以:提案者提出一个提案前,首先要和足以形成多数派的批准者进行通信,获得他们进行的最近一次批准活动的编号,并且得到他们不批准比当前提案编号小的提案的承诺(prepare 过程),之后根据回收的信息决定这次提案的 value,形成提案开始投票。当获得多数批准者批准后,提案获得通过。这个简略的过程经过进一步细化后就形成了 Paxos 算法。

    如果一个批准者在 prepare 过程中回答了一个编号为n的提案,但是在开始对 n 进行投票前,又批准另一个提案编号小于n的提案,如果两个提案具有不同的 value,这个批准就会违背 P2c。因此在 prepare 过程中,批准者对于”不再批准编号小于 n 的提案”的承诺是保证算法正确的基础。所以又有一个对 P1 的加强:

P1a:当且仅当批准者没有收到编号大于 n 的提案请求时,批准者批准编号为 n 的提案。

现在已经可以提出完整的算法了。

通过一个决议分为两个阶段:

  1. prepare 阶段:
    1. 提案者选择一个提案编号 n 并将 prepare 请求发送给批准者中的一个多数派;
    2. 批准者收到 prepare 消息后,如果提案的编号大于它已经回复的所有 prepare 消息,则批准者将自己上次的批准回复给提案者,并承诺不再批准小于 n 的提案;
  2. 批准阶段:
    1. 当一个提案者收到了多数批准者对 prepare 的回复后,就进入批准阶段。它要向回复 prepare 请求的批准者发送批准的请求,包括编号 n 和根据 P2c 决定的 value(如果根据 P2c 没有决定 value,那么它可以自由决定 value)。
    2. 在不违背自己向其他提案者的承诺的前提下,批准者收到批准请求后即批准这个请求。

    这个过程在任何时候中断都可以保证正确性。

    这个算法的正确性和完备性就是上面的证明。但是为了便于理解,我们可以用我们上面的例子来描述一个决议过程。这个描述仅仅是便于大家理解。描述中有一些在实际算法上不合适的地方,这里不考虑。

    批准者是A1-A5.申请者是S系列。提案是决定L1的取值。

    首先S2提出编号是0001S2的提案, 内容是L1=S2.我们采用 提案者:编号[值]这种记录方式记做 S2:0001S2[S2]。它要先进行prepare阶段,于是向A1,A2,A3,A4发送了请求。

    同时S1提出S1:0001S1[S1],发给A1-A5.进行prepare。

    我们来看这5个A的回复:我们用 批准者[收到的提案:给出的回复]这个格式来描述一下A系列的行为。回复有三种1 ok表示做出承诺 (没有做出批准的时候)2 null表示没有给回复(提案编号比承诺过的编号小)3value表示做出承诺,同时给出value(已经做出过批准的时候)

     A1[0001S1:ok] [0001S2:ok]  A1先接到s1的prepare请求,做出了承诺,又接到s2的请求,做出了承诺 现在A1的承诺是不批准<0001S2 A1要记住他的承诺和值 记做 A1-(0001S2,null)

     A3[0001S2:ok] [0001S1:null]  A3-(0001S2,null)

     A2[0001S1:ok]                     A2-(0001S1,null)

     A5[0001S1:ok]                     A5-(0001S1,null)

    注意,到此时,S1已经收到了一个多数派的回复(A1,A2,A5) A3因为违反承诺,没有给S1回复。这时S1开始批准阶段的请求,其中提案的值是S1(因为prepare阶段没有批准者返回值)。请求顺序是 A5,A4,A3,A2,A1(A3虽然没有给回复,但是是可以向它发批准请求的。算法优化的问题现在不讨论,只讲正确性)。那么这个时候的回复应该是什么样呢?

    A5[0001S1:ok] A5要记住当前的提案编号是0001S1 value是S1。记做 A5-(0001S1,S1)

    A4 我们假设A4由于网络原因,没有收到prepare阶段的请求,直接受到了批准阶段请求。按照规则,他应该做出批准,并且记住状态

    A4[0001S1:ok]       A4-(0001S1,S1)

    A3 已经做出承诺不能批准所以不回复

    A3[0001S1:null]     A3-(0001S2,null)

    A2[0001S1:ok]      A2-(0001S1,S1)

   已经达到多数派,提案通过,A1其实不会给出回复。不过没关系了。现在我们再看后来的提议者S2. S2向A1,A2,A3,A4发出请求,到目前为止,只有A1,A3收到了请求。我们继续推演后续的A收到请求的反应。

    假设现在A4收到请求 考虑A4的状态A4-(0001S1,S1)  因为新的提议编号更大,所以A4要回复给S2提议的值,状态不变

    A4[0001S2:S1]      A4-(0001S1,S1)

    A2[0001S2:S1]      A2-(0001S1,S1)

    这个时候S2妄图提出值为S2的提案的企图没能得到多数派的同意,不会进入批准阶段了。

————————————————————————–

    这个例子只是形象的看了一下算法是如何工作的。实际算法的正确性和完备性是上面的证明过程。这种举例只是为了帮助形象化理解,有兴趣的同学可以自己再推演一下其他情况,比如S2胜出,比如S1在得到多数派prepare ok后 只向两个A发出了批准请求然后挂掉等情况。

    真正在做算法实现的时候还是有一些注意的事情的,比如我们假设一个比较极端的情况,S1在prepare时候向A1-A5发申请,都得到了回复。然后S2 也发了一遍申请,当然也都得到回复,这个时候S1发出提案,实际上是得不到回复的。现在假设S2突然挂掉。那么对于我们的例子,没人得到锁。如果是用来选 master,到现在还没人是master。这个时候S1应该再做一次提案采用更大的一个编号(这就是为什么我的例子中,提案编号前面是几位的序号)。但是这样实现,又面临着S1,S2交替增大编号,谁也得不到批准的问题。这些问题都是需要注意的(实现的时候足够注意就可以解决)。这些不属于本文讨论内容,这里只是提醒一下。