一、文件页和匿名页
1、文件页
当发生了内存泄漏时,或者运行了大内存的应用程序,导致系统的内存资源紧张时,系统又会如何应对呢?内存回收和 OOM 杀死进程。
内存资源紧张导致的 OOM(Out Of Memory),指的是系统杀死占用大量内存的进程,释放这些内存,再分配给其他更需要的进程。
内存回收,也就是系统释放掉可以回收的内存,比如缓存和缓冲区,就属于可回收内存。它们在内存管理中,通常被叫做文件页(File-backed Page)。
大部分文件页,都可以直接回收,以后有需要时,再从磁盘重新读取就可以了。而那些被应用程序修改过,并且暂时还没写入磁盘的数据(也就是脏页),就得先写入磁盘,然后才能进行内存释放。这些脏页,一般可以通过两种方式写入磁盘。
可以在应用程序中,通过系统调用 fsync ,把脏页同步到磁盘中;
也可以交给系统,由内核线程 pdflush 负责这些脏页的刷新。
除了缓存和缓冲区,通过内存映射获取的文件映射页,也是一种常见的文件页。它也可以被释放掉,下次再访问的时候,从文件重新读取。
2、匿名页
应用程序动态分配的堆内存,也就是我们在内存管理中说到的匿名页(Anonymous Page),这些内存自然不能直接释放。但是,如果这些内存在分配后很少被访问,似乎也是一种资源浪费。是不是可以把它们暂时先存在磁盘里,释放内存给其他更需要的进程?
这正是 Linux 的 Swap 机制。Swap 把这些不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。
二、Swap工作原理
在Linux下,SWAP的作用类似Windows系统下的“虚拟内存”。当物理内存不足时,拿出部分硬盘空间当SWAP分区(虚拟成内存)使用,从而解决内存容量不足的情况。说白了就是把一块磁盘空间或者一个本地文件(以下讲解以磁盘为例),当成内存来使用。它包括换出和换入两个过程。
换出,就是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存。
换入,则是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来。
Swap 其实是把系统的可用内存变大了。即使服务器的内存不足,也可以运行大内存的应用程序。事实上,内存再大,对应用程序来说,也有不够用的时候。
系统总是在物理内存不够时,才进行Swap交换。swap大小是有上限的,一旦swap使用完,操作系统会触发OOM-Killer机制,把消耗内存最多的进程kill掉以释放内存。
1、swap使用典型场景:
一个很典型的场景就是,即使内存不足时,有些应用程序也并不想被 OOM 杀死,而是希望能缓一段时间,等待人工介入,或者等系统自动释放其他进程的内存,再分配给它。
我们常见的笔记本电脑的休眠和快速开机的功能,也基于 Swap 。休眠时,把系统的内存存入磁盘,这样等到再次开机时,只要从磁盘中加载内存就可以。这样就省去了很多应用程序的初始化过程,加快了开机速度。
Swap 是为了回收内存,那么 Linux 到底在什么时候需要回收内存呢?前面一直在说内存资源紧张,又该怎么来衡量内存是不是紧张呢?
1、直接内存回收
在内存分配时发现没有足够空闲内存时会立刻触发内存回收。
一个最容易想到的场景就是,有新的大块内存分配请求,但是剩余内存不足。这个时候系统就需要回收一部分内存(比如前面提到的缓存),进而尽可能地满足新内存请求。这个过程通常被称为直接内存回收。
2、定期回收内存
开启了一个守护进程(swapd进程)周期性对系统内存进行检查,在可用内存降低到特定阈值之后主动触发内存回收。专门的内核线程用来定期回收内存,也就是 kswapd0。为了衡量内存的使用情况,kswapd0 定义了三个内存阈值(watermark,也称为水位),分别是:
- 页最小阈值(pages_min):
通过内核选项 /proc/sys/vm/min_free_kbytes 来间接设置 - 页低阈值(pages_low):
根据页最小阈值计算,计算方式如下:
pages_low = pages_min*5/4
- 页高阈值(pages_high):
根据页最小阈值计算,计算方式如下:
pages_high = pages_min*3/2
- 剩余内存,则使用pages_free 表示。
kswapd0 定期扫描内存的使用情况,并根据剩余内存落在这三个阈值的空间位置,进行内存的回收操作。
- 剩余内存小于页最小阈值,说明进程可用内存都耗尽了,只有内核才可以分配内存。
- 剩余内存落在页最小阈值和页低阈值中间,说明内存压力比较大,剩余内存不多了。这时 kswapd0 会执行内存回收,直到剩余内存大于高阈值为止。
- 剩余内存落在页低阈值和页高阈值中间,
- 说明内存有一定压力,但还可以满足新内存请求。
- 剩余内存大于页高阈值,说明剩余内存比较多,没有内存压力。
一旦剩余内存小于页低阈值,就会触发内存的回收。
三、swappiness
内存回收的内存既包括了文件页,又包括了匿名页。
- 对文件页的回收,当然就是直接回收缓存,或者把脏页写回磁盘后再回收。
- 对匿名页的回收,其实就是通过 Swap 机制,把它们写入磁盘后再释放内存。
两种不同的内存回收机制,那么在实际回收内存时,到底该先回收哪一种呢?
Linux 提供了一个 /proc/sys/vm/swappiness 选项,用来调整使用 Swap 的积极程度。swappiness 的范围是 0-100,数值越大,越积极使用 Swap,也就是更倾向于回收匿名页;数值越小,越消极使用 Swap,也就是更倾向于回收文件页。
# cat /proc/sys/vm/swappiness
60
# sysctl -a |grep vm.swappiness
vm.swappiness=60
上面的60代表的是:当物理内存被使用(100-60)%的时候才会使用到swap,
- vm.swappiness=0:表示最大限度使用物理内存,然后使用swap
- vm.swappiness=100:表示积极的使用swap分区。
- vm.swappiness=60:默认值
值得注意的是,swappiness不是内存的百分比,而是调整 Swap 积极程度的权重,即使你把它设置成 0,当剩余内存 + 文件页小于页高阈值时,还是会发生 Swap。
四、NUMA 与 Swap
很多情况下,你明明发现了 Swap 升高,可是在分析系统的内存使用时,却很可能发现,系统剩余内存还多着呢。为什么剩余内存很多的情况下,也会发生 Swap 呢?
正是处理器的 NUMA (Non-Uniform Memory Access)架构导致的。
1、NUMA
在 NUMA 架构下,多个处理器被划分到不同 Node 上,且每个 Node 都拥有自己的本地内存空间。而同一个 Node 内部的内存空间,实际上又可以进一步分为不同的内存域(Zone),比如直接内存访问区(DMA)、普通内存区(NORMAL)、伪内存区(MOVABLE)等,如下图所示:
既然 NUMA 架构下的每个 Node 都有自己的本地内存空间,那么,在分析内存的使用时,我们也应该针对每个 Node 单独分析。
numactl 命令,来查看处理器在 Node 的分布情况,以及每个 Node 的内存使用情况。
$ numactl --hardwareavailable: 1 nodes (0)node 0 cpus: 0 1node 0 size: 7977 MBnode 0 free: 4416 MB...
输出信息中,系统中只有一个Node 0 ,而且编号为 0 和 1 的两个 CPU, 都位于 Node 0 上。另外,Node 0 的内存大小为 7977 MB,剩余内存为 4416 MB。
2、与swap的关系
三个内存阈值(页最小阈值、页低阈值和页高阈值),都可以通过内存域在 proc 文件系统中的接口 /proc/zoneinfo 来查看。
比如,下面就是一个 /proc/zoneinfo 文件的内容示例:
$ cat /proc/zoneinfo
...
Node 0, zone Normal
pages free 227894
min 14896
low 18620
high 22344
...
nr_free_pages 227894
nr_zone_inactive_anon 11082
nr_zone_active_anon 14024
nr_zone_inactive_file 539024
nr_zone_active_file 923986
...
- pages 处的 min、low、high,就是上面提到的三个内存阈值,而 free 是剩余内存页数,它跟后面的 nr_free_pages 相同。
- nr_zone_active_anon 和 nr_zone_inactive_anon,分别是活跃和非活跃的匿名页数。
- nr_zone_active_file 和 nr_zone_inactive_file,分别是活跃和非活跃的文件页数。
从这个输出结果可以发现,剩余内存远大于页高阈值,所以此时的 kswapd0 不会回收内存。
当然,某个 Node 内存不足时,系统可以从其他 Node 寻找空闲内存,也可以从本地内存中回收内存。具体选哪种模式,可以通过 /proc/sys/vm/zone_reclaim_mode
来调整。它支持以下几个选项:
默认的 0 ,表示既可以从其他 Node 寻找空闲内存,也可以从本地回收内存。
1、2、4 都表示只回收本地内存,2 表示可以回写脏数据回收内存,4 表示可以用 Swap 方式回收内存。
五、Swap使用高定位分析
开启 Swap 后,
- 设置 /proc/sys/vm/min_free_kbytes ,来调整系统定期回收内存的阈值
- 设置 /proc/sys/vm/swappiness ,来调整文件页和匿名页的回收倾向。
那么,当 Swap 使用升高时,要如何定位和分析呢?如下案例:
free 命令,查看 Swap 的使用情况。若看到,Swap 的大小是 0,这说明机器没有配置 Swap。
配置、开启 Swap:要清楚,Linux 本身支持两种类型的 Swap,即 Swap 分区和 Swap 文件。以 Swap 文件为例:
# 创建Swap文件,Swap 文件的大小为 8GB
$ fallocate -l 8G /mnt/swapfile
# 修改权限只有根用户可以访问
$ chmod 600 /mnt/swapfile
# 配置Swap文件
$ mkswap /mnt/swapfile
# 开启Swap
$ swapon /mnt/swapfile
3、执行 free 命令,确认 Swap 配置成功,free 输出中,Swap 空间以及剩余空间都从 0 变成了 8GB,说明 Swap 已经正常开启。
4、dd 命令,模拟大文件的读取
# 写入空设备,实际上只有磁盘的读请求
$ dd if=/dev/sda1 of=/dev/null bs=1G count=2048
5、运行 sar 命令,查看内存各个指标的变化情况
# 间隔1秒输出一组数据# -r表示显示内存使用情况,-S表示显示Swap使用情况
# sar -r -S
Linux 3.10.0-693.2.2.el7.x86_64 03/01/2023 _x86_64_ (2 CPU)
12:00:01 AM kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
12:10:01 AM 300752 7709704 96.25 209848 3696428 5875012 62.38 4440860 2859360 64
12:20:01 AM 272836 7737620 96.59 210312 3697144 5930336 62.97 4465256 2859592 84
12:30:01 AM 325168 7685288 95.94 212052 3658600 5882996 62.46 4414308 2861332 16
12:40:01 AM 327688 7682768 95.91 212524 3659952 5880216 62.43 4411544 2861876 60
12:50:01 AM 285232 7725224 96.44 212980 3660600 5946364 63.14 4450056 2862036 72
01:00:01 AM 313912 7696544 96.08 213360 3661196 5894736 62.59 4426120 2862068 216
12:00:01 AM kbswpfree kbswpused %swpused kbswpcad %swpcad
12:10:01 AM 1254128 153824 10.93 14208 9.24
12:20:01 AM 1254128 153824 10.93 14208 9.24
12:30:01 AM 1254128 153824 10.93 14208 9.24
12:40:01 AM 1254128 153824 10.93 14208 9.24
12:50:01 AM 1254128 153824 10.93 14208 9.24
01:00:01 AM 1254128 153824 10.93 14208 9.24
可以看到,sar 的输出结果是两个表格,第一个表格表示内存的使用情况,第二个表格表示 Swap 的使用情况。其中,各个指标名称前面的 kb 前缀,表示这些指标的单位是 KB。
去掉前缀后,大部分指标都已经见过了,剩下的几个新出现的指标,kbcommit,表示当前系统负载需要的内存。它实际上是为了保证系统内存不溢出,对需要内存的估计值。%commit,就是这个值相对总内存的百分比。
- kbactive,表示活跃内存,也就是最近使用过的内存,一般不会被系统回收。
- kbinact,表示非活跃内存,也就是不常访问的内存,有可能会被系统回收。
清楚了界面指标的含义后,结合具体数值,来分析相关的现象。可以清楚地看到,总的内存使用率(%memused)在不断增长,从开始的 23% 一直长到了 98%,并且主要内存都被缓冲区(kbbuffers)占用。具体来说:
刚开始,剩余内存(kbmemfree)不断减少,而缓冲区(kbbuffers)则不断增大,由此可知,剩余内存不断分配给了缓冲区。
一段时间后,剩余内存已经很小,而缓冲区占用了大部分内存。这时候,Swap 的使用开始逐渐增大,缓冲区和剩余内存则只在小范围内波动。
6、运行下面的 cachetop 命令,观察缓存的使用情况:
注意:bcc-tools需要内核版本为4.1或者更新的版本,如果你用的是CentOS,那就需要手动升级内核版本后再安装。
# yum install -y bcc-tools
# vim /etc/profile
export PATH=$PATH:/usr/share/bcc/tools
# source /etc/profile
# cachetop 5
11:43:15 Buffers MB: 101 / Cached MB: 994 / Sort: HITS / Order: ascending
PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT%
1851 root python 2 0 0 100.0% 0.0%
311 root jbd2/vda1-8 14 9 12 8.7% 4.3%
18609 root cachetop 25 0 0 100.0% 0.0%
通过 cachetop 的输出,我们看到,dd 进程的读写请求只有 50% 的命中率,并且未命中的缓存页数(MISSES)为 41022(单位是页)。这说明,正是案例开始时运行的 dd,导致了缓冲区使用升高。
7、为什么 Swap 也跟着升高了呢?直观来说,缓冲区占了系统绝大部分内存,还属于可回收内存,内存不够用时,不应该先回收缓冲区吗?
进一步通过 /proc/zoneinfo ,观察剩余内存、内存阈值以及匿名页和文件页的活跃情况。
观察 /proc/zoneinfo 中这几个指标的变化情况:
# -d 表示高亮变化的字段
# -A 表示仅显示Normal行以及之后的15行输出
Every 2.0s: grep -A 15 Normal /proc/zoneinfo Wed Mar 1 11:44:25 2023
Node 0, zone Normal
pages free 34975
min 16140
low 20175
high 24210
scanned 0
spanned 15728640
present 15728640
managed 15465972
nr_free_pages 34975
nr_alloc_batch 909
nr_inactive_anon 281
nr_active_anon 9394411
nr_inactive_file 2512528
nr_active_file 2935864
nr_unevictable 0
可以发现,剩余内存(pages_free)在一个小范围内不停地波动。当它小于页低阈值(pages_low) 时,又会突然增大到一个大于页高阈值(pages_high)的值。
结合刚刚用 sar 看到的剩余内存和缓冲区的变化情况,我们可以推导出,剩余内存和缓冲区的波动变化,正是由于内存回收和缓存再次分配的循环往复。
当剩余内存小于页低阈值时,系统会回收一些缓存和匿名内存,使剩余内存增大。其中,缓存的回收导致 sar 中的缓冲区减小,而匿名内存的回收导致了 Swap 的使用增大。
紧接着,由于 dd 还在继续,剩余内存又会重新分配给缓存,导致剩余内存减少,缓冲区增大。
8、如果多次运行 dd 和 sar,你可能会发现,在多次的循环重复中,有时候是 Swap 用得比较多,有时候 Swap 很少,反而缓冲区的波动更大。
换句话说,系统回收内存时,有时候会回收更多的文件页,有时候又回收了更多的匿名页。
查看 swappiness 的配置:
# cat /proc/sys/vm/swappiness
swappiness 显示的是默认值 60,这是一个相对中和的配置,所以系统会根据实际运行情况,选择合适的回收类型,比如回收不活跃的匿名页,或者不活跃的文件页。
9、Swap 换出的是哪些进程的内存?
proc 文件系统,用来查看进程 Swap 换出的虚拟内存大小,它保存在 /proc/pid/status 中的 VmSwap 中。
查看使用 Swap 最多的进程:
# 按VmSwap使用量对进程排序,输出进程名称、进程ID以及SWAP用量
# for file in /proc/*/status ; do awk '/VmSwap|Name|^Pid/{printf $2 " " $3}END{ print ""}' $file; done | sort -k 3 -n -r | head
writeback 51
watchdogd 59
watchdog/7 42
watchdog/6 37
watchdog/5 32
watchdog/4 27
watchdog/3 22
watchdog/2 17
watchdog/1 12
watchdog/0 11
从这里你可以看到,使用 Swap 比较多的是 dockerd 和 docker-containe 进程,所以,当 dockerd 再次访问这些换出到磁盘的内存时,也会比较慢。
这也说明了一点,虽然缓存属于可回收内存,但在类似大文件拷贝这类场景下,系统还是会用 Swap 机制来回收匿名内存,而不仅仅是回收占用绝大部分内存的文件页。
六、总结
- 在内存资源紧张时,Linux 会通过 Swap ,把不常访问的匿名页换出到磁盘中,下次访问的时候再从磁盘换入到内存中来。可以设置 /proc/sys/vm/min_free_kbytes,来调整系统定期回收内存的阈值;也可以设置 /proc/sys/vm/swappiness,来调整文件页和匿名页的回收倾向。
- 当 Swap 变高时,你可以用 sar、/proc/zoneinfo、/proc/pid/status 等方法,查看系统和进程的内存使用情况,进而找出 Swap 升高的根源和受影响的进程。
- 反过来说,通常降低 Swap 的使用,可以提高系统的整体性能。要怎么做呢?几种常见的降低方法。
- 禁止 Swap,现在服务器的内存足够大,所以除非有必要,禁用 Swap 就可以了。随着云计算的普及,大部分云平台中的虚拟机都默认禁止 Swap。
- 如果实在需要用到 Swap,可以尝试降低 swappiness 的值,减少内存回收时 Swap 的使用倾向。
- 响应延迟敏感的应用,如果它们可能在开启 Swap 的服务器中运行,你还可以用库函数 mlock() 或者 mlockall() 锁定内存,阻止它们的内存换出。
评论区