原文

RabbitMQ 持久层旨在在没有配置的情况下在大多数主要场景中给出一个好的结果。然而,一些配置有时比较有用。这篇文章会解释你可以怎样配置它。建议你在开始任何行动之前阅读完它。

持久层是如何工作的

首先,一些背景:所有 persistenttransient 的消息都可以被刷盘。 Persistent 消息到达消息后会尽可能快地刷盘,而 transient 的消息仅会在内存有压力的情况下被刷盘以便释放内存。当可能时, Persistent 消息也会保持在内存中,并仅会在内存有压力时才会被淘汰。持久层是指将这两种类型的消息( persistenttransient )持久化到磁盘的机制。

在这篇文章中,我们说 队列 是指一个非镜像队列( unmirrored queue )或者是一个主队列( a queue master)又或者是一个从队列(a queue slave)。队列镜像(Queue mirroring) 发生在持久化之上(happen “above”)。

持久层有两个组件:队列索引(queue index) 和 消息存储(message store)。队列索引是负责维护在一个队列中给定的消息的位置,以及它是否被投递和确认(ack) 了。因此,每一个队列都有一个队列索引。

消息存储(message store) 是一个对消息进行 键-值对 的存储,在服务器中的所有队列中共享。消息(内容,和其他任何属性,以及/或者消息头)既可以直接保存在队列索引中,也可以写到消息存储(message store) 中。在技术中有两种消息存储(一个是 transient ,另一个是 persistent 消息),但它们通常被一起视作为 消息存储 (the message store)。

内存消耗

在内存压力下,持久层会试图将尽可能多地刷盘,并尽可能多地从内存中移除(消息)。但这仍然有一些东西它们必须保留在内存中:

  • 每个队列的中对每一条未确认(unacknowledged)消息的一些元数据(metadata)。消息自身可以从内存中移除,如果它的目的是消息存储。

  • 消息存储需要一个索引。默认的消息存储索引对每一条存储的消息都会使用少量的内存。

在队列索引中的消息

将消息写到队列索引的优缺点:

优点

  1. 消息可以在一个操作来进行刷盘,而不是要两个操作;对于小型消息来说,这是比较可观的收益。

  2. 保存在队列索引的消息,并不需要消息存储索引中的条目,因此当在进行 paged out 时,并不会有内存代价。

缺点

  1. 队列索引在内存中保留固定数量的记录块;如果有非小型消息写到队列索引的话,内存使用就会比较明显。

  2. 如果消息被一个交换机路由到多个队列,消息就需要被写到多个队列索引中。如果这样一条消息被保存到消息存储中,仅有一个副本需要被写。

  3. 这些存储在队列索引的未确认(Unacknowledged)消息,会一直保留在内存中。

继续

意图将一个非常小的消息存储在队列索引中作为一个优化,并且将所有其他消息写到消息存储中。这可以通过配置项 queue_index_embed_msgs_below 来控制。默认情况下,消息的序列化大小小于 4096 个字节的(包括属性和头)是被存储在队列索引中的。

当从磁盘读取消息时,每一个队列索引需要保留至少一个段文件(one segment file)在内存中。段文件包含了 16384 条消息记录。如果加大 queue_index_embed_msgs_below 的话,请小心,增加一点可能会导致大量的内存占用。

意外限制持久化性能

对于持久化来说由于持久化受限于文件句柄的数量或与它一起工作的异步线程的数量,它可能表现平平。在这两种情况下,当你有大量的队列同时需要访问磁盘时就会发生这种情况。

太少的文件句柄

RabbitMQ 服务器典型情况下会受限于它可以打开的文件句柄数量(至少是在 Unix 上面)。每一条正在运行的网络连接都要一个文件句柄,剩下可用的是队列使用的。在考虑完网络连接之后,如果要磁盘访问的队列比文件句柄更多的话,要磁盘访问的队列就会在它们自己之间共享文件句柄;在文件句柄被返回之前每个获取该句柄的队列使用一会,完成之后就会被下一个队列使用。

这是为了防止服务器由于有大量的磁盘访问的队列而崩溃,但代价比较昂贵。management 插件可以为每个集群中的节点显示 I/O 统计信息;也会显示读、写、seek 等等的频率,并也会显示重新打开的频率——即文件句柄被循环利用的频率。一个拥有比较少量文件句柄的繁忙服务器,可能会有数百每秒的重新打开文件句柄的处理——在这种情况下明显地提高它的性能就是允许它打开更加多的文件句柄。

太少的异步线程

Erlang 虚拟机会创建一个异步线程池来处理长时间运行的文件I/O 操作。这是在所有的队列中共享的。当每一个激活的文件 I/O 操作发生时,它都需要一条异步纯种。只是少量的异步线程的话就会伤害性能了。

注意,异步线程的情况并不完全类似于文件句柄的情况。如果一个队列顺序地执行许多 I/O 操作,最好的执行情况是它持有一个文件句柄,然后所有的操作都在该句柄上执行;否则我们可能要刷盘,然后进行大量的 seek 并且要使用额外的 CPU 时间来精确它。然而,队列并不能从拥有一个异步线程地通过顺序地执行操作来得益(事实上,它们并不能这样子做)。

因此,理想的情况下所有正在执行 I/O 流操作的队列都应该有足够的文件句柄,并且有足够的异步线程为存储层可以合理地执行同时需要一定数量的 I/O 操作。

然而缺少异步线程而导致性能的问题并不太明显(一般情况下这也不太可能;所以首先检查其他的东西)。拥有太少异步线程的症状包括当服务器也许忙于持久化时,短期内每秒的 I/O 操作数下降为 0(正如 management 插件报告的那样),同时,报告的每次 I/O 操作的时间在增加。

异步线程的数量可以如文档这里描述的一样,通过传递 +A 参数给 Erlang 虚拟机来配置,并且典型的情况下是通过环境变量 RABBITMQ_SERVER_ERL_ARGS 来配置。默认值为 +A 64 。改变这个值之前,尝试几个不同的值来验证下比较好。

替换消息存储索引实现

正如上面提到,每一条写到消息存储里的消息会为它的索引条目使用小量的内存。在 RabbitMQ 中,消息存储索引是一种可拔插的,其他的实现可以作为插件来使用,这可以移除这个限制。(我们的服务器不带有任何实现的原因是,它们都是使用本地代码的)。注意,这种插件典型情况下会使消息存储运行得更慢。