持久化配置
概述
本指南涵盖了一些会影响节点吞吐量、延迟和 I/O 特性的可配置参数。在得出任何结论之前,建议完整阅读本指南并熟悉 使用 PerfTest 进行基准测试。
相关指南包括
RabbitMQ 持久化概述
现代 RabbitMQ 版本提供了多种队列类型以及流(Streams)
- 仲裁队列 (Quorum queues):已复制、持久化,侧重于数据安全
- 流 (Streams):一种支持不同于传统队列操作的、已复制且持久化的数据结构
- 经典队列 (Classic queues):原始队列类型,自 RabbitMQ 4.0 起仅支持单副本
这些队列类型具有不同的存储实现,可调整的配置设置也有所不同。
流
流使用基于日志的存储机制,在内存中保留的数据极少(主要是尚未写入磁盘的操作数据)。尽管如此,当客户端使用 RabbitMQ 流协议时,它们能提供极佳的吞吐量。
由于流对磁盘 I/O 的要求非常高,其吞吐量会随着消息大小的增加而降低。它们从现代 SSD 和 NVMe 存储中获益良多。
流不提供与存储相关的可调参数。
仲裁队列
仲裁队列使用由 RabbitMQ 的 Raft 实现提供的基于日志的存储机制。它们在内存中保留的数据极少(主要是尚未写入磁盘的操作数据)。
由于仲裁队列在执行任何操作前都会将所有数据持久化到磁盘,因此建议使用尽可能快的磁盘。
由于仲裁队列对磁盘 I/O 的高度依赖,其吞吐量会随着消息大小的增加而降低。
影响仲裁队列资源使用的主要存储相关设置是预写日志 (WAL) 分段大小限制,即内存表移动到磁盘时的限制。换句话说,每个仲裁队列在稳定负载下能够在内存中保留不超过该数值的消息数据。
该限制可以进行控制
# Flush current WAL file to a segment file on disk once it reaches 32 MiB in size
raft.wal_max_size_bytes = 32000000
由于内存无法保证被 运行时即时释放,我们建议为 RabbitMQ 节点分配至少 3 倍于有效 WAL 文件大小限制的内存。在高吞吐量系统中则需要更多,建议从 4 倍起步。
经典队列
经典队列有两种可用的存储实现:v1(原始版本)和 v2(RabbitMQ 3.10 及更高版本提供)。
队列版本
自 RabbitMQ 3.10.0 起,Broker 引入了经典队列的新实现,称为 版本 2。版本 2 队列具有新的索引文件格式和实现,以及一种新的按队列存储文件格式,以取代直接在索引中嵌入消息的方式。
版本 2 的主要改进是在高内存压力下的稳定性得到提升。
在 RabbitMQ 3.10.0 中,版本 1 仍然是默认设置。可以在版本 1 和版本 2 之间进行切换。
可以使用 queue-version 策略键更改版本。通过策略设置新版本后,队列将立即转换磁盘上的数据。可以升级到版本 2 或降级到版本 1。请注意,对于大型队列,转换可能需要一些时间,并且在转换过程中队列将不可用。
默认版本可以通过在 rabbitmq.conf 中设置 classic_queue.default_version 来配置。
# makes classic queues use a more efficient message storage
# and queue index implementations
classic_queue.default_version = 2
经典队列 v1 持久化概述
首先,介绍一些背景信息:持久化消息和瞬态消息都可以写入磁盘。持久化消息在到达队列后会立即写入磁盘,而瞬态消息仅在为了缓解内存压力而被从内存中移除时才会写入磁盘。持久化消息也会尽可能保留在内存中,仅在内存压力下才会被移出。 “持久化层”是指用于将这两类消息存储到磁盘的机制。
在本页中,我们所说的“队列”是指非复制队列、队列领导者或队列镜像。队列镜像是在持久化之上的“一层”。
持久化层有两个组件:队列索引 (queue index) 和 消息存储 (message store)。队列索引负责维护关于给定消息在队列中位置的信息,以及它是否已被传递和确认。因此,每个队列对应一个队列索引。
消息存储是一个针对每个 vhost 中所有队列共享的消息键值存储。消息(消息体和任何元数据字段:属性和/或头部)既可以直接存储在队列索引中,也可以写入消息存储中。从技术上讲,有两个消息存储(一个用于瞬态消息,一个用于持久化消息),但它们通常被统称为“消息存储”。
内存开销
在内存压力下,持久化层会尽可能将数据写入磁盘,并从内存中移除尽可能多的内容。然而,有些内容必须保留在内存中
- 每个队列为每条 未确认 (unacknowledged) 的消息维护一些元数据。如果消息的目的地是消息存储,则消息本身可以从内存中移除。
- 消息存储需要一个索引。默认的消息存储索引为存储中的每条消息使用少量内存。
在队列索引中嵌入消息
将消息写入队列索引既有优点也有缺点。
此功能的主要优点是
- 消息可以在一次操作中写入磁盘,而不是两次;对于微小消息,这可以带来实质性的增益。
- 写入队列索引的消息不需要在消息存储索引中条目,因此在页面移出 (paged out) 时不会产生内存开销。
缺点是
- 队列索引将固定数量的记录块保存在内存中;如果将非微小的消息写入队列索引,内存使用量可能会非常大。
- 如果一条消息被交换机路由到多个队列,则该消息需要写入多个队列索引。如果此类消息写入消息存储,则只需写入一个副本。
- 目的地为队列索引的未确认消息始终保留在内存中。
- 使用 版本 2 时仍需要两次写入。
其目的是将非常小的消息作为一种优化手段存储在队列索引中,而将所有其他消息写入消息存储。这由配置项 queue_index_embed_msgs_below 控制。默认情况下,序列化大小小于 4096 字节(包括属性和头部)的消息会存储在队列索引中。
从磁盘读取消息时,每个队列索引需要至少在内存中保留一个段文件。段文件包含 16,384 条消息的记录。因此,如果增加 queue_index_embed_msgs_below,请务必谨慎;微小的增加可能会导致大量内存被占用。
影响性能的操作系统和运行时限制
持久化可能会表现不佳,因为持久化器在可使用的文件句柄或异步线程数量上受到限制。这两种情况都可能发生在当有大量队列需要同时访问磁盘时。
文件句柄太少
RabbitMQ 服务器在可打开的 文件句柄数量上受到限制。每个运行的网络连接都需要一个文件句柄,其余的供队列使用。如果在考虑网络连接后,访问磁盘的队列数量超过了文件句柄数量,那么访问磁盘的队列将共享这些文件句柄;每个队列在被取走并交给另一个队列之前,只能使用一段时间的文件句柄。
这可以防止服务器因磁盘访问队列过多而崩溃,但代价可能会很大。管理插件可以显示集群中每个节点的 I/O 统计信息;除了显示读取、写入、寻道等速率外,它还会显示文件句柄流转率——即文件句柄以此方式被回收的速率。文件句柄不足的繁忙服务器可能会每秒进行数百次重新打开操作——在这种情况下,如果提供更多文件句柄,其性能可能会有显著提升。
经典队列 v1:替代消息存储索引实现
如上所述,写入消息存储的每条消息都会为其索引条目使用少量内存。经典队列 v1 使用的消息存储索引在 RabbitMQ 中是可插拔的,其他实现作为插件提供,可以消除此限制。
它们没有随 RabbitMQ 分发包提供的原因是它们都使用了本地代码。请注意,此类插件通常会使消息存储运行得更慢。