Storage Engine 架构
存储引擎是消息队列的核心,决定了系统的性能上限、成本下限和能服务的场景范围。RobustMQ 采用插件化存储设计,提供 Memory、RocksDB、File Segment 三种引擎,通过统一的 Storage Adapter 对上层 Broker 屏蔽差异,使同一个集群可以按 Topic 粒度灵活选择存储策略。
为什么要插件化存储
单一存储引擎无法覆盖消息队列的所有场景。消息队列的典型使用场景大致可以分为五类:
| 场景 | 代表产品 | 核心需求 |
|---|---|---|
| 低延迟高吞吐 | Kafka | 持久化、多副本、高吞吐、毫秒级延迟 |
| 高吞吐低成本 | Pulsar(分层存储) | 大数据量、用对象存储降成本 |
| 百万 Topic/Partition | RocketMQ | 海量分区、数据隔离、不追求极致吞吐 |
| 极低延迟高 QPS | NATS | 允许丢失、微秒级延迟、纯内存 |
| 边缘轻量部署 | 无标准方案 | 零外部依赖、轻量、资源受限 |
这五个场景的存储需求差异极大,单一引擎必然在某些维度上妥协。RobustMQ 的答案是插件化:用一套架构,通过配置选择,覆盖从边缘到云端、从内存到对象存储的全部场景。
插件化不是为了"支持很多存储",而是在架构上留足扩展空间——当业务遇到未预料的场景,可以自己实现 Storage Adapter,而不是 fork 代码。
三种存储引擎
Memory(内存存储)
纯内存 + DashMap 实现,支持 offset、tag、key、timestamp 四种索引,进程重启后数据丢失。
目标场景:实时行情推送、传感器采样、游戏临时状态——允许少量数据丢失,但对延迟极其敏感。
副本策略:
- 单副本:延迟 100µs,节点重启数据全丢,适合极致性能场景
- 双副本(acks=1):写入时 Leader 立即返回,后台异步复制到第二副本,延迟 100-200µs,可靠性明显提升,两个副本同时故障才丢数据
内存容量管理:通过大小(如 1GB)或时间(如 5 分钟)限制单个 Segment,到达阈值自动封存并创建新 Segment。Sealed 的内存 Segment 可配置异步持久化到磁盘或 S3,在不影响写入延迟的情况下保留数据。
RocksDB(本地 KV 持久化)
基于 RocksDB 的本地持久化存储,使用专用 Column Family 存储消息,通过写锁避免并发冲突。
目标场景:单机部署、本地测试、边缘场景。无需集群协调,零外部依赖,持久化可靠。
局限性:数据不在节点间同步,集群模式下不同 Broker 节点无法共享数据,不适合生产集群。
File Segment(分段文件日志)
RobustMQ 核心的生产级存储引擎,专为集群化、高吞吐、低延迟设计。Kafka 的典型场景(日志收集、CDC、实时数据管道)是其主战场。
File Segment 核心设计
File Segment 是 RobustMQ 存储层最复杂的部分,下面逐一介绍其核心设计思路。
高并发下的顺序保证:I/O Pool
消息队列的基本要求是:同一个 Partition 内消息的 Offset 严格连续。高并发写入时,传统做法是每个 Partition 一把锁,但锁的持有时间包含了磁盘 I/O,高并发下竞争严重,且每个线程只写一条消息就释放,导致大量小 I/O。
RobustMQ 的方案是 I/O Pool:用固定数量的 I/O Worker(如 16 个)管理所有 Partition。通过 partition_id % worker_count 固定映射,同一 Partition 的请求总是路由到同一个 Worker。
Worker 的核心是批量处理:先阻塞等待第一个请求,然后非阻塞地收集后续请求,可能一次收集几百上千个。这些请求按 partition 分组批量处理,一次 fsync 持久化几百上千条消息。
效果:从"每条消息一次小写入"变成"批量消息一次大写入";用队列排队代替锁等待;内存占用只跟 Worker 数量相关,与 Partition 数量无关。
同时,利用 Rust 的 Bytes 类型(Arc 引用计数,clone 不拷贝数据)实现零拷贝——从网络接收到磁盘写入,数据本身只有一份,不同地方持有引用,降低 CPU 和内存带宽压力。
索引:稀疏索引 + 同步构建
RobustMQ 用 RocksDB 存储四种索引:offset 索引、时间索引、key 索引、tag 索引。
同步构建而非异步:Worker 批量处理 1000 条记录时,同时构建这 1000 条的所有索引,通过 RocksDB WriteBatch 一次性写入。数据文件一次 I/O,索引一次 I/O,总共两次。批量写入下索引构建约增加 1ms,换来的是:索引立即可用、数据与索引强一致、崩溃恢复简单。
稀疏 offset 索引:不是每条记录都建索引,而是每 1000 条一个索引点,记录该 Offset 对应的文件位置。查询时通过 RocksDB 定位最近索引点,再从该位置顺序扫描最多 1000 条。1000 万条记录只需约 240KB 索引空间,查询延迟约 2ms。
一致性协议:ISR 而非 Quorum
存算分离下,Segment 分布在多个 Storage Node,副本间一致性是核心问题。RobustMQ 选择了 ISR(In-Sync Replicas)而非 Quorum。
Quorum 的问题:Broker 并发写 3 个 Storage Node,等待 2 个确认即返回成功。表面上逻辑正确,但任何副本都可能不完整——写 1000 条,A 副本缺 100 条,B 缺 120 条,C 缺 80 条。顺序消费时消费者可能因遇到"空洞"误以为消费完了,实际漏掉了后续数据。修复这个问题需要复杂的后台对账逻辑,工程风险高,初期很难做好。
ISR 的选择:每个 Active Segment 有一个 Leader,维护 ISR 列表(同步副本集合)。写入成功意味着数据已复制到 ISR 所有副本,Leader 和 ISR Follower 都有完整数据,读取 100% 成功,无需后台修复。Follower 通过 Pull 模式主动批量拉取,高 QPS 场景下网络请求从百万级降到百级。
通过合理的 Segment 分布,Leader 分散在不同 Storage Node,不会形成单点瓶颈,Broker 层仍是无状态的。同时提供灵活的 acks 配置:
| acks 值 | 语义 | 性能 |
|---|---|---|
all | 等待所有 ISR 副本确认 | 强一致,最慢 |
quorum | 等待多数派确认 | 平衡 |
1 | 只等 Leader 确认 | 最快,可能丢数据 |
Active Segment vs Sealed Segment
采用 ISR 后,如果每个 Segment 都有 Leader,1000 个 Shard × 每个 100 个 Segment = 需要管理 10 万个 Leader,选举、ISR 维护、心跳开销极大。
RobustMQ 的解决方案是区分两种状态:
Active Segment:正在写入的活跃段,有 Leader 和 ISR 机制,Follower 持续 Pull 复制。
Sealed Segment:Segment 写满(如 1GB)或达到时间阈值时,Leader 等待所有 ISR Follower 完全追上并验证一致性,确认所有副本完整后标记为 Sealed,释放 Leader 角色。Sealed Segment 没有 Leader,所有副本平等,可从任意副本读取。
效果:Leader 数量 = Shard 数量(而非 Segment 总数)。1000 个 Shard 只需 1000 个 Leader,管理开销降低 100 倍。99% 的历史数据读取压力分散到所有 Storage Node,充分利用存储资源。
无迁移扩容
Kafka 扩容需要在 Broker 间拷贝 Partition 数据,耗时几小时甚至几天,影响线上业务。
分段设计天然解决了这个问题:新增 Storage Node 时,不迁移任何历史数据,只需等待当前 Active Segment 写满,新 Segment 自动分配到新节点,立即承担流量。高流量场景下 1GB 的 Segment 几分钟到几十分钟就写满,新节点迅速接入。整个过程对业务透明,无性能损失。
历史数据保留在原节点作为 Sealed Segment 提供读取。需要释放空间时,通过分层存储后台异步迁移到 S3,不影响业务。
分层存储
Sealed Segment 的不可变特性让分层存储实现极为简单:文件不变、副本完整,直接从任意副本上传 S3,更新元数据即可,失败重试。
| 数据层级 | 存储位置 | 延迟 | 说明 |
|---|---|---|---|
| 热数据(Active Segment) | 本地 SSD | 毫秒级 | 3 副本 + ISR,最低延迟 |
| 温数据(近期 Sealed) | 本地 SSD/HDD | 毫秒级 | 任意副本可读 |
| 冷数据(历史 Sealed) | S3/MinIO/HDFS | ~50ms | 成本极低,容量无限 |
以每天写入 1TB、保留 1 年为例:本地存储成本约 21 万美元,引入分层存储后降至约 1 万美元,节省 95%。
冷数据迁移到 S3 时还可以转换为 Parquet 格式,Spark、Hive 等分析工具可以直接查询,让 RobustMQ 天然成为数据湖的源头,无需 ETL。
两种存储文件模型
File Segment 支持两种底层文件组织方式:
| 模型 | 描述 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Partition 独立文件 | 每个 Partition 独占文件,类似 Kafka | 顺序读写,极致吞吐 | 大量 Partition 时小文件多,元数据压力大 | 低延迟高吞吐,Topic 数量不多 |
| Partition 共享文件 | 多个 Partition 共享文件,类似 RocketMQ | 支持百万级 Partition | 随机读性能下降 | 海量 Topic/Partition,数据隔离 |
设计哲学:务实主义
回顾 Storage Engine 的设计,每个技术决策都基于实际问题:
- 选 ISR 而非 Quorum:看重数据完整性的简单保证,避免复杂的后台修复逻辑
- 选同步构建索引:批量写入下开销小,但换来立即可用和强一致
- Active/Sealed 分离:简单优化即可大幅降低 Leader 管理成本
- 分段设计:自然解决数据迁移难题,无需额外机制
我们相信,在存储层这样的基础组件上,应该选择经过验证的成熟方案,并在此基础上做针对性优化,而不是追求理论完美但工程风险高的方案。把已有技术(顺序文件、批量处理、ISR、稀疏索引)组合在清晰的架构下,用合理的策略选择,才是正确的路径。
