了解 RabbitMQ 3.4 中的内存使用情况
“我的队列使用了多少内存?”这是一个很容易提出的问题,但回答起来却有些复杂。RabbitMQ 3.4 让您更清楚地了解队列如何使用内存。这篇博文讨论了这个问题,并解释了队列内存使用的一般情况。
背景知识
首先,我们需要了解 Erlang 是如何管理内存的。Erlang 与大多数带有垃圾回收机制的语言不同,它没有全局堆(global heap)。相反,每个进程都有一个独立的私有堆。在 RabbitMQ 的语境中,进程可能是队列、通道、连接等。这意味着整个系统不需要在每次需要进行垃圾回收时都完全暂停;相反,每个进程按照自己的节奏回收垃圾。
这很好,但当消息在 RabbitMQ 中传递时,它会经过几个不同的进程。我们希望在此过程中避免过多的拷贝。因此,Erlang 为二进制数据(binaries)提供了一种不同的内存管理方案,这在 RabbitMQ 内部被用于许多地方,其中最引人注目的是消息体。二进制数据在进程间是共享的,并采用引用计数机制(引用由进程持有,并与其他数据一起进行垃圾回收)。
这如何应用于 RabbitMQ
这意味着 RabbitMQ 中消息体所使用的内存是在进程间共享的。这种共享也发生在队列之间:如果交换机将一条消息路由到多个队列,消息体在内存中只会存储一次。
因此我们可以看出,回答“这个队列占用了多少内存?”是一个难题——如果我们排除队列可能引用的任何二进制内存,会导致计数偏低;如果包含它,则可能导致计数偏高。
旧版本的 RabbitMQ 在处理这一困境时没有采取太多措施;它们将队列的“内存使用量”报告为进程内存的大小(即不包括任何引用的二进制数据),并在全局内存分解中显示一块巨大的“二进制内存使用量”。当时没有办法进行深入调查。
RabbitMQ 3.4 为我们提供了更好的指导,包括从上到下和从下到上的视角。首先,让我们从整体上查看内存使用情况。

这里与我们过去看到的情况有几点不同。整体内存使用分解现在有了更多的类别,并且新增了一个二进制内存“分解”。
我们单独展示二进制内存分解有两个原因;一是计算它的开销可能相当大(我们必须遍历服务器使用的所有内存;如果存在大量小型二进制文件,这可能需要一些时间);二是由于上述的二进制共享方式,我们无法保证其总和与整体内存分解中显示的大小完全一致。
但我们在这里可以看出,几乎所有的二进制使用都是由于队列中的消息造成的。这张截图取自一个相对静态的代理服务器,所以这是我们预期的结果。
那么队列呢?
好的,但是到底是哪些队列占用了所有这些内存?我们可以通过查看每个队列的详情页面来调查(这些信息当然也可以通过 rabbitmqctl 获取,但图片看起来更直观)。

在这里我们可以看到 RabbitMQ 3.4 的另一个新特性:队列维护了它所包含的消息体总字节数。因此我们看到该队列包含 1.2GB 的消息体内容,其中 420MB 在内存中。我们可以假设这 420MB 全部处于队列使用的二进制内存中。该队列还使用了 421MB 的进程内存(巧合的是,数值非常接近)——这包括消息属性、头部信息以及关于每条消息的元数据。
因此,说“这个队列占用了 841MB 内存”是合理的——前提是消息体没有与其他队列共享。
顺便提一下,请注意这里的“内存中(In memory)”和“持久化(Persistent)”并不是反义词:非持久化消息在内存压力下可能会被换出(paged out),而持久化消息也可能存在于内存中。请参阅 文档 以了解关于分页(paging)的更多信息。
我们也可以在队列列表视图中查看此信息。

(在这里,我点击了 “+/-” 链接来添加显示内存使用情况的列,并为了清晰起见移除了其他一些列。)
当然,这仍然无法给出队列占用内存的完美计数;在一个动态系统中,这大概是不可能的。但它使我们离真相更近了一步。