内存占用推理
概述
操作人员需要能够推断节点的内存使用情况,包括绝对值和相对值(“哪些内容占用最多的内存”)。这是系统监控的一个重要方面。
本指南重点介绍节点报告(监控)的内存占用情况的推理。它还附带了一些密切相关的指南
RabbitMQ 提供了报告和帮助分析节点内存使用的工具
rabbitmq-diagnostics memory_breakdown
rabbitmq-diagnostics status
包含上述细分作为一部分- Prometheus 和 Grafana 基于监控可以观察一段时间内的内存细分
- 管理 UI 在节点页面上提供了与
rabbitmq-diagnostics status
相同的细分 - HTTP API 提供与管理 UI 相同的信息,对监控很有用
- rabbitmq-top 和
rabbitmq-diagnostics observer
提供了更细粒度的top 类似的每个 Erlang 进程视图
在推断节点内存使用情况时,获取节点内存细分应该是第一步。
请注意,所有测量都或多或少近似,基于底层运行时或内核在特定时间点返回的值,通常在 5 秒的时间窗口内。
在容器和 Kubernetes 中运行 RabbitMQ
当 RabbitMQ 在使用 cgroups 的环境中运行时,即在各种容器化环境和 Kubernetes 中,必须考虑与内存限制 和内核页面缓存相关的某些方面,尤其是在使用流和超级流的集群中。
内存使用细分
RabbitMQ 节点可以报告其内存使用细分。细分以类别列表(如下所示)和该类别的内存占用情况的形式提供。
每个类别都是该类型的所有进程或表的运行时报告的内存占用的总和。这意味着连接类别是所有连接进程使用的内存总和,通道类别是所有通道进程使用的内存总和,ETS 表是节点上所有内存表使用的内存总和,依此类推。
内存细分的工作原理
内存使用细分报告目标节点上按类别分配的内存分布
- 连接(进一步细分为四个类别:读取器、写入器、通道、其他)
- 仲裁队列 副本
- 流 副本
- 经典队列消息存储和索引
- 二进制堆引用
- 节点本地指标(管理插件 统计数据库)
- 内部模式数据库表
- 插件,包括传输消息的协议,例如Shovel 和Federation,以及它们的内部队列
- 已分配但尚未使用的内存内存
- 代码(字节码、模块元数据)
- ETS(内存键/值存储)表
- 原子表
- 其他
通常,类别之间没有重叠(没有对同一内存进行双重计算)。插件和运行时版本可能会影响这一点。
使用 CLI 工具生成内存使用细分
生成内存细分的一种常见方法是通过 rabbitmq-diagnostics memory_breakdown
。
quorum_queue_procs: 0.4181 gb (28.8%)
binary: 0.4129 gb (28.44%)
allocated_unused: 0.1959 gb (13.49%)
connection_other: 0.1894 gb (13.05%)
plugins: 0.0373 gb (2.57%)
other_proc: 0.0325 gb (2.24%)
code: 0.0305 gb (2.1%)
quorum_ets: 0.0303 gb (2.09%)
connection_readers: 0.0222 gb (1.53%)
other_system: 0.0209 gb (1.44%)
connection_channels: 0.017 gb (1.17%)
mgmt_db: 0.017 gb (1.17%)
metrics: 0.0109 gb (0.75%)
other_ets: 0.0073 gb (0.5%)
connection_writers: 0.007 gb (0.48%)
atom: 0.0015 gb (0.11%)
mnesia: 0.0006 gb (0.04%)
msg_index: 0.0002 gb (0.01%)
queue_procs: 0.0002 gb (0.01%)
reserved_unallocated: 0.0 gb (0.0%)
报告字段 | 类别 | 详细信息 |
total | 由有效内存计算策略报告的总量(见上文) | |
connection_readers | 连接 | 负责连接解析器和大多数连接状态的进程。它们的大部分内存都归因于 TCP 缓冲区。节点拥有的客户端连接越多,此类别使用的内存就越多。有关更多信息,请参阅网络指南。 |
connection_writers | 连接 | 负责序列化传出协议帧并写入客户端连接套接字的进程。节点拥有的客户端连接越多,此类别使用的内存就越多。有关更多信息,请参阅网络指南。 |
connection_channels | 通道 | 客户端连接使用的通道越多,此类别使用的内存就越多。 |
connection_other | 连接 | 与客户端连接相关的其他内存 |
quorum_queue_procs | 队列 | |
queue_procs | 队列 | 经典队列领导者、索引和保存在内存中的消息。排队消息的数量越多,通常分配给此部分的内存就越多。但是,这在很大程度上取决于队列类型和属性。有关更多信息,请参阅内存、经典队列。 |
metrics | 统计数据库 | 节点本地指标。节点主机连接、通道、队列越多,要收集和保留的统计信息就越多。有关更多信息,请参阅管理插件指南。 |
stats_db | 统计数据库 | 聚合和预先计算的指标、节点间 HTTP API 请求缓存以及与统计数据库相关的其他所有内容。有关更多信息,请参阅管理插件指南。 |
binaries | 二进制文件 | 运行时二进制堆。此部分的大部分内容通常是消息正文和属性(元数据)。 |
plugins | 插件 | 诸如Shovel、Federation 或协议实现(如STOMP)之类的插件可能会在内存中累积消息。 |
allocated_unused | 预分配内存 | 由运行时分配但尚未使用。 |
reserved_unallocated | 预分配内存 | 由内核分配/保留,但不是运行时 |
mnesia | 内部数据库 | 虚拟主机、用户、权限、队列元数据和状态、交换机、绑定、运行时参数等。 |
quorum_ets | 内部数据库 | Raft 实现的 WAL 和其他内存表。其中大部分会定期移动到磁盘。 |
other_ets | 内部数据库 | 某些插件可以使用 ETS 表来存储其状态 |
code | 代码 | 字节码和模块元数据。这在空白/空节点上仅应占用两位数的内存百分比。 |
other | 其他 | RabbitMQ 无法分类的所有其他进程 |
使用管理 UI 生成内存使用细分
管理 UI 可用于生成内存细分图表。此信息可在节点指标页面上获得,可从概述中访问
在节点指标页面上,向下滚动到内存细分按钮
内存和二进制堆细分计算成本可能很高,并且会在按下“更新”按钮时按需生成
还可以显示系统中各种内容(例如连接、队列)使用的二进制堆的细分
使用 HTTP API 和 curl 生成内存使用细分
可以通过向 /api/nodes/{node}/memory
端点发出 GET
请求,通过HTTP API 生成内存使用细分。
curl -s -u guest:guest http://127.0.0.1:15672/api/nodes/rabbit@mercurio/memory | python -m json.tool
{
"memory": {
"atom": 1041593,
"binary": 5133776,
"code": 25299059,
"connection_channels": 1823320,
"connection_other": 150168,
"connection_readers": 83760,
"connection_writers": 113112,
"metrics": 217816,
"mgmt_db": 266560,
"mnesia": 93344,
"msg_index": 48880,
"other_ets": 2294184,
"other_proc": 27131728,
"other_system": 21496756,
"plugins": 3103424,
"queue_procs": 2957624,
"total": 89870336
}
}
还可以使用对 /api/nodes/{node}/memory
端点的 GET
请求检索相对细分。请注意,报告的相对值四舍五入为整数。此端点旨在用于相对比较(识别主要贡献类别),而不是精确计算。
curl -s -u guest:guest http://127.0.0.1:15672/api/nodes/rabbit@mercurio/memory/relative | python -m json.tool
{
"memory": {
"allocated_unused": 32,
"atom": 1,
"binary": 5,
"code": 22,
"connection_channels": 2,
"connection_other": 1,
"connection_readers": 1,
"connection_writers": 1,
"metrics": 1,
"mgmt_db": 1,
"mnesia": 1,
"msg_index": 1,
"other_ets": 2,
"other_proc": 21,
"other_system": 19,
"plugins": 3,
"queue_procs": 4,
"reserved_unallocated": 0,
"total": 100
}
}
内存细分类别
连接
这包括客户端连接(包括Shovels 和Federation 链接)和通道使用的内存,以及传出的连接(Shovels 和 Federation 上游链接)。大多数内存通常由 TCP 缓冲区使用,这些缓冲区在 Linux 上默认自动调整为大约 100 kB。可以降低 TCP 缓冲区的大小,但代价是连接吞吐量成比例下降。有关详细信息,请参阅网络指南。
通道也会消耗 RAM。通过优化应用程序使用的通道数量,可以减少该数量。可以使用 channel_max
配置设置限制连接上的最大通道数
channel_max = 16
请注意,某些基于 RabbitMQ 客户端构建的库和工具可能会隐式地需要一定数量的通道。找到最佳值通常需要反复试验。
队列和消息
队列、队列索引和队列状态使用的内存。入队的消息将部分计入此类别。
当内存压力过大时,队列将将其内容交换到磁盘。此行为的具体方式取决于队列属性、客户端是否将消息发布为持久或瞬态消息,以及节点的持久化配置。
消息体不会显示在此处,而是在“二进制数据”中。
消息存储索引
默认情况下,消息存储使用所有消息的内存中索引,包括已分页到磁盘的消息。插件允许使用基于磁盘的实现替换它。
插件
插件使用的内存(Erlang客户端除外,Erlang客户端计入“连接”类别,管理数据库单独计算)。此类别将包含一些用于协议插件(例如 STOMP 和 MQTT)的每个连接内存,以及插件(例如 Shovel 和 Federation)入队的消息。
预分配内存
运行时(虚拟机分配器)预分配但尚未使用的内存。下面将详细介绍。
内部数据库
内部数据库(Mnesia)表保留其所有数据的内存副本(即使在磁盘节点上)。通常,只有当队列、交换机、绑定、用户或虚拟主机数量很多时,此值才会很大。插件也可以将数据存储在同一数据库中。
管理(统计信息)数据库
统计信息数据库(如果启用了管理插件)。在集群中,大多数统计信息都存储在节点本地。跨节点请求需要聚合集群中的统计信息,可以进行缓存。缓存的数据将在此类别中报告。
二进制数据
运行时中共享二进制数据使用的内存。此内存的大部分是消息体和元数据。
对于某些工作负载,二进制数据堆可能很少被垃圾回收。可以使用 rabbitmqctl force_gc
强制进行回收。以下几个命令强制回收并报告释放最多二进制堆引用的顶级进程
rabbitmqctl eval 'recon:bin_leak(10).'
rabbitmqctl force_gc
对于不提供 rabbitmqctl force_gc
的 RabbitMQ 版本,请使用
rabbitmqctl eval 'recon:bin_leak(10).'
rabbitmqctl eval '[garbage_collect(P) || P <- processes()].'
其他 ETS 表
除了统计信息数据库和内部数据库表所属的表之外的其他内存中表。
代码
代码(字节码、模块元数据)使用的内存。此部分通常相当恒定且相对较小(除非节点完全为空且不存储任何数据)。
原子
原子使用的内存。应该相当恒定。
使用 rabbitmq-top 进行每个进程分析
rabbitmq-top 是一个插件,有助于识别消耗最多内存或调度程序(CPU)时间的运行时进程(“轻量级线程”)。
该插件与 RabbitMQ 一起提供。使用以下命令启用它:
[sudo] rabbitmq-plugins enable rabbitmq_top
该插件向管理 UI添加了新的管理选项卡。一个选项卡根据以下指标之一显示顶级进程
- 使用的内存
- 归约(调度程序/CPU 消耗的单位)
- Erlang 邮箱长度
- 对于
gen_server2
进程,内部操作缓冲区长度
第二个选项卡显示 ETS(内部键/值存储)表。这些表可以按使用的内存量或行数排序
预分配内存
Erlang 内存细分报告仅报告当前正在使用的内存,而不是为以后使用而分配的内存或操作系统保留的内存。操作系统工具(如 ps
)报告的已用内存可能比运行时报告的更多。
此内存包括已分配但未使用,以及未分配但操作系统已保留的内存。这两个值都取决于操作系统和 Erlang 虚拟机分配器设置,并且可能发生很大波动。
这两个部分中的值的计算方式取决于 vm_memory_calculation_strategy
设置。如果策略设置为 erlang
,则不会报告未使用的内存。如果内存计算策略设置为 allocated
,则不会报告操作系统保留的内存。因此,rss
是从内核和运行时提供最多信息的策略。
当节点在长时间运行的节点上报告大量已分配但未使用的内存时,这可能是运行时内存碎片的指示。不同的分配器设置集可以减少碎片并提高有效使用内存的百分比。正确的设置集取决于工作负载和消息有效负载大小分布。
运行时的内存分配器行为可以进行调整,请参阅erl 和erts_alloc 文档。
内核页面缓存
除了 RabbitMQ 节点直接分配和使用的内存之外,该节点读取的文件还可以由操作系统缓存。此缓存提高了 I/O 操作效率,并在操作系统检测到很大一部分可用内存正在使用时被逐出(清除)。
使用RabbitMQ 流的工作负载通常会导致内核页面缓存大小变大,尤其是在消费者访问跨越几天或几周的消息时。
一些监控工具不会将页面缓存的大小包含在进程监控指标中。其他工具将其添加到进程的驻留集大小 (RSS) 占用空间中。这可能导致混淆:页面缓存不是由 RabbitMQ 节点维护或控制的。它由操作系统内核维护、控制和逐出(清除)。
这在基于Kubernetes 的部署(不使用 cgroup v2)中尤其常见,并且使用基于较旧发行版的容器镜像运行 RabbitMQ,这些发行版使用 cgroups v1。
强烈建议使用 Kubernetes 1.25.0 及以下发行版,因为它们对内核页面缓存内存计费采用了更合理的方法
- CentOS Stream 9 或更高版本
- Fedora 31 或更高版本
- Ubuntu 21.10 或更高版本
- Debian 11 Bullseye 或更高版本
大型页面缓存大小告诉我们有关工作负载的信息?
通常,大型页面缓存大小仅表示工作负载 I/O 密集型,并且可能使用具有大型数据集的流。它并不表示节点存在内存泄漏:当系统检测到可用内存不足时,内核将清除缓存。
在非容器化环境中检查页面缓存(在虚拟机或物理机中)
在非容器化环境(例如 RabbitMQ 节点在虚拟机或裸机硬件上运行)中,请使用
cat /proc/meminfo | grep -we "Cached"
检查内核页面缓存的大小。
在容器化环境中检查页面缓存大小
在 Kubernetes 等容器化环境中,可以使用以下两个 /sys
伪文件系统路径来检查 RSS 和页面缓存占用空间
cat /sys/fs/cgroup/memory/memory.stat
cat /sys/fs/cgroup/memory/memory.usage_in_bytes
这两个关键指标分别命名为 rss
(用于驻留集大小)和 cache
(用于页面缓存)。
内存使用监控
建议生产系统监控所有集群节点的内存使用情况,理想情况下应进行细分,并结合基础设施级别的指标。通过将细分类别与其他指标相关联,例如并发连接数或入队消息数,可以检测出源于特定于应用程序的行为的问题(例如连接泄漏或没有消费者的队列不断增长)。
队列内存
一条消息使用多少内存?
一条消息有多个部分会占用内存
- 有效负载:>= 1 字节,大小可变,通常为几百字节到几百千字节
- 协议属性:>= 0 字节,大小可变,包含标头、优先级、时间戳、回复到等。
- RabbitMQ 元数据:>= 720 字节,大小可变,包含交换机、路由键、消息属性、持久性、重新传递状态等。
- RabbitMQ 消息排序结构:16 字节
包含 1KB 有效负载的消息在考虑属性和元数据后将占用 2KB 的内存。
某些消息可以存储在磁盘上,但其元数据仍保留在内存中。
队列使用多少内存?
一条消息有多个部分会占用内存。每个队列都由一个 Erlang 进程支持。如果队列被复制,则每个副本都是一个在单独的集群节点上运行的单独的 Erlang 进程。
由于队列的每个副本(无论是领导者还是跟随者)都是单个 Erlang 进程,因此可以保证消息排序。多个队列意味着多个 Erlang 进程,这些进程获得均匀的 CPU 时间。这确保了任何队列都不能阻塞其他队列。
可以通过 HTTP API 获取单个队列的内存使用情况
curl -s -u guest:guest http://127.0.0.1:15672/api/queues/%2f/queue-name |
python -m json.tool
{
..
"memory": 97921904,
...
"message_bytes_ram": 2153429941,
...
}
memory
:队列进程使用的内存,包含消息元数据(每条消息至少 720 字节),不包含超过 64 字节的消息有效负载message_bytes_ram
:消息有效负载使用的内存,无论大小如何
如果消息较小,则消息元数据可能比消息有效负载使用更多的内存。包含 1 字节有效负载的 10,000 条消息将使用 10KB 的 message_bytes_ram
(有效负载)和 7MB 的 memory
(元数据)。
如果消息有效负载很大,则不会反映在队列进程内存中。包含 100 KB 有效负载的 10,000 条消息将使用 976MB 的 message_bytes_ram
(有效负载)和 7MB 的 memory
(元数据)。
发布/消费时为什么队列内存会增长和缩小?
Erlang 为每个 Erlang 进程使用分代垃圾回收。垃圾回收针对每个队列进行,独立于所有其他 Erlang 进程。
当垃圾回收运行时,它会在释放未使用的内存之前复制已使用的进程内存。这可能导致队列进程在垃圾回收期间使用多达两倍的内存,如下所示(队列包含大量消息)
垃圾回收期间队列内存增长是否需要关注?
如果 Erlang VM 尝试分配超过可用内存的内存,则 VM 本身将崩溃或被 OOM killer 杀死。当 Erlang VM 崩溃时,RabbitMQ 将丢失所有非持久性数据。
垃圾回收可能会短暂地将队列使用的内存翻倍。但是,鉴于
- 垃圾回收过程在不同时间对不同的队列进行
- 并且鉴于在现代 RabbitMQ 版本中,所有类型的队列(经典、仲裁、流)要么根本不将消息存储在内存中,要么仅将有限数量的消息存储在内存中
垃圾回收过程导致 RabbitMQ 节点整体内存使用量出现大幅峰值的可能性非常小。
总内存使用计算策略
RabbitMQ 可以使用不同的策略来计算节点使用多少内存。历史上,节点从运行时获取此信息,报告使用了多少内存(不仅仅是分配了多少)。这种策略称为 legacy
(erlang
的别名),往往会低估内存使用量,不建议使用。
有效的策略使用 vm_memory_calculation_strategy
键进行配置。有两个主要选项
-
rss
使用特定于操作系统的查询内核方法来查找节点操作系统进程的 RSS(驻留集大小)值。此策略最精确,并且在 Linux、MacOS、BSD 和 Solaris 系统上默认使用。当使用此策略时,RabbitMQ 每秒运行一次短暂的子进程。 -
allocated
是一种查询运行时内存分配器信息的策略。它通常非常接近rss
策略报告的值。此策略在 Windows 上默认使用。
vm_memory_calculation_strategy
设置也会影响内存细分报告。如果设置为 legacy
(erlang
)或 allocated
,则不会报告某些内存细分字段。本指南后面将更详细地介绍这一点。
以下配置示例使用 rss
策略
vm_memory_calculation_strategy = rss
类似地,对于 allocated
策略,请使用
vm_memory_calculation_strategy = allocated
要了解节点使用什么策略,请参阅其有效配置。