说multi-paxos以前先简要说一下paxos算法
paxos是在多个成员之间对某个值(提议)达成一致的一致性协议。这个值能够是任何东西。好比多个成员之间进行选主,那么这个值就是主的身份。在把multi-paxos协议应用在日志同步中时,这个值就是一条日志。网上讲paxos的文章已经不少了,这里简要说明一下。网络
paxos分为prepare和accept两个阶段。协议中有两个主要的角色,proposer和acceptor。app
value被majority accept以前,每一个acceptor能够accept多个不一样的值。可是,一旦一个value被majority accept(即value达成一致),那么这个value就不会变了。由于prepare阶段会将该value给找出来,随后accept阶段会使用这个value,后续的全部的提案都会选择这个value。优化
须要注意的是,每一个阶段都是收到majority的响应后即开始处理。而且因为机器会宕机,acceptor须要对acceptedProposalID, acceptedValue和minProposal进行持久化。spa
从流程中能够看出prepare有两个做用:日志
能够看出,一次paxos达成一致至少须要两次网络交互。blog
paxos是对一个值达成一致,multi-paxos是运行多个paxos instance来对多个值达成一致,每一个paxos instance对一个值达成一致。在一个支持多写而且强一致性的系统中,每一个节点均可以接收客户端的写请求,生成redo日志而后开始一个paxos instance的prepare和accept过程。这里的问题是每条redo日志到底对应哪一个paxos instance。索引
在日志同步应用中,用log id来区分不一样的paxos instance。每条日志都由一个id惟一标示,这个log id标识一个paxos instance,这个paxos instance达成一致,即对应的日志内容达成一致,即majority的成员accept了这个日志内容。在一个由N个机器(每一个机器既承担proposer也承担acceptor角色)组成的集群(一般叫作paxos group)中,每一个proposer均可以产生redo日志而且进行paxos instance,那么每条redo日志到底使用哪一个log id? 显然,每一个proposer都会选择本身知道的尚未达成一致的最小的log id来做为此次日志的log id,而后运行paxos协议,显然,多个proposer可能会选择同一个log id(最典型的场景就是空集群启动的状况下),最终,只有一个proposer可以成功,那么其余的proposer就须要选择更大的未达成一致的log id来运行paxos。显然,这种冲突是很是严重的,会有不少的proposer成功不了进而选择更大的log id来运行paxos。ip
在真实的系统中,好比chubby, spanner,都会在paxos group中选择一个成员做为leader,只有leader可以propose日志,这样,prepare阶段就不会存在冲突,至关于对整个log文件作了一次prepare,后面这些日志均可以选用同一个proposal id。这样的话,每条日志只须要一次网络交互就能达成一致。回顾一下文章开头提到paxos中须要每一个成员须要记录3个值,minProposal,acceptedProposal,acceptedValue,其中后面两个值能够直接记录在log中,而第一个值minProposal能够单独存在一个文件中。因为这里后面的日志均可以选用同一个proposal id,显然,在大部分时间内,minProposal都不须要改变。这正是multi-paxos的精髓同步
对于paxos来讲,主的身份无所谓,主不须要像raft那样拥有最全的已经commit的日志。因此选主算法无所谓,好比你们都给机器ip最大的机器投票,或者给日志最多的投票,或者干脆直接运行一次paxos,值的内容就是主的身份。显然,因为对新主的身份无限制,那么,新主颇有可能没有某些已经达成一致的日志,这个时候,就须要将这些已达成一致的日志拉过来,另外,新主也有可能没有某些还未达成一致的日志。以下图所示:
图中,恢复以前,log id等于3的日志C已经在多数派上达成了一致,可是在新主上没有。好比log id等于4的日志D在多数派上没有达成一致,在新主上也没有。
新主向全部成员发送查询最大log id的请求,收到majority的响应后,选择最大的log id做为日志恢复的结束点。图中,若是收到的majority不包括2号成员,那么log id=6为恢复结束点。若是收到的majority包括2号成员,那么log id=7为恢复结束点。这里取majority的意义在于恢复结束点包含任何的majority达成一致的日志。拿到log id后,从头开始扫描日志,对于每条日志都运行paxos协议确认一次:若是日志以前已经达成一致了,好比日志A,B,C,E,F,那么再次运行paxos的prepare阶段会把日志内容找出来做为accept阶段的值,不影响结果。若是日志以前并无达成一致,好比日志D,那么当返回的majority中包含3号成员时,D会被选出来看成accept阶段的值,当返回的majority中不包含3号成员时,那么D实际上不会被选出,这时主能够选择一个dummy日志做为accept阶段的值。
能够看出,若是日志很是多,每次重启后都要对每条日志作一次paxos,那么恢复时间可想而知。在上面的例子中,A,B,E已经达成一致,作了无用功。paxos协议中,只有主即proposer知道哪些日志达成了一致,acceptor不知道,那么很容易想到的一个优化就是proposer将已经达成一致的日志id告诉其余acceptor,acceptor写一条确认日志到日志文件中。后续重启的时候,扫描本地日志只要发现对应的确认日志就知道这条日志已经达成多数派,不须要从新使用paxos进行确认了。这种作法有一个问题,考虑以下场景:
旧主成功的给本身和2号成员发送了确认日志,可是没有给3号成员发送成功旧挂了,而后2号成员被选为新主,那么新主不会对log id=3的日志从新运行paxos,由于本机已经存在确认日志。这样的话,3号成员就回放log id=3的日志到上层了。解决这个问题的作法就是followers须要主动的向主询问日志到底有没有达成一致,若是有,则本身补充确认日志。
宕机重启后,对未达成一致的日志从新运行paxos时,如log id=4的日志,若是返回的majority中不包含3号成员,那么日志D不会被找出来,这样就须要将3号成员的log id=4日志置未一条无操做的日志记做NOP日志,D最终也就不会造成多数派。因为multi-paxos容许日志乱序接收,而且日志的长度几乎都不同,因此在磁盘上log id是乱序的,因此从物理上说,每一个成员的日志不是如出一辙的。那么要把log id=4的日志覆盖写成NOP日志也就比较麻烦,须要为每条日志维护索引。实现上能够不覆盖写,直接append一条log id=4的NOP日志到日志文件,这样没有问题,由于回放的时候只会回放能找到确认日志的日志到上层应用中。
multi-paxos处理成员变动比较简单,规定第i条日志参与paxos同步的时候,其成员组是第i-k条日志包含的成员组(每条日志里面都包含成员组)。
multi-paxos只是保证你们对日志达成一致。可是具体multi-paxos运用到真实的系统中时,从应用层面上看,可能会出现一些诡异的问题。考虑以下场景:
如图,1号主写了A,B,C,D,其中B,C,D没有造成多数派,而后A宕机了,2号被选为了主,客户端过来读不到B,C,D,而后B没写任何东西,就挂了,这个时候,A起来后从新被选为主,对B,C,D从新运行paxos,把B,C,D达成了一致,这个时候客户端再次过来读,又能读到B,C,D了。对于multi-paxos自己来讲,并无什么不对的地方,可是上层应用的语义出现了问题:曾经读不到的东西,什么都没作,又能读到了。
解决这个问题的方法是经过主提供服务以前必须成功写入一条start working日志来解决。以下图:
如图,每一个成员成为主提供服务以前都要首先写一条start working日志,只有达成多数派才能提供服务。1号在从新成为主以后,经过对log id=2的日志运行paxos,将2号start日志恢复了出来,而后对C和D运行paxos恢复出来后,后续回放的时候,若是发现后面日志中带有的timestamp(其实时leader上任时间)比start working带有的timestamp更小,那么就不回放到上层。随后客户端来读仍然读不到B,C,D,先后保持一致。