从 Basic Paxos 到 Multi-Paxos

前言

Paxos视频,很详细的了解了Paxos算法和Multi-Paxos的实现,这篇博客是学习笔记

目标:复制日志

+ 复制日志 => 复制状态机 + 所有服务器按照相同的顺序执行相同的命令 + 一致性模型确保正确的日志复制 + 只要大部分服务器是运行的系统就能工作 + 失败的模型:失败-停止(非拜占庭),延迟/丢失信息

Basic Paxos

  • Proposers:
    • 活动的:提出特定的值被选中
    • 处理客户请求
  • Acceptors:
    • 被动的:响应消息给proposers
    • 回应达成共识的投票
    • 保存选中的值
    • 想知道哪个值被选中
  • 每个服务器都包含proposer和acceptor

一个Acceptor

+ Q:如果acceptor挂掉了怎么办? + A:多个acceptors,选中被大多数acceptors的值

问题:平分投票

+ Q:Acceptor只接受第一个到达的值? + A:如果同时提出提案,不会选中任何值,所以Acceptors必须可以选择多个不同的值

问题:选择冲突

  • Q:Acceptor接受所有到达的值?
  • A:可能会导致选择多个值(如下图中的S3),所以一旦一个值被选中,以后的提案必须提出/选择一样的值(两阶段提交)

仍然有选择冲突

下图的流程: 1. s1看到此时其他服务器没有接受某个值,之后提出red值 2. s5当时也看到此时没有其他服务器没有接受某个值,就提出blue值 3. blue很快就被s3、s4、s5接受,而s3之后也会接受red值 + s1的提案必须被终止(s3必须拒绝它) + 因此提案必须有序,拒绝旧的提案

提案值

  • 每个提案有一个唯一的值
    • 大的优先级高
    • 提案者必须选择一个比之前见过的还大的提案值
  • 一个简单的方法:
    • 一个提案值包括 轮值+服务器ID
    • 每个服务器保存 maxRound
    • 为产生一个新的提案值:
      • 递增 maxRound
      • 与服务器ID关联
    • 提案者必须将 maxRound 持久化到磁盘,防止提案值在机器宕机重启后重用

两阶段方法

  • 阶段一:Prepare RPCs
    • 找到已经被选中的值
    • 阻止旧的提案
  • 阶段二:Accept RPCs
    • 请求acceptors接受值

流程

例子

在之后提出一个提案有三种可能: 1. 之前的值已经被选中了 + 新的Proposer会发现它并使用它 + 先说明一下方框内写的是什么意思,P表示Prepare,A表示Accept, 3.1中的3表示 Round Number,1表示由s1提出,X为acceptedValue + 可以看到之后的提案了解到X已经被选中,s3、s4、s5也会选中同样的值

  1. 之前没有值被选中,但已经被接受

    • 新的Proposer会发现它并使用它
    • 与前一种情况类似
  2. 之前没有值被选中,也没看到值被接受

    • 后来的Proposer会使用自己的值
    • 旧的值会被拒绝

活锁

其它

  • 只有 proposer 知道哪个值被选中了,acceptor是不知道的
  • 如果其它服务器想知道,就必须运行paxos

Multi-Paxos

  • 用日志项将 Basic Paxos 分成每一轮
    • 添加 index 参数到 Prepare 和 Accept

概述

  • 怎么选择哪个日志项来存放给定的客户请求?
  • 性能优化:
    • 使用 leader 减少 proposer 冲突
    • 消除大部分 Prepare 请求
  • 复制的完整型
  • 客户协议
  • 配置变更

选择日志项

  • 当客户传来请求:
    • 找到第一个没有被选中的日志项
    • 运行 Basic Paxos,在这个 index, propose 客户命令
    • Prepare 返回 acceptedValue?
      • Yes,结束选中 acceptedValue
      • No,选中客户命令

  • 当接收到请求时,服务器 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 没有发现其他服务器上有接受值。

注意事项: + 在这种方式下,单个服务器可以同时处理多个客户端请求,也就是说前一个客户端请求会找到记录 3- ,下一个客户端请求就会找到记录 4- ,只要我们为不同的请求使用不同的记录,它们都能以并行的方式独立运行。 + 不过,当进入到状态机后,就必须以一定的顺序来执行命令,命令必须与它们在日志内的顺序一致,也就是说只有当记录 3- 完成执行后,才能执行记录 4- 。

提高效率

  • Basic Paxos 是低效的
    1. 多个 proposers 冲突问题
    2. 对于选中一个值需要两轮RPCs(Prepare、Accept)
  • 解决:
    1. 选一个 leader
      • 在一个时刻,只有一个 Proposer
    2. 消除大部分 Prepare RPCs
      • 一轮 Prepare 完整的日志,而不是单条记录。
      • 一旦完成了 Prepare,它就可以通过 Accept 请求,同时创建多条记录。这样就不需要多次使用 Prepare 阶段。这样就能减少一半的 RPCs。

领导选举

选举领导者的方式有很多,这里只介绍一种由 Leslie Lamport 建议的简单方式。

  • 让有 最高 ID 的服务器作为领导者
  • 可以通过每个服务器定期(每 T ms)向其他服务器 发送心跳消息 的方式来实现。这些消息包含发送服务器的 ID
  • 如果它们没有能收到某一具有高 ID 的服务器的心跳消息,这个间隔(通常是 2T ms)需要设置的足够长,让消息有足够的通信传递时间。所以,如果这些服务器没有能接收到高 ID 的服务器消息,然后它们会自己选举成为领导者。
  • 也就是说,首先它会从客户端接受到请求,其次在 Paxos 协议中,它会 同时扮演 proposer 和 acceptor
  • 如果机器能够接收到来自高 ID 的服务器的心跳消息,它就不会作为 leader,如果它接收到客户端的请求,那么它会 拒绝 这个请求,并告知客户端与 leader 进行通信。
  • 非 leader 服务器不会作为 proposer,只会作为 acceptor
  • 这个机制的优势在于,它不太可能出现两个 leader 同时工作的情况,即使这样,如果出现了两个 leader,Paxos 协议还是能正常工作,只是不是那么高效而已。
  • 应该注意的是,实际上大多数系统都不会采用这种选举方式,它们会采用基于 租约 的方式(lease based approach),这比上述介绍的机制要复杂的多,不过也有其优势。

减少 Prepare 的 RPC 请求

  • 为什么需要 Prepare?
    • 需要使用提议序号来 阻止 旧的提议
    • 使用 Prepare 阶段来检查已经被接受的值,这样就可以使用这些值来替代原本自己希望接受的值。
  1. 第一个问题是阻止所有的提议,我们可以通过改变提议序号的含义来解决这个问题, 将提议序号全局化,代表完整的日志 ,而不是为每个日志记录都保留独立的提议序号。这么做要求我们在完成一轮准备请求后,当然我们知道这样做会锁住整个日志,所以后续的日志记录不需要其他的准备请求。

  2. 第二个问题有点讨巧。因为在不同接受者的不同日志记录里有很多接受值,为了处理这些值,我们扩展了准备请求的返回信息。和之前一样,准备请求仍然返回 acceptor 所接受的最高 ID 的提议,它只对当前记录这么做,不过除了这个, acceptor 会查看当前请求的后续日志记录,如果后续的日志里没有接受值 ,它还会返回这些记录的标志位 noMoreAccepted

  3. 使用了这种领导者选举的机制,领导者会达到一个状态,每个 acceptor 都返回 noMoreAccepted ,领导者知道所有它已接受的记录。所以一旦达到这个状态,对于单个 acceptor 我们不需要再向这些 acceptor 发送准备请求,因为它们已经知道了日志的状态。

  4. 不仅如此,一旦从集群 大多数 acceptor 那获得 noMoreAccepted 返回值 ,我们就 不需要发送准备的 RPC 请求 。也就是说, leader 可以简单地发送 Accept 请求,只需要一轮的 RPC 请求。这个过程会一直执行下去,唯一能改变的就是有其他的服务器被选举成了 leader ,当前 leader 的 Accept 请求会被拒绝,整个过程会重新开始。

复制的完整性

  • 之前的复制是不完整的
    • 日志项只需要复制到大多数,我们需要复制到 全部
    • 只有 proposer 知道某个日志项被选中了,我们需要所有服务器都知道哪些日志项被选中了

解决办法:

  1. 在后台持续尝试 Accept RPCs 直到所有 acceptors 返回
  2. 追踪被选中的日志项
    • 让acceptedProposal[i] = 无穷大,这样这个日志项就不可以被覆盖,也就是说被选中了
    • 每个服务器还会保持一个 firstUnchosenIndex:表示未被标识选定的最小下标值
  3. proposer 告诉 acceptors 已经被选中的日志项
    • Proposer 在 Accept RPCs 中包含它的 firstUnchosenIndex
    • Acceptor 将选中第i个日志项,如果:
      • i < request.firstUnchosenIndex
      • acceptedProposal[i] == request.proposal
    • 结果:acceptors知道大多数被选中的日志项

该 acceptor 在 accept 之前的第6个日志项的提议序号为 3.4 , 这时它收到一个提议序号也为 3.4 的 accepte 请求,并且 请求的 firstUnchosenIndex = 7,大于之前 3.4 所在的 6,所以将选中第6个日志项,同时该请求的 index = 8,将 3.4 的请求放到第8个日志项

这个机制无法解决所有的问题。问题在于 acceptor 可能会接收到来自于不同 proposer 的某些日志记录,这里记录 4 可能来自于之前轮次的服务器 S5 ,不幸的是这种情况下, acceptor 是无法知道该记录是否已被选定。它也可能是一个已失效过时的值。

我们知道它已经被 proposer 选定,但是它可能应该被另外一个值所取代。所以还需要多做一步。

  1. 旧 leader 的日志项
    • Acceptor 返回它的 fristUnchosenIndex 在 Accept回答中
    • 如果 proposer 的 firstUnchosenIndex > acceptor回答的 firstUnchosenIndex(说明 acceptor 中的某些状态不确定),proposer就会在后台发送 Success RPC
    • Success(index, v):通知 acceptor 该日志项被选中了
      • acceptedValue[index] = v
      • acceptedProposal[index] = 无穷大
      • return firstUnchosenIndex
      • 如果需要(可能存在多个不确定的状态),Proposer 发送额外的Success RPC

客户端协议

  • 发送命令给 leader
    • 如果不知道哪个是 leader,随便选择一个服务器
    • 如果该服务不是 leader,重定向 给 leader
  • leader 直到日志项被选中并且被 leader 的状态机执行才回应客户
  • 如果请求超时(leader 挂了)
    • 客户重新发送命令给其他服务器
    • 最终重定向给新 leader
    • 给新 leader 重试请求

问题

  • 如果 laeder 在执行完命令但在回应之前挂掉
    • 该命令不能执行两次
  • 解决:客户端需要为每条命令提供一个 唯一的 id

    • 服务器会将该 id 存入日志项中
    • 状态机会记录每个客户最近执行的命令,即 最高 id 序号
    • 在执行命令之前,状态机会检查该命令如否被执行过,如果执行过:
      • 忽略
      • 返回之前执行的结果
  • 结果:只要客户端不崩溃,就能获得 exactly once 的保证,每个客户端命令仅被集群执行一次。如果客户端出现崩溃,就处于 at most once 的情况,也就是说客户端的命令可能执行,也可能没有执行。但是如果客户端是活着的,这些命令只会执行一次

配置变更

同一时刻不可以出现两个不重叠的多数派

解决办法:

  • Leslie Lamport 建议的 Paxos 配置变更方案是用 日志 来管理这些变更,当前的配置作为日志的一条记录存储,并与其他的日志记录一同被复制同步。
  • 所以下图中 1-C1、3-C2 表示两个配置信息,其他的用来存储普通的命令。这里有趣的是,配置所使用每条记录是由它的更早的记录所决定的。
  • 这里有一个系统参数 å 来决定这个更早是多早,假设 å 为 3,我们这里有两个配置相关的记录,C1 存于记录 1 中,C2 存于记录 3 中,这也意味着 C1 在 3 条记录内不会生效,也就是说,C1 从记录 4 开始才会生效。C2 从记录 6 开始才会生效。所以当我们选择记录 1、2、3 时,生效的配置会是 C1 之前的那条配置,这里我们将其标记为 C0 。
  • 这里的 å 是在系统启动时配置好的参数,这个参数可以用来限制同时使用的配置信息,也就是说,我们是无法在 i+å 之前选择使用记录 i 中的配置的。因为我们无法知道哪些服务器使用哪些配置,也无法知道大多数所代表的服务器数量。所以如果 å 的值很小时,整个过程是序列化的,每条记录选择的配置都是不同的,如果 å 为 3 ,也就意味着同时有三条记录可以使用相同的配置,如果 å 大很多时,事情会变得更复杂,我们需要长时间的等待,让新的配置生效。也就是说如果 å 的值是 1000 时,我们需要在 1000 个记录之后才能等到这个配置生效。

总结

  • Basic Paxos:
    • Prepare 阶段
    • Acceptor 阶段
  • Multi-Paxos:
    • Choosing log entries
    • Leader election
    • Eliminting most Prepare requests
    • Full infomation propagation
  • 客户协议
  • 配置变更