内存占用情况分析
概述
运维人员需要能够分析节点的内存使用情况,包括绝对值和相对值(“哪个占用内存最多”)。这是系统监控的一个重要方面。
本指南侧重于分析节点报告的(监控的)内存占用情况。它还附带了一些密切相关的指南
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 节点可以报告其内存使用情况分解。分解以类别列表(如下所示)和该类别的内存占用量形式提供。
每个类别都是该类型的所有进程或表的运行时报告的内存占用量的总和。这意味着 connections 类别是所有连接进程使用的内存总和,channels 类别是所有通道进程使用的内存总和,ETS tables 是节点上所有内存表的内存总和,依此类推。
内存分解工作原理
内存使用情况分解报告目标节点上按类别分配的内存分布
- Connections(进一步分为四个类别:readers、writers、channels、other)
- Quorum queue 副本
- Stream 副本
- 经典队列消息存储和索引
- 二进制堆引用
- 节点本地指标(管理插件 统计数据库)
- 内部模式数据库表
- 插件,包括传输消息的协议,例如 Shovel 和 Federation,以及它们的内部队列
- 已分配但尚未使用的内存
- 代码(字节码、模块元数据)
- ETS(内存键/值存储)表
- Atom 表
- 其他
通常,类别之间没有重叠(同一内存没有重复计算)。插件和运行时版本可能会影响这一点。
使用 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 | 队列 | Quorum 队列进程,包括当前选出的领导者和追随者。每个队列的内存占用量可以被限制。有关更多信息,请参阅 Quorum 队列指南。 |
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 可用于生成内存分解图表。此信息可在节点指标页面上找到,该页面可以从概述访问
在节点指标页面上,向下滚动到内存分解按钮
内存和二进制堆分解的计算成本可能很高,并且在按下“Update”按钮时按需生成
还可以显示系统中各种事物(例如,连接、队列)的二进制堆使用情况分解
使用 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 上,默认情况下,TCP 缓冲区会自动调整大小约为 100 kB。可以减少 TCP 缓冲区大小,但会以连接吞吐量成比例下降为代价。有关详细信息,请参阅网络指南。
通道也消耗 RAM。通过优化应用程序使用的通道数量,可以减少该数量。可以使用 channel_max
配置设置限制连接上的最大通道数
channel_max = 16
请注意,某些构建在 RabbitMQ 客户端之上的库和工具可能隐式地需要一定数量的通道。找到最佳值通常需要反复试验。
队列和消息
队列、队列索引、队列状态使用的内存。排队的消息部分会为此类别做出贡献。
当内存压力过大时,队列会将其内容交换到磁盘。这的确切行为取决于队列属性、客户端是否将消息发布为持久性或瞬态,以及节点的持久性配置。
消息体不在此处显示,而是在“二进制文件”中显示。
消息存储索引
默认情况下,消息存储使用所有消息的内存索引,包括分页到磁盘的消息。插件允许使用基于磁盘的实现替换它。
插件
插件使用的内存(Erlang 客户端除外,Erlang 客户端在“连接”下计数,管理数据库单独计数)。此类别将包括协议插件(如 STOMP 和 MQTT)的一些每连接内存,以及插件(如 Shovel 和 Federation)排队的消息。
预分配内存
运行时(VM 分配器)预分配但尚未使用内存。下面将更详细地介绍这一点。
内部数据库
内部数据库 (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 表
除了属于统计数据库和内部数据库表的 ETS 表之外的其他内存表。
代码
代码(字节码、模块元数据)使用的内存。此部分通常相当恒定且相对较小(除非节点完全空白且不存储任何数据)。
Atom
Atom 使用的内存。应该相当恒定。
使用 rabbitmq-top 进行每进程分析
rabbitmq-top 是一个插件,可帮助识别占用最多内存或调度程序 (CPU) 时间的运行时进程(“轻量级线程”)。
该插件随 RabbitMQ 一起提供。使用以下命令启用它
[sudo] rabbitmq-plugins enable rabbitmq_top
该插件向管理 UI 添加了新的管理选项卡。一个选项卡按以下指标之一显示顶级进程
- 已用内存
- 归约(调度程序/CPU 消耗单位)
- Erlang 邮箱长度
- 对于
gen_server2
进程,内部操作缓冲区长度
第二个选项卡显示 ETS(内部键/值存储)表。这些表可以按使用的内存量或行数排序
预分配内存
Erlang 内存分解报告仅报告当前正在使用的内存,而不是已分配供以后使用或操作系统保留的内存。诸如 ps
之类的 OS 工具可以报告比运行时使用的内存更多的内存。
此内存由已分配但未使用,以及操作系统保留但未分配的内存组成。这两个值都取决于 OS 和 Erlang VM 分配器设置,并且可能会大幅波动。
这两个部分中的值的计算方式取决于 vm_memory_calculation_strategy
设置。如果策略设置为 erlang
,则不会报告未使用的内存。如果内存计算策略设置为 allocated
,则不会报告操作系统保留的内存。因此,rss
是提供来自内核和运行时最多信息的策略。
当节点在长时间运行的节点上报告大量已分配但未使用的内存时,这可能是运行时内存碎片化的指标。不同的分配器设置可以减少碎片化并提高有效使用内存的百分比。正确的设置集取决于工作负载和消息有效负载大小分布。
可以调整运行时的内存分配器行为,请参阅运行时指南、erl 和 erts_alloc 文档。
内核页面缓存
除了 RabbitMQ 节点直接分配和使用的内存外,该节点读取的文件还可以由操作系统缓存。此缓存提高了 I/O 操作效率,并在 OS 检测到已使用高百分比的可用内存时被驱逐(清除)。
使用 RabbitMQ streams 的工作负载通常会导致内核页面缓存大小很大,尤其是在消费者访问跨越数天或数周的消息时。
某些监控工具不将页面缓存的大小包括在进程监控指标中。其他工具将其添加到进程的常驻集大小 (RSS) 占用量中。这可能会导致混淆:页面缓存不是由 RabbitMQ 节点维护或控制的。它由操作系统内核维护、控制和驱逐(清除)。
这在 基于 Kubernetes 的 部署 (不使用 cgroup v2 的部署) 中尤其常见,并且使用基于使用 cgroups v1 的较旧发行版的容器镜像运行 RabbitMQ。
强烈建议使用 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 版本中,所有类型的队列(经典队列、quorum 队列、流)要么根本不将消息存储在内存中,要么仅在内存中存储有限数量的消息
由垃圾回收过程引起的整体 RabbitMQ 节点内存使用量显着激增的可能性非常小。
总内存使用量计算策略
RabbitMQ 可以使用不同的策略来计算节点使用的内存量。从历史上看,节点从运行时获取此信息,报告使用了多少内存(不仅仅是分配了多少内存)。此策略称为 legacy
(erlang
的别名),倾向于低报,不建议使用。
有效策略使用 vm_memory_calculation_strategy
键配置。有两个主要选项
-
rss
使用特定于 OS 的方法查询内核,以查找节点 OS 进程的 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
要了解节点使用的策略,请参阅其有效配置。