RobustMQ 存储层的三种模式
存储引擎是消息队列的核心。它决定了系统的性能上限、成本下限和能服务的场景范围。在设计 RobustMQ 的存储层时,我花了很长时间思考一个问题:如何用一套架构,既能支持 Kafka 的大吞吐场景,又能支持海量 topic 场景,还能支持 NATS 的极致性能场景?
研究了业界各种方案后,我的答案是:不是用一种存储引擎解决所有问题,而是提供三种存储引擎,根据场景特征自动选择。本文分享一下 RobustMQ 存储层的设计思路和实现考虑。
设计考量与场景选择
这个设计背后有几个核心考量。
RobustMQ 既然定位的是 ALL in One的场景。上层已经可以适配多种协议,那么架构的核心部分:存储层。他的场景覆盖的完整性必须是第一位的。在前面我们提到过,消息队列的场景很多样:有的需要极致性能、有的需要海量 topic、有的需要大吞吐、有的需要极低的成本。在我看来,单一存储引擎无法同时优化这些场景。所以在工程落地的角度,我准备在代码抽象、精简的基础上去提供三种引擎,每种针对特定场景优化,让 RobustMQ 可以去服务更广泛的场景。
我们需要保证架构的可扩展性要够高。也就是虽然初期可能只用一种存储,但架构设计要能支持未来的扩展。统一的抽象层、清晰的分层、通用的 ISR 机制,这些保证了以后加新引擎或新功能时,不需要大规模重构。
工程实现的可行性同样重要。三种存储引擎不是一开始就全做,而是分阶段验证。每个阶段都有明确的目标和可验证的成果。这样既保证了方向正确,又避免了过度设计。
成本和性能需要平衡。不是所有数据都用最高等级的保护(多副本 file segment),而是根据数据特征分层处理。临时数据用 memory(成本最低),一般数据用 rocksdb 单副本(性能和成本平衡),关键数据用 file segment 多副本(可靠性最高)。精细化的资源分配,让整体成本更低。
存储引擎的选择不是用户手动配置,而是根据协议和配置自动决定。MQTT 协议的消息,根据是否持久化选择Memory或RocksDB存储类型,因为MQTT的特点是Topic特别多,有离线消息的概念,但是每个Topic的数据量可能不高。如果是海量 Topic 场景,默认用 rocksdb(避免文件爆炸),如果是大吞吐,少量topic,用 file segment(性能更好)。
Kafka 协议的消息默认用 file segment,因为 Kafka 的语义就是持久化日志。但用户可以为特定 topic 配置使用 memory(比如临时的监控指标)或 rocksdb(如果这个 topic 数据量小但 topic 总数很多)。
这种自动选择的逻辑是基于协议特征的,不是复杂的 AI 分析。简单、可预测、可调试。而且用户始终可以通过配置覆盖默认选择,保持对系统的掌控。
三种存储引擎
RobustMQ 的存储层支持三种引擎:Memory、RocksDB 和 File Segment。每种引擎针对不同的场景特征。
Memory 存储引擎是纯内存的,不做任何持久化。消息写入后直接存在内存中,读取时从内存返回。这种设计让延迟降到微秒级,吞吐量可以达到千万级 QPS。代价是数据不持久化,进程重启或节点故障,数据直接丢失。
Memory 引擎适合的场景是数据可以丢失的实时通信。比如 MQTT QoS 0 的设备心跳、监控系统的实时指标、服务间的临时通知。这些数据的价值在于当前状态,历史数据没有意义,不需要持久化。使用 memory 存储,性能最优,成本最低。
RocksDB 存储引擎解决的是海量 topic 的问题。当系统有几万甚至上百万个 topic,但每个 topic 的数据量都很小时,传统的"一个 topic 一组文件"的方案会导致文件系统爆炸。RocksDB 把所有 topic 的消息混合存储,通过 key(topic_id:offset)来区分。底层是 LSM-Tree,无论多少 topic,物理文件就是那几个 SST 文件。
RocksDB 引擎的典型场景是 IoT 设备管理和多租户 SaaS。每个设备一个 topic,100 万个设备就是 100 万个 topic。每个租户独立的 topic,几十万租户就是几十万 topic。这些场景下,大部分 topic 的数据量很小,每秒可能就几条到几十条消息。用 file segment 会产生海量文件,用 RocksDB 文件数量可控,而且 LSM-Tree 对高并发小块写入很友好。
File Segment 存储引擎是为大吞吐场景设计的。消息按 offset 顺序写入固定大小的 segment 文件(比如 1GB),写满后创建新 segment。这种 append-only 的设计让顺序写入性能达到极致,单机可以支持几十万 QPS。而且 segment 文件可以直接用于数据湖集成,转成 Parquet 格式供分析使用。
File Segment 引擎适合 Kafka 的典型场景:日志收集、用户行为追踪、CDC、实时数据管道。这些场景单个 topic 的数据量就很大,每秒几万到几十万消息,但 topic 数量不多(几十到几百个)。顺序写入、顺序读取,file segment 的性能最优。
统一抽象与分层设计
三种存储引擎虽然实现不同,但对上层提供统一的接口。这个抽象层是整个设计的关键。
存储引擎的接口很简单:append 写入消息返回 offset,fetch 根据 offset 读取消息,current_offset 返回当前最新位置。协议层和副本同步层只需要调用这个接口,不需要知道底层是哪种引擎。
这种抽象带来了巨大的灵活性。协议层实现 MQTT 或 Kafka 时,代码逻辑是一样的,都是调用统一接口。底层换成哪种存储引擎,协议层不需要改动。副本同步机制也是通用的,通过这个接口从 Leader 拉取数据、写入 Follower,无论底层是什么存储。
更重要的是,这种分层让每一层可以独立优化。存储引擎专注于性能(如何更快地写、更快地读),副本同步层专注于可靠性(如何保证数据不丢、如何快速故障恢复),协议层专注于兼容性(如何完整实现 Kafka 语义)。每一层的职责清晰,互不干扰。
副本机制的通用性和存储切换
ISR 副本同步机制是通用的,对三种存储引擎都适用。这是架构设计的一个亮点。
用户配置 topic 的副本数,系统根据副本数自动决定是否启用 ISR。单副本的 topic 不需要 ISR,数据直接写本地存储,读取也直接从本地读。多副本的 topic 启用 ISR,Leader 负责写入,Follower 从 Leader 同步数据。
这个机制对三种存储都一样。双副本的 memory topic,Leader 写入内存,Follower 从 Leader 拉取数据写入自己的内存。三副本的 file segment topic,Leader 写入本地文件,Follower 拉取数据写入各自的文件。五副本的 rocksdb topic,逻辑也完全一样。
ISR 机制只需要实现一次,就能服务所有存储引擎。这大幅降低了实现复杂度,也简化了测试。测试 ISR 的正确性和测试存储引擎的功能可以分离进行,最后组合验证。
当 topic 的数据特征变化时,可能需要切换存储引擎。比如一个 topic 刚创建时数据量小用 rocksdb,随着业务增长,每秒几万条消息,就应该切换到 file segment。
我们的处理方式是不做数据迁移,而是在读取时做聚合。切换发生时,新数据写入新的存储引擎,老数据继续留在原来的存储。读取时,系统从两个存储都读取数据,按 offset 排序后返回给客户端。等老数据过了保留期(比如 7 天),直接删除。
这个方案避免了数据迁移的所有复杂度:不需要后台迁移任务,不需要处理迁移中的一致性问题,不需要担心迁移失败如何回滚。利用消息队列数据会过期的天然特性,把迁移转化为等待过期。简单、可靠、优雅。
当前现进展和总结
三种存储引擎的基本框架已经完成开发。Memory、RocksDB 和 File Segment 都实现了统一的存储接口,协议层可以透明地调用不同引擎。通过配置或协议特征,系统能够自动选择合适的存储引擎,存储切换的聚合读取机制也已验证可行。
ISR 副本同步机制是下一步的重点。虽然架构设计已经考虑了通用的 ISR 机制(对三种引擎都适用),但具体实现还未启动。这包括 Leader 选举、Follower 同步、故障恢复等分布式复制的核心逻辑。把副本同步做稳定后,才能真正提供生产级别的高可用保证。
这种分阶段的实现路径既验证了架构设计的可行性,又保持了工程上的可控。存储引擎本身的逻辑已经打通,接下来聚焦在分布式一致性上,一步一个脚印把基础打牢。
RobustMQ 的存储层采用三种引擎(memory、rocksdb、file segment)+ 统一抽象层 + 通用 ISR 的架构。这个设计可以覆盖从极致性能到海量 topic 到大吞吐的各种场景,同时保持架构的简洁性和扩展性。
存储引擎根据协议特征自动选择,用户也可以手动配置。副本数灵活可配,ISR 机制通用于所有引擎。存储切换通过聚合读取处理,避免了数据迁移的复杂度。
这不是理论创新,而是工程组合。把已有的存储技术(顺序文件、LSM-Tree、内存存储)组合在统一的架构下,用清晰的抽象层管理,用合理的策略选择。这就是基础软件在成熟阶段的创新方式:不追求全新的理论,而是把工程实现做到极致。
实现路径是分阶段的,三种引擎的基本框架已完成,副本同步机制即将启动。架构设计考虑得很远,但实现保持务实。这样既保证了长期的扩展性,又避免了过度设计的风险。
基础打好了,才能走更远。这是 RobustMQ 存储层设计的核心理念。
