🗳️ 共识算法:Raft & Paxos

如何让一组节点就"真相"达成一致
数据采集:2025-05 · 参考:raft.github.io, etcd.io, consul.io, USENIX ATC 2014

什么是共识?

共识问题是分布式系统最根本的问题:如何让多个独立节点就某个值达成一致决定,且这个决定是最终的、不可撤销的。

📋 共识的三个保证

安全性 (Safety):永远不会返回错误结果——所有节点一旦决定,就决定了同一个值。
活跃性 (Liveness):只要多数节点存活且能通信,就一定能做出决定。
容错性 (Fault Tolerance):5 节点集群容忍 2 节点故障,3 节点集群容忍 1 节点故障。

共识的典型应用场景:

Raft:为了可理解性而设计

Raft 由 Diego Ongaro 和 John Ousterhout 在 2014 年发表(In Search of an Understandable Consensus Algorithm),获 USENIX ATC 最佳论文奖。核心理念:Paxos 难以理解是它的问题,不是共识本身的问题。

Raft 的三个子问题

Raft 将共识分解为三个相对独立的子问题,每个都可以独立理解、实现和测试:

👑 Leader Election

集群中必须有一个 Leader,所有写请求通过 Leader 处理。Leader 周期性发送心跳维持权威。如果 Follower 在 Election Timeout 内没收到心跳,就发起选举。

状态机:Follower → Candidate → Leader

📝 Log Replication

Leader 接收客户端请求,追加到自己的日志,然后复制到所有 Follower。当多数 Follower 确认后,条目被提交(committed),Leader 通知客户端成功。

关键约束:如果两个日志条目有相同的 term 和 index,则它们存储的命令相同

🔒 Safety

Raft 的安全性保证:如果一个条目在某个 term 被提交,那么所有更高 term 的 Leader 都会包含这个条目。这通过选举限制实现——Candidate 必须让自己的日志至少和多数节点一样新才能赢得选举。

Raft 选举流程

1
Follower 超时
无心跳
2
转为 Candidate
Term + 1
3
投自己一票
发 RequestVote
4
获多数票
成为 Leader
5
发送心跳
维持权威

🎭 Raft 选举模拟器

S1
Follower
Term 1
S2
Follower
Term 1
S3
Leader
Term 1
S4
Follower
Term 1
S5
Follower
Term 1
初始状态:S3 是 Term 1 的 Leader,正常心跳中

日志复制流程

1
客户端发
写请求
2
Leader 追加
本地日志
3
AppendEntries
发到 Follower
4
多数确认
→ 提交
5
应用到
状态机

Raft 代码示例(Go 伪代码)

// Leader Election 核心逻辑
func (rf *Raft) tickElection() {
    rf.electionTimer.Reset(randomTimeout(150ms, 300ms))
    if rf.state != Leader {
        rf.startElection()
    }
}

func (rf *Raft) startElection() {
    rf.state = Candidate
    rf.currentTerm++
    rf.votedFor = rf.me  // 投自己一票
    votes := 1
    
    for _, peer := range rf.peers {
        if peer == rf.me { continue }
        go func(peer int) {
            reply := &RequestVoteReply{}
            ok := rf.sendRequestVote(peer, &RequestVoteArgs{
                Term:         rf.currentTerm,
                CandidateId:  rf.me,
                LastLogIndex: rf.log.lastIndex(),
                LastLogTerm:  rf.log.lastTerm(),
            }, reply)
            if ok && reply.VoteGranted {
                votes++
                if votes > len(rf.peers)/2 && rf.state == Candidate {
                    rf.becomeLeader()
                }
            }
        }(peer)
    }
}

// Log Replication 核心逻辑
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
    // 1. Reply false if term < currentTerm
    if args.Term < rf.currentTerm {
        reply.Success = false
        return
    }
    // 2. Reply false if log doesn't contain entry at prevLogIndex
    if !rf.log.hasEntry(args.PrevLogIndex, args.PrevLogTerm) {
        reply.Success = false
        return
    }
    // 3. Delete conflicting entries, append new ones
    rf.log.truncateAndAppend(args.Entries)
    // 4. Update commitIndex
    if args.LeaderCommit > rf.commitIndex {
        rf.commitIndex = min(args.LeaderCommit, rf.log.lastIndex())
    }
    reply.Success = true
}

Paxos:共识的奠基者

Paxos 由 Leslie Lamport 在 1989 年提出(1998 年正式发表),是分布式共识的理论基石。Raft 本质上是 Paxos 的一个特定变体,但做了关键的设计选择来提升可理解性。

Basic Paxos

Basic Paxos 解决单值共识。三个角色:Proposer(提出提案)、Acceptor(投票接受)、Learner(学习结果)。

Phase 1a
Proposer 发
Prepare(n)
Phase 1b
Acceptor 承诺
不接受 <n 的提案
Phase 2a
Proposer 发
Accept(n, value)
Phase 2b
Acceptor 接受
多数通过→决定

Raft vs Paxos 对比

维度RaftPaxos
设计目标可理解性最小化消息数量
Leader强 Leader——所有请求通过 Leader弱 Leader(Multi-Paxos 优化)
日志管理连续日志,简单覆盖冲突允许日志空洞,更灵活但更复杂
成员变更Joint Consensus(两阶段)依赖外部配置管理
性能与 Paxos 等价(论文证明)理论最优消息复杂度
实现难度中等——分解为独立子问题高——缺乏工程指导
生产使用etcd, Consul, CockroachDB, TiKVGoogle Chubby, Spanner (变体)

⚠️ Paxos 的问题

Lamport 本人也承认 Paxos 太难理解。他的论文用古希腊议会做比喻,很多人读完还是不知道怎么实现。Google Chubby 的作者 Mike Burrows 说:"There are significant gaps between the description of the Paxos algorithm and the needs of a real-world system." 这正是 Raft 诞生的原因。

生产实践:谁在用 Raft?

🔑 etcd — Kubernetes 的核心

etcd 是 Kubernetes 的状态存储,所有集群配置、Pod 状态、Service 发现都存在 etcd 中。使用 Raft 保证一致性。

关键数据(来源:etcd.io):

  • 名称来源:/etc + distributed = etcd
  • 支持线性一致性读
  • Watch API 监控变更(K8s 的核心机制)
  • 事务:字段比较 + 读 + 写
  • Lease 原语:分布式锁和选举
  • Kubernetes API Server 持久化所有状态到 etcd
  • vs ZooKeeper:动态成员变更、MVCC、gRPC 协议
  • vs Consul:etcd 专注 KV 存储,Consul 更像服务发现框架

🌐 Consul — HashiCorp 的服务网络

Consul 使用 Raft 实现 Leader Election 和日志复制,提供服务发现、Service Mesh、配置管理。

关键数据(来源:hashicorp.com):

  • 内置健康检查 + DNS 服务
  • Service Mesh:mTLS 加密服务间通信
  • Kubernetes 集成:Sidecar 自动注入
  • 支持 HTTP/JSON API(比 ZooKeeper 的 Jute 协议友好)
  • Raft 共识保证 Catalog 一致性
  • Gossip 协议用于成员管理和健康检查

🗄️ CockroachDB & TiKV

CockroachDB 和 TiKV 都使用 Raft(变体)实现多副本一致性。不同的是,它们将 Raft 运行在每个 Range/Region 上,而不是全局——这是一个关键的架构决策:

  • 全局 Raft:所有写请求经过一个 Leader,简单但吞吐受限于单节点
  • 分片 Raft:每个数据分片有自己的 Raft 组,Leader 分散在不同节点,吞吐量随分片数线性扩展

etcd Raft 实操

启动 3 节点 etcd 集群

# 节点 1
etcd --name node1 \
  --listen-client-urls http://10.0.0.1:2379 \
  --advertise-client-urls http://10.0.0.1:2379 \
  --listen-peer-urls http://10.0.0.1:2380 \
  --initial-advertise-peer-urls http://10.0.0.1:2380 \
  --initial-cluster node1=http://10.0.0.1:2380,node2=http://10.0.0.2:2380,node3=http://10.0.0.3:2380 \
  --initial-cluster-token my-etcd-cluster \
  --initial-cluster-state new

# 写入数据
etcdctl put /config/key "value"
etcdctl get /config/key

# 查看 Leader
etcdctl endpoint status --write-out=table

# 查看集群成员
etcdctl member list --write-out=table

# 分布式锁
etcdctl lock my-lock -- ./my-script.sh

# Leader 选举
etcdctl elect my-election "candidate-value"

Watch API(Kubernetes 的核心机制)

# Python 示例:监听 etcd key 变更
import etcd3

etcd = etcd3.client()

# Watch 一个前缀下的所有 key
events_iterator, cancel = etcd.watch_prefix('/services/')

for event in events_iterator:
    if isinstance(event, etcd3.events.PutEvent):
        print(f"Service registered: {event.key} = {event.value}")
    elif isinstance(event, etcd3.events.DeleteEvent):
        print(f"Service deregistered: {event.key}")

# 事务:CAS (Compare-And-Swap)
etcd.transaction(
    compare=[etcd.transactions.version('/lock') == 0],
    success=[etcd.transactions.put('/lock', 'owner-1')],
    failure=[]
)

🤖 Agent 系统中的共识

多 Agent 协调的本质

当多个 Agent 需要就"下一步做什么"达成一致时,这就是一个共识问题。考虑以下场景:

  • 任务分配:5 个 Agent,3 个任务——谁做什么?需要共识
  • Leader Agent:选一个 Agent 协调其他 Agent——这正是 Leader Election
  • 状态同步:所有 Agent 必须看到相同的对话历史——Log Replication
  • 决策提交:Agent A 做了决定,其他 Agent 需要确认——Commit 流程

AutoGen GroupChat = 弱共识

AutoGen 的 GroupChat 使用一个 GroupChatManager 来决定"下一个说话的 Agent 是谁"。这本质上是一个单 Leader 的弱共识——如果 Manager 挂了,整个对话就停了。没有选举机制,没有日志复制。

如果要构建生产级的多 Agent 系统,需要考虑:

  • Manager 的 Leader Election(Raft 式选举)
  • 对话历史的 Log Replication(任何 Agent 能接替 Manager)
  • Agent 成员变更(类似 Raft 的 Joint Consensus)

CrewAI 的编排 = 集中式调度

CrewAI 使用 Crew 来编排 Agent,本质上是一个集中式调度器。这比 AutoGen 更结构化,但仍然是单点——如果 Crew Manager 失败,需要外部重启。

Agent 架构知识库 的联系:多 Agent 编排模式的选择(集中式 vs 分布式)直接影响系统的可用性和扩展性。

常见误区与陷阱

❌ "Raft 解决了所有分布式一致性问题"

Raft 解决的是复制日志的一致性。它不解决:跨分片事务、读写分离的一致性、客户端看到的线性一致性(需要额外的 ReadIndex 机制)。

❌ "多数派就够了"

多数派保证了决策能达成,但不保证性能可接受。如果 5 节点中 2 个慢节点,虽然能工作,但每次写入要等最慢的 Follower 确认。实际部署中,网络延迟的差异比故障更常见。

❌ "Raft Leader 选举总是很快"

Raft 选举依赖随机超时(150-300ms)。在最坏情况下,多个 Candidate 同时竞选,可能需要多轮选举才能产生 Leader。分区间隔意味着服务中断。

🔗 延伸阅读