当前位置: 首页 > news >正文

监控 Linux 系统上的内存使用情况

大家好!我是大聪明-PLUS

从我记事起,我就一直对 Linux 内存计数器着迷:你看计数器htop——就 CPU 消耗而言,一切似乎都是 +/- 清晰的,但内存的计算方式总是与你最初预期的不同,很长一段时间以来,我对它的工作原理有着相当天真和错误的理解。

随着时间的推移,一些事情变得更加清晰,并且在一定程度上对底层工作原理有了一定的理解。在某个时候,我产生了一个实际需求,需要了解实际系统中内存的去向——而这件事再次证明了,在某些地方,系统的结构相当隐晦,这个问题并不总是容易回答。除了实际需求之外,我家里有一台服务器,里面存储着大量的指标,我一直想把它们以清晰的形式显示出来,这样以后就可以实时观察系统在各种进程发生时的行为。

在本文中,我将尝试解释如何实现此类监控以及如何解读其结果。需要说明的是,我从未从事过内核开发——以下所有信息仅基于个人经验、对内核源代码的粗略阅读以及大量的 Google 搜索。因此,我可能在某些方面存在不准确甚至完全错误的地方,但希望错得不要太多。

Linux 内存管理入门

如果不了解内核计数器究竟测量什么,那么查看内核计数器就毫无意义,因此,让我们先来描述一下它内部工作原理的基本原理(为了简化事情,否则,您需要写一整本书,而不是一篇文章)。

释放内存

或许首先值得一提的是,如果你观察一个运行了一段时间的系统,它通常会有非常少量的可用内存。这很正常,因为 Linux 将可用内存定义为完全空闲且未存储任何内容的内存。但这部分资源非常宝贵,不能闲置——内核总是试图充分利用所有可用内存,将其分配给缓存,以提高系统性能,但在需要时可以快速释放。因此,在大多数系统中,页面缓存通常会占用所有可用内存。

页面缓存

如果您编写一个程序将一些数据写入文件,然后该程序(或甚至另一个程序)读取它,您可能会注意到一个有趣的特性:即使文件非常大(千兆字节),但小于可用内存量,那么写入操作和从文件中读取操作都会非常快地发生 - 比磁盘处理它们的速度快得多。

重点在于,当程序将数据写入磁盘(执行write(2)系统调用)时,该系统调用的功能通常只是将数据写入内存并立即返回。只有此时(异步),内核才会将数据写入磁盘。写入后,数据通常保留在内存中,后续对同一文件的read(2)调用可以立即从内存中读取数据,而无需访问磁盘。

这个内核子系统称为页面缓存,其基本思想(简化)如下:在使用任何块设备时,所有读写操作(除非另有明确请求 - 参见O_DIRECTopen(2) )都通过页面缓存进行。如果内核需要从磁盘读取信息,它会以 4 KiB 的页面大小将其读入页面缓存,然后通过页面缓存访问数据。写入时,信息也首先进入页面缓存,然后才会异步刷新到磁盘(除非通过fsync(2)提前请求)。这导致了一个有趣的特性:在一般没有内存压力的情况下(如下所述),写入总是即时的(因为我们本质上是写入内存,而不是磁盘),但读取可能需要很长时间(如果文件尚未缓存在页面缓存中)。

总的来说,页面缓存是一种用途广泛且功能强大的机制,几乎随处可见。例如:

  1. 使用mmap(2),您可以将文件加载到内存中,并像操作普通内存一样对其进行操作。由于预取机制,当您访问特定的内存位置时,操作系统会自动加载数据,甚至在您尝试访问之前就会加载。

  2. 您知道程序是如何加载执行的吗?一个简单(且看似合乎逻辑)的答案是:内核分配一块内存,从磁盘读取二进制文件,然后转移控制权执行。但实际上,事情要有趣得多:启动应用程序时,Linux 本质上会通过mmap(2)将二进制文件映射到进程内存中,并将控制权转移到相应的指令。然后,当处理器在这些指令之间跳转时,它会将数据加载到页面缓存中,并从那里返回。因此,即使非常大的二进制文件,如果实际执行的代码只有一小部分,也会消耗少量内存。然而,还有另一个副作用:在内存压力较大的情况下(见下文),系统可能会刷新已加载二进制文件的页面缓存,即使您没有交换文件,您也会遇到与操作系统将数据交换到磁盘时完全相同的延迟。

缓冲区

令人惊讶的是,这个计数器是最令人困惑的。我在谷歌上找到的所有信息都自相矛盾,文档甚至指出“原始磁盘块的相对临时存储不应该太大(20MB 左右)”——尽管我经常看到它显示 GB 级。

但实际上,这很简单:在早期版本的 Linux 中,页面缓存子系统现在执行的工作是由两个独立的子系统执行的。现在情况已经不同了,但/proc/meminfo旧的行为仍然存在,并且计数器Cached会显示Buffers不同缓存类型的统计信息:

  • Cached负责在通过文件系统访问文件时缓存文件内容。

  • Buffers它还负责缓存其他所有内容:包含文件系统元数据的块、磁盘布局以及直接读取磁盘时的原始块。

换句话说:

  • Cached由于以下命令而增加:cat /dev/urandom > outcat big_file > /dev/null

  • Buffersls -laR / > /dev/null在和的情况下增加dd if=/dev/sda of=/dev/null bs=10M status=progress

匿名记忆

从技术角度来看,页缓存是一个内存页面,用于缓存块设备的内容,因此与块设备绑定。与页缓存不同的是,页缓存是匿名内存,它没有备份文件,独立存在。

在这种情况下,无需讨论不必要的细节,最正确的说法是匿名内存本质上是进程的所有用户空间内存:堆栈、全局变量和堆。

shmem(共享内存)

共享内存指的是以下内容:

  1. 使用mmap(2) +创建的匿名内存块,MAP_ANONYMOUS | MAP_SHARED用于在多个进程之间共享。

  2. tmpfs– 完全驻留在内存中的虚拟文件系统。例如,/run– 就是tmpfs。某些发行版也会挂载tmpfs/tmp– 中,所以切勿在那里保存大文件!这就是/var/tmp(始终驻留在磁盘上)的用途。

  3. POSIX IPC API(shm_overview(7),sem_overview(7) ),实际上是在 Linux 中相同的 API 之上实现的tmpfs,它安装在 中/dev/shm

顺便提一下,一个有趣且不太明显的事实:在 Linux 中,所有文件系统都在页面缓存之上运行——因此,你放入其中的所有内容都tmpfs算作页面缓存。这只是一个不太常见的页面缓存——它下面没有文件,只有内存中的“缓存”页面。

交换

swap 是磁盘上的可选分区或文件,当长时间未使用或内核感觉可用内存不足时,内核可以转储匿名和 tmpfs 内存。

当进程访问已换出的内存时,内核不会立即将内存从交换区移至 RAM。它首先会将所需的页面加载到内存中,而不会将其从交换区中移除(这样,如果这些页面没有及时换出,内核可以快速地再次移除它们)。这种页面状态称为“交换区缓存”。

还有各种附加选项,包括zswap,它会在实际交换之前构建内存缓存,以压缩格式存储交换的页面。这是一个非常有趣的功能——我强烈推荐它。

总的来说,关于交换(交换的必要性)的话题颇具争议,值得另写一篇文章。

页表

您可能已经知道,每个进程都在其自己的虚拟内存空间中运行。虚拟内存是在硬件级别( MMU )

实现的抽象:对于每个进程,内核都会创建将虚拟地址映射到物理地址的表,在执行过程中,处理器会使用这些表为当前进程虚拟化内存。

这些表是多级的,以最小化它们的大小,以适应典型情况,即一个进程只使用其虚拟空间的一小部分 - 除了一些特殊情况,例如一组进程共享相同的内存块,页表可以被认为是对正在使用的物理内存的固定税,因此它们的大小总是可预测的并且相对较小。

活跃/不活跃/不可驱逐

所有匿名、页面缓存和交换缓存内存分为:

  • 活跃的——最近访问过的页面;

  • 不活跃 – 长时间未访问的页面;

如果内存不足,内核会先尝试清除非活动页面,然后再处理活动页面。

简单来说,活跃/不活跃划分如下:

  1. 每个页面都有accessed一个-bit(在页表级别)。

  2. 如果内核因任何原因处理对该页面的访问,它就会设置此标志。

  3. 如果进程不通过内核访问页面,则该标志由MMU设置。

  4. 内核定期扫描页面,如果发现设置accessed位,则清除该位并将给定页面从非活动状态提升为活动状态。

需要注意的是,非活动页面不应被视为“N 秒内未访问的页面”——它并非如此。区分活动/非活动页面的目的并非跟踪页面活动,而是为了确定所有可用页面的回收优先级。因此,这种区分实际上相当随意,反映的是页面的相对需求,而非其实际使用情况。

slab、kmalloc、vmalloc

为了完成工作,内核需要为存储当前系统状态的各种结构体分配内存。为此,内核提供了一个slab分配器。其本质如下:最常用的结构体(例如,描述进程的结构体task_struct)拥有自己的内存池,可以访问该内存池并请求为新对象分配内存。当我们将对象返回到内存池时,内存并不会立即释放(因为我们预计新的分配请求可能会在短时间内到达)。因此,slab 分配器通常包含一定量的内存,如果其他地方需要,可以随时快速释放。

此外,slab 中的对象类型本身可以标记为可回收——这意味着它本质上是一个缓存,并且可以根据需要移除此类对象。这类对象最典型的例子可能是 dentry/inode 缓存,与页面缓存不同,它们缓存的是文件元数据和目录内容,而不是数据。有了它们,当你执行类似 [unclear/unclear] 的操作时open("/a/b/c/d/e", ...),系统不必每次都遍历所有目录来查找最终文件(负面搜索结果也会被缓存)。

通常,slab 分配器用于频繁且大量创建的结构。但是,如果您只需要在本地分配一些内存,则可以使用 malloc(3) 分配器kmalloc()(含义类似于malloc(3)),它实际上是 slab 分配器的超集。对于分配大块内存(但不保证物理页面一致性),可以使用伙伴分配器vmalloc(),它与 slab 无关,直接从伙伴分配器分配内存。

可回收内存和内存压力

在运行过程中,内核监视内存的当前状态并:

  1. 将未使用的内存移至交换区,以便更有效地利用它(例如,用于页面缓存);

  2. 如果有迹象表明内存可能很快就会耗尽,则主动释放内存;

  3. 当内存不足时积极释放内存。

为此,在内核中:

  1. 触发主动和积极内存回收的可用内存量阈值可配置。主动内存回收会提前为已分配的后台线程回收内存;积极内存回收会为进程本身回收内存,迫使进程花费 CPU 时间来寻找空闲的内存页。

  2. 有一种想法是,在内存不足的情况下,可以刷新上面提到的所有缓存(但是,如果是脏页缓存,则必须先将其写入磁盘)。

  3. 内存分为活动内存和非活动内存,内核首先尝试驱逐非活动内存页面。

内核堆栈

你可能知道,进程中的每个线程都有自己的堆栈。但有趣的是,它实际上有两个:用户空间和内核空间。我记得很久以前,我曾认为系统调用就像调用某个服务的 API:我们发送一个请求,它被放入队列等待处理,然后被搁置,直到另一端有人有时间处理它。但内核的工作方式并非如此。:) 当你进行系统调用时,会发生上下文切换——本质上,你的线程会继续工作——但是处理系统调用的内核代码现在正在执行,而这段代码需要自己的堆栈才能运行。

user顺便说一下,CPU 消耗被分为和正是出于这个原因system:即,这确实是特定进程/线程(内核术语中的任务)的运行时间,但在不同的上下文中。

监视/proc/meminfo

我先声明一下,我不会试图在本文中设定任何特别宏大的目标,尽可能详细地监控所有可能的情况。我将仅限于提供的信息/proc/meminfo

幸运的是,在指标收集方面,一切都已为我们完成——Prometheus将所有计数器显示/proc/meminfo为指标node_memory_*。然而,解释结果值需要一些工作:首先,其中一些值在它们所占的内存页面上重叠,并且并不总是清楚需要减去或加总哪些值才能得到细分。其次,它们的文档有时非常模糊,而且并不总是能立即清楚它们究竟测量什么。

下面,每个标题代表一个单独的图表,我们将从中形成我们的仪表板。

内存使用情况

首先,让我们尝试创建一个图表,从鸟瞰的角度查看整体情况,将“实际使用”的内存与各种缓存分开:

  • Free = MemFree

  • Caches = Cached - Shmem + Buffers + KReclaimable

  • Used = MemTotal - Free - Caches

已用内存

这里我们详细说明了所有非缓存的内存:

  • Anonymous = AnonPages

  • Slab = SUnreclaim

  • Swap cached = SwapCached

  • zswap = Zswap– zswap子系统消耗的内存

  • Page tables = PageTables + SecPageTables

  • Kernel stacks = KernelStack

  • vmalloc = VmallocUsed - KernelStack

  • percpu = Percpu

  • shmem = Shmem

缓存

这里我们将更详细地展示哪些缓存占用了多少空间:

  • Page cache = Cached - Shmem

  • Buffers = Buffers

  • Slab = SReclaimable

  • Misc = KReclaimable - SReclaimable

让我们启动它sync && echo 3 > /proc/sys/vm/drop_caches并查看系统实际上可以释放多少个缓存(剧透:不是全部)。

值得注意的是,也可能出现相反的情况——可回收内存的实际数量比乍一看的要大。

未知

尽管我们希望详细列出所有内存,但不可能做到 100% 准确——因为有些分配是直接由伙伴分配器进行的,并且不旋转任何计数器。因此,分配的内存中的一部分(通常很小)将超出我们的计算范围,在这里记住这一点很重要:

Unknown = MemTotal - MemFree - (AnonPages + SwapCached + Zswap + SUnreclaim + VmallocUsed + PageTables + SecPageTables + Percpu) - (Cached + Buffers + KReclaimable)

顺便说一句,这里有必要解释一下什么是页面MemTotal。内核启动时会检测系统中所有可用的内存,并为每个物理页面创建一个struct page结构体,供伙伴分配器使用。MemTotal页面大小反映了可供分配的总内存,粗略地说,等于页面数量struct page乘以页面大小(不包括伙伴分配器本身占用的内存,以及为硬件保留的内存和包含内核代码的内存)。这就是为什么它总是小于物理内存大小的原因。

可用内存

这里我们只有一个指标——MemAvailable它表示应用程序在不切换到交换的情况下可分配的内存估计量。

它的计算方法如下:占用所有空闲页面,将整个页面缓存和可回收的内核内存添加到其中,并根据水位线、内存压力以及仍然不可能完全回收所有内存的事实进行调整,因为事实上,系统正常运行仍然需要其中的一部分。

内存交换

这里我们将尝试从回收功能的角度展示内核如何看待用户内存:

  • Active anonymous = Active(anon)

  • Inactive anonymous = Inactive(anon)

  • Active page cache = Active(file)

  • Inactive page cache = Inactive(file)

  • Unevictable = Unevictable

页面缓存写回

在此图中,我们将监控脏页缓存的容量以及系统如何处理将其页面同步到磁盘。

  • Writeback = Writeback– 当前正在写入磁盘的脏页缓存量

  • Dirty = Dirty- 仍在等待轮到的脏页

交换使用情况

  • Cashed = SwapCached– 交换缓存在内存中的页面

  • zswapped = Zswapped– 交换到zswap的内存量

  • Swapped out = SwapTotal - SwapFree - SwapCached - Zswapped– 内存卸载(而不是缓存)到磁盘

zswap

这里我有自己的zswap导出器,它允许我额外监控:

  1. 我们实际获得的压缩程度;

  2. 填充 zswap 池;

  3. 页面飞过 zswap 的原因。

数据一致性

这可能并不明显,但它输出的计数器/proc/meminfo(以及 proc/sysfs 中的许多其他文件)可能彼此不一致。具体来说,在收集统计信息时,内核会以原子方式读取/计算各个值,但不使用任何锁来确保一致性。这是内核开发人员刻意做出的妥协,以确保收集用于监控的数据(这种不一致性通常并不重要)不会降低系统速度。因此,如果您将它们用于比绘制图表更重要的事情(例如,从其他数字中减去一些数字偶尔会产生负值),则值得考虑这一事实。

结论

上面,我们监控了整个系统。这当然非常有趣,但它会立即让您想要更进一步——看看哪个服务触发了图表上的特定变化。这要归功于 systemd 和 cgroups!systemd 将整个系统划分为易于理解的进程组(与 PID 不同,进程组具有人类可读的名称,而且最重要的是,数量有限)——如果您准备好迎接数量大幅增加的指标,您可以单独监控每个服务,从而将系统的可观察性提升到一个全新的水平。

在本文中,我不会尝试涵盖所有可能涉及的内容,因此我只是暗示了这种可能性——也许有一天这会成为另一篇文章的主题。我编写它的目的并非为了除我之外的任何人使用它,但也许它可以为某些人提供灵感,或作为如何设置此类监控的一个实际示例。

http://www.dtcms.com/a/528349.html

相关文章:

  • 湖北省住房与建设厅网站高品质的网站开发
  • 智慧校园建设方案-6PPT(32页)
  • Spring的@Cacheable取缓存默认实现
  • MySQL-TrinityCore异步连接池的学习(七)
  • 2020应该建设什么网站建网站的论坛
  • 华为OD机考双机位A卷 - Excel单元格数值统计 (C++ Python JAVA JS GO)
  • SpringBoot集成Elasticsearch | Elasticsearch 7.x专属HLRC(High Level Rest Client)
  • 广东省住房城乡建设厅门户网站免费下载手机app
  • 信创入门指南:一文掌握信息技术应用创新的核心要点
  • 基于鸿蒙UniProton的物联网边缘计算:架构设计与实现方案
  • 基于Swin Transformer的脑血管疾病中风影像诊断系统研究
  • 宝安第一网站东莞关键词优化软件
  • 篮球论坛|基于SprinBoot+vue的篮球论坛系统(源码+数据库+文档)
  • SQL 进阶:触发器、存储过程
  • ansible快速准备redis集群环境
  • 公司网站制作效果长沙网站制造
  • 数据结构之堆
  • 【Linux学习笔记】日志器与线程池设计
  • 【Linux系统编程】编辑器vim
  • 鸿蒙ArkTS入门教程:小白实战“易经”Demo,详解@State、@Prop与List组件
  • 扩散模型与UNet融合的创新路径
  • 从入门到精通的鸿蒙学习之路——基于鸿蒙6.0时代的生态趋势与实战路径
  • 704.力扣LeetCode_二分查找
  • 如何做企业网站宣传wordpress 显示空白
  • 机器学习库的线性回归预测
  • 旅游网站开发研究背景北京欢迎您
  • 做网站要学什么东西企业网站运维
  • Orleans Grain Directory 系统综合分析文档
  • 从PN结到GPIO工作模式
  • 面向社科研究者:用深度学习做因果推断(三)