土法炼钢兴趣小组的算法知识备份

【分布式系统百科】共识协议的工程权衡:Raft vs Multi-Paxos vs EPaxos 实测对比

文章导航

分类入口
distributed
标签入口
#consensus#raft#paxos#epaxos#engineering#benchmark#distributed-systems

目录

共识协议的工程权衡:Raft vs Multi-Paxos vs EPaxos 实测对比

在本系列的第三部分”共识”中,我们从共识问题的定义出发,先后走过了 Paxos 的理论优雅、Raft 的可理解性革命、EPaxos 的无主优化、Viewstamped Replication 与 PBFT 的拜占庭世界,以及 HotStuff 的线性化改进。每一篇文章都聚焦于一种协议的内在机制。但当你真正站在系统架构师的位置上,面对的问题从来不是”这个协议的证明是否正确”,而是:在我的场景下,我应该选哪一个?选了之后会踩到什么坑?代价有多大?

这篇文章是第三部分的收官之作。我们不再推导任何新的协议——而是把前面所有协议拉到同一张桌子上,用工程的尺子去量。性能数据来自已发表的论文与官方基准测试,选型框架来自生产实践的积累,隐藏成本来自那些论文不会写、但你上线之后一定会遇到的问题。

一、为什么需要”工程权衡”视角

学术论文比较共识协议时,通常关注三个属性:安全性(Safety)、活性(Liveness)和消息复杂度(Message Complexity)。这三者当然重要,但它们无法回答工程师的日常问题:

理论上,Raft 和 Multi-Paxos 在消息复杂度上是等价的——都需要一轮广播加一轮确认(两阶段提交日志条目),摊销下来每条日志条目需要 O(n) 条消息。EPaxos 在无冲突场景下可以做到一轮快速路径。但工程中的性能差异,往往不来自消息轮次,而来自实现层面的大量细节:批处理(Batching)策略、流水线(Pipelining)深度、日志存储引擎、网络序列化格式、内存分配模式。

这就是为什么我们需要跳出理论,用实测数据说话。

性能基准速查表

在深入分析之前,先给出一张综合性能速查表。以下数据来源于 etcd 官方基准测试、ZooKeeper 原始论文(Hunt et al., 2010)、EPaxos 论文(Moraru et al., 2013)以及 CockroachDB/TiKV 的公开性能报告,测试环境均为三节点(或五节点)集群、SSD 存储、千兆以太网(同机房场景):

flowchart TB
    subgraph 性能对比矩阵
        direction TB
        H["协议 / 指标"]
        R["Raft<br/>etcd 三节点"]
        M["Multi-Paxos<br/>ZAB/ZooKeeper"]
        E["EPaxos<br/>论文原型"]
    end

    subgraph 写吞吐["写吞吐 ops/sec"]
        R1["串行: ~800<br/>128并发: ~35,000<br/>256并发 batching: ~50,000+"]
        M1["100%写: ~21,000<br/>读写混合 80/20: ~60,000+"]
        E1["低冲突: ~55,000<br/>高冲突: ~25,000<br/>零冲突 Fast Path: ~70,000"]
    end

    subgraph 写延迟["写延迟"]
        R2["p50: 1.2ms<br/>p99: 8~15ms<br/>p999: 30~80ms"]
        M2["p50: 1.5ms<br/>p99: 10~20ms"]
        E2["Fast Path p50: 0.8ms<br/>Slow Path p50: 2.5ms<br/>p99: 15~40ms"]
    end

    subgraph 读延迟["线性化读延迟"]
        R3["ReadIndex: ~1ms<br/>LeaseRead: ~0.3ms<br/>Serializable: ~0.1ms"]
        M3["同步读: ~1.2ms<br/>本地读: ~0.1ms"]
        E3["任意副本: ~0.5ms<br/>需执行序重建"]
    end

    subgraph 故障恢复["Leader故障恢复"]
        R4["1~2秒<br/>election timeout主导"]
        M4["0.5~3秒<br/>取决于日志同步量"]
        E4["无需选举<br/>其他副本即刻接管"]
    end

    H --> R & M & E
    R --> R1 & R2 & R3 & R4
    M --> M1 & M2 & M3 & M4
    E --> E1 & E2 & E3 & E4

上表中的数据展示了三种协议在不同维度上的典型表现。Raft 在单机房中等并发下可达 35,000+ ops/sec 的写入吞吐;EPaxos 在零冲突场景下理论吞吐最高,但高冲突时退化明显;Multi-Paxos(ZAB)在读密集型负载下表现出色,读吞吐可随节点数线性扩展。需要强调的是,这些数字高度依赖硬件配置、value 大小和并发模型,应作为量级参考而非绝对基准。

工程评估的五个维度

在本文中,我们采用以下五个维度来评估共识协议的工程适用性:

维度 关注点 典型度量
吞吐与延迟 稳态性能表现 ops/sec、p50/p99 延迟
故障恢复 Leader 故障后恢复时间 选举耗时、不可用窗口
跨地域性能 广域网部署下的退化程度 跨机房 RTT 对吞吐的影响
可理解性与可维护性 团队上手难度 代码行数、调试难度、社区资源
生态与工具链 可复用的实现和周边工具 成熟库数量、测试工具、监控支持

二、性能基准测试:从论文到实测

性能对比是工程选型中最受关注的部分,也是最容易被误导的部分。不同论文中的基准测试,硬件环境、负载模型、数据大小各不相同,直接比较数字毫无意义。我们的策略是:引用原始数据源,描述观察到的模式(Pattern),而不是孤立地比较数字。

2.1 etcd(Raft)的官方基准

etcd 项目维护了一套持续更新的性能基准(etcd Performance Benchmarking),测试环境通常为三节点集群,使用 SSD 存储和千兆以太网。根据 etcd 官方文档中的基准测试结果,我们可以观察到以下模式:

写入性能模式:

读取性能模式:

这揭示了 Raft 系统的一个核心权衡:读写一致性越强,读性能越接近写性能的天花板;一致性要求越松,读性能可以突破共识层的瓶颈。

2.2 ZooKeeper(ZAB)的论文基准

ZooKeeper 使用 ZAB(ZooKeeper Atomic Broadcast)协议,这是一种与 Multi-Paxos 思想相近的原子广播协议。在 ZooKeeper 的原始论文(Hunt et al., 2010, “ZooKeeper: Wait-free Coordination for Internet-scale Systems”,发表于 USENIX ATC 2010)中,作者提供了详细的性能测试数据,测试环境为由多台服务器组成的集群。

论文中观察到的核心模式:

2.3 Leader 瓶颈效应

无论是 Raft 还是 ZAB,只要使用强 Leader 模型,就不可避免地面临 Leader 瓶颈(Leader Bottleneck)问题:

客户端请求 ──→ [Leader] ──→ 复制到 Follower ──→ 等待多数派 ACK ──→ 提交并响应
                  ↑
            所有写入都经过这里

Leader 节点承担的职责包括:

  1. 接收所有写请求(客户端通常被重定向到 Leader)
  2. 序列化日志条目(确定全局顺序)
  3. 广播日志条目到所有 Follower(网络 I/O)
  4. 等待多数派确认(延迟受最慢多数派节点影响)
  5. 提交条目并应用到状态机(磁盘 I/O)
  6. 响应客户端

这意味着 Leader 的 CPU、网络带宽和磁盘 I/O 同时受到压力。在生产中,我们常常看到 Follower 节点的资源利用率只有 Leader 的 30%-50%——这是 Leader 模型的固有代价。

EPaxos 的理论优势正体现在此处: 由于 EPaxos 没有固定 Leader,任何副本都可以直接处理客户端请求并发起共识,写入负载可以分散到所有节点上,理论上吞吐量可以随节点数线性扩展。但这一优势的前提是命令之间冲突率足够低——我们将在后面详细讨论这个条件。

2.4 延迟分布的尾部效应

在评估共识协议性能时,p50(中位数)延迟往往不够用。生产系统更关心的是 p99 甚至 p999 延迟,因为用户体验由最慢的那次请求决定。

共识协议的尾部延迟来源主要有三个:

1. 多数派等待的”长尾放大”效应

假设三节点集群中,每个节点的响应延迟分布如下(独立同分布):

百分位 单节点延迟
p50 1ms
p99 10ms
p999 50ms

Raft 需要等待至少两个节点(多数派)确认。等待多数派的 p99 延迟不等于单节点的 p99——它等于”三个节点中第二快的那个的 p99”。通过概率分析可以得出,多数派等待的尾部延迟会比单节点的尾部延迟更高。节点数越多,这个效应越显著。五节点集群需要等待三个节点中第三快的确认,尾部放大更严重。

2. 磁盘 fsync 的延迟抖动

Raft 和 ZAB 都要求在响应 AppendEntries 之前将日志条目持久化到磁盘(fsync)。SSD 的 fsync 延迟通常在 0.1-1ms,但在 SSD 内部垃圾回收(GC)或写入放大触发时,延迟可能飙升到 10-100ms。这是 p99 延迟的主要贡献者之一。

etcd 的运维指南中明确建议使用专用 SSD 并监控磁盘延迟指标(wal_fsync_duration_seconds),正是为了控制这一尾部延迟来源。

3. Go 语言 GC 停顿(etcd 特有)

etcd 使用 Go 语言编写,Go 的垃圾回收器(GC)虽然在 Go 1.8+ 之后有了显著改进(停顿时间通常在 1ms 以下),但在堆内存较大时仍可能引入毫秒级别的停顿。这些停顿如果恰好发生在 Raft 关键路径上(如 Leader 处理 AppendEntries 响应时),会直接体现在尾部延迟中。

2.5 故障恢复时间

Leader 故障后的恢复时间(Recovery Time)由以下几部分组成:

故障恢复时间 = 故障检测时间 + 选举时间 + 日志同步时间

Raft 的选举机制:

ZAB 的恢复机制:

配置权衡:

election timeout 是一个典型的”检测速度 vs 稳定性”权衡。timeout 设置得越短,故障检测越快,但误判的风险也越高——网络抖动可能导致不必要的选举,选举本身也有成本(短暂的写入中断)。etcd 文档建议 election timeout 至少为 RTT 的 10 倍,以避免网络延迟引发的误选举。

# etcd 配置示例
ETCD_HEARTBEAT_INTERVAL: 100        # 心跳间隔 100ms
ETCD_ELECTION_TIMEOUT: 1000         # 选举超时 1000ms(= 10 × heartbeat)

2.6 跨数据中心延迟的影响

当共识集群跨越多个数据中心部署时,节点间 RTT 从亚毫秒级跳升到几十甚至几百毫秒级。这对以 Leader 为中心的协议影响巨大:

同机房 vs 跨机房的延迟对比:

部署模式 典型 RTT 对共识延迟的影响
同机架(Same Rack) < 0.1ms 延迟可忽略
同机房跨机架 0.1-0.5ms 影响较小
同城双机房 1-3ms 写延迟增加 2-6ms(一轮 RTT)
跨城(如北京-上海) 20-40ms 写延迟增加 40-80ms
跨洲(如中美) 100-200ms 写延迟增加 200-400ms

对于 Raft 和 Multi-Paxos,每次写入至少需要一轮 Leader 到多数派节点的 RTT。如果 Leader 在北京,两个 Follower 分别在上海和广州,那么写入延迟的下界就是 Leader 到上海节点的 RTT(因为只需要等最快的多数派)。但如果 Leader 在北京、一个 Follower 在东京、一个在旧金山,写入延迟的下界就是北京到东京的 RTT——约 40-60ms。

EPaxos 在跨数据中心场景的优势:

EPaxos 的无 Leader 设计在这个场景下有一个关键优势:客户端可以联系最近的副本发起共识,而不需要先把请求路由到远端的 Leader。如果三个副本分别部署在北京、上海、广州,北京的客户端可以直接找北京的副本发起 Fast Path,只需要等待上海或广州中较近的一个确认。这比”请求先到北京 Leader,Leader 等上海确认”要快。

但 EPaxos 的 Fast Path 条件是命令之间没有冲突。一旦冲突率升高,EPaxos 退化为 Slow Path(需要额外一轮通信),性能优势会被抵消。Moraru 等人在 EPaxos 论文中展示的基准测试表明,在低冲突率(如不同 key 的写入)下 EPaxos 吞吐量优于 Multi-Paxos 约 2 倍,但在高冲突率(如相同 key 的并发写入)下性能接近甚至不如 Multi-Paxos。

下面用时序图展示 Raft 在三数据中心部署下的典型写入路径,帮助直观感受跨地域延迟的影响:

sequenceDiagram
    participant C as 客户端<br/>北京
    participant L as Leader<br/>DC1 北京
    participant F1 as Follower<br/>DC2 上海
    participant F2 as Follower<br/>DC3 广州

    Note over L,F2: DC1-DC2 RTT: 25ms | DC1-DC3 RTT: 35ms

    C->>L: 写入请求
    Note over L: 写入本地WAL<br/>~0.5ms

    par 并行复制
        L->>F1: AppendEntries
        L->>F2: AppendEntries
    end

    F1-->>L: ACK (25ms RTT)
    Note over L: 收到多数派ACK<br/>提交日志

    L-->>C: 写入成功 (总延迟 ~26ms)

    F2-->>L: ACK (35ms RTT)
    Note over F2: 不影响提交延迟<br/>但保证三副本一致

该时序图展示了 Raft 跨三数据中心写入的关键路径:Leader 在北京,只需等待上海(最近的多数派)确认即可提交,写入延迟约 26ms。广州的 Follower ACK 虽然晚到,但不影响提交延迟。这也解释了为什么副本放置策略如此重要——将 Leader 和至少一个 Follower 放在距离最近的两个数据中心,可以显著降低写入延迟。

2.7 批处理与流水线优化

理论上 Raft 每条日志条目需要一轮 RTT。但在实际实现中,批处理(Batching)和流水线(Pipelining)是两项关键优化,它们可以显著提升吞吐量:

批处理(Batching):

将多个客户端请求的日志条目合并到一个 AppendEntries RPC 中发送。这摊销了网络和磁盘的固定开销:

// etcd/raft 中的批处理示意
// Leader 在发送 AppendEntries 前,会收集当前所有待发送的日志条目
// 然后一次性打包发送
func (r *raft) bcastAppend() {
    r.forEachProgress(func(id uint64, pr *Progress) {
        if id == r.id {
            return
        }
        r.sendAppend(id) // 每个 sendAppend 会包含多条日志条目
    })
}

批处理的效果是:在高并发下,每次网络往返可以携带多条日志条目,等效吞吐量 = 批大小 × (1 / RTT)。etcd 的实现中,批大小受限于单个消息的最大字节数(默认 1MB)。

流水线(Pipelining):

不等待前一批 AppendEntries 的 ACK,就发送下一批。这让 Leader 可以同时有多个”在途”(in-flight)的批次:

时间 →
Leader:  [Batch1 发送] [Batch2 发送] [Batch3 发送] ...
                 ↓            ↓            ↓
Follower: [Batch1 收到]  [Batch2 收到]  [Batch3 收到]
                 ↓            ↓            ↓
ACK:      [ACK1]        [ACK2]        [ACK3]

流水线的效果是:吞吐量不再受限于单次 RTT,而是受限于网络带宽和磁盘写入速度。etcd/raft 库支持可配置的 in-flight 消息上限(MaxInflightMsgs),默认值为 256。

这两项优化结合后,实际 Raft 实现的吞吐量可以比”每次写入都等一轮 RTT”的朴素实现高出一到两个数量级。这也是为什么理论上的消息复杂度分析不能直接预测实际性能——实现层的优化空间巨大。

三、选型决策矩阵

基于前一节的性能分析和本系列中对各协议机制的深入讨论,我们可以构建一个选型决策框架。这个框架不是一个简单的”A 比 B 好”的排名,而是一组根据你的具体约束条件来缩小选择范围的判断准则。

3.1 决策树

下图展示了一个简化的决策树,用于快速定位候选协议:

共识协议选型决策树

决策树的核心逻辑:

  1. 首先区分故障模型:你的系统需要容忍拜占庭故障(节点可能作恶)还是只需要容忍崩溃故障(节点要么正常工作要么停机)?这是最大的分水岭。绝大多数企业内部系统只需要容忍崩溃故障。
  2. 在崩溃容错(CFT)阵营中,区分部署拓扑:所有节点是否在同一个数据中心内?如果是,Raft 几乎总是最佳选择。如果需要跨数据中心,再进一步判断。
  3. 跨数据中心时,评估冲突率:如果不同数据中心的客户端经常写入相同的 key(高冲突),EPaxos 的 Fast Path 失效,Multi-Paxos 的稳定 Leader 更可靠。如果冲突率很低,EPaxos 的无 Leader 架构可以降低跨地域延迟。
  4. 在拜占庭容错(BFT)阵营中,区分规模:节点数量在百级以下时,HotStuff 或 Tendermint 是成熟选择。节点数量更大时,基于 DAG 的 BFT 协议(如 Narwhal/Bullshark)在通信复杂度上更有优势。

以下 Mermaid 流程图将上述决策逻辑可视化,帮助架构师在具体场景下快速定位候选协议:

flowchart TD
    Start["需要共识协议"] --> FaultModel{"故障模型?"}

    FaultModel -->|崩溃容错 CFT| Deploy{"部署拓扑?"}
    FaultModel -->|拜占庭容错 BFT| BFTScale{"节点规模?"}

    Deploy -->|单数据中心| Latency{"延迟敏感度?"}
    Deploy -->|跨数据中心| Conflict{"命令冲突率?"}

    Latency -->|标准 1~5ms 可接受| Raft1["Raft<br/>推荐: etcd/raft, Hashicorp Raft"]
    Latency -->|亚毫秒级要求| Pipeline["Raft + 激进 Batching/Pipeline<br/>如 dragonboat"]

    Conflict -->|低冲突<br/>不同 key 居多| GeoQ{"团队有 EPaxos 经验?"}
    Conflict -->|高冲突<br/>热点 key 多| RaftGeo["Raft/Multi-Paxos<br/>+ 精心的副本放置<br/>推荐: TiKV Placement Rules"]

    GeoQ -->|是| EPaxos1["EPaxos<br/>注意: 成熟实现极少"]
    GeoQ -->|否| FlexQ["Multi-Paxos + 灵活 Quorum<br/>或 Raft + 地域感知路由"]

    BFTScale -->|"< 100 节点"| HotStuff1["HotStuff / Tendermint"]
    BFTScale -->|">= 100 节点"| DAG["DAG-BFT<br/>Narwhal/Bullshark"]

    style Raft1 fill:#e8f5e9
    style Pipeline fill:#e8f5e9
    style RaftGeo fill:#fff3e0
    style EPaxos1 fill:#fce4ec
    style FlexQ fill:#fff3e0
    style HotStuff1 fill:#e3f2fd
    style DAG fill:#e3f2fd

该决策树从故障模型出发,逐步细化到部署拓扑、冲突率和团队能力三个维度。绿色节点表示最成熟、风险最低的选择;橙色表示需要额外工程投入的中等风险选择;红色表示高风险但理论性能最优的选择;蓝色表示拜占庭容错场景的推荐方案。大多数企业场景会落在左上角的 Raft 分支。

3.2 生产部署案例分析

选型框架只是理论指导。更有价值的是看真实系统做了什么选择,以及它们接受了什么代价。

案例一:etcd – Raft 的标杆实现

etcd 是 Kubernetes 的核心存储,选择 Raft 几乎是必然的。etcd 的设计约束是:元数据存储、数据量小(推荐 < 8GB)、写入频率中等(百到千 ops/sec)、强一致性要求。在这些约束下,Raft 的 Leader 瓶颈根本不构成问题。etcd 团队在 Raft 上的工程投入集中在 ReadIndex 和 LeaseRead 优化读路径、批处理提升写入吞吐、以及 Learner 节点支持安全的成员变更。其三节点集群在 256 并发下可达约 50,000 ops/sec 的写入吞吐,p99 延迟在 10~15ms 范围内(SSD、同机房)。

案例二:TiKV – Raft + Multi-Raft 分组

TiKV 面对的挑战比 etcd 大一个量级:它是一个通用 KV 存储引擎,需要支持 TB 级数据和数十万 ops/sec 的写入。单个 Raft 组无法承载这个规模。TiKV 的解决方案是 Multi-Raft:将数据按 Range 切分为多个 Region(默认 96MB),每个 Region 独立运行一个 Raft 组。这样写入负载被分散到不同 Region 的 Leader 上,消除了单 Leader 瓶颈。TiKV 同时使用 Placement Rules 精确控制每个 Region 副本的位置,在跨城部署时确保多数派在延迟最低的机房内。权衡在于:Multi-Raft 引入了跨 Region 事务协调的复杂度(需要两阶段提交),以及 Region 调度和负载均衡的运维成本。

案例三:CockroachDB – Raft + 地理分区

CockroachDB 同样使用 Raft,并且面对全球部署的需求。它的策略是 地理分区(Geo-Partitioning):允许用户按地理位置划分数据的 Leaseholder(类似 Leader),使得欧洲用户的数据由欧洲节点持有 Lease,亚洲用户的数据由亚洲节点持有 Lease。这在保持 Raft 强一致性的同时,将读延迟降到本地网络级别。写入仍然需要跨地域共识,但通过精心的副本放置(例如三副本中两个在本地区域),写入延迟也被控制在可接受范围内。CockroachDB 没有选择 EPaxos,尽管理论上 EPaxos 更适合全球部署——原因正是 EPaxos 的工程复杂度和缺乏成熟实现。

3.3 EPaxos 工程困境:为什么理论优势未能转化为生产部署

EPaxos 在论文中展示了令人印象深刻的性能数据:零冲突场景下吞吐量接近 Multi-Paxos 的 2 倍,且无 Leader 瓶颈。但截至目前,几乎没有大规模生产系统采用 EPaxos。原因是多方面的:

依赖图的工程复杂度:EPaxos 用命令间的依赖关系(而非全局日志序号)来确定执行顺序。每个副本需要维护一个依赖图,并在提交后通过拓扑排序确定执行序列。这个依赖图在故障恢复时的重建逻辑极其复杂——需要处理部分提交、冲突检测遗漏、以及多副本间依赖图的不一致。论文对恢复协议的描述只有几段话,但工程实现可能需要数千行代码,且每一行都可能引入正确性 bug。

冲突率的不可预测性:EPaxos 的性能高度依赖冲突率,而冲突率在生产中很难预测和控制。一个看似低冲突的工作负载,可能因为热点 key 的出现(如秒杀、热门帖子)而突然变为高冲突,导致 EPaxos 频繁退化到 Slow Path。相比之下,Raft 的性能特征更加稳定和可预测。

调试和可观测性的困难:Raft 的全局有序日志是一个巨大的调试优势——你可以从任何节点导出日志,按索引对齐,快速定位不一致。EPaxos 的偏序日志和依赖图使得状态重放和问题定位困难得多。目前没有成熟的 EPaxos 调试工具、可视化工具或监控指标体系。

缺乏成熟实现的恶性循环:没有生产部署 -> 没有工程经验积累 -> 没有成熟的开源实现 -> 更没有人敢在生产中使用。这个循环目前没有被打破的迹象。

3.4 结构化对比表

评估维度 Raft Multi-Paxos EPaxos
消息复杂度(每条目) O(n) O(n) O(n) 快速路径;O(n) 慢速路径
通信轮次(稳态) 1 RTT(Leader→多数派) 1 RTT(Leader→多数派) 1 RTT(Fast)或 2 RTT(Slow)
Leader 依赖 强 Leader 强 Leader(Distinguished Proposer) 无固定 Leader
写入负载分布 集中于 Leader 集中于 Leader 分散到所有副本
读取优化 ReadIndex / LeaseRead Lease / 本地读 任意副本可发起
日志顺序 全局有序(Leader 决定) 全局有序(Leader 决定) 偏序(按依赖关系排列)
冲突处理 不适用(Leader 串行化) 不适用(Leader 串行化) 冲突需额外轮次 + 依赖图
跨数据中心适用性 中等(受 Leader 位置限制) 中等(灵活 Quorum 配置) 高(无 Leader 限制)
可理解性 高(设计目标) 极低
成熟实现数量 多(etcd/raft, Hashicorp Raft, …) 中(libpaxos, Phxpaxos, …)
生产部署案例 etcd, TiKV, CockroachDB, Consul Chubby, Spanner(内部) 极少公开案例

3.5 场景化推荐

下面按常见的工程场景给出具体推荐:

场景一:单机房、三到五节点的元数据存储

典型例子:配置中心、服务发现、分布式锁。

推荐:Raft。理由:单机房内 RTT 极低,Leader 瓶颈不明显;Raft 的可理解性和工具链成熟度远超其他选项;etcd 和 Consul 都是经过大规模生产验证的 Raft 实现。没有理由在这个场景中使用更复杂的协议。

场景二:跨城容灾、三机房五副本部署

典型例子:金融交易系统的跨城热备。

推荐:Raft + 精心的副本放置。理由:将 Leader 和两个 Follower 放在主数据中心(或两个距离最近的数据中心),确保多数派在低延迟范围内。跨城的两个副本作为”灾备追随者”(Non-voting Learner),不参与投票但接收日志复制。这样正常情况下写入延迟由主数据中心的内部 RTT 主导,灾难发生时可以手动将远端副本提升为投票成员。TiKV 的 Placement Rules 和 CockroachDB 的 Zone Configurations 都支持这种精细的副本放置策略。

场景三:全球部署、每个地区都有写入需求

典型例子:全球化社交网络的用户数据、CDN 配置管理。

推荐:评估 EPaxos 或 Multi-Paxos + 灵活 Quorum。理由:如果每个地区都有大量写入,Leader 无论放在哪里都会导致远端用户的高延迟。EPaxos 的无 Leader 架构允许每个地区的客户端联系本地副本发起共识。但前提是命令冲突率可控——如果不同地区很少写入相同的 key,EPaxos 可以保持 Fast Path。如果冲突率高,可以考虑 Multi-Paxos 配合灵活的 Quorum 配置(如网格 Quorum)来优化。

但需要坦诚的是:EPaxos 在生产中的部署案例极少,目前没有广泛使用的成熟开源实现。如果你的团队没有深入理解 EPaxos 内部机制的能力,这个选择风险很高。更务实的方案可能是:使用 Raft(如 CockroachDB 或 TiDB),在应用层做地域感知的路由和分区,将冲突最小化。

场景四:区块链或需要拜占庭容错的联盟环境

典型例子:联盟链、需要在不信任节点间达成共识的系统。

推荐:HotStuff 或 Tendermint。理由:如本系列第 16 篇所述,HotStuff 将 BFT 共识的通信复杂度优化到线性级别,Tendermint 则在区块链生态中有广泛应用。如果节点数量超过数百,可以考虑基于 DAG 的变体。

场景五:嵌入式共识(库而非服务)

典型例子:你在构建一个分布式数据库,需要在存储引擎中嵌入共识模块。

推荐:使用 etcd/raft Go 库或 Hashicorp Raft 库,而不是自己实现。理由:见下文”不要自己实现共识协议”一节。

3.6 “大多数情况下选 Raft”的理由

在实践中,我们的建议可以归结为一句话:除非你有明确的理由不选 Raft,否则选 Raft。

这不是因为 Raft 在理论上最优——事实上在跨数据中心低冲突场景下 EPaxos 理论上更优,在灵活 Quorum 配置上 Multi-Paxos 更灵活。而是因为:

  1. Raft 的可理解性是一个工程优势,而非学术虚荣。 你能理解的协议,才是你能正确实现、调试和运维的协议。
  2. Raft 的生态最成熟。 etcd/raft、Hashicorp Raft、lni/dragonboat 等库都经过了大规模生产验证。
  3. Raft 的性能在绝大多数场景下足够好。 通过批处理、流水线和 Learner 节点等优化,Raft 可以覆盖从单机房到跨城容灾的大部分部署模式。
  4. 你的瓶颈很可能不在共识层。 在大多数系统中,真正的性能瓶颈在磁盘 I/O、网络带宽、应用层逻辑或数据模型设计上,而不在共识协议的消息轮次上。过早优化共识层是典型的”优化错误的地方”。

四、隐藏的工程成本

论文和基准测试展示的是共识协议最光鲜的一面。但在生产中运行一个共识集群,有大量论文不会讨论的隐藏成本。

4.1 可理解性的量化

Diego Ongaro 在他的博士论文(“Consensus: Bridging Theory and Practice”,Stanford University, 2014)中做了一个有趣的实验:他让斯坦福大学的学生分别学习 Raft 和 Paxos,然后通过测试来比较学习效果。结果显示,学生在学习 Raft 后的测试成绩显著高于学习 Paxos 后的成绩。

这个实验的意义不仅在于学术教育——它直接影响工程实践:

可理解性维度 Raft Multi-Paxos EPaxos
核心论文长度 18 页(会议版) 原始 Paxos 不足 10 页,但以晦涩著称 约 30 页,且依赖图处理复杂
参考实现复杂度 etcd/raft 约 5000 行 Go 参考实现差异大 几乎没有成熟参考实现
新工程师上手时间 1-2 周可理解核心逻辑 3-4 周,需反复阅读 4-8 周,依赖图和恢复逻辑极其复杂
调试难度 中等(Leader 日志为全局有序,便于追踪) 高(多 Proposer 并发时状态空间爆炸) 极高(偏序日志 + 依赖图使状态重放困难)

可理解性的成本不是一次性的。它在以下场景中持续发挥作用:

4.2 运维复杂度

共识集群的日常运维涉及以下工作,每一项都有各自的复杂度:

日志压缩(Log Compaction)与快照(Snapshotting):

共识日志不能无限增长——它会耗尽磁盘空间,也会导致新节点追赶(Catch-up)时间过长。因此需要定期截断旧日志,并保存状态机的快照。

Raft 论文中明确定义了快照机制:当日志长度超过阈值时,Leader 生成当前状态机的快照,然后截断已快照覆盖的日志条目。新加入的节点可以先接收快照,再从快照之后的日志开始追赶。

// etcd 快照触发条件示例
// 当已应用的日志索引 - 上次快照索引 > SnapshotCount 时触发
if appliedIndex - snapshotIndex > SnapshotCount {
    snapshot, err := rc.getSnapshot()
    // ...
    rc.raftStorage.CreateSnapshot(appliedIndex, &confState, data)
}

快照的工程挑战在于:

成员变更(Membership Change):

在生产中,扩缩容(添加/移除节点)是常见操作。Raft 论文定义了两种成员变更方法:联合共识(Joint Consensus)和单节点变更(Single-Node Change)。etcd 采用了更安全的单节点变更方式——每次只添加或移除一个节点,变更本身作为一条特殊的日志条目通过共识提交。

成员变更的工程陷阱:

监控指标:

一个健康的共识集群至少需要监控以下指标:

# etcd Prometheus 指标示例
etcd_server_leader_changes_seen_total          # Leader 切换次数
etcd_server_proposals_committed_total          # 已提交的提案总数
etcd_server_proposals_applied_total            # 已应用的提案总数
etcd_server_proposals_pending                  # 待处理的提案数
etcd_server_proposals_failed_total             # 失败的提案数
etcd_disk_wal_fsync_duration_seconds           # WAL fsync 延迟
etcd_disk_backend_commit_duration_seconds      # 后端存储提交延迟
etcd_network_peer_round_trip_time_seconds      # 节点间 RTT
etcd_server_slow_apply_total                   # 慢应用次数

这些指标中,proposals_pending 突然升高通常意味着 Leader 过载或网络分区;wal_fsync_duration_seconds 突然升高意味着磁盘出现问题;leader_changes_seen_total 频繁增加意味着集群不稳定。没有这些监控,共识集群就是在”盲飞”。

4.3 社区生态对比

维度 Raft Multi-Paxos EPaxos
主要开源实现 etcd/raft (Go)、Hashicorp Raft (Go)、lni/dragonboat (Go)、SOFAJRaft (Java)、openraft (Rust) libpaxos (C)、Phxpaxos (C++, 微信) 学术原型为主
生产级别用户 etcd (Kubernetes)、TiKV、CockroachDB、Consul、RethinkDB Google Chubby/Spanner(内部实现)、微信(Phxpaxos)、阿里巴巴 无广泛公开案例
TLA+ 规约 有(Ongaro 提供官方版本) 有(Lamport 提供) 有(论文附带)
Jepsen 测试 etcd, CockroachDB, TiDB 等多个系统已接受 Jepsen 测试 间接(通过使用 Paxos 的系统)
教程与学习资源 极丰富(MIT 6.824 课程教学用例) 丰富但理解门槛高 稀缺

4.4 测试共识实现:三种利器

实现共识协议后,如何验证其正确性?这是一个独立于选型的重要话题。三种主流方法各有侧重:

1. TLA+(Temporal Logic of Actions)形式化验证

TLA+ 由 Leslie Lamport 创建,是一种用于描述和验证并发系统的形式化规约语言。你可以用 TLA+ 精确描述共识协议的状态机,然后使用模型检查器(TLC)穷举所有可能的状态转换路径,验证安全性和活性属性。

Raft 和 Paxos 都有官方的 TLA+ 规约。实践中,TLA+ 主要用于验证协议设计的正确性,而不是实现代码的正确性——它无法检测编程语言层面的 bug(如 off-by-one 错误、内存泄漏),但可以捕获设计层面的逻辑错误(如”是否存在某个执行序列使两个节点对同一日志索引提交了不同的值”)。

Amazon 的工程师在一篇著名的实践报告中(Newcombe et al., “How Amazon Web Services Uses Formal Methods”, CACM 2015)分享了他们使用 TLA+ 发现 DynamoDB 和 S3 中关键 bug 的经验。报告指出,TLA+ 发现的 bug 中有些是”难以想象”的——即使是经验丰富的工程师也不会在代码审查中发现的极端边界情况。

2. Jepsen 分布式系统测试

Jepsen 是 Kyle Kingsbury 开发的分布式系统测试框架。它的核心思路是:在真实的网络环境中运行被测系统,注入各种故障(网络分区、节点崩溃、时钟偏移、磁盘故障),同时执行一系列操作,最后验证操作历史是否满足指定的一致性模型(如线性一致性)。

Jepsen 已经对大量分布式系统进行了测试,发现了许多关键 bug:

Jepsen 的价值在于它测试的是完整系统(包括网络栈、磁盘、操作系统交互),而不仅仅是共识模块本身。它能发现”协议正确但实现有 bug”或”单个模块正确但模块间交互有 bug”的问题。

3. 确定性模拟测试(Deterministic Simulation Testing)

这是 FoundationDB 团队推广的一种测试方法。核心思想是:将所有非确定性来源(网络、磁盘、时钟)替换为可控的模拟版本,然后在一个确定性的调度器下执行系统代码。由于所有非确定性都被消除,每次执行同一个随机种子都会产生完全相同的行为,使得 bug 可以被精确重现。

FoundationDB 声称使用这种方法累积运行了数百万次模拟测试小时,发现了大量在传统测试中无法触发的 bug。TiKV 的 Chaos Engineering 实践也借鉴了类似的思路。

# 确定性模拟测试的伪代码框架
class DeterministicSimulator:
    def __init__(self, seed):
        self.rng = Random(seed)          # 确定性随机数生成器
        self.network = SimNetwork(self.rng)  # 模拟网络(可注入延迟、丢包、分区)
        self.disk = SimDisk(self.rng)        # 模拟磁盘(可注入延迟、错误)
        self.clock = SimClock()              # 模拟时钟(可控制时间推进)

    def run(self, nodes, workload, steps):
        for step in range(steps):
            # 随机选择一个事件:客户端请求、网络消息送达、磁盘操作完成、故障注入
            event = self.schedule_next_event()
            event.execute()
            self.check_invariants()  # 每步都验证不变量

五、“不要自己实现共识协议”

这一节的标题可能是本文最重要的结论之一。

5.1 共识实现为什么如此困难

共识协议的正确性证明通常只有几页。但从证明到可在生产中运行的代码之间,有一道巨大的鸿沟。这道鸿沟的来源包括:

1. 论文中省略的工程细节

Raft 论文描述了一个清晰的状态机,但以下问题论文只是一笔带过或完全没有涉及:

2. 并发和时序带来的状态空间爆炸

一个三节点 Raft 集群中,可能同时发生的事件包括:客户端请求到达、AppendEntries RPC 发送/接收/超时、RequestVote RPC 发送/接收/超时、心跳超时、选举超时、磁盘 fsync 完成、快照生成/传输、成员变更日志提交……这些事件可以以任意顺序交织,产生的状态空间是天文数字。

每一种交织顺序都必须保证安全性属性(同一个日志索引不会被提交不同的值),这要求实现中的每一个 if/else 分支都是正确的。任何一个被遗漏的边界情况,都可能在某种罕见的事件交织下触发数据不一致。

3. 分布式系统特有的故障模式

除了常见的节点崩溃和网络延迟,实际生产环境中还有许多”意料之外”的故障模式:

5.2 著名的共识实现 Bug

历史上,即使是由经验丰富的工程师团队构建的共识实现,也会出现严重的 bug。以下是一些有据可查的案例:

Raft 实现中的 Bug:

Diego Ongaro 在其博士论文中提到,他调查了多个第三方 Raft 实现(论文发表时已有数十个开源 Raft 实现),发现其中有相当比例存在正确性问题。常见的错误模式包括:

ZooKeeper 的已知问题:

ZooKeeper 在长期运行中也发现过多个一致性相关的 bug,在其 JIRA 问题跟踪器中有记录。这些 bug 涉及:

这些 bug 通常需要特定的故障序列才能触发——例如”在 Leader 提交了一个配置变更后、发出心跳前、恰好发生网络分区”这样的场景。在正常运行中它们可能永远不会出现,但在生产规模的长时间运行中,这些”不可能”的事件最终一定会发生。

5.3 库 vs 服务:构建还是购买

如果你被说服了”不要自己实现共识协议”,那下一个问题是:使用共识库还是使用共识服务?

使用共识库(如 etcd/raft Go 库):

适用场景:你正在构建一个需要嵌入式共识的系统(如分布式数据库、分布式存储系统),并且: - 你需要对共识层有完全的控制(自定义状态机、自定义存储后端、自定义网络传输)。 - 你的团队有足够的分布式系统经验。 - 你愿意承担集成和运维的复杂度。

etcd/raft 库的设计理念是”只提供核心的 Raft 状态机逻辑,网络和存储由使用者自己实现”。这给了最大的灵活性,但也意味着你需要自己处理:

TiKV 和 CockroachDB 都是基于 Raft 库构建的成功案例。但两个项目的工程团队规模都在数十到数百人级别——这不是一两个人能完成的工作。

// 使用 etcd/raft 库的极简示例骨架
import "go.etcd.io/raft/v3"

// 1. 创建 Raft 节点
cfg := &raft.Config{
    ID:              0x01,
    ElectionTick:    10,
    HeartbeatTick:   1,
    Storage:         newStorage(),
    MaxInflightMsgs: 256,
    MaxSizePerMsg:   1024 * 1024,
}
node := raft.StartNode(cfg, peers)

// 2. 事件循环:你需要自己驱动 Raft 状态机
for {
    select {
    case <-ticker.C:
        node.Tick()  // 驱动时钟
    case rd := <-node.Ready():
        // 持久化日志条目到 WAL(你自己实现)
        saveToStorage(rd.HardState, rd.Entries, rd.Snapshot)
        // 发送消息到其他节点(你自己实现网络层)
        sendMessages(rd.Messages)
        // 将已提交的条目应用到状态机(你自己实现)
        applyCommitted(rd.CommittedEntries)
        // 通知 Raft 当前批次处理完成
        node.Advance()
    case err := <-errorCh:
        log.Fatal(err)
    }
}

使用共识服务(如 etcd、ZooKeeper、Consul):

适用场景:你需要共识能力(分布式锁、配置管理、服务发现、Leader 选举),但不需要自定义共识层的内部行为。

这是绝大多数团队应该选择的方式。使用 etcd 或 ZooKeeper 作为外部依赖,通过它们的客户端 API 获取共识能力:

# 使用 etcd 实现分布式锁的示例
import etcd3

client = etcd3.client(host='etcd-host', port=2379)

# 创建租约(Lease),10 秒自动过期
lease = client.lease(10)

# 尝试获取锁
lock = client.lock('my-distributed-lock', ttl=10)
lock.acquire()

try:
    # 临界区:只有持有锁的进程才能执行这里的代码
    do_critical_work()
finally:
    lock.release()

决策对比表:

考量 使用库 使用服务
控制粒度 完全控制 API 层面控制
实现复杂度 极高(需要数月到数年) 低(集成客户端库即可)
运维复杂度 你的系统即是共识集群 需要单独运维共识服务
性能开销 无额外网络跳转 多一次网络往返
团队要求 需要分布式系统专家 普通后端工程师即可
适用系统 分布式数据库、分布式存储 应用层服务、配置管理、微服务协调

5.4 一个务实的决策流程

你需要共识能力吗?
├── 是的,我在做分布式存储/数据库
│   ├── 团队 > 10 人,有分布式系统经验 → 考虑使用 Raft 库(etcd/raft, openraft)
│   └── 团队较小或经验不足 → 基于现有分布式数据库(TiDB, CockroachDB)构建
├── 是的,我需要分布式锁/配置中心/服务发现
│   └── 使用 etcd / ZooKeeper / Consul 作为服务
└── 不确定
    └── 你可能不需要。先问自己:能不能用单节点解决?

六、第三部分回顾与展望

6.1 共识之旅回顾

在本系列的第三部分中,我们走过了共识问题的完整光谱:

文章 主题 核心贡献
第 11 篇 共识问题导论 定义了共识问题,介绍了 FLP 不可能性定理
第 12 篇 Paxos 展示了第一个实用的共识算法,理解了 Proposer/Acceptor/Learner 的角色分工
第 13 篇 Raft 以可理解性为设计目标,将共识分解为 Leader 选举、日志复制、安全性三个子问题
第 14 篇 EPaxos 去除了 Leader 瓶颈,引入了命令依赖图实现无主共识
第 15 篇 VR 与 PBFT 从崩溃容错走向拜占庭容错,理解了 2f+1 vs 3f+1 的区别
第 16 篇 HotStuff 将 BFT 共识的通信复杂度从二次降到线性,引入了流水线化的视图切换
第 17 篇(本文) 工程权衡 从性能、选型、成本三个维度对比了主流共识协议的工程实践

回顾这七篇文章,我们可以提炼出几条关于共识的核心洞察:

1. 共识的本质是在不可靠的基础设施上构建可靠的抽象。 FLP 定理告诉我们,在异步系统中确定性的共识是不可能的。所有实用的共识协议都通过某种形式的妥协来绕过这一不可能性——Paxos 和 Raft 依赖 Leader 和超时机制来保证活性(但在极端情况下可能暂时无法推进),PBFT 需要 3f+1 个节点来容忍 f 个拜占庭故障。

2. 没有”最好”的共识协议,只有最适合当前场景的。 Raft 的优势在可理解性和生态,Multi-Paxos 的优势在灵活性,EPaxos 的优势在跨地域低延迟场景,HotStuff 的优势在拜占庭容错的通信效率。选型的关键是理解你的约束条件。

3. 实现正确的共识比理解共识难一个数量级。 论文中优雅的状态机转换规则,在面对磁盘故障、网络灰色故障、部分写入、时钟偏移等现实世界的复杂性时,会膨胀出大量的工程细节。这就是为什么我们反复强调”不要自己实现共识协议”。

4. 工程选型中,生态和可维护性的权重往往高于理论性能。 一个你的团队能理解、能调试、能安全运维的系统,比一个理论最优但只有原作者能维护的系统,有更高的长期价值。

6.2 从共识到复制:第四部分预告

共识回答的核心问题是:多个节点如何就一个值(或一系列值)达成一致?

但在真实的分布式系统中,共识只是故事的一部分。一旦我们知道了如何达成一致,下一个问题自然就是:如何高效地将数据复制到多个节点上?

这就是本系列第四部分”复制(Replication)“将要探讨的主题。

共识和复制的关系可以这样理解:

用一句话总结:共识告诉我们如何达成一致(How to Agree),复制告诉我们如何高效地复制数据(How to Copy)。

第四部分的第一篇文章将从最基础、也是最广泛使用的复制模式开始——主从复制(Leader-Follower Replication)。我们将看到,一个看似简单的”一主多从”架构,在面对故障转移、复制延迟、读写分离等现实问题时,会引发多少工程挑战。


延伸阅读:

参考资料:

  1. Ongaro, D. (2014). Consensus: Bridging Theory and Practice. PhD Dissertation, Stanford University.
  2. Ongaro, D. & Ousterhout, J. (2014). In Search of an Understandable Consensus Algorithm. USENIX ATC 2014.
  3. Lamport, L. (2001). Paxos Made Simple. ACM SIGACT News.
  4. Moraru, I., Andersen, D. G., & Kaminsky, M. (2013). There Is More Consensus in Egalitarian Parliaments. SOSP 2013.
  5. Hunt, P., Konar, M., Junqueira, F. P., & Reed, B. (2010). ZooKeeper: Wait-free Coordination for Internet-scale Systems. USENIX ATC 2010.
  6. Yin, M., Malkhi, D., Reiter, M. K., Gueta, G. G., & Abraham, I. (2019). HotStuff: BFT Consensus with Linearity and Responsiveness. PODC 2019.
  7. Newcombe, C., Rath, T., Zhang, F., Munteanu, B., Brooker, M., & Deardeuff, M. (2015). How Amazon Web Services Uses Formal Methods. Communications of the ACM, 58(4).
  8. Junqueira, F. P., Reed, B. C., & Serafini, M. (2011). Zab: High-performance broadcast for primary-backup systems. DSN 2011.

上一篇:HotStuff 与现代 BFT:从三轮到两轮的优化之路 下一篇:主从复制:最古老也最常用的复制拓扑

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-13 · distributed

【分布式系统百科】Raft 深度重写:从论文的 18 页到 etcd 的 15000 行

Raft 论文 18 页就能读完,但 etcd/raft 用了 15000 行 Go 才把它变成能在生产环境跑的代码。这篇文章从论文的每一个核心机制出发,逐一拆解工程实现中论文没说的东西:PreVote、ReadIndex、LeaderTransfer、ConfChange V2、流水线复制、Async Apply,以及 TiKV 的 Multi-Raft 实践。最后做一次精确的 Paxos 对比,并坦诚讨论 Raft 的已知缺陷。

2026-04-01 · distributed

Raft:让共识算法不再是黑魔法

Paxos 被引用了几千次,能正确实现它的人不超过几十个。Raft 用可理解性换工程落地,它的 Leader Election、Log Replication 和 Safety 三板斧,撑起了 etcd、TiKV 和大半个云原生基础设施。


By .