动机 #
Redis协议已经成为事实意义上的key-value存储协议标准。除了官方的Redis实现以外,我们看到还有各种兼容Redis协议的实现:
- Valkey:Linux基金会官方fork的Redis 7.2分支,采用BSD许可证,是Redis更换SSPL许可证后社区推出的真正开源替代方案,完全兼容Redis协议和持久化机制。
- Dragonfly:追求极致性能的现代多线程内存数据库,可提供相比Redis高达25倍的吞吐量和更低的尾延迟,但采用BSL许可证(4年后转Apache 2.0)。
- KeyDB:Snapchat收购维护的Redis多线程分支,在100%兼容Redis API的基础上增加了主动复制(Active Replication)和Flash存储扩展能力,但更新已放缓。
- Kvrocks:Apache顶级项目,基于RocksDB实现数据持久化到磁盘的分布式KV存储,支持数十TB级数据且成本仅为内存方案的1/5-1/10,适合大容量低成本场景。
但是,上面的任何一个实现,都没有在分布式系统的强一致性上走得更远,在这个维度,它们依然采用了Redis原生实现的最终一致性。
最开始,我创建coredb项目,是为了利用raft共识算法,实现一个满足强一致性且兼容redis协议的服务。我知道一定有人会有疑问:大家使用Redis类的系统,是为了缓存数据,在这类型的项目里,一般都会选择CAP中的AP,把可用性放在第一位,而非一致性。
但是,回到文章最开始的结论:Redis协议已经成为事实意义上的key-value存储协议标准。如果在这个大前提下,它的生态价值不应仅仅局限于传统的内存缓存。通过为其引入强一致性的持久化存储,我们可以赋予它全新的生命力和应用场景——正如 HTTP 协议从早期的网页传输协议,最终演变为无处不在的通信基石一样。
最开始,我构建的项目只有coredb,这是一个采用Raft算法+rocksdb的强一致且兼容Redis协议的服务,也就是说,可以继续使用redis客户端访问这个服务,但是它满足强一致性:只要数据写入时返回成功,意味着至少在集群中的半数以上节点写入成功。
在开发过程中我意识到,“Raft + RocksDB” 的架构组合具有极高的通用价值。考虑到许多开发者可能也需要这样一套可靠的底层基座,去构建他们自己专属的强一致性存储系统,我便将这部分核心逻辑进行了解耦,单独抽离出了 Rockraft 这个基础框架项目。
设计与实现 #
Rockraft采用Rust开发,这是我目前最喜欢的系统编程语言:类型安全且内存安全,这两个特性是我最喜欢的Rust语言特性,有了这两个特性在编程时会更加放心。目前Rust语言的常见Raft实现有以下两个:
Rust生态中最成熟、生产验证最充分的Raft实现,被近千个生产环境采用。它源自etcd的Go实现移植,但完全用Rust重写,保证了线程安全和内存安全。
架构特点:
- 核心共识模块:仅提供纯共识算法核心(Raft状态机),不包含日志存储、网络传输或状态机实现
- 高度可定制:需自行实现Storage Trait(日志存储)和RaftMessage传输层,灵活性极高
- 多Raft支持:TiKV基于此实现了Multi-Raft架构,支持海量Region分片
功能完整性:
- ✅ Leader选举、日志复制、成员变更(Joint Consensus)
- ✅ PreVote机制避免网络分区干扰
- ✅ Leader Lease读取优化
- ✅ Snapshot快照传输
- ✅ CheckQuorum检查机制
raft-rs的生产用户包括:TiKV(分布式事务KV数据库)
注意:该库已进入维护模式,新功能开发放缓,建议新项目考虑OpenRaft
2、OpenRaft 🚀 现代化异步架构
设计理念:完全异步事件驱动,不依赖定时tick,消息批处理优化高吞吐。
核心亮点:
- 事件驱动架构:基于Raft事件而非轮询,避免空转,大幅提升资源利用率
- 统一API:单一Raft类型,通过RaftLogStorage、RaftStateMachine、RaftNetwork三个Trait扩展存储和网络层
- 完善的成员变更:采用更通用的Joint Consensus,支持任意成员变更(单次可增删多节点),而非单步变更
- 内置可观测性:集成tracing日志和分布式追踪,支持编译时调整日志级别
- 手动控制:支持手动触发选举(trigger_elect)、快照(trigger_snapshot)、日志清理(purge_log),便于运维
功能特性:
- ✅ 线性一致性读取(ensure_linearizable)
- ✅ Learner(Non-voter)角色支持
- ✅ 动态心跳/选举开关控制
- ⛔️ 不支持单步成员变更(设计取舍,倾向更安全的Joint Consensus)
它的生产用户:Databend(云原生数仓)、CnosDB(时序数据库)、RobustMQ(云原生消息队列)。
Rockraft最终采用了Openraft做为底层的raft算法库,不仅因为它采用更现代化的异步架构、可定制性强,还因为我就是这个项目的贡献者 之一 :)。
Rockraft 的架构如图所示:
架构四大核心模块包括:
-
RPC / 通信层: 负责承接上层客户端的读写请求,以及集群内部节点之间的通讯(如 Leader 发送心跳、复制日志、Follower 参与选举等)。 RaftNetwork 是 openraft 定义的抽象接口,Rockraft 在此层实现了底层的网络传输逻辑。
-
共识引擎层 (openraft): 这是节点的大脑(Raft Core),完全基于 openraft 库。它负责维护 Raft 状态机的所有状态转移(Leader、Follower、Candidate、Learner),处理选举倒计时,计算日志的 Commit Index 等。它本身是“无状态”且与存储解耦的。
-
存储抽象适配层: 这是 Rockraft 项目的核心胶水层。openraft 规定了两个核心 Trait:RaftLogStorage 和 RaftStateMachine。 Rockraft 实现了这两个 Trait,告诉 Raft 引擎:“当你需要存日志时,调用我的方法;当你需要把数据应用到业务时,也调用我的方法”。
-
物理存储层 (RocksDB): 数据的最终落地点。为了追求极致性能,Rockraft 通常会在底层共用同一个 RocksDB 实例,但通过列族 (Column Family, CF) 技术进行物理/逻辑隔离:
- CF: Raft Logs:专门存放按递增 Index 排列的 Raft 日志,利用 RocksDB 高效的顺序写特性。
- CF: Hard State:存储 Raft 的硬状态(如当前的 Term、把票投给了谁),保证节点重启后不会发生脑裂。
- CF: State Machine:存储真正被 Commit 的业务数据。当日志达成多数派共识后,才会将日志中的业务操作在这里执行回放(如 Put/Delete Key)。
如何使用 #
使用Rockraft实现一个强一致服务也很简单,需要在服务的配置中指定一个地址和端口,用于集群内的节点采用Raft协议通信,以对写入行为达成一致。例如Rockraft自带HTTP服务例子,它是一个以HTTP协议接口接收用户的请求,服务内部以Rockraft在集群中同步数据。
| 方法 | 端点 | 功能 | 一致性要求 |
|---|---|---|---|
| GET | /get?key={key} |
查询指定 key 的值 | 线性一致性读 |
| POST | /set |
设置 key-value | 仅 Leader |
| POST | /delete |
删除指定 key | 仅 Leader |
| POST | /batch_write |
原子批量写入(多操作事务) | 仅 Leader |
| POST | /txn |
条件事务执行(CAS 支持) | 仅 Leader |
| POST | /getset |
原子获取旧值并设置新值 | 仅 Leader |
| GET | /prefix?prefix={prefix} |
前缀扫描查询 | 本地读取 |
| GET | /members |
获取集群成员列表 | 本地读取 |
| POST | /join |
添加节点到集群 | 仅 Leader(自动转发) |
| POST | /leave |
从集群移除节点 | 仅 Leader(自动转发) |
| GET | /health |
健康检查(返回 Leader 状态) | 本地读取 |
| GET | /metrics |
集群指标(任期、日志索引等) | 本地读取 |
以下是通过HTTP接口写入数据的流程:
┌─────┐ ┌─────────┐ ┌──────────┐ ┌─────────────┐ ┌──────────────┐
│Client│────►│ Axum │────►│ RaftNode │────►│ OpenRaft │────►│RocksLogStorage│
└─────┘ │ Handler │ │ write() │ │ │ │ Append Log │
└─────────┘ └──────────┘ └──────┬──────┘ └──────────────┘
│
│ Raft Consensus
│ (Replicate to Quorum)
▼
┌──────────────┐
│RocksStateMachine
│ Apply Log │
│ │
│┌────────────┴┐
││ RocksDB │
││ Put Key/Val│
│└─────────────┘
└──────────────┘
该服务的配置文件:
node_id = 1
http_addr = "127.0.0.1:8001"
[raft]
address = "127.0.0.1:7001"
advertise_host = "localhost"
join = ["localhost:7002", "localhost:7003"]
在这里:
- 127.0.0.1:8001: 是接收HTTP客户端请求的地址端口。
- 127.0.0.1:7001: 用于集群内节点的Raft协议通信的地址端口。
- join:是一个地址列表,其中的都是都是raft.address的地址,表示启动后马上向这些地址发起假如集群的请求,join为空表示系统以单机形式启动。
RaftNode 提供的公共 API 包括:
| 方法 | 说明 |
|---|---|
write(entry) |
写入一条日志(经 Raft 复制) |
batch_write(req) |
原子批量写入多个 KV |
read(req) |
读取 KV(从 Leader 状态机读取) |
txn(req) |
执行条件事务 |
getset(key, value) |
原子性地获取旧值并设置新值,这个接口为了实现Redis协议的getset命令而设计 |
scan_prefix(req) |
按前缀扫描 KV |
add_node(req) |
添加节点到集群 |
remove_node(req) |
从集群移除节点 |
get_members(req) |
获取集群成员列表 |
shutdown() |
优雅关闭节点 |
目前,Rockraft支持的条件事务,支持 8 种比较操作:
enum TxnOp {
Exists, // 键存在
NotExists, // 键不存在
Equal(Vec<u8>), // 值等于
NotEqual(Vec<u8>),// 值不等于
Greater(Vec<u8>), // 值大于(字典序)
Less(Vec<u8>), // 值小于
GreaterEqual(Vec<u8>),
LessEqual(Vec<u8>),
}
在example目录中有一个完整使用Rockraft实现了强一致HTTP协议的key-value服务的例子。
结语 #
Rockraft 仍然在持续进化,目前能想到的优化方向包括:
- 目前raft日志存储在底层rocksdb的一个特殊列族里,也许可以参考Raft Engine项目的实现来优化这部分的存储性能。
- Openraft原生目前还不支持多raft组,这部分也许可以在Rockraft中实现。
- 使用chaos-mesh等工具,增加chaos测试。
同时,也可以关注基于Rockraft实现的coredb项目,如我在文章一开始时而言:Redis协议已经是事实意义上的key-value存储通用协议,完全可以基于这个协议做出更多有别于纯内存缓存的事情来,coredb就是其中向CP方向的一个探索。