上一篇讲了经典队列在内存和磁盘间做取舍的策略,但没往下戳一层——这些消息在 RabbitMQ 内部到底存在哪?答案和 RabbitMQ 的实现语言有关:它是 Erlang 写的,而 Erlang 有一个叫 ETS 的核心数据结构,几乎所有消息存储都绕不开它。
理解 ETS 和持久化的关系,能解释几个实际现象:为什么 broker 重启慢(要从磁盘重建 ETS)、为什么大量非持久化消息也会吃内存(它们全在 ETS)、为什么内存碎片会影响队列性能(ETS 表的内存管理)。这一篇把这些底层机制讲清楚。
ETS:Erlang 的内存表
ETS 全称 Erlang Term Storage,是 Erlang/OTP 内置的内存级键值存储。它不是磁盘数据库,而是直接操作 Erlang 进程内存的高性能表。特点:
- 存在内存里,读写极快(微秒级),但进程/broker 重启就没了。
- 支持多种表类型(set、ordered_set、bag),可按 key 高效查找。
- 是 Erlang 里做「大量数据共享」的标准手段——进程间共享大表,用 ETS 而不是消息传递。
RabbitMQ 大量使用 ETS。队列的消息索引、连接状态、信道信息、路由表……很多运行时状态都在 ETS 里。对我们理解存储最关键的一点是:消息(无论是否持久化)在运行时都由 ETS 承载。
持久化消息的两份存储
一条持久化消息(deliveryMode=2)进队列后,在 RabbitMQ 内部其实有「两份」:
- 磁盘上的持久化存储:消息写进 broker 的消息存储(message store,基于一种 append-only 的文件结构),这是「权威副本」,broker 崩溃后靠它恢复。
- ETS 里的缓存/索引:消息的元数据(和可能的消息体)放在 ETS,用于消费时快速定位和读取。
持久化消息的写入路径是:消息进来 → 写消息存储(磁盘)→ 在 ETS 建索引/缓存。消费时优先从 ETS 读,ETS 里没有的消息体再去磁盘读。
这个设计的意义:持久化 ≠ 只在磁盘。持久化消息也有内存占用(ETS 索引/缓存)。所以「持久化消息不占内存」是误解——它只是「崩溃不丢」,运行时照样吃 ETS 内存。
非持久化消息:全在 ETS
非持久化消息(deliveryMode=1)的命运完全不同:
- 不写消息存储(磁盘上没有)。
- 完全放 ETS(消息体也在 ETS)。
这意味着非持久化消息的消费极快(纯内存读),但有两个代价:一是 broker 重启全丢,二是它们占的是实打实的 ETS 内存,不能被页出到磁盘(因为根本没写盘)。
所以一个反直觉的现象:大量非持久化消息积压,比持久化消息积压更危险。持久化消息积压时,至少消息体可以页出到磁盘,ETS 里只留索引;非持久化消息积压,全部堆在 ETS,内存压力直接拉满,且没有磁盘这个缓冲。
这也是为什么生产环境的核心队列几乎都用持久化消息——不仅是为了不丢,也是为了在积压时有磁盘做缓冲。
broker 重启时的恢复
理解了 ETS 和磁盘存储的关系,就能看懂 broker 重启为什么慢,以及它在恢复什么:
- broker 启动,ETS 是空的(内存清空了)。
- broker 扫描磁盘上的消息存储,把所有持久化消息的索引重建到 ETS。
- 重建完成后,队列才能正常服务。
这个「重建 ETS」的过程,就是 broker 重启慢的主要原因。队列里积压的持久化消息越多,重建索引越久——积压百万条消息的队列,重启可能要几分钟甚至更久。这期间队列是不可用的。
这个现象的推论:避免在大量积压时重启 broker。如果非重启不可,先想办法让积压消费掉,或者重启窗口选在低峰。这是运维层面要注意的。
内存碎片与 ETS
ETS 表的内存由 Erlang VM 管理,长期运行后会出现内存碎片——频繁的插入、删除(消息进出队列)会让 ETS 的内存出现空洞,导致「逻辑上没多少消息,但内存占用下不来」。
内存碎片的表现:rabbitmqctl status 看到的内存使用很高,但实际队列里消息不多。这是典型的碎片问题。
RabbitMQ 提供 vm_memory_high_watermark 和 garbage collection 来治理,但碎片本质上是 Erlang 内存管理的特性,难以完全消除。严重的碎片只能靠重启 broker(让 Erlang VM 回收全部内存)来解决。这也是为什么 RabbitMQ broker 建议定期滚动重启(在集群模式下逐个重启,不影响服务)。
持久化的性能影响
持久化带来可靠性,代价是性能。几个量化的认知:
- 持久化消息的写入吞吐,通常是非持久化的 1/3 到 1/2(要写磁盘)。
- fsync 策略影响延迟:RabbitMQ 可以配置多久 fsync 一次(把磁盘缓冲刷到真正的存储)。频繁 fsync 延迟低但吞吐降,稀疏 fsync 吞吐高但崩溃时可能丢最近一小段。这是可靠性和性能的调节点。
- 磁盘 IO 是瓶颈:持久化队列的吞吐上限,往往卡在磁盘写速度。用 SSD 比机械盘快一个数量级,这是 RabbitMQ 部署的基本要求。
收束:理解存储层才能调优
这一篇没有可操作的配置项,但它建立了一个底层认知:RabbitMQ 的消息存储,本质是 ETS(内存)+ 消息存储(磁盘)的组合,持久化消息两份都有,非持久化消息只在 ETS。这个认知能解释后面所有性能现象——积压时的内存压力、重启的慢、碎片的治理、持久化的吞吐代价。
下一篇讲经典队列在「高可用」上的尝试和失败——镜像队列为什么被官方废弃,这是 RabbitMQ 存储演进里最重要的一课。
关于十三Tech
我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。我相信 AI 是程序员的最佳搭档。想跟完这套「图解 RabbitMQ」,欢迎关注公众号 「十三Tech」。

