分布式一致性协议:Paxos协议的代码实现与测试

115 阅读12分钟

简介

参考链接 blog.csdn.net/cxy_hust/ar…

Paxos算法是分布式系统中的一种广泛使用的共识算法,适用于在多个节点之间就一个值达成一致。此实现的主要目的是让多个节点(提议者和接受者)在网络不可靠的情况下,通过多轮提议过程,最终达成对某个值的共识。

核心思想

  • 多数决策 Paxos的基本假设是:在一个包含多个节点的系统中,只要获得了大多数节点(通常超过一半)的支持,系统就可以认为达成了共识。因此,协议设计的关键在于如何确保即便某些节点发生故障,只要剩余的节点达成共识,系统整体就能继续运作。
  • 提案的唯一性和递增性 Paxos通过提案编号来保证提案的唯一性和递增性,每个提案都带有一个全局唯一且递增的编号,这样当出现多个提案时,系统可以根据提案编号来选定唯一的提案。编号较大的提案会被优先考虑,从而避免多个提案冲突导致的决策混乱。
  • 两阶段流程 Paxos协议使用了两阶段提交的流程来保证一致性:

第一阶段(Prepare 阶段):提议者(Proposer)向接受者(Acceptor)发送一个准备请求(带有提案编号的 Prepare 请求),接受者承诺不会接受编号更小的提案,并返回“承诺”消息。

第二阶段(Accept 阶段):如果提议者获得了多数接受者的承诺,它将带着提案值向接受者发起提案请求(Propose 请求)。接受者在没有承诺编号更高的提案的情况下接受该提案并返回确认消息(Accept),完成提案的达成。

  1. 处理并发提案和冲突 在分布式环境中,多个提议者可能会同时提出不同的提案。Paxos协议通过提案编号的比较来处理这些冲突:当多个提议者的提案编号不同,接受者会根据编号大小决定是否接受某个提案,确保最终只有一个提案达成共识。
  2. 容忍节点故障 Paxos不要求系统中的所有节点都在线。即使部分节点故障,剩余的大多数节点依然可以通过协商来达成共识。这种容错机制使得 Paxos 非常适合在不可靠网络和分布式系统中使用。

实现的Paxos算法的实现具有以下功能:

  • 提议过程:多个提议者(Proposer)会提出提案,包括提案编号和提议值,发送给所有接受者(Acceptor)。
  • 接受过程:接受者对提议者的提案编号和值进行评估,根据协议规则来承诺或拒绝提案。
  • 共识达成:如果一个提议者的提案获得了大多数接受者的承诺和接受(过半),就达成共识。共识达成后,其他提议者退出。
  • 错误处理和退避机制:如果提案未能获得足够的承诺或接受,提议者会重试并逐渐增加退避时间,避免高频率的冲突。

此代码的主要类和组件包括:

  • PaxosMessage:定义了Paxos算法中的消息类型(PREPARE、PROMISE、PROPOSE、ACCEPT和ERROR)和消息的结构。 -MessageManager:负责在提议者和接受者之间传递消息。它管理每个节点的收件箱,提供消息发送、广播、共识标识等功能。
  • Acceptor:Paxos算法中的接受者角色。负责接收提议者的PREPARE和PROPOSE消息,评估提案是否符合承诺条件,并返回PROMISE或ACCEPT消息。
  • Proposer:提议者角色,生成提案编号并将PREPARE和PROPOSE消息广播给所有接受者。当获得足够的承诺和接受时,提议者成功达成共识。
  • ProposalNumberGenerator:负责生成全局唯一的提案编号,确保每次提案都拥有不同的编号。 -PaxosTest:测试主类,创建并启动提议者和接受者线程,实现Paxos协议的分布式模拟。

代码实现

 package org.example.分布式一致协议;
 ​
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Random;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
 ​
 import static java.lang.Thread.sleep;
 ​
 class PaxosMessage {
     enum Type { PREPARE, PROMISE, PROPOSE, ACCEPT,ERROR }
     final Type type;
     final int proposalNumber;
     final String value;
     final int proposerId;
 ​
     public PaxosMessage(Type type, int proposalNumber, String value, int proposerId) {
         this.type = type;
         this.proposalNumber = proposalNumber;
         this.value = value;
         this.proposerId = proposerId;
     }
 }
 ​
 class MessageManager {
     private final Map<String, BlockingQueue<PaxosMessage>> inboxMap = new HashMap<>();
 ​
     private volatile boolean consensusReached = false;  // 共识是否达成,volatile保证可见性
 ​
     // 是否开启共识达成即结束
     private boolean isConsensusReachedExit = true;
 ​
     // 设置是否开启共识达成即结束
     public void setConsensusReachedExit(boolean consensusReached) {
         isConsensusReachedExit = consensusReached;
     }
 ​
     public synchronized void setConsensusReached() {
         consensusReached = true;
     }
 ​
     public boolean isConsensusReached() {
         if(isConsensusReachedExit)
             return consensusReached;
         return false;
     }
     // 注册对象及其收件箱
     public void register(String id, BlockingQueue<PaxosMessage> inbox) {
         inboxMap.put(id, inbox);
     }
 ​
     // 发送消息给特定对象(接受者的返回消息是发送给特定提议者的)
     public void send(String targetId, PaxosMessage message) throws InterruptedException {
         if (inboxMap.containsKey(targetId)) {
             inboxMap.get(targetId).put(message);
         }
     }
 ​
     // 广播消息给所有接受者(提议者的消息则都是广播给所有接受者的)
     public void broadcast(PaxosMessage message) throws InterruptedException {
 ​
         for (Map.Entry<String, BlockingQueue<PaxosMessage>> entry : inboxMap.entrySet()) {
             // 将消息放入接受者的收件箱
             if(entry.getKey().startsWith("acceptor")){
                 entry.getValue().put(message);
             }
         }
 ​
 ​
     }
 }
 ​
 class Acceptor implements Runnable {
     private final String id;
     private final BlockingQueue<PaxosMessage> inbox;
     private final MessageManager manager;
     private int promisedNumber = -1;
     private String acceptedValue = null;
 ​
     public Acceptor(String id, BlockingQueue<PaxosMessage> inbox, MessageManager manager) {
         this.id = id;
         this.inbox = inbox;
         this.manager = manager;
     }
 ​
     @Override
     public void run() {
         try {
             while (true) {
                 PaxosMessage message = inbox.take();
                 if (message.type == PaxosMessage.Type.PREPARE) {
                     /*
                     Phase 1b - acceptor 承诺(Promise):
                     如果 n 高于接受者从任何提议者接收到的每个先前的提议编号,那么接受者必须向提议者返回一条消息,我们称其为“承诺”,以忽略所有将来的提议数量少的提议比 n。
                         如果接受者在过去某个时候接受了提议者的提议(也就是第二阶段中批准的提议),则在对提议者的答复中必须包含先前接受的的提议编号(例如 m)和相应的接受(批准)值(例如 w)。
                     否则(即,n 小于或等于接受者从任何提议者收到的任何先前提议编号),接受者可以忽略收到的提议。在这种情况下,Paxos 不必工作。
                         但是,为了优化起见,发送拒绝(Nack)响应将告诉提议者它停止以 n 作为提议的序号去建立共识(这是优化手段,因为即使不主动告诉,其提议的序号也会增长)。
                      */
                     String log = "Acceptor - " + id + ",  收到PREPARE,提案编号为: " + message.proposalNumber;
 ​
                     if (message.proposalNumber > promisedNumber) {
                         log += ",  发送承诺,提案编号为: " + message.proposalNumber;
                         System.out.println(log);
                         promisedNumber = message.proposalNumber;
                         manager.send("proposer-" + message.proposerId, new PaxosMessage(PaxosMessage.Type.PROMISE, message.proposalNumber, acceptedValue, message.proposerId));
                     }else {
                         log += ",  拒绝承诺";
                         System.out.println(log);
                         manager.send("proposer-" + message.proposerId, new PaxosMessage(PaxosMessage.Type.ERROR, message.proposalNumber, acceptedValue, message.proposerId));
                     }
                 } else if (message.type == PaxosMessage.Type.PROPOSE) {
                     /*
                     Phase 2b - acceptor 发送 Accepted 消息:
                         如果接受者从提议者接收到 Accept 信息(n,v),分为两种情况:
                         1 如果:它尚未承诺(在 Paxos 协议的阶段 1b 中)仅考虑提议序号大于 n 的提议时,则它应该将(刚接收到的 Accept 消息的)值 v 注册为(协议)的接受值,并向提议者和每个学习者(通常是提议者本身)发送一条接受消息。
                         2 否则:它可以忽略这个 Accept 消息。
                      */
                     String log = "Acceptor - " + id + ",  收到PROPOSE,提案编号为: " + message.proposalNumber + ",提案值为: " + message.value;
                     if (message.proposalNumber >= promisedNumber) {
                         log += ",  发送接受,提案值为: " + message.value;
                         System.out.println(log);
                         promisedNumber = message.proposalNumber;
                         acceptedValue = message.value;
                         manager.send("proposer-" + message.proposerId, new PaxosMessage(PaxosMessage.Type.ACCEPT, message.proposalNumber, acceptedValue, message.proposerId));
                     }else {
                         log += ",  拒绝接受";
                         System.out.println(log);
                         manager.send("proposer-" + message.proposerId, new PaxosMessage(PaxosMessage.Type.ERROR, message.proposalNumber, acceptedValue, message.proposerId));
                     }
                 }
             }
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
         }
     }
 }
 ​
 class Proposer implements Runnable {
     private final String id;
     private final BlockingQueue<PaxosMessage> inbox;
     private final MessageManager manager;
     private int proposalNumber = 0;
     private String proposalValue;
     private final int numAcceptors;
     private final ProposalNumberGenerator proposalNumberGenerator;
     private int count = 0;  // 计数器
     private long backoffTime = 100;  // 退避时间
     private long maxBackoffTime = 100;  // 退避时间
 ​
     public Proposer(String id, BlockingQueue<PaxosMessage> inbox, MessageManager manager, ProposalNumberGenerator proposalNumberGenerator, String proposalValue, int numAcceptors,long backoffTime, long maxBackoffTime) {
         this.id = id;
         this.inbox = inbox;
         this.manager = manager;
         this.proposalValue = proposalValue;
         this.numAcceptors = numAcceptors;
         this.proposalNumberGenerator = proposalNumberGenerator;
         this.backoffTime = backoffTime;
         this.maxBackoffTime = maxBackoffTime;
     }
 ​
     @Override
     public void run() {
         try {
             while (!manager.isConsensusReached()) {
                 // 随机等待一段时间
                 if(count++ > 0) {
                     sleep(backoffTime);
                     backoffTime = (long) Math.min(Math.pow(backoffTime, count), maxBackoffTime); // 退避时间最大限制在2000ms
                 }
 ​
                 //sleep(new Random().nextInt(500));  // 随机等待一段时间
 ​
 ​
                 proposalNumber = proposalNumberGenerator.getProposalNumber();
 ​
                 // 广播 PREPARE 消息给所有接受者
                 System.out.println("-----------Proposer " + id + " 开始广播提案,  提案编号为: " + proposalNumber + "-----------");
                 manager.broadcast(new PaxosMessage(PaxosMessage.Type.PREPARE, proposalNumber, null, Integer.parseInt(id.split("-")[1])));
 ​
                 // 等待 PROMISE 响应
             /*
                 (1) Phase 2a - proposer 发送 Accept 消息:
                 1 如果提议者从接受者的一个仲裁集合中获得大部分承诺,则需要为其提议设置值 v。
                 2 如果任何接受者先前已接受过一个提议,那么他们会将这个提议发送给提议者,
                     提议者现在必须将其提议值 v 设置为与接受者报告的(与这些接受者最高提议编号关联的)提议值(也就是提议者之前接受过的提议),我们称之为 z。
                 3 如果到目前为止,没有一个接受者接受过提议,则提议者可以选择其最初想要提议的值,例如 x。
                     在这个阶段,提议者会发送一个 Accept 信息:(n,v)给一个接受者个仲裁集合。(n 就是之前提议者发给接受者的准备信息里的提议里的提议序号。v=z 或 者 v=x (当所有接受者都没有接受过提议时。)),
                     这个 accept 请求可以理解为一个请求:“请接受这个提议!”。
 ​
              */
                 int promiseCount = 0;
                 int total = 0;
                 while (promiseCount < (numAcceptors / 2) + 1 && total<numAcceptors) {
                     PaxosMessage response = inbox.take();
                     System.out.println("Proposer - " + id + ",  收到消息类型: " + response.type + ", 提案编号: " + response.proposalNumber);
 ​
                     if (response.type == PaxosMessage.Type.PROMISE && response.proposalNumber == proposalNumber) {
                         promiseCount++;
                         // 如果接受者已经接受了提议,则使用接受的提议值
                         if (response.value != null) {
                             System.out.println("Proposer - " + id + ",  收到接受者的提案值: " + response.value+", 将自己的提案值设置为接受者的提案值");
                             proposalValue = response.value;
                         }
                     }else if(response.type == PaxosMessage.Type.ERROR && response.proposalNumber > proposalNumber){
                         // response.proposalNumber >= proposalNumber 防止接收到之前的错误信息
                         System.out.println("Proposer - " + id + ",  收到ERROR,其收到提案编号为: " + response.proposalNumber + ",重新提案");
                         break;
                     }
                     total++;
                 }
                 System.out.println("Proposer - " + id + ",  收到的承诺数: " + promiseCount);
                 if(promiseCount < (numAcceptors / 2) + 1){
                     System.out.println("Proposer - " + id + ",  的提案未达成共识,提案编号为: " + proposalNumber + ",重新提案");
                     continue;
                 }
 ​
                 // 广播 PROPOSE 消息
                 manager.broadcast(new PaxosMessage(PaxosMessage.Type.PROPOSE, proposalNumber, proposalValue, Integer.parseInt(id.split("-")[1])));
 ​
                 // 等待 ACCEPT 响应
                 total = 0;
                 int acceptCount = 0;
                 while (acceptCount < (numAcceptors / 2) + 1 && total<numAcceptors) {
                     PaxosMessage response = inbox.take();
                     if (response.type == PaxosMessage.Type.ACCEPT && response.proposalNumber == proposalNumber) {
                         acceptCount++;
                         // 如果接受者已经接受了提议,则使用接受的提议值
                         if (response.value != null) {
                             System.out.println("Proposer - " + id + ",  收到接受者的提案值: " + response.value+", 将自己的提案值设置为接受者的提案值");
                             proposalValue = response.value;
                         }
                     }else if(response.type == PaxosMessage.Type.ERROR&& response.proposalNumber > proposalNumber){
                         System.out.println("Proposer - " + id + ",  收到ERROR,其收到提案编号为: " + response.proposalNumber + ",重新提案");
                         break;
                     }
                     total++;
                 }
                 System.out.println("Proposer - " + id + ",  收到的接受数: " + acceptCount);
                 if(acceptCount < (numAcceptors / 2) + 1) {
                     System.out.println("Proposer - " + id + ",  的提案未达成共识,提案编号为: " + proposalNumber + ",重新提案");
 ​
                 }else {
                     System.out.println("Proposer - " + id + ",  的提案达成共识,共识值为: " + proposalValue);
                     manager.setConsensusReached();
                     return;
                 }
             }
             if(manager.isConsensusReached()){
                 System.out.println("Proposer - " + id + ",  已经有提案达成共识,该提议者结束退出");
                 return;
             }
 ​
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
         }
     }
 }
 // 提议编号生成器
 class  ProposalNumberGenerator{
     private int proposalNumber = 1;
     public synchronized int getProposalNumber(){
         return proposalNumber++;
     }
 }
 ​
 public class PaxosTest {
     public static void main(String[] args) {
         int numAcceptors = 1000;
         int numProposers = 50;
         boolean isConsensusReachedExit = false;  // 是否开启共识达成即结束
         long backoffTime = 1000;  // 退避时间
         long maxBackoffTime = 2000;  // 最大退避时间
         // 输出以上配置信息
         System.out.println("numAcceptors: " + numAcceptors);
         System.out.println("numProposers: " + numProposers);
         System.out.println("isConsensusReachedExit: " + isConsensusReachedExit);
         System.out.println("backoffTime: " + backoffTime);
         System.out.println("maxBackoffTime: " + maxBackoffTime);
 ​
         // 消息管理器
         MessageManager manager = new MessageManager();
         manager.setConsensusReachedExit(isConsensusReachedExit);
         // 全局唯一自增的提议编号生成器
         ProposalNumberGenerator proposalNumberGenerator = new ProposalNumberGenerator();
 ​
         // 注册接受者和启动线程
         for (int i = 0; i < numAcceptors; i++) {
             String acceptorId = "acceptor-" + i;
             BlockingQueue<PaxosMessage> inbox = new LinkedBlockingQueue<>();
             manager.register(acceptorId, inbox);
             new Thread(new Acceptor(acceptorId, inbox, manager)).start();
         }
 ​
         // 注册提议者和启动线程
         for (int i = 1; i <= numProposers; i++) {
             String proposerId = "proposer-" + i;
             BlockingQueue<PaxosMessage> inbox = new LinkedBlockingQueue<>();
             manager.register(proposerId, inbox);
             new Thread(new Proposer(proposerId, inbox, manager,proposalNumberGenerator, "Value" + i, numAcceptors, backoffTime,maxBackoffTime)).start();
         }
     }
 }
 ​

测试

测试1

初始设置

  • 参与者:3个接受者(acceptor-0、acceptor-1、acceptor-2)和2个提议者(proposer-1、proposer-2)。
  • 后退机制:backoffTime为1000ms,maxBackoffTime为2000ms。
  • 共识退出条件:isConsensusReachedExit为false,允许多个提议者继续提案,即使已有共识。

第一轮提议 (proposer-2 发起)

  • 提案编号和值:proposer-2生成提案编号1,提议值为Value2。
  • PREPARE阶段:proposer-2广播PREPARE消息,所有接受者发送PROMISE响应,承诺该提案编号1。
  • PROPOSE阶段:proposer-2广播PROPOSE消息,所有接受者同意并发送ACCEPT响应。
  • 共识达成:proposer-2收到足够的ACCEPT,达成共识,最终共识值为Value2。

第二轮提议 (proposer-1 发起)

  • 提案编号和值:proposer-1生成提案编号2,初始提议值为Value1。
  • 提议值调整:proposer-1收到PROMISE响应,继承接受者的提议值Value2。
  • PROPOSE阶段:proposer-1广播PROPOSE消息,所有接受者同意并发送ACCEPT响应。
  • 共识达成:proposer-1收到足够的ACCEPT,达成共识,最终共识值为Value2。

结果分析

  • 一致性保证:最终共识值为Value2,符合Paxos的一致性要求。
  • 此次运行结果验证了在提议间隔较大的情况下,系统依然实现了共识。

image.png

测试2

在这次测试中,配置了共识达成后提议者退出的选项。日志显示,当 proposer-2 的提案成功达成共识后,proposer-1 在执行完当前循环后,进入 while 判断条件,检测到共识已达成,从而成功退出。这表明,当设置了 isConsensusReachedExit 为 true 时,Paxos 算法实现能够在一个提议者达成共识后让其他提议者检测到共识状态并顺利退出,避免了不必要的额外提案尝试。

image.png

测试3

在这次测试中,设置为不在共识达成后退出,可用来模拟发生网络故障,剩下的提议者继续进行操作。从日志中可以看到在proposer-2的提案达成共识后,proposer-1继续执行,且两个提案最终得到的共识值一致。

image.png

测试4

在这次测试中,采用了33个接受者和6个提议者的配置。尽管增加了接受者和提议者的数量,最终6个提议者依然成功达成了一致的共识值,从输出中可以看出,所有提议者的最终共识值都是Value5。这表明该实现的Paxos算法在较大规模的提议者和接受者情况下,依然能够保持一致性,确保所有提议者达成同一共识值。

image.png