Skip to content

RobustMQ 存储层:File Segment 的设计与实现

存储引擎是消息队列的核心,决定了系统的性能上限、成本下限和能服务的场景范围。RobustMQ 的存储层规划了三种引擎(Memory、RocksDB、File Segment)来覆盖不同场景,但 File Segment 是基础,也是当前完整实现的核心。

File Segment 针对 Kafka 的典型场景设计:日志收集、用户行为追踪、CDC、实时数据管道。这些场景的特点是单个 topic 数据量大(每秒几万到几十万消息),但 topic 数量不多(几十到几百个)。本文分享 File Segment 的设计思路和实现细节。

核心挑战:高并发下的顺序保证

消息队列的存储层有个基本要求:同一个 Partition 内的消息 offset 必须严格连续。高并发写入时,如何既保证顺序,又实现高性能?

传统方案用锁控制 offset 分配。每个 Partition 一把锁,线程获取锁、分配 offset、写入数据、释放锁。问题在于每个线程通常只写一条消息就释放锁,导致频繁的小 I/O 操作。而且锁的持有时间包含了磁盘操作,高并发时锁竞争严重。

I/O Pool:批量处理的关键

我们的解决方案是 I/O Pool 架构。使用固定数量的 I/O Worker(比如 16 个)管理所有 Partition。通过 partition_id % worker_count 固定映射,保证同一 Partition 的请求总是路由到同一个 Worker。

Worker 的批量处理是核心。Worker 先阻塞等待第一个请求,然后非阻塞地收集后续请求(可能几百上千个)。这些请求按 partition_id 分组后批量处理:同一 Partition 的多个请求顺序分配 offset,所有记录加入缓冲区,缓冲区达到阈值(比如 100 条)时,一次性序列化并写入磁盘。

一次 fsync 持久化几百上千条消息,而不是每条消息一次 fsync。相比锁方案的"每条消息一次小写入",对磁盘性能的影响是数量级的差异。

内存占用只跟 Worker 数量有关。16 个 Worker 意味着 16MB 的 channel 内存,即使管理 10000 个 Partition,总内存也只需约 100MB。Worker 数量通常设置为 CPU 核心数,对 SSD 存储已能充分利用 I/O 能力。

零拷贝:降低 CPU 开销

数据从网络到磁盘,我们使用 Rust 的 Bytes 类型实现零拷贝。Bytes 内部用 Arc 引用计数,clone() 只增加引用计数,不拷贝数据。

数据流是:网络接收到 BytesMut → freeze() 转为 Bytes(零拷贝)→ 发送到 channel 时 clone()(只增加引用计数)→ Worker 接收后批量处理 → 写入时直接访问原始数据。从头到尾,数据本身只有一份,只是在不同地方持有引用。这节省了 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 一致性机制

RobustMQ 采用存算分离架构,Storage Node 管理 Segment,Broker 无状态。我们选择了 ISR 而不是 Quorum 作为一致性协议。

ISR 是 Kafka 的核心设计。每个 Segment 有一个 Leader,维护 ISR 列表(同步副本集合)。写入成功意味着数据复制到 ISR 中的所有副本。Follower 通过 Pull 模式从 Leader 拉取数据,大 QPS 小消息场景下性能更高,因为 Follower 可以批量拉取,网络请求从百万级降到百级。

核心优势是数据完整性。Storage Leader 有全量数据,ISR Follower 也有全量数据。读取时从 Leader 或任意 ISR Follower 读,100% 能读到数据,不会有空洞。实现简单可靠,无需复杂的后台修复机制。

Quorum 方式虽然无单点,但有个关键问题:任何副本都可能不完整。Broker 并发写 3 个 Storage Node,等待 2 个确认就返回成功,可能某个副本缺数据。虽然每条消息都满足 Quorum,但没有一个副本是完整的。读取时可能遇到空洞,需要重试其他副本。顺序消费场景更严重,消费者可能因为遇到空洞误以为消费完了,实际漏掉了后续数据。Quorum 依赖复杂的后台修复逻辑,工程复杂度高。

考虑工程实现的复杂度和风险,我们选择了 ISR。虽然 Storage 层有 Leader,但通过合理的 Segment 分布,Leader 分散在不同 Storage Node,不会形成单点瓶颈。Broker 层仍是无状态的,存算分离的优势保留。

我们提供了灵活的 acks 配置:acks=all 等待所有 ISR 副本(强一致),acks=quorum 等待多数派(平衡),acks=1 只等 Leader(最快但可能丢数据)。

Active vs Sealed Segment

我们在 ISR 基础上做了重要优化:区分 Active Segment 和 Sealed Segment。

Active Segment 是正在写入的活跃段,有 Leader 和 ISR 机制,Follower 持续 Pull 复制。Segment 写满(比如 1GB)或达到时间阈值时,封存为 Sealed Segment。Sealed Segment 停止写入,Leader 等待所有 ISR Follower 完全追上并验证一致性。确认所有副本完整一致后,标记为 Sealed,释放 Leader 角色。之后这个 Segment 没有 Leader,所有副本平等,可从任意副本读取。

优势很明显。Leader 数量等于 Shard 数量,不是 Segment 总数。假设 1000 个 Shard,每个有 100 个 Segment(99 封存,1 活跃),我们只需 1000 个 Leader,而不是 10 万个。Leader 管理开销(选举、ISR 维护、心跳)降低 100 倍。而且 99% 的历史数据读取压力分散到所有 Storage Node,充分利用存储资源。

无迁移扩容

分段设计自然解决了 Kafka 的扩容痛点。Kafka 扩容需要重新分配 Partition,在 Broker 间拷贝大量数据,耗时长、占带宽,可能影响线上业务。

RobustMQ 的扩容完全不同。新增 Storage Node 时,不需要迁移历史数据,只需等待当前 Active Segment 写满。新 Segment 会自动分配到新节点,开始承担流量。高流量场景下 1GB 的 Segment 可能几分钟到几十分钟就写满,新节点很快分担负载。整个过程对业务透明,无性能损失。

历史数据保留在原节点,作为 Sealed Segment 提供读取。如果需要释放空间,可以通过分层存储将 Sealed Segment 迁移到 S3,后台异步进行,不影响业务。缩容时也类似,停止在要下线节点创建新 Segment,等 Active Segment 写满封存,迁移或保留在 S3,节点即可下线。

分层存储

Sealed Segment 的不可变特性让分层存储实现很简单。

热数据的 Active Segment 在本地 SSD,3 副本 + ISR,提供最低延迟和最高吞吐。温数据的 Recent Sealed Segment 在本地存储(SSD 或 HDD),任意副本可读,延迟稍高但仍快。冷数据的 Old Sealed Segment 迁移到 S3,延迟增加到 50ms 但成本极低且容量无限。

迁移过程很简单。Sealed Segment 不可变且所有副本完整,从任意副本读取完整文件,上传 S3,更新元数据即可。不需要复杂协调,失败了重试。本地只保留近期热数据(比如 7 天),历史数据全在 S3。对于每天写入 1TB、保留 1 年的场景,存储成本可从 21 万美元降到 1 万美元,节省 95%。

设计理念

在设计 File Segment 时,我们坚持:简单可靠比复杂完美更有价值。

选择 ISR 而不是 Quorum,看重数据完整性的简单保证。选择同步构建索引,批量写入下开销小但换来立即可用。选择 Active 和 Sealed 分离,简单优化就能大幅降低管理成本。选择分段设计,自然解决数据迁移难题。

我们选择经过验证的成熟方案,在此基础上做针对性优化,而不是追求理论完美但工程风险高的方案。File Segment 充分借鉴了 Kafka、Pulsar、RocketMQ 的经验和教训,力求在高性能、高可靠、低成本之间找到最佳平衡。

总结

File Segment 通过 I/O Pool、零拷贝、批量写入、稀疏索引等技术,实现了高性能和高可靠的平衡。Active vs Sealed Segment 大幅降低了 Leader 管理开销,分段机制实现了无迁移扩容,分层存储提供了成本优化。

这不是理论创新,而是工程组合。把已有技术(顺序文件、批量处理、ISR)组合在清晰的架构下,用合理的策略选择。File Segment 作为 RobustMQ 存储层的基础,为未来的 RocksDB 和 Memory 引擎扩展打好了地基。

基础打好了,才能走更远。