RobustMQ:关于消息队列存储层的一些想法
如果你关注消息队列领域,会发现最近几年涌现了很多新的开源项目。仔细看这些项目,会发现一个有趣的现象:它们的核心叙事几乎都是"Kafka替代"。
为什么都盯着Kafka?因为Kafka足够标准化,成为了流处理的事实标准。同时Kafka的架构也有一些众所周知的问题:弹性扩展困难、存储成本高、运维复杂度大。从业务角度看,确实有解决这些痛点的真实需求。
但如果深入分析这些项目的技术差异,会发现最核心的区别其实在存储层。存储层的设计决定了延迟、可靠性、吞吐、成本等各方面的表现。可以说,存储层的设计决定了一个消息队列项目的成败。
RobustMQ的定位是All In One的统一消息平台,设计之初就希望能匹配所有消息中间件的使用场景。这意味着我们的存储引擎必须能够承载各种不同的场景需求。这段时间我一直在思考:存储引擎应该怎么设计?
为什么要存算分离
几乎所有新一代消息队列都在谈存算分离,核心原因是弹性和成本。
先说弹性。消息队列作为存储组件,会遇到一个尴尬的问题:如果存算一体,为了保证消息数据的顺序性,扩容时往往需要迁移数据。数据迁移耗时很长,导致系统无法快速弹性伸缩。而在云原生时代,弹性是刚需。
再说成本。成本问题分两个方向:
第一是资源不匹配的问题。运维中经常遇到两种情况:要么存储空间不够但CPU还闲着,要么CPU不足但存储空间还有一大堆。前者扩容浪费CPU,后者扩容浪费存储。存储和计算资源必须能够独立扩展。
第二是存储介质的成本差异。云原生时代,本地硬盘或云盘成本很高,而对象存储是目前最便宜的存储选择。将冷数据迁移到对象存储,可以大幅降低成本。当然这需要牺牲一些延迟,但对很多场景来说,这个trade-off是值得的。
存算分离不是为了架构的先进性,而是实际业务需求倒逼出来的必然选择。
消息队列的五大场景
RobustMQ希望支持消息队列领域的所有主要场景。基于我们的调研和理解,主要包括五类:
场景一:低延迟高吞吐场景。这是Kafka的主场,流式处理和大数据的核心需求。要求数据持久化、多副本保障、高吞吐量、低延迟。这也是行业的标准场景,必须支持好。
场景二:高吞吐低成本场景。当业务规模到一定程度,存储成本成为大头。这时候引入对象存储,用成本换取一些延迟的增加。适合数据量大、成本敏感的场景。
场景三:百万Topic/Partition场景。有些业务需要大量分区做数据隔离和顺序保证。比如多租户场景,每个租户一个topic;IoT场景,每个设备一个partition。这需要存储能够支撑海量分区而不崩溃。
场景四:极低延迟高QPS场景。金融交易等场景要求端到端延迟在1毫秒以内,QPS极高。这种场景通常允许数据丢失,追求的是极致的速度。内存存储、零拷贝、完全规避磁盘IO。
场景五:边缘消息队列场景。边缘计算环境资源受限,要求无外部依赖、轻量级、高内聚。只能用内存或本地文件,存储模型要足够简单。
这五个场景,基本涵盖了消息队列的主要应用方向。单一的存储引擎很难同时满足所有场景的需求。
两种存储模型的权衡
消息队列的存储都是Append Only(只追加),这是为了保证数据顺序性。但具体实现上,主要有两种模型。
模型一:一个Partition一个文件。这是Kafka采用的方式。优点很明显:顺序写、顺序读,性能极高。配合零拷贝技术,吞吐量可以拉满。缺点也很明显:小文件太多会影响硬盘性能,元数据压力大。这个模型适合低延迟高吞吐场景,但别搞太多Partition。
模型二:多个Partition共享文件。这是RocketMQ的选择。优点是能支持百万级分区,小文件少,元数据压力小。代价是无法顺序读,读性能会下降。这个模型适合需要海量Topic/Partition做数据隔离的场景。
两种模型的核心就是在读写性能和分区数量支持之间做取舍。功能越简单,性能越高;功能越复杂,性能越低。这是工程上的必然。
插件化存储的设计
单一存储模型无法满足所有需求,那怎么办?
RobustMQ的答案是:从零构建插件化存储方案,让用户根据场景灵活选择。具体包括三个维度:
第一,支持两种存储模型。Partition独立文件模型和Partition共享文件模型都支持,用户根据场景选择。需要极致性能?用独立文件模型。需要海量分区?用共享文件模型。
第二,支持多种存储引擎。Memory(内存)、RocksDB(本地KV)、Journal Engine(日志引擎)、MySQL(关系数据库)、对象存储(S3/OSS)等。每种引擎有不同的延迟、成本、可靠性特征,覆盖不同场景。
第三,副本策略灵活配置。不是所有场景都需要多副本。允许用户根据业务特性选择单副本或多副本,在性能和可靠性之间灵活权衡。
通过这种架构,RobustMQ能够适配从边缘到云端、从低延迟到低成本的各种场景需求。用户在创建Topic时,可以指定使用哪种存储引擎、哪种存储模型、多少个副本,系统自动适配。
关于副本的重新思考
这里有个常见的质疑:单副本的内存或文件存储,节点挂了数据不就丢了吗?还有实际用处吗?
传统观点认为消息队列必须要副本,否则数据丢失无法接受。但仔细想想,这个结论并不绝对,关键要看业务特性。
很多场景下,消息队列主要用于实时数据分发,业务允许数据丢失。比如股票最新价格,后面的数据会覆盖前面的,历史价格丢了对业务没影响。传感器最新温度读数,只关心最新值,历史数据丢了也无所谓。实时监控数据,本来就是追求极低延迟和高QPS,允许少量丢失。
Nats的核心竞争力就是内存分发、极低延迟、极高QPS,根本不做副本。Apache IGGY主打极低延迟和高吞吐,用的是文件存储加零拷贝加MMAP,也不强制副本。
另一个有意思的场景是MQTT。默认情况下MQTT不持久化数据,接收到消息时如果没有订阅者,会直接丢弃。MQTT通过Connector将数据导入下游存储引擎,Connector本身也是一种订阅。
还有云原生场景。底层云盘本身就提供了多副本存储,已经保证数据不会丢。这时候应用层如果是单副本,影响只是该节点的分区暂时不可用,但数据还在云盘上。在某些业务场景下,这完全可以接受。
所以是否需要副本,要看具体场景。单一的副本策略很难满足所有需求。RobustMQ的插件化存储,让用户可以根据业务特性自己选择。
语言的性能差异
我们做过Redpanda和Kafka的压测对比。在相同硬件环境下,Redpanda的延迟表现明显优于Kafka。这说明什么?在架构和优化都做到位的前提下,编程语言本身的性能差异会产生显著影响。
Rust和C++这些编译型语言,在性能敏感的场景中相比Java有天然优势。没有GC停顿,内存布局可控,可以做到更极致的优化。这不是语言偏见,而是技术特性决定的。
这也是RobustMQ选择Rust的重要原因之一。我们要做极低延迟、高性能的消息队列,语言的选择从一开始就很关键。
下一步的规划
本文分享的是我们在存储层的一些思考,但坦白说,具体的实现细节还没有完全想清楚。这是一个持续探索和优化的过程。
我们的思路是:先适配MQTT场景实现存储,在实践中验证设计的合理性,然后一步步完善整体的存储模型。MQTT场景会是我们的第一个深度验证,Kafka场景会是第二个,每个场景都会带来新的挑战和启发。
长期来看,我们还有一个设想:能否在消息队列的基础上,适配一些简单的检索聚合场景,提供轻量级的日志分析能力?业界已经有成熟的方案如ELK,但能否有更轻量、更简单的替代方案?这只是初步想法,具体怎么做还需要深入思考。
存储引擎的设计是RobustMQ最核心的技术挑战之一。我们会持续探索,持续优化,在实践中验证理念。
从内核开始,一步一个脚印。这是我们的选择,也是我们的坚持。
RobustMQ正在构建插件化的存储引擎,支持从边缘到云端的多种场景。欢迎关注我们的GitHub,参与技术讨论。
