Raft 实现库横向评测:tikv/raft-rs、openraft 与 actix-raft 的正确性与性能
Raft 实现库横向评测:tikv/raft-rs、openraft 与 actix-raft 的正确性与性能
一、Raft 实现库的选型困境
Rust 生态中有三个主流 Raft 实现库:tikv/raft-rs(TiKV 的生产级实现)、openraft(独立 Raft 库,关注易用性)、actix-raft(基于 Actix 框架的异步 Raft)。选型困境:raft-rs 正确性经过 Jepsen 验证但 API 复杂;openraft API 简洁但生产验证较少;actix-raft 与 Actix 框架绑定且维护不活跃。
七月的选型评估中,正确性是首要约束——共识协议的正确性是系统可靠性的基石,性能其次。三个库的正确性验证程度不同:raft-rs 有 Jepsen 测试报告和 TiKV 生产验证;openraft 有自建的单元和集成测试但无 Jepsen 验证;actix-raft 缺少系统性测试且维护不活跃。
二、三个 Raft 库的架构差异对比模型
从架构层面分析三个库的设计差异和正确性保证机制。
raft-rs:生产级正确性保证
raft-rs 是 TiKV 的 Raft 实现,从 etcd 的 Go 版本移植而来。核心设计:同步 API + 外部异步驱动。Raft 状态机通过step方法接收消息、通过ready方法输出需要处理的操作(日志写入、消息发送、状态推进)。外部驱动负责异步执行 IO 操作并将结果反馈给状态机。
正确性保证:Jepsen 测试报告验证了 raft-rs 在网络分区、时钟漂移、进程故障下的正确性。TiKV 的生产部署进一步验证了在真实负载下的稳定性。正确性保证程度是三个库中最高的。
API 复杂度最高:需要手动驱动 Raft 状态机——每轮循环调用ready、处理 IO、推进状态。框架不自动管理 Raft 状态的持久化和消息发送。但复杂度也意味着灵活性——可以自定义存储引擎、消息传输、状态管理。
性能特征:单节点 QPS 约 50K-100K(无 IO 纯状态机推进)。IO 性能取决于外部驱动的实现——TiKV 使用 RocksDB 作为存储引擎,性能受 RocksDB 配置影响。
openraft:易用性优先的异步 Raft
openraft 的设计目标是"易用性"——异步 API 直接集成 tokio,开发者无需手动驱动状态机。核心设计:Raft对象提供init、client_read、client_write、add_learner等高层异步方法,内部自动管理状态推进和 IO。
正确性保证:openraft 有自建的单元测试和集成测试覆盖正常路径和分区场景,但无 Jepsen 验证。正确性保证程度中等——未经过第三方独立验证。
API 简洁度最高:初始化后直接调用raft.client_write(data)即可,无需手动驱动。框架自动管理日志持久化、消息发送、快照生成。代价是灵活性较低——存储引擎和消息传输的选择受限。
性能特征:单节点 QPS 约 30K-50K。tokio 的异步 IO 比手动驱动有额外开销(任务调度、Channel 传递),但简化了开发流程。
动态成员变更:openraft 支持动态成员变更(添加/移除节点)且 API 简洁。raft-rs 也支持但需要手动处理配置变更的中间状态。这是 openraft 的显著优势。
actix-raft:Actix 框架绑定的 Raft
actix-raft 基于 Actix 框架的 actor 模型实现 Raft。每个 Raft 节点是一个 actor,消息通过 actor 系统传递。核心设计:actor 模型的天然隔离性——每个 actor 独立处理消息,状态修改在 actor 内完成,无需外部锁。
正确性保证:缺少系统性测试框架,无 Jepsen 验证,无已知的生产部署案例。正确性保证程度最低。
维护状态:actix-raft 的最后一次重大更新在 2020 年,之后仅偶尔修复小问题。库的维护不活跃意味着未跟进 Raft 的最新优化(如 Pre-Vote、ReadIndex)。
适用场景极为有限:仅在团队已有 Actix 框架经验且需要 Raft 功能时考虑。其他场景应优先选择 raft-rs 或 openraft。
三、Raft 库正确性验证框架的实现
以下代码展示 Raft 实现库的正确性验证框架和性能基准测试。
/// Raft 正确性验证:线性一致性检查 struct LinearizabilityChecker { // 操作历史记录 history: Vec<OperationRecord>, // 并发模型 concurrency_model: ConcurrencyModel, } struct OperationRecord { // 操作类型 op: RaftOperation, // 调用开始时间 invoke_time: Instant, // 返回完成时间 return_time: Instant, // 操作结果 result: OperationResult, } enum RaftOperation { Write { key: String, value: String }, Read { key: String }, } /// 线性一致性验证:检查操作历史是否可线性化 impl LinearizabilityChecker { /// 验证:所有读操作返回的值必须是最近的写操作写入的值 /// 且不存在"读到未来值"的情况 fn verify_linearizability(&self) -> Result<(), LinearizabilityError> { // 构建线性化点:每个操作选一个时间点 // 线性化点在 invoke_time 和 return_time 之间 let writes = self.history.iter() .filter(|r| matches!(r.op, RaftOperation::Write { .. })) .collect(); let reads = self.history.iter() .filter(|r| matches!(r.op, RaftOperation::Read { .. })) .collect(); // 验证每个读操作的返回值 for read in reads { let key = match &read.op { RaftOperation::Read { key } => key, _ => unreachable(), }; // 找到在 read 线性化点之前的最近的 write let latest_write = writes.iter() .filter(|w| w.return_time <= read.invoke_time) .filter(|w| match &w.op { RaftOperation::Write { key: k, .. } => k == key, _ => false, }) .max_by_key(|w| w.return_time); // 检查读操作返回的值是否与最近的写一致 match (latest_write, &read.result) { (Some(write), OperationResult::ReadResult(value)) => { let write_value = match &write.op { RaftOperation::Write { value, .. } => value, _ => unreachable(), }; if value != write_value { return Err(LinearizabilityError::StaleRead { expected: write_value.clone(), actual: value.clone(), }); } } (None, OperationResult::ReadResult(value)) => { if value != "" { return Err(LinearizabilityError::UnexpectedValue(value.clone())); } } _ => {} } } Ok(()) } } /// Raft 库性能基准测试配置 struct RaftBenchmark { library: RaftLibrary, node_count: u32, storage_engine: StorageEngine, network_latency_ms: u64, } enum RaftLibrary { RaftRs, OpenRaft, ActixRaft } /// 性能基准测试结果 struct RaftBenchmarkResult { library: RaftLibrary, // 写操作延迟 P50/P99 write_p50_ms: f64, write_p99_ms: f64, // 读操作延迟 P50/P99(线性一致性读) read_p50_ms: f64, read_p99_ms: f64, // 吞吐量 ops/s throughput: f64, // 选举恢复时间:leader 故障后新 leader 选出时间 election_recovery_ms: f64, // 成员变更延迟 membership_change_ms: f64, } /// 综合评分:正确性优先,性能其次 fn evaluate_raft_library( correctness: CorrectnessLevel, perf: RaftBenchmarkResult, ) -> f64 { let correctness_score = match correctness { CorrectnessLevel::JepsenVerified => 1.0, CorrectnessLevel::SelfTested => 0.7, CorrectnessLevel::Untested => 0.3, }; let perf_score = perf.throughput / max_throughput; // 权重:正确性 60%, 性能 40% // 原因:共识协议的正确性是系统可靠性的基石 correctness_score * 0.6 + perf_score * 0.4 }四、选型的场景匹配矩阵
raft-rs 适用场景:生产级共识服务(正确性最高优先级)、需要自定义存储引擎(如 RocksDB/自定义 LSM)、需要灵活的消息传输(如 gRPC/自定义协议)、TiKV 生态集成。禁用场景:快速原型验证(API 复杂)、团队无 Raft 驱动经验(需手动管理 Ready)、需要简洁 API(不如 openraft)。
openraft 适用场景:快速原型验证(API 简洁)、tokio 生态集成(异步 API)、需要动态成员变更(API 最简洁)、中小规模部署(正确性中等但足够)。禁用场景:正确性最高优先级(无 Jepsen 验证)、需要自定义存储引擎(存储选择受限)、大规模生产部署(生产验证案例少)。
actix-raft 适用场景:仅限于已有 Actix 框架经验的团队。禁用场景:新项目选型(正确性验证不足、维护不活跃)、需要最新 Raft 优化(Pre-Vote/ReadIndex 未实现)、需要灵活存储引擎。
正确性优先原则:共识协议的正确性是系统可靠性的基石。一个有 Jepsen 验证的 Raft 实现即使性能低 30%,也比一个无验证但性能高 30% 的实现更值得选择。因为共识协议的错误是"静默的数据不一致"——看起来正常运行但数据已损坏。
结论
- Raft 库选型的首要约束是正确性而非性能——共识协议错误是静默的数据不一致。
- raft-rs 有 Jepsen 验证和 TiKV 生产验证,正确性保证程度最高但 API 最复杂。
- openraft 的异步 API 最简洁,但缺少 Jepsen 验证,正确性保证程度中等。
- actix-raft 维护不活跃且缺少系统性测试,仅限已有 Actix 经验的团队。
- 正确性优先原则:Jepsen 验证比性能领先更重要,共识协议错误代价远超性能差距。
