02. 共识算法:Raft 与 Paxos

深入理解 Raft 领导者选举与日志复制、Paxos 协议、Multi-Paxos 优化与 PBFT 拜占庭容错

共识算法是分布式系统的核心,解决了多个节点如何就某个值达成一致的问题。Raft 以可理解性著称,Paxos 以理论严谨闻名,PBFT 则应对拜占庭故障场景。

1. 共识问题

1.1 什么是一致性共识?

在分布式系统中,多个节点需要就某个值达成一致(agreement),且:

  • 安全性 (Safety):所有正确节点决定的值相同,且该值是某个正确节点提出的
  • 活性 (Liveness):最终所有正确节点都会决定一个值

1.2 共识算法分类

类别代表故障类型节点数
非拜占庭容错Raft、Paxos、ZAB崩溃停止2f+1 容忍 f 故障
拜占庭容错PBFT、Tendermint任意/恶意3f+1 容忍 f 故障

2. Raft 算法

Raft 将共识问题分解为三个相对独立的子问题:

  1. 领导者选举 (Leader Election):选出唯一的 Leader
  2. 日志复制 (Log Replication):Leader 将日志条目复制到 Follower
  3. 安全性 (Safety):保证状态机安全执行

2.1 节点状态

Follower ──超时未收到心跳──→ Candidate ──获得多数票──→ Leader
    ↑←──────────心跳───────────┘←────────心跳──────────┘
状态行为
Follower被动响应 Leader/Candidate 的请求
Candidate发起投票请求,竞争成为 Leader
Leader处理所有客户端请求,复制日志

2.2 领导者选举

class RaftNode:
    def __init__(self, node_id, peers):
        self.id = node_id
        self.peers = peers  # 其他节点列表
        self.state = 'Follower'
        self.current_term = 0
        self.voted_for = None
        self.log = []  # (term, command)
        
        # 超时设置(随机化避免活锁)
        self.election_timeout = random.randint(150, 300)  # 150-300ms
        self.last_heartbeat = time.time()
    
    def start_election(self):
        """Follower 超时后转为 Candidate 发起选举"""
        self.state = 'Candidate'
        self.current_term += 1
        self.voted_for = self.id
        votes = 1  # 自己投自己
        
        # 并行发送 RequestVote RPC
        for peer in self.peers:
            if self.request_vote(peer):
                votes += 1
        
        # 获得多数票则成为 Leader
        if votes > len(self.peers) // 2:
            self.become_leader()
    
    def request_vote(self, peer):
        """请求投票 RPC"""
        args = {
            'term': self.current_term,
            'candidate_id': self.id,
            'last_log_index': len(self.log) - 1,
            'last_log_term': self.log[-1][0] if self.log else 0
        }
        # 发送 RPC 并等待响应
        response = peer.handle_request_vote(args)
        return response.get('vote_granted', False)
    
    def handle_request_vote(self, args):
        """处理投票请求"""
        if args['term'] < self.current_term:
            return {'term': self.current_term, 'vote_granted': False}
        
        if (self.voted_for is None or self.voted_for == args['candidate_id']) \
           and self.is_log_at_least_up_to_date(args):
            self.voted_for = args['candidate_id']
            return {'term': self.current_term, 'vote_granted': True}
        
        return {'term': self.current_term, 'vote_granted': False}

选举规则

  1. 每个任期 (term) 内最多一个 Leader
  2. 候选者的日志必须至少和投票者的日志一样新
  3. 随机选举超时(150-300ms)避免分裂投票

2.3 日志复制

    def append_entries(self, peer):
        """Leader 发送心跳/日志复制 RPC"""
        prev_log_index = self.next_index[peer.id] - 1
        entries = self.log[prev_log_index + 1:]
        
        args = {
            'term': self.current_term,
            'leader_id': self.id,
            'prev_log_index': prev_log_index,
            'prev_log_term': self.log[prev_log_index][0] if prev_log_index >= 0 else 0,
            'entries': entries,
            'leader_commit': self.commit_index
        }
        
        response = peer.handle_append_entries(args)
        
        if response['success']:
            self.next_index[peer.id] = len(self.log)
            self.match_index[peer.id] = len(self.log) - 1
        else:
            # 日志不一致,回退 next_index 重试
            self.next_index[peer.id] -= 1
    
    def handle_append_entries(self, args):
        """处理日志复制请求"""
        if args['term'] < self.current_term:
            return {'term': self.current_term, 'success': False}
        
        self.last_heartbeat = time.time()
        self.current_term = args['term']
        self.state = 'Follower'
        self.voted_for = None
        
        # 检查日志一致性
        if args['prev_log_index'] >= 0:
            if args['prev_log_index'] >= len(self.log):
                return {'term': self.current_term, 'success': False}
            if self.log[args['prev_log_index']][0] != args['prev_log_term']:
                return {'term': self.current_term, 'success': False}
        
        # 追加新条目
        self.log = self.log[:args['prev_log_index'] + 1] + args['entries']
        
        # 更新 commit_index
        if args['leader_commit'] > self.commit_index:
            self.commit_index = min(args['leader_commit'], len(self.log) - 1)
        
        return {'term': self.current_term, 'success': True}

2.4 安全性保证

    def is_log_at_least_up_to_date(self, args):
        """判断候选者日志是否至少一样新"""
        my_last_index = len(self.log) - 1
        my_last_term = self.log[-1][0] if self.log else 0
        
        candidate_last_index = args['last_log_index']
        candidate_last_term = args['last_log_term']
        
        if candidate_last_term != my_last_term:
            return candidate_last_term > my_last_term
        return candidate_last_index >= my_last_index

Raft 安全性的关键约束:

  1. 选举限制:只有日志最新的候选者才能当选
  2. 提交规则:Leader 只提交当前任期的日志条目
  3. Leader 完备性:已提交的日志条目一定存在于所有未来 Leader 中

2.5 成员变更

    def handle_configuration_change(self, new_config):
        """Joint Consensus:联合共识保证安全成员变更"""
        # 第一阶段:Cold + New 联合配置
        joint_config = self.old_config.union(new_config)
        self.configuration = joint_config
        
        # 在联合配置中达到多数,才认为日志提交
        # 第二阶段:完全切换到新配置
        if self.is_majority_in_joint_config():
            self.configuration = new_config

3. Paxos 协议

3.1 Basic Paxos

Paxos 的角色:

  • Proposer:提议者,提出值
  • Acceptor:接受者,投票决定是否接受
  • Learner:学习者,学习已被选定的值

两个阶段:

Phase 1: Prepare

  1. Proposer 选择提案号 n,发送 Prepare(n)
  2. Acceptor 如果 n 大于之前承诺的任何提案号,承诺不再接受小于 n 的提案,并返回已接受的值

Phase 2: Accept

  1. Proposer 收到多数响应后,发送 Accept(n, v)(v 是响应中值最大的,或自己提议的)
  2. Acceptor 如果没有承诺更大的提案号,接受该值

3.2 Paxos 伪代码

class Acceptor:
    def __init__(self):
        self.promised_n = -1  # 已承诺的最大提案号
        self.accepted_n = -1  # 已接受的最大提案号
        self.accepted_v = None  # 已接受的值
    
    def prepare(self, n):
        if n > self.promised_n:
            self.promised_n = n
            return {'promised': True, 'accepted_n': self.accepted_n, 
                    'accepted_v': self.accepted_v}
        return {'promised': False}
    
    def accept(self, n, v):
        if n >= self.promised_n:
            self.promised_n = n
            self.accepted_n = n
            self.accepted_v = v
            return {'accepted': True}
        return {'accepted': False}

class Proposer:
    def __init__(self, acceptors):
        self.acceptors = acceptors
        self.proposal_n = 0
    
    def propose(self, value):
        self.proposal_n += 1
        
        # Phase 1: Prepare
        promises = []
        for ac in self.acceptors:
            resp = ac.prepare(self.proposal_n)
            if resp['promised']:
                promises.append(resp)
        
        if len(promises) <= len(self.acceptors) // 2:
            return False  # 未获多数
        
        # 选择值:如果有已接受的值,选最大的 n 对应的值
        accepted_values = [(p['accepted_n'], p['accepted_v']) 
                          for p in promises if p['accepted_n'] >= 0]
        if accepted_values:
            value = max(accepted_values, key=lambda x: x[0])[1]
        
        # Phase 2: Accept
        accepts = 0
        for ac in self.acceptors:
            resp = ac.accept(self.proposal_n, value)
            if resp['accepted']:
                accepts += 1
        
        return accepts > len(self.acceptors) // 2

3.3 Multi-Paxos

Basic Paxos 每个值都需要两阶段,效率低。Multi-Paxos 通过选出一个隐式 Leader 来优化:

  1. 先用一轮 Paxos 选出 Leader
  2. Leader 跳过 Phase 1,直接发起 Phase 2
  3. Leader 变更时才需要重新 Phase 1

这实际上趋近于 Raft 的设计——Multi-Paxos + Leader = Raft 的雏形。

4. PBFT:实用拜占庭容错

PBFT (Practical Byzantine Fault Tolerance) 在 3f+1 个节点中容忍 f 个拜占庭故障。

4.1 PBFT 三阶段协议

Client → [REQUEST] → Primary (Leader)

Phase 1: Pre-Prepare
Primary 广播 ⟨PRE-PREPARE, v, n, d, m⟩ 给所有 Replica

Phase 2: Prepare
每个 Replica 接受后广播 ⟨PREPARE, v, n, d, i⟩
收到 2f 条匹配的 PREPARE 后进入 prepared 状态

Phase 3: Commit
广播 ⟨COMMIT, v, n, d, i⟩
收到 2f+1 条匹配的 COMMIT 后提交执行

Reply to Client: ⟨REPLY, v, t, c, i, r⟩

4.2 PBFT 关键要点

要素说明
视图 (View)Leader 轮次,每轮一个 Primary
视图更换超时后触发,选新的 Primary
检查点定期同步状态,减少日志冗余
消息认证每条消息用数字签名或 MAC 认证
应用Hyperledger Fabric (solo/kafka → Raft)、Tendermint

5. 共识算法对比

维度RaftPaxosPBFT
可理解性⭐⭐⭐ 优秀⭐ 学术化⭐⭐ 中等
性能⭐⭐ 单 Leader 瓶颈⭐⭐ 同上⭐⭐ 三阶段开销大
容错类型崩溃停止崩溃停止拜占庭故障
最少节点3 (容忍1故障)34 (容忍1故障)
Leader 选举显式选举隐式 (Multi-Paxos)视图轮换
日志压缩Snapshot检查点检查点
代表产品etcd, MongoDB, TiKVChubby, PaxosStoreFabric, Tendermint

6. etcd 中的 Raft 实战

6.1 etcd 架构

Client → gRPC/HTTP → etcd Server
                    ├── WAL (Write Ahead Log)
                    ├── Snapshot
                    └── BoltDB (后端存储)

6.2 etcdctl 操作

# 写数据
etcdctl put /config/database/host "db.example.com"
etcdctl put /config/database/port "5432"

# 读数据
etcdctl get /config/database/host

# 带前缀查询
etcdctl get /config/ --prefix

# 监听变更(Watch)
etcdctl watch /config/database/ --prefix

# 事务操作
etcdctl txn -i
put("/user/balance", "90")

# 集群状态
etcdctl endpoint status --cluster -w table

6.3 etcd 集群部署

# 三节点集群
etcd --name node1 \
  --initial-advertise-peer-urls http://192.168.1.1:2380 \
  --listen-peer-urls http://192.168.1.1:2380 \
  --listen-client-urls http://192.168.1.1:2379,http://127.0.0.1:2379 \
  --advertise-client-urls http://192.168.1.1:2379 \
  --initial-cluster-token etcd-cluster-1 \
  --initial-cluster "node1=http://192.168.1.1:2380,node2=http://192.168.1.2:2380,node3=http://192.168.1.3:2380" \
  --initial-cluster-state new

# 查看集群成员
etcdctl member list -w table

6.4 用 etcd 实现分布式锁

import etcd3

client = etcd3.Client(host='localhost', port=2379)

# 分布式锁(基于租约 + 前缀机制)
lock = client.lock('my-resource-lock', ttl=10)
lock.acquire()
try:
    # 执行业务逻辑
    process_critical_section()
finally:
    lock.release()

Lease 机制确保:持有锁的客户端崩溃后,锁会自动释放(TTL 到期)。

7. Raft 的变种与优化

变种改进点代表
Pre-Vote预投票避免无谓的任期增加etcd
CheckQuorumLeader 主动探测集群多数etcd
Joint Consensus两阶段成员变更Raft 论文
ParallelRaft乱序提交提高吞吐PolarFS (阿里云)
Multi-Raft多组 Raft 并行TiKV, CockroachDB

总结

场景推荐算法理由
键值存储配置中心Raft (etcd)简单、可靠、生态成熟
分布式数据库Multi-Raft (TiKV)多 region 并行
区块链公链PBFT / PoS拜占庭环境
企业联盟链PBFT / RaftFabric 默认
需要强一致性的元数据Paxos / RaftChubby、ZooKeeper

共识算法的工程实践要点:

  1. Raft 足够好:绝大多数场景用 Raft 即可,Paxos 只作为理论基础
  2. 注意磁盘 IO:WAL 同步刷盘是保证安全性的关键,SSD 是标配
  3. 网络分区测试:用 Jepsen 或 Chaos Mesh 验证分区恢复行为
  4. 监控 Leader 切换:频繁的 Leader 切换说明网络或配置有问题
  5. 批量提交:小请求批量化可显著提升吞吐

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「distributed-systems」更多文章

  1. 分布式高可用架构模式:多活、容灾、降级与 K8s 编排高可用
  2. 分布式链路追踪实战:OpenTelemetry、Jaeger 与 W3C Trace Context
  3. 分布式缓存深度策略:Redis Cluster、一致性哈希与多级缓存架构