共识算法是分布式系统的核心,解决了多个节点如何就某个值达成一致的问题。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 将共识问题分解为三个相对独立的子问题:
- 领导者选举 (Leader Election):选出唯一的 Leader
- 日志复制 (Log Replication):Leader 将日志条目复制到 Follower
- 安全性 (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}
选举规则:
- 每个任期 (term) 内最多一个 Leader
- 候选者的日志必须至少和投票者的日志一样新
- 随机选举超时(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 安全性的关键约束:
- 选举限制:只有日志最新的候选者才能当选
- 提交规则:Leader 只提交当前任期的日志条目
- 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
- Proposer 选择提案号 n,发送 Prepare(n)
- Acceptor 如果 n 大于之前承诺的任何提案号,承诺不再接受小于 n 的提案,并返回已接受的值
Phase 2: Accept
- Proposer 收到多数响应后,发送 Accept(n, v)(v 是响应中值最大的,或自己提议的)
- 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 来优化:
- 先用一轮 Paxos 选出 Leader
- Leader 跳过 Phase 1,直接发起 Phase 2
- 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. 共识算法对比
| 维度 | Raft | Paxos | PBFT |
|---|---|---|---|
| 可理解性 | ⭐⭐⭐ 优秀 | ⭐ 学术化 | ⭐⭐ 中等 |
| 性能 | ⭐⭐ 单 Leader 瓶颈 | ⭐⭐ 同上 | ⭐⭐ 三阶段开销大 |
| 容错类型 | 崩溃停止 | 崩溃停止 | 拜占庭故障 |
| 最少节点 | 3 (容忍1故障) | 3 | 4 (容忍1故障) |
| Leader 选举 | 显式选举 | 隐式 (Multi-Paxos) | 视图轮换 |
| 日志压缩 | Snapshot | 检查点 | 检查点 |
| 代表产品 | etcd, MongoDB, TiKV | Chubby, PaxosStore | Fabric, 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 |
| CheckQuorum | Leader 主动探测集群多数 | etcd |
| Joint Consensus | 两阶段成员变更 | Raft 论文 |
| ParallelRaft | 乱序提交提高吞吐 | PolarFS (阿里云) |
| Multi-Raft | 多组 Raft 并行 | TiKV, CockroachDB |
总结
| 场景 | 推荐算法 | 理由 |
|---|---|---|
| 键值存储配置中心 | Raft (etcd) | 简单、可靠、生态成熟 |
| 分布式数据库 | Multi-Raft (TiKV) | 多 region 并行 |
| 区块链公链 | PBFT / PoS | 拜占庭环境 |
| 企业联盟链 | PBFT / Raft | Fabric 默认 |
| 需要强一致性的元数据 | Paxos / Raft | Chubby、ZooKeeper |
共识算法的工程实践要点:
- Raft 足够好:绝大多数场景用 Raft 即可,Paxos 只作为理论基础
- 注意磁盘 IO:WAL 同步刷盘是保证安全性的关键,SSD 是标配
- 网络分区测试:用 Jepsen 或 Chaos Mesh 验证分区恢复行为
- 监控 Leader 切换:频繁的 Leader 切换说明网络或配置有问题
- 批量提交:小请求批量化可显著提升吞吐
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。