这篇文章以一种易于理解的方式来解释 Multi-Paxos 的机制。算法
一种实现方式是用一组基础 Paxos 实例,每条记录都有一个独立的 Paxos 实例,要想这么作只须要为每一个 Prepare 和 Accept 请求增长一个小标索引(index),用来选择特定的记录,全部的服务器为日志里的每条记录都保有独立的状态。安全
上图展现了一个请求的完整周期。服务器
从客户机开始,它向服务器发送所需执行的命令,它将命令发送至其中一台服务器的 Paxos 模块。网络
这台服务器运行 Paxos 协议,让该条命令(shl)被选择做为日志记录里的值。这须要它与其余服务器之间进行通讯,让全部的服务器都达成一致。并发
服务器等待全部以前的日志被选定后,新的命令将被应用到状态机。app
这时服务器将状态机里的结果返回给客户端。性能
这是个基本机制,后面会做详细介绍。3d
本页介绍了 Multi-Paxos 所需解决的一些基本问题,让 Multi-Paxos 在实际中得以正确运行。日志
这里须要注意的是,基础 Paxos 已经有很是完整地描述,并且分析也证实它是正确的。很是易于理解。可是 Multi-Paxos 确不是这样,它的描述很抽象,有不少选择处理方式,没有一个是很具体的描述。并且,Multi-Paxos 并无为咱们详细的描述它是如何解决这些问题的。code
这篇文章以一种易于理解的方式来解释 Multi-Paxos 的机制。如今咱们尚未实现它,也尚未证实它的正确性,因此后续解释可能会有 bug 。但但愿这些内容能够对解决问题、构建可用的 Multi-Paxos 协议有所帮助
第一个问题是当接收到客户端请求时如何选择日志槽,咱们以上图中的例子来阐述如何作到这点。假设咱们的集群里有三台服务器,因此 “大多数” 指的是 2 。这里展现了当客户端发送命令(jmp)时,每台服务器上日志的状态,客户端但愿这个请求值能在日志中被记录下来,并被状态机执行。
当接收到请求时,服务器 S1 上的记录可能处于不一样的状态,服务器知道有些记录已经被选定(1-mov,2-add,6-ret),在后面我会介绍服务器是如何知道这些记录已经被选定的。服务器上也有一些其余的记录(3-cmp),但此时它还不知道这条记录已经被选定。在这个例子中,咱们能够看到,实际上记录(3-cmp)已经被选定了,由于在服务器 S3 上也有相同的记录,只是 S1 和 S3 还不知道。还有空白记录(4-,5-)没有接受任何值,不过其余服务器上相应的记录可能已经有值了。
如今来看看发生些什么:
当 jmp 请求到达 S1 后,它会找到第一个没有被选定的记录(3-cmp),而后它会试图让 jmp 做为该记录的选定值。为了让这个例子更具体一些,咱们假设服务器 S3 已经下线。因此 Paxos 协议在服务器 S1 和 S2 上运行,服务器 S1 会尝试让记录 3 接受 jmp 值,它正好发现记录 3 内已经有值(3-cmp),这时它会结束选择并进行比较,S1 会向 S2 发送接受(3-cmp)的请求,S2 会接受 S1 上已经接受的 3-cmp 并完成选定。这时(3-cmp)变成粗体,不过还没能完成客户端的请求,因此咱们返回到第一步,从新进行选择。找到当前尚未被选定的记录(此次是记录 4-),这时 S1 会发现 S2 相应记录上已经存在接受值(4-sub),因此它会再次放弃 jmp ,并将 sub 值做为它 4- 记录的选定值。因此此时 S1 会再次返回到第一步,从新进行选择,当前未被选定的记录是 5- ,此次它会成功的选定 5-jmp ,由于 S1 没有发现其余服务器上有接受值。
当下一次接收到客户端请求时,首先被查看的记录会是 7- 。
在这种方式下,单个服务器能够同时处理多个客户端请求,也就是说前一个客户端请求会找到记录 3- ,下一个客户端请求就会找到记录 4- ,只要咱们为不一样的请求使用不一样的记录,它们都能以并行的方式独立运行。不过,当进入到状态机后,就必须以必定的顺序来执行命令,命令必须与它们在日志内的顺序一致,也就是说只有当记录 3- 完成执行后,才能执行记录 4- 。
下一个须要解决的就是效率问题。在以前描述过的内容中存在两个问题:
第一个问题就是当有多个 提议者(proposer) 同时工做时,仍然会有可能存在竞争冲突的状况,有些请求会被要求从新开始,可能你们还会记得在 基础 Paxos 里介绍过的死锁状况。一样的情况也能够在这里发生,当集群压力过大时,这个问题会很是明显,若是有不少客户端并发的请求集群,全部的服务器都试图在同一条记录上进行值的选定,就可能会出现系统失效或系统超负荷的状况。
第二个问题就是每次客户端的请求都要求两轮的远程调用,第一轮是提议的准备(Prepare)请求阶段,第二轮是提议的接受(Accept)请求阶段。
为了让事情更有效率,这里会作两处调整。首先,咱们会安排单个服务器做为活动的 提议者(proposer) ,全部的提议请求都会经过这个服务器来发起。咱们称这个服务器为 领导者(leader) 。其次,咱们有可能能够消除几乎全部的准备(Prepare)请求,为了达到目的,咱们能够为 领导者(leader) 使用一轮提议准备(Prepare),可是准备的对象是完整的日志,而不是单条记录。一旦完成了准备(Prepare),它就能够经过使用接受(Accept)请求,同时建立多条记录。这样就不须要屡次使用准备(Prepare)阶段。这样就能减小一半的 RPC 请求。
选举领导者的方式有不少,这里只介绍一种由 Leslie Lamport 建议的简单方式。这个方式的想法是,由于服务器都有它们本身的 ID ,让有最高 ID 的服务器做为领导者。能够经过每一个服务器按期(每 T ms)向其余服务器发送心跳消息的方式来实现。这些消息包含发送服务器的 ID ,固然同时全部的服务器都会监控它们从其余服务器处收到的心跳检测,若是它们没有能收到某一具备高 ID 的服务器的心跳消息,这个间隔(一般是 2T ms)须要设置的足够长,让消息有足够的通讯传递时间。因此,若是这些服务器没有能接收到高 ID 的服务器消息,而后它们会本身选举成为领导者。也就是说,首先它会从客户端接受到请求,其次在 Paxos 协议中,它会同时扮演 提议者(proposer) 和 接受者(acceptor) 两种角色。若是机器可以接收到来自高 ID 的服务器的心跳消息,它就不会做为领导者,若是它接收到客户端的请求,那么它会拒绝这个请求,并告知客户端与 领导者(leader) 进行通讯。另一件事是,非 领导者(leader) 服务器不会做为 提议者(proposer) ,只会做为 接受者(acceptor) 。这个机制的优点在于,它不太可能出现两个 领导者(leader) 同时工做的状况,即便这样,若是出现了两个 领导者(leader) ,Paxos 协议仍是能正常工做,只是否是那么高效而已。
应该注意的是,实际上大多数系统都不会采用这种选举方式,它们会采用基于租约的方式(lease based approach),这比上述介绍的机制要复杂的多,不过也有其优点。
另外一个提升效率的方式就是减小准备请求的 RPC 调用次数,咱们几乎能够摆脱全部的准备(Prepare)请求。为了理解它的工做方式,让咱们先来回忆一下为何咱们须要准确请求(Prepare)。首先,咱们须要使用提议序号来阻止老的提议,其次,咱们使用准备阶段来检查已经被接受的值,这样就可使用这些值来替代本来本身但愿接受的值。
第一个问题是阻止全部的提议,咱们能够经过改变提议序号的含义来解决这个问题,咱们将提议序号全局化,表明完整的日志,而不是为每一个日志记录都保留独立的提议序号。这么作要求咱们在完成一轮准备请求后,固然咱们知道这样作会锁住整个日志,因此后续的日志记录不须要其余的准备请求。
第二个问题有点讨巧。由于在不一样接受者的不一样日志记录里有不少接受值,为了处理这些值,咱们扩展了准备请求的返回信息。和以前同样,准备请求仍然返回 接受者(acceptor) 所接受的最高 ID 的提议,它只对当前记录这么作,不过除了这个, 接受者(acceptor) 会查看当前请求的后续日志记录,若是后续的日志里没有接受值,它还会返回这些记录的标志位 noMoreAccepted 。
最终若是咱们使用了这种领导者选举的机制,领导者会达到一个状态,每一个 接受者(acceptor) 都返回 noMoreAccepted ,领导者知道全部它已接受的记录。因此一旦达到这个状态,对于单个 接受者(acceptor) 咱们不须要再向这些 接受者(acceptor) 发送准备请求,由于它们已经知道了日志的状态。
不只如此,一旦从集群大多数 接受者(acceptor) 那得到 noMoreAccepted 返回值,咱们就不须要发送准备的 RPC 请求。也就是说, 领导者(leader) 能够简单地发送接受(Accept)请求,只须要一轮的 RPC 请求。这个过程会一直执行下去,惟一能改变的就是有其余的服务器被选举成了 领导者(leader) ,当前 领导者(leader) 的接受(Accept)请求会被拒绝,整个过程会从新开始。
这个问题的目的是让全部的 接受者(acceptor) 都能彻底接受到日志的最新信息。如今算法并无提供完整的信息。例如,日志记录可能没有在全部的服务器上被完整复制,所选择的值只是在大多数服务器上被接受。但咱们要保证的就是每条日志记录在每台服务器上都被彻底复制。第二个问题是,如今只有 提议者(proposer) 知道某个已被选定的特定值,知道的方式是经过收到大多数 接受者(acceptor) 的响应,但其余的服务器并不知道记录是否已被选定。例如, 接受者(acceptor) 不知道它们存储的记录已被选定,因此咱们还想通知全部的服务器,让它们知道已被选定的记录。提供这种完整信息的一个缘由在于,它让全部的服务器均可以将命令传至它们的状态机,而后经过这个状态机执行这些命令。因此这些状态机能够和领导者服务器上的状态机保持一致。若是我没有这么作,他们就没有日志记录也不知道哪一个日志记录是被选定的,也就没法在状态机中执行这些命令。
下面会经过四步来解释这个过程:
第一步,在咱们达成仲裁以前不会中止接受(Accept)请求的 RPC 。也就是说若是咱们知道大多数服务器已经选定了日志记录,那么就能够继续在本地状态机中执行命令,并返回给客户端。可是在后台会不断重试这些 Accept RPC 直到得到全部服务器的应答,因为这是后台运行的,因此不会使系统变慢。这样就能保证在本服务器上建立的记录能同步到其余服务器上,这样也就提供了完整的复制。但这并无解决全部问题,由于也可能有其余更早的日志记录在服务器崩溃前只有部分已复制,没有被完整复制。
第二步,每台服务器须要跟踪每一个已知被选中的记录,须要作到两点:首先,若是服务器发现一条记录被选定,它会为这条记录设置 acceptedProposal 值为无穷大 ∞ 。这个标志表示当前的提议已被选定,这个无穷大 ∞ 的意义在于,永远不会再覆盖掉这个已接受的提议,除非得到了另一个有更高 ID 的提议,因此使用无穷大 ∞ 能够知道,这个提议再也不会被覆盖掉。除此以外,每台服务器还会保持一个 firstUnchosenIndex 值:这个值是表示未被标识选定的最小下标位置。这个也是已接受提议值不为无穷大 ∞ 的最低日志记录
第三步, 提议者(proposer) 为 接受者(acceptor) 提供已知被选定的记录信息,它以捎带的方式在接受请求中提供相关的信息。每条由 提议者(proposer) 发送给 接受者(acceptor) 的请求都包括首个未被选定值的下标索引位置 firstUnchosenIndex ,换句话说 接受者(acceptor) 如今知道全部记录的提议序号低于这个值的都已经被选定,它能够用这个来更新本身。为了解释这个问题,咱们用例子来进行说明,假设咱们有一个 接受者(acceptor) 里的日志如上图所示。在它接收到接受请求以前,日志的信息里知道的提议序号为 一、二、三、5 已经被标记为了选定,记录 四、6 有其余提议序号,因此它们尚未被认定是已选定的。如今假设接收到接受请求
Accept(proposal=3.4, index=8, value=v, firstUnchosenIndex=7)
它的提议序号是 3.4 ,firstUnchosenIndex 的值为 7 ,这也意味着在 提议者(proposer) 看来,全部 1 至 6 位的记录都已经被选定, 接受者(acceptor) 使用这个信息来比较提议序号,以及日志记录里全部已接受的提议序号,若是存在任意记录具备相同的提议序号,那么就会标记为 接受者(acceptor) 。在这个例子中,日志记录 6 有匹配的提议序号 3.4 ,因此 接受者(acceptor) 会标记这条记录为已选定。之因此能这样,是由于 接受者(acceptor) 知道相关信息。首先,由于 接受者(acceptor) 知道当前这个日志记录来自于发送接受消息的同一 提议者(proposer) ,咱们同时还知道记录 6 已经被 提议者(proposer) 选定,并且咱们还知道, 提议者(proposer) 没有比这个日志里更新的值,由于在日志记录里已接受的提议序号值与 提议者(proposer) 发送的接受消息中的提议序号值相同,因此咱们知道这条记录在选定范围之内,它仍是咱们所能知道的, 提议者(proposer) 里可能的最新值。因此它必定是一个选定的值。因此 接受者(acceptor) 能够将这条记录标记为已选定的。由于同时咱们还接收到关于新记录 8 的请求,因此在接收到接受消息以后,记录 8 处提议序号值为 3.4 。
这个机制没法解决全部的问题。问题在于 接受者(acceptor) 可能会接收到来自于不一样 提议者(proposer) 的某些日志记录,这里记录 4 可能来自于以前轮次的服务器 S5 ,不幸的是这种状况下, 接受者(acceptor) 是没法知道该记录是否已被选定。它也多是一个已失效过期的值。咱们知道它已经被 提议者(proposer) 选定,可是它可能应该被另一个值所取代。因此还须要多作一步。
这一系列机制能够保证最终,全部的服务器里的日志记录均可以被选定,并且它们知道已被选定。在一般状况下,是不会有额外开销的,额外的开销仅存在与领导者被切换的状况,这个时间也很是短暂。
Multi-Paxos 第五个问题是客户端如何与系统进行交互的。若是客户端想要发送一条命令,它将命令发送给当前集群的 领导者(leader) 。若是客户端正好刚启动,它并不知道哪一个服务器是做为 领导者(leader) 的,这样它会向任一服务器发送命令,若是服务器不是 领导者(leader) 它会返回一些信息,让客户端重试并向真正的 领导者(leader) 发送命令。一旦 领导者(leader) 收到消息, 领导者(leader) 会为命令肯定选定值所处位置,在肯定以后,就会将这个命令传递给它本身的状态机,一旦状态机执行命令后,它就会将结果返回给客户端。客户端会一直向某一 领导者(leader) 发送命令,知道它没法找到这个 领导者(leader) 为止,例如, 领导者(leader) 可能会崩溃,此时客户端的请求会发送超时,在此种状况下,客户端会随便选择任意随机选取一台服务器,并对命令进行重试,最终集群会选择一个新的 领导者(leader) 并重试请求,最终请求会成功获得应答。
可是这个重试机制存在问题。
若是 领导者(leader) 已经成功执行了命令,在响应前的最后一秒崩溃了?这时客户端会尝试在新 领导者(leader) 下重试命令,这样就可能会致使相同的命令被执行两次,这是不容许发生的,咱们须要保证的是每一个命令都仅执行一次。为了达到目的,客户端须要为每条命令提供一个惟一的 ID ,这个 ID 能够是客户端的 ID 以及一个序列号,这条记录包含客户端发送给服务器的信息,服务器会记录这个 ID 以及命令的值,同时,当状态机执行命令时,它会跟踪最近的命令的信息,即最高 ID 序号,在它执行新命令以前,它会检查命令是否已经被执行过,因此在 领导者(leader) 崩溃的状况下,客户端会以新的 领导者(leader) 来重试,新的 领导者(leader) 能够看到全部已执行过的命令,包括旧 领导者(leader) 崩溃以前已执行的命令,这样它就不会重复执行这条命令,只会返回首次执行的结果。
结果就是,只要客户端不崩溃,就能得到 exactly once 的保证,每一个客户端命令仅被集群执行一次。若是客户端出现崩溃,就处于 at most once 的状况,也就是说客户端的命令可能执行,也可能没有执行。可是若是客户端是活着的,这些命令只会执行一次。
最后的一个问题在配置变动的状况。
这里说的系统配置信息指的是参与共识性协议的服务器信息,一般也就是服务器的 ID ,服务器的网络地址。这些配置的重要性在于,它决定了仲裁过程,当前仲裁的大多数表明什么。若是咱们改变服务器数量,那么结果也会发生变化。咱们有对配置提出变动的需求,例如若是服务器失败了,咱们可能须要切换并替代这台服务器,又或咱们须要改变仲裁的规模,咱们但愿集群更加可靠(好比从 5 台服务器提高到 7 台)。
这些变动须要很是当心,由于它会改变仲裁的规模。
另一点就是须要保证在任什么时候候都不能出现两个不重叠的多数派,这会致使同一日志记录选择不一样值的状况。假设咱们将集群内服务器从 3 台提高到 5 台时,在某些状况下,有些服务器会相信旧的配置是有效的,有些服务器会认为新配置是有效的。这可能会致使最上图最左侧的两台服务器还以旧的配置信息进行仲裁,选择的值是(v1),而右侧的三台服务器认为新配置是有效的,因此 3 台服务器构成了大多数,这三台服务器会选择一个不一样的值(v2)。这是咱们不但愿发发生的。
Leslie Lamport 建议的 Paxos 配置变动方案是用日志来管理这些变动,当前的配置做为日志的一条记录存储,并与其余的日志记录一同被复制同步。因此上图中 1-C一、3-C2 表示两个配置信息,其余的用来存储普通的命令。这里有趣的是,配置所使用每条记录是由它的更早的记录所决定的。这里有一个系统参数 å 来决定这个更早是多早,假设 å 为 3,咱们这里有两个配置相关的记录,C1 存于记录 1 中,C2 存于记录 3 中,这也意味着 C1 在 3 条记录内不会生效,也就是说,C1 从记录 4 开始才会生效。C2 从记录 6 开始才会生效。因此当咱们选择记录 一、二、3 时,生效的配置会是 C1 以前的那条配置,这里咱们将其标记为 C0 。这里的 å 是在系统启动时配置好的参数,这个参数能够用来限制同时使用的配置信息,也就是说,咱们是没法在 i+å 以前选择使用记录 i 中的配置的。由于咱们没法知道哪些服务器使用哪些配置,也没法知道大多数所表明的服务器数量。因此若是 å 的值很小时,整个过程是序列化的,每条记录选择的配置都是不一样的,若是 å 为 3 ,也就意味着同时有三条记录可使用相同的配置,若是 å 大不少时,事情会变得更复杂,咱们须要长时间的等待,让新的配置生效。也就是说若是 å 的值是 1000 时,咱们须要在 1000 个记录以后才能等到这个配置生效。
参考来源:
2013 Paxos lecture, Diego Ongaro
Wiki: Byzantine fault tolerance