Paxos推导过程

2025/01/01

Tags: 分布式 paxos 分布式共识问题

paxos 是一个分布式共识算法, 就是用来解决分布式系统中, 多副本数据如何保证读写一致性的问题.

不完美的副本数据同步机制

假设我们有个分布式存储系统, 数据在写入时, 需要把数据写入到其他节点: 副本数据从一个节点复制到其他节点, 有几种复制办法:

同步复制

# node1, node2, node3 

a=x --> node1 --> node2
              ------> node3
                           --> done

数据在一次写入时, 需要同时写入 node1, node2, node3 三个节点, 写入完成才算是一次写入成功;

同步复制有什么问题:

  1. 性能低下; 写入性能会受制于节点数量;
  2. 没有容错, 任何一个节点写入失败, 则系统不可用;

异步复制

a=x --> node1 
             --> done
             async( node1  --> node2 ; node1  --> node3 ) 

数据在一次写入时, 只要写入一个节点, 则认定写入成功, node1 写入 node2; node1 写入 node3 是异步复制, 不影响整体写入结果;

异步复制有什么问题:

  1. 数据可能存在不一致, 当 async( node1 –> node2) 写入失败时, node2 和 node1 上的数据就不一致;

半同步复制

数据在一次写入时, 数据必须写入一定量的副本(不是全部), 这样多副本则提供了较高的可靠性;

a=x --> node1 
              --> node2
                       --> done
                       async( node1  --> node3 )            

半同步复制存在什么问题:

  1. 数据可能不一致, 但比纯异步复制要好, 保证尽可能多的节点能达到一致;

多副本同步如何保证读写一致性

基于以上三种复制的思路, 可以发现半同步复制是比纯异步和纯同步方式更优的解决方案, 可以基于这个思路继续推导;

在半同步复制中, 虽然最终所有节点也可能出现不一致, 但是可以保证大多数的节点达到数据一致, 如果在读取数据时, 也能保证从大多数节点中读, 那基于写入的“大多数一致性” , 数据在读取时, 也能做到读的 “大多数一致性” .

quorum

对于大多数节点的定义: 超过整个集合半数以上的节点, 可以被称为一个 多数派 Quorum , Quorumsize=(N/2)+1; (下文统称为 Quorum),例如,一个三个节点的系统,可能的 Quorum 有如下情况:

node1, node2, node3

1. node1, node2
2. node2, node3
3. node1, node3

在 Quorum 读取时, 可能出现以下情况:

       t1     t2
node1  a=x    a=y
node2  a=x    a=y
node3  a=x    a=x

增加版本

针对上述这种情况, 考虑 t1 时刻, t2 时刻 node 中的数据存在变化, 其实数据就自然而然带上了版本的概念, 在读取时, 需选取最高版本的数据;

再来看一下 Quorum 的写入,假设有以下场景:

       t1      t2      t3
node1  a=x1    a=x2    a=x3
node2  a=x1    a=x2    a=x3
node3  a=x1    a=x1    [a=x3, a=x2]

由于给数据加上版本后, 还需要保证数据不被历史版本的数据覆盖, 因此 每个 node 在接收写入请求时, 需要拒绝掉比当前版本更小的数据写入. 因此, 每个node 需要记录自己上一次写入数据的版本.

还有个关键问题是: 版本如何确定?

由于 node 记录了上一次写入的数据版本, 那么在 client 写入之前, 需要先去查询一下当前最新的版本, 版本号也需要遵循多数派Quorum和最新的原则, 然后 client 生成新的版本之后执行写入操作;

并发写入问题

假定此时有两个client p1, p2 并发向 node 写入:

t1 时刻, p1, p2 读取最新的版本, v=2

p1 ----------->    node1,  node2,  node3     <-----------p2   # t1 时刻
                   a=x1,   a=x2,   a=x2

t2 时刻, p1,p2 同时写入, 此时 p1 p2 的版本相同:v=3

p1 ----------->    node1,  node2,  node3     <-----------p2   # t2 时刻
   (v++;a=x3)      a=x1,   a=x2,   a=x2       (v++;a=y3)

t3 时刻, 如果此时 node1 接收了p1, node2 接收了p2, 此时节点的数据完全不一致!

p1 ----------->    node1,  node2,  node3     <-----------p2   # t3 时刻
     (v++;a=x3)    a=x3,   a=y3,   a=x2       (v++;a=y3)

如何解决并发写入的问题?

解决这个问题, node 需要拒绝掉同一个version下的其他 client 的写入, 也就是说 node 需要记录下来最新的verion中, 上一次它同意写入的 client, node 要记住的前提是client 需要提前告知node, 其实就是client 在 t2 时刻之前, 还需要进行一次 Quorum 读, 告诉node, I‘ll write !

另外, 同一个 version 下, node 记录多个 client 的写前读时, 只能记录最后一次的写前读的 client ;

改进后的流程如下:

t1 时刻, p1, p2 读取最新的版本, v=2

p1 ----------->    node1,  node2,  node3      <-----------p2  # t1 时刻
                   a=x1,   a=x2,   a=x2

t2 时刻, node 会记录下最后一次的写前读取的 client p2;

p1 ----------->    node1,  node2,  node3      <-----------p2  # t2 时刻, 写前读取
   (I'll write)    a=x1,   a=x2,   a=x2        (I'll write)
     
p1 <-----------    node1,  node2,  node3                      # t2.1 时刻, node record p1
   (ok)            a=x1,   a=x2,   a=x2
                                     
                   node1,  node2,  node3      ----------->p2  # t2.2 时刻, node record p2
                   a=x1,   a=x2,   a=x2           (ok)

t3 时刻, node 接受了 p2 的写请求, 对于 p1 的写入请求会拒绝, 最终一个 quorum 完成写入;

p1 ----------->    node1,  node2,  node3                      # t3 时刻, p1 写入
   (write,a=x3)    a=x1,   a=x2,   a=x2           
 
p1 <-----------    node1,  node2,  node3                      # t3.1 时刻, reject p1
   (reject)        a=x1,   a=x2,   a=x2

                   node1,  node2,  node3      <-----------p2  # t3.2 时刻, p2 写入
                   a=y3,   a=y3,   a=x2        (write,a=y3)

推导总结

基于上述推导, 如何保证副本同步的强一致性?

paxos 算法描述

角色划分

注意这里的三个角色里没有客户端的角色,三种角色都是服务端,算法的流程也是侧重于服务端多个node如何交互;

Client       Proposer          Acceptor          Learner
  |----写请求--->|                  |                |
  |             |---Prepare(n)---->|                |
  |             |<--Promise(n)-----|                |
  |             |---Accept(n,v)--->|                |
  |             |<--Accepted(n)----|                |
  |             |                  |----Learn(v)--->|
  |<----OK------|                  |                |

算法流程

max_received_n 是算法的核心机制,首先,其必须为单调递增,保证在多个提案中,总有一个最大的,也就是 Acceptor 只保留最大的提案号,并拒绝比本地记录小的提案,当一个提案被拒绝时,说明此时的提案号已经不是最新的了,需要重新生成提案;

在Accept阶段,读取提案的数据时,也需要读取 max_accepted_n 最大版本的数据 value ,保证数据是最新的;Acceptor 在此阶段接受到Accept请求时,也判断 n ≥ max_received_n,并且拒绝掉更小编号的提案,这就意味着在两个提案同时发生时,可能会有一个被拒绝:

  1. Proposer A 发起提案n=5,进入Accept阶段。
  2. Proposer B 发起提案n=6:
  3. 在Prepare阶段,Acceptor承诺n=6。
  4. Proposer B 发现大多数Acceptor承诺了n=6,进入Accept阶段。
  5. Proposer A 的Accept请求因n=5 < 6被拒绝,需重新发起更高编号的提案(如n=7)。
  6. 结果:提案n=6成功取代n=5。