要理解和调整内核的内存管理行为,首先需要了解它的工作方式以及它与其他子系统的协作方式。
内存管理子系统,也称为虚拟内存管理器,随后将被称为“VM”。VM 的作用是管理整个内核和用户程序的物理内存(RAM)的分配。它还负责为用户进程提供虚拟内存环境(通过带有 Linux 扩展的 POSIX API 管理)。最后,当内存不足时,VM 会通过修剪缓存或交换“匿名”内存来释放 RAM。
在检查和调整 VM 时,最重要的是理解它的缓存是如何管理的。VM 缓存的基本目标是最小化由交换和文件系统操作(包括网络文件系统)产生的 I/O 成本。这是通过避免 I/O 或以更好的模式提交 I/O 来实现的。
空闲内存会被这些缓存根据需要使用和填充。可用于缓存和匿名内存的内存越多,缓存和交换的效率越高。但是,如果遇到内存不足的情况,缓存将被修剪或内存将被交换出去。
对于特定的工作负载,可以首先提高性能的方法是增加内存并减少必须修剪或交换内存的频率。其次是更改内核参数来更改缓存的管理方式。
最后,工作负载本身也应该被检查和调整。如果允许应用程序运行更多的进程或线程,VM 缓存的有效性可能会降低,如果每个进程都在文件系统的自己的区域中运行。内存开销也会增加。如果应用程序分配自己的缓冲区或缓存,更大的缓存意味着可用于 VM 缓存的内存更少。但是,更多的进程和线程意味着有更多机会重叠和流水线 I/O,并且可能更好地利用多个核心。为了获得最佳结果,需要进行实验。
内存分配可以归类为“固定”(也称为“不可回收”)、“可回收”或“可交换”。
匿名内存倾向于程序堆和栈内存(例如,>malloc())。它是可回收的,除非在特殊情况下,例如 mlock 或没有可用的交换空间。在可以回收匿名内存之前,必须将其写入交换空间。交换 I/O(包括页面调入和调出)往往不如页面缓存 I/O 高效,因为分配和访问模式不同。
文件数据的缓存。当文件从磁盘或网络读取时,其内容存储在页面缓存中。如果内容在页面缓存中是最新的,则不需要磁盘或网络访问。tmpfs 和共享内存段计入页面缓存。
当将文件写入时,新数据存储在页面缓存中,然后再写回磁盘或网络(使其成为写回缓存)。如果页面有尚未写入的新数据,则称为“脏”页面。未分类为脏的页面是“干净”页面。如果内存不足,可以通过简单地释放它们来回收干净的页面缓存页面。在回收脏页面之前,必须先使其干净。
这是块设备的页面缓存类型(例如,/dev/sda)。文件系统通常在访问其磁盘上的元数据结构(例如 inode 表、分配位图等)时使用缓冲缓存。缓冲缓存可以像页面缓存一样被回收。
当应用程序写入文件时,页面缓存会变脏,缓冲缓存也可能变脏。当脏内存的数量达到以字节为单位的指定页面数(vm.dirty_background_bytes),或者当脏内存达到总内存的特定比例(vm.dirty_background_ratio),或者当页面变脏的时间超过指定的时长(vm.dirty_expire_centisecs)时,内核开始从最先变脏的文件开始写回页面。背景字节和比例是互斥的,设置一个会覆盖另一个。刷新线程在后台执行写回,并允许应用程序继续运行。如果 I/O 无法跟上应用程序使页面缓存变脏的速度,并且脏数据达到关键设置(vm.dirty_bytes 或 vm.dirty_ratio),则应用程序开始被节流,以防止脏数据超过此阈值。
VM 监控文件访问模式,并可能尝试执行预读。预读将尚未请求的文件中的页面读取到页面缓存中。这是为了允许提交更少、更大的 I/O 请求(更有效)。并且为了实现 I/O 的流水线化(在应用程序运行时同时执行 I/O)。
在 openSUSE Leap 15.6 上运行的应用程序可以分配比旧版本更多的内存。这是因为 glibc 在分配用户空间内存时更改了其默认行为。有关这些参数的说明,请参阅 https://gnu.ac.cn/s/libc/manual/html_node/Malloc-Tunable-Parameters.html。
要恢复类似于旧版本的行为,应将 M_MMAP_THRESHOLD 设置为 128*1024。可以通过应用程序中的 mallopt() 调用或通过在运行应用程序之前设置环境变量 MALLOC_MMAP_THRESHOLD_ 来完成此操作。
可回收的内核内存(上述缓存)会在内存不足时自动修剪。大多数其他内核内存不能轻易减少,而是给定内核的工作负载的属性。
减少用户空间工作负载的要求可以减少内核内存使用量(更少的进程、更少的文件和套接字等)。
在调整 VM 时,应理解某些更改需要时间才能影响工作负载并完全生效。如果工作负载全天都在变化,则在不同的时间可能表现不同。在某些条件下提高吞吐量的更改,在其他条件下可能会降低吞吐量。
/proc/sys/vm/swappiness
此控件用于定义内核相对于页面缓存和其他缓存,积极交换匿名内存的程度。增加该值会增加交换量。默认值为 60。
交换 I/O 往往比其他 I/O 效率低得多。但是,某些页面缓存页面比不太使用的匿名内存访问更频繁。应该在此处找到正确的平衡。
如果在减慢速度期间观察到交换活动,则可能值得降低此参数。如果存在大量的 I/O 活动,并且系统中的页面缓存量相对较小,或者如果存在大型休眠应用程序,则增加此值可以提高性能。
交换出去的数据越多,系统重新调入数据所需的时间就越长。
/proc/sys/vm/vfs_cache_pressure
此变量控制内核回收用于缓存 VFS 缓存的内存的倾向,相对于页面缓存和交换。增加此值会增加回收 VFS 缓存的速度。
很难知道应该何时更改此设置,除非通过实验。命令 slabtop(procps 包的一部分)显示内核使用的顶部内存对象。vfs 缓存是“dentry”和“*_inode_cache”对象。如果这些对象相对于页面缓存消耗了大量的内存,则可能值得尝试增加压力。也可以帮助减少交换。默认值为 100。
/proc/sys/vm/min_free_kbytes
这控制着保留用于特殊保留的内存量,包括“原子”分配(不能等待回收的分配)。通常不应降低此值,除非正在仔细调整内存使用量(通常对嵌入式而不是服务器应用程序有用)。如果日志中经常看到“页面分配失败”消息和堆栈跟踪,可以增加 min_free_kbytes,直到错误消失。如果这些消息不常见,则无需担心。默认值取决于 RAM 的数量。
/proc/sys/vm/watermark_scale_factor广义地说,空闲内存具有高、低和最小值水印。当达到低水印时,kswapd 会唤醒以在后台回收内存。它会保持唤醒状态,直到空闲内存达到高水印。当达到最小值水印时,应用程序将停止并回收内存。
watermark_scale_factor 定义了节点/系统在唤醒 kswapd 之前剩余的内存量,以及 kswapd 回到休眠状态之前需要释放的内存量。单位是 10,000 的分数。默认值 10 表示节点/系统中可用内存之间的距离为 0.1%。最大值为 1000,即内存的 10%。
频繁在直接回收中停滞的工作负载,由 /proc/vmstat 中的 allocstall 统计,可以通过更改此参数来受益。同样,如果 kswapd 过早进入休眠状态,如 kswapd_low_wmark_hit_quickly 统计所示,则可能表明为了避免停滞而保留的空闲页数太少。
自 openSUSE Leap 10 以来,写回行为的一个重要变化是,对基于文件的 mmap() 内存的修改会立即被记为脏内存(并受到写回的影响)。而之前只有在取消映射、调用 msync() 系统调用或在内存压力较大时才会受到写回的影响。
有些应用程序不希望 mmap 修改受到这种写回行为的影响,并且性能可能会降低。增加写回比率和时间可以改善这种类型的减慢。
/proc/sys/vm/dirty_background_ratio
这是总可用和可回收内存的百分比。当脏页缓存量超过此百分比时,写回线程开始写入脏内存。默认值为 10 (%)。
/proc/sys/vm/dirty_background_bytes
这包含脏内存的数量,当达到此数量时,后台内核刷新线程开始写回。dirty_background_bytes 是 dirty_background_ratio 的对应项。如果其中一个被设置,另一个会自动读取为 0。
/proc/sys/vm/dirty_ratio
与 dirty_background_ratio 相似的百分比值。当超过此值时,想要写入页缓存的应用程序会被阻塞,并等待内核后台刷新线程减少脏内存的数量。默认值为 20 (%)。
/proc/sys/vm/dirty_bytes
此文件控制与 dirty_ratio 相同的可调参数,但是脏内存的数量以字节为单位,而不是可回收内存的百分比。由于 dirty_ratio 和 dirty_bytes 都控制相同的可调参数,如果其中一个被设置,另一个会自动读取为 0。dirty_bytes 的允许最小值是两个页面(以字节为单位);任何低于此限制的值都将被忽略,并将保留旧配置。
/proc/sys/vm/dirty_expire_centisecs
数据在内存中处于脏状态的时间超过此间隔后,下次唤醒刷新线程时会将其写入。过期是基于文件 inode 的修改时间来衡量的。因此,来自同一文件的多个脏页都将在超过间隔时写入。
dirty_background_ratio 和 dirty_ratio 共同决定了页缓存写回行为。如果这些值增加,更多的脏内存将在系统中保留更长的时间。允许系统中存在更多的脏内存,可以提高吞吐量,避免写回 I/O 并提交更优的 I/O 模式。但是,更多的脏内存可能会损害在需要回收内存或在数据完整性(“同步点”)时需要将其写回磁盘时的延迟。
/sys/block/<bdev>/queue/read_ahead_kb
如果一个或多个进程正在顺序读取文件,内核会提前读取某些数据以减少进程等待数据可用所需的时间。实际提前读取的数据量是动态计算的,基于 I/O 的顺序程度。此参数设置单个文件内核提前读取的最大数据量。如果您发现从文件的大量顺序读取不够快,可以尝试增加此值。增加过多可能会导致预读抖动,即用于预读的页缓存会在使用之前被回收,或者由于大量的无用 I/O 而导致速度变慢。默认值为 512 (KB)。
透明大页 (THP) 提供了一种动态分配大页的方法,要么由进程按需分配,要么由内核线程 khugepaged 延迟分配。这种方法不同于使用 hugetlbfs 手动管理它们的分配和使用。具有连续内存访问模式的工作负载可以从 THP 中受益匪浅。在运行具有连续内存访问模式的合成工作负载时,可以观察到 1000 倍的页面错误减少。
在某些情况下,THP 可能不受欢迎。具有稀疏内存访问模式的工作负载由于过度内存使用而可能无法通过 THP 获得良好的性能。例如,每次发生错误时可能会使用 2 MB 的内存,而不是 4 KB,最终导致过早的页面回收。
THP 的行为可以通过内核参数 transparent_hugepage= 或通过 sysfs 进行配置。例如,可以通过添加内核参数 transparent_hugepage=never、重建 grub2 配置并重新启动来禁用它。使用以下命令验证是否禁用了 THP:
# cat /sys/kernel/mm/transparent_hugepage/enabled
always madvise [never]如果已禁用,则会在方括号中显示值 never,如上面的示例所示。值为 always 强制尝试并在发生错误时使用 THP,但如果分配失败,则推迟到 khugepaged。值为 madvise 仅为应用程序明确指定的地址空间分配 THP。
/sys/kernel/mm/transparent_hugepage/defrag
此参数控制应用程序在分配 THP 时付出的努力。值为 always 是 openSUSE 42.1 及更早版本支持 THP 的默认值。如果不可用 THP,应用程序会尝试碎片整理内存。如果内存碎片化且不可用 THP,则可能会在应用程序中产生大的停顿。
值为 madvise 表示 THP 分配请求仅在应用程序显式请求时才会进行碎片整理。这是 openSUSE 42.2 及更高版本的默认值。
defer 仅在 openSUSE 42.2 及更高版本中可用。如果不可用 THP,应用程序将回退到使用小页面,并唤醒 kswapd 和 kcompactd 内核线程以在后台碎片整理内存,然后 khugepaged 稍后将分配 THP。
最终选项 never 在不可用 THP 时使用小页面,但不会采取任何其他操作。
当 transparent_hugepage 设置为 always 或 madvise 时,khugepaged 会自动启动,如果设置为 never,则会自动关闭。通常,它以低频率运行,但可以对其行为进行调整。
/sys/kernel/mm/transparent_hugepage/khugepaged/defrag 值为 0 将禁用 khugepaged,即使 THP 仍可能在发生错误时使用。这对于受益于 THP 但无法容忍 khugepaged 尝试更新应用程序内存使用情况时发生的停顿的延迟敏感型应用程序非常重要。
/sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan此参数控制 khugepaged 在单次扫描中扫描的页面数。扫描识别可以重新分配为 THP 的小页面。增加此值将以牺牲 CPU 使用率为代价更快地在后台分配 THP。
/sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecskhugepaged 在每次扫描后会休眠一段由此参数指定的时间,以限制 CPU 使用量。减少此值将以牺牲 CPU 使用率为代价更快地在后台分配 THP。值为 0 将强制持续扫描。
/sys/kernel/mm/transparent_hugepage/khugepaged/alloc_sleep_millisecs
此参数控制 khugepaged 在后台等待 kswapd 和 kcompactd 采取行动时休眠多长时间,以防无法分配 THP。
khugepaged 的剩余参数很少用于性能调整,但已在 /usr/src/linux/Documentation/vm/transhuge.txt 中完全记录。
有关完整的 VM 可调参数列表,请参阅 /usr/src/linux/Documentation/sysctl/vm.txt(在安装 kernel-source 包后可用)。
一些可以帮助监控 VM 行为的简单工具
vmstat:此工具提供了 VM 正在执行操作的良好概述。有关详细信息,请参阅第 2.1.1 节,“vmstat”。
/proc/meminfo:此文件提供了内存使用情况的详细细分。有关详细信息,请参阅第 2.4.2 节,“详细内存使用情况:/proc/meminfo”。
slabtop:此工具提供了有关内核 slab 内存使用的详细信息。buffer_head、dentry、inode_cache、ext3_inode_cache 等是主要的缓存。此命令与 procps 包一起提供。
/proc/vmstat:此文件提供了内部 VM 行为的详细细分。其中的信息是特定于实现的,并不总是可用。一些信息在 /proc/meminfo 中重复,其他信息可以通过实用程序以友好的方式呈现。为了获得最大的实用性,需要随着时间的推移监控此文件以观察变化率。最重要的信息如下
pgscan_kswapd_*, pgsteal_kswapd_*这些分别报告自系统启动以来 kswapd 扫描和回收的页面数。这些值之间的比率可以解释为回收效率,低效率意味着系统难以回收内存并且可能正在抖动。轻微的活动通常不必担心。
pgscan_direct_*, pgsteal_direct_*这些分别报告应用程序直接扫描和回收的页面数。这与 allocstall 计数器的增加相关。这比 kswapd 活动更严重,因为这些事件表明进程正在停滞。结合 kswapd 和高比率的 pgpgin、pgpout 和/或高比率的 pswapin 或 pswpout 的高活动表明系统正在严重抖动。
可以使用跟踪点获得更详细的信息。
thp_fault_alloc, thp_fault_fallback这些计数器分别对应于应用程序直接分配的 THP 数量以及 THP 不可用并使用小页面的次数。通常,高回退率是无害的,除非应用程序对 TLB 压力敏感。
thp_collapse_alloc, thp_collapse_alloc_failed这些计数器对应于由 khugepaged 分配的 THP 数量以及 THP 未可用并使用小页面的次数。 高回退率意味着系统碎片化,即使应用程序的内存使用情况允许,THP 也未被使用。 这仅对对 TLB 压力敏感的应用程序来说是一个问题。
compact_*_scanned, compact_stall, compact_fail, compact_success当启用 THP 且系统碎片化时,这些计数器可能会增加。 compact_stall 在应用程序分配 THP 时发生停滞时递增。 其余计数器记录扫描的页面数、成功或失败的碎片整理事件的数量。