1. 概述
内存泄漏会导致系统内存资源逐渐耗尽,影响系统稳定性。当内存资源紧张时,系统通常有两种应对方式:
- OOM(Out Of Memory)杀死进程
系统会杀死占用大量内存的进程,释放内存资源以供其他进程使用。这种机制简单直接,在前面的内容中已有详细介绍。 - 内存回收机制
系统会优先释放可回收的内存资源,如文件页(File-backed Page)。
-
- 未修改的文件页:可以直接释放,需要时从磁盘重新加载。
- 脏页(Dirty Page) :需要先写入磁盘,然后才能释放。写入磁盘的方式有:
-
-
- 应用程序通过
fsync
系统调用主动同步。 - 内核线程
pdflush
自动刷新。
- 应用程序通过
-
-
- 文件映射页:通过内存映射的文件页也属于文件页,释放后可从文件重新加载。
此外,还有一种不可直接释放的内存------匿名页(Anonymous Page),即应用程序动态分配的堆内存。这类内存由于可能再次被访问,通常不能直接释放。但对于很少被访问的匿名页,系统可以通过 Swap 机制进行处理:
- Swap 机制:将不常访问的内存写入磁盘,从而释放内存资源供其他进程使用。当这些内存再次被访问时,再从磁盘读入。
接下来深入探讨 Swap 的工作原理,以及如何优化 Swap 的使用。
1.1. Swap 原理
Swap 说白了就是把一块磁盘空间或者一个本地文件(以下讲解以磁盘为例),当成内存来使用。它包括换出和换入两个过程。
- 所谓换出,就是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存。
- 而换入,则是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来。
简单来说换出就是从内存到磁盘,换入就从磁盘到内存。这一点不能记混。所以换出产生的写I/O、相反换入产生的读I/O。
Linux 系统中,Swap 机制的目的是回收内存资源,确保系统能够正常运行。那么,系统在什么情况下需要回收内存,又如何衡量内存是否紧张呢?
需要回收内存的场景
- 直接内存回收
当有新的大块内存分配请求,但剩余内存不足时,系统会回收一部分内存(如缓存)以满足新请求。 - 定期内存回收
系统中有一个专门的内核线程kswapd0
,负责定期回收内存,避免内存持续紧张的情况。
衡量内存是否紧张
kswapd0
使用内存水位(watermark)来评估系统的内存状态,具体包括以下三个阈值:
- pages_min(最小水位) :表示系统能够正常运行所需的最低内存。
- pages_low(低水位) :低于此水位时,
kswapd0
开始回收内存。 - pages_high(高水位) :高于此水位时,系统无需回收内存,表示内存充足。
系统剩余的空闲内存用 pages_free 表示。当 pages_free
接近或低于这些阈值时,系统会启动相应的内存回收机制以缓解压力。
在 Linux 系统中,kswapd0 不仅定期扫描内存的使用情况,还会根据剩余内存相对于三个水位阈值的位置,决定是否执行内存回收操作,以及回收的力度。以下是具体的内存状态判断和处理逻辑:
内存状态及处理逻辑
- 剩余内存 < 页最小阈值(pages_min)
-
- 状态:进程可用内存耗尽,只有内核可以分配内存。
- 处理:系统内存已极度紧张,必须通过强制回收释放内存资源。
- 页最小阈值 ≤ 剩余内存 < 页低阈值(pages_low)
-
- 状态:内存压力较大,剩余内存不足。
- 处理:
kswapd0
会主动执行内存回收操作,直到剩余内存超过页高阈值为止。
- 页低阈值 ≤ 剩余内存 < 页高阈值(pages_high)
-
- 状态:内存有一定压力,但还能满足新内存分配请求。
- 处理:系统暂时不会触发内存回收。
- 剩余内存 ≥ 页高阈值(pages_high)
-
- 状态:剩余内存充足,没有任何内存压力。
- 处理:
kswapd0
不需要执行回收操作,系统运行正常。
当剩余内存小于 页低阈值(pages_low) 时,系统会触发内存回收操作。这个 页低阈值 的设置与 页最小阈值(pages_min) 有关,而 页最小阈值 可以通过内核选项 /proc/sys/vm/min_free_kbytes
来间接配置。
计算公式
其他两个阈值是根据 页最小阈值 按以下公式计算得出的:
- 页低阈值(pages_low) = 页最小阈值(pages_min) × 5 / 4
- 页高阈值(pages_high) = 页最小阈值(pages_min) × 3 / 2
通过调整 min_free_kbytes
的值,可以间接影响这三个阈值,从而改变系统内存回收的行为和效率。
1.2. Swap与NUMA
在分析系统内存使用时,可能会遇到一种情况:尽管系统显示剩余内存很多,但 Swap 却仍在升高。这种现象通常与处理器的 NUMA(Non-Uniform Memory Access)架构有关。
什么是 NUMA?
在 NUMA 架构下,多个处理器被划分到不同的 Node,每个 Node 拥有独立的本地内存空间。同一个 Node 内部的内存空间还可以进一步划分为多个内存域(Zone),如:
- DMA(直接内存访问区)
- NORMAL(普通内存区)
- MOVABLE(伪内存区)
尽管这些内存域的具体含义暂时不必深究,但在分析内存使用时,我们需要针对每个 Node 的内存空间分别进行检查。
NUMA 内存使用情况查看
使用 numactl 命令可以查看处理器在不同 Node 上的分布情况,以及每个 Node 的内存使用状态。例如:
yaml
$ numactl --hardware
available: 1 nodes (0)
node 0 cpus: 0 1
node 0 size: 7977 MB
node 0 free: 4416 MB
NUMA 架构与 Swap 的关系
了解了 NUMA 的架构和查看方法后,我们来探讨它与 Swap 的关系。NUMA 中的内存回收和阈值监控,直接影响了 Swap 的触发机制。
查看内存域的阈值
NUMA 架构下,每个内存域的三个阈值(页最小阈值 、页低阈值 、页高阈值 )可以通过 /proc/zoneinfo 文件查看。例如:
shell
$ 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
...
关键指标说明
- min、low、high:分别对应页最小阈值、页低阈值和页高阈值。
- pages free :剩余内存页数,与 nr_free_pages 相同。
- nr_zone_active_anon 和 nr_zone_inactive_anon:活跃和非活跃的匿名页数。
- nr_zone_active_file 和 nr_zone_inactive_file:活跃和非活跃的文件页数。
从示例中可以看到,剩余内存(pages free )远大于页高阈值,因此此时的 kswapd0 不会触发内存回收。
Node 内存不足时的策略
当某个 Node 内存不足时,系统会有两种策略:
- 从其他 Node 寻找空闲内存。
- 从本地 Node 回收内存。
具体选择的策略可以通过 /proc/sys/vm/zone_reclaim_mode 调整,该选项支持以下值:
- 0(默认值) :允许从其他 Node 寻找空闲内存,同时也可以回收本地内存。
- 1:仅回收本地内存。
- 2:允许回写脏数据来回收本地内存。
- 4:允许使用 Swap 回收本地内存。
在内存回收机制中,Linux 会同时回收文件页和匿名页。
- 文件页的回收:直接回收缓存,或将脏页写回磁盘后再释放内存。
- 匿名页的回收:通过 Swap 机制,将其写入磁盘后释放内存。
那么,在实际回收内存时,系统优先回收哪一种呢?这取决于 /proc/sys/vm/swappiness
的配置。
关于 swappiness
- 作用:调整使用 Swap 的积极程度。
- 取值范围:0-100。
-
- 数值越大,越倾向于回收匿名页(积极使用 Swap)。
- 数值越小,越倾向于回收文件页(消极使用 Swap)。
需要注意,即使将 swappiness
设置为 0,当系统剩余内存和文件页小于页高阈值时,仍可能触发 Swap。
那么,当 Swap 使用升高时,要如何定位和分析呢?下面,我们就来看一个磁盘 I/O 的案例,实战分析和演练。
2. 案例
环境准备:
- 操作系统:Ubuntu 18.04
- 机器配置:2 CPU,8GB 内存
- 你需要预先安装 sysstat 等工具,如 apt install sysstat
终端 1:
vbnet
$ free
total used free shared buff/cache available
Mem: 8169348 331668 6715972 696 1121708 7522896
Swap: 0 0 0
如果已经使用了swap,可以通过关闭Swap在重新开启的方式清空Swap
css
swapoff -a && swapon -a
若还没有配置Swap,参考如下命令
shell
# 创建Swap文件
$ fallocate -l 8G /mnt/swapfile
# 修改权限只有根用户可以访问
$ chmod 600 /mnt/swapfile
# 配置Swap文件
$ mkswap /mnt/swapfile
# 开启Swap
$ swapon /mnt/swapfile
执行dd命令,模拟大文件读取
javascript
# 写入空设备,实际上只有磁盘的读请求
$ dd if=/dev/sda3 of=/dev/null bs=1G count=2048
终端 2
makefile
# 间隔1秒输出一组数据
# -r表示显示内存使用情况,-S表示显示Swap使用情况
$ sar -r -S 1
04:39:56 kbmemfree kbavail kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
04:39:57 6249676 6839824 1919632 23.50 740512 67316 1691736 10.22 815156 841868 4
04:39:56 kbswpfree kbswpused %swpused kbswpcad %swpcad
04:39:57 8388604 0 0.00 0 0.00
04:39:57 kbmemfree kbavail kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
04:39:58 6184472 6807064 1984836 24.30 772768 67380 1691736 10.22 847932 874224 20
04:39:57 kbswpfree kbswpused %swpused kbswpcad %swpcad
04:39:58 8388604 0 0.00 0 0.00
...
04:44:06 kbmemfree kbavail kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
04:44:07 152780 6525716 8016528 98.13 6530440 51316 1691736 10.22 867124 6869332 0
04:44:06 kbswpfree kbswpused %swpused kbswpcad %swpcad
04:44:07 8384508 4096 0.05 52 1.27
新指标介绍:
- kbcommit,表示当前系统负载需要的内存。它实际上是为了保证系统内存不溢出,对需要内存的估计值。%commit,就是这个值相对总内存的百分比。
- kbactive,表示活跃内存,也就是最近使用过的内存,一般不会被系统回收。
- kbinact,表示非活跃内存,也就是不常访问的内存,有可能会被系统回收。
以下是这些内存和 Swap 使用指标的表格形式:
指标 | 描述 | 示例值 |
---|---|---|
kbmemfree | 当前空闲的物理内存量(KB) | 111648 |
kbavail | 可用内存量(包括空闲内存和可回收的缓存内存,KB) | 999228 |
kbmemused | 已使用的物理内存量(KB) | 7958468 |
%memused | 物理内存使用百分比 | 98.62% |
kbbuffers | 缓存的内存量(KB) | 1035896 |
kbcached | 用于缓存文件数据的内存量(KB) | 45508 |
kbcommit | 已提交的内存量(KB) | 591116 |
%commit | 已提交内存占总内存的百分比 | 4.82% |
kbactive | 活跃的内存量(频繁访问的数据,KB) | 33468 |
kbinact | 不活跃的内存量(暂不访问的数据,KB) | 1084000 |
kbdirty | 被修改但尚未写入磁盘的内存量(脏页,KB) | 12 |
kbswpfree | 空闲的 Swap 空间量(KB) | 4132596 |
kbswpused | 已使用的 Swap 空间量(KB) | 61704 |
%swpused | Swap 空间使用百分比 | 1.47% |
kbswpcad | 被压缩的 Swap 内存量(KB) | 15356 |
%swpcad | 被压缩的 Swap 内存占总 Swap 空间的百分比 | 24.89% |
- 刚开始,剩余内存(kbmemfree)不断减少,而缓冲区(kbbuffers)则不断增大,由此可知,剩余内存不断分配给了缓冲区。
- 一段时间后,剩余内存已经很小,而缓冲区占用了大部分内存。这时候,Swap 的使用开始逐渐增大,缓冲区和剩余内存则只在小范围内波动。
转换成表格,看的更直观一点:
指标 | 04:39:56 PM | 04:39:57 PM | 04:39:58 PM | 04:44:06 PM | 变化趋势 |
---|---|---|---|---|---|
kbmemfree | 6249676 | 6249676 | 6184472 | 152780 | 显著下降 |
kbavail | 6839824 | 6839824 | 6807064 | 6525716 | 稍微下降 |
kbmemused | 1919632 | 1919632 | 1984836 | 8016528 | 显著上升 |
%memused | 23.50% | 23.50% | 24.30% | 98.13% | 显著上升 |
kbbuffers | 740512 | 740512 | 772768 | 6530440 | 显著上升 |
kbcached | 67316 | 67316 | 67380 | 51316 | 稍微下降 |
kbcommit | 1691736 | 1691736 | 1691736 | 1691736 | 稳定 |
%commit | 10.22% | 10.22% | 10.22% | 10.22% | 稳定 |
kbactive | 815156 | 815156 | 847932 | 867124 | 稍微上升 |
kbinact | 841868 | 841868 | 874224 | 6869332 | 显著上升 |
kbdirty | 40 | 40 | 200 | 0 | 显著下降 |
kbswpfree | 8388604 | 8388604 | 8388604 | 8388604 | 稳定 |
kbswpused | 0 | 0 | 0 | 0 | 稳定 |
%swpused | 0.00% | 0.00% | 0.00% | 0.00% | 稳定 |
kbswpcad | 0 | 0 | 0 | 0 | 稳定 |
%swpcad | 0.00% | 0.00% | 0.00% | 0.00% | 稳定 |
buffers升高可以通过cachetop命令查看
erlang
$ cachetop 5
12:28:28 Buffers MB: 6349 / Cached MB: 87 / Sort: HITS / Order: ascending
PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT%
18280 root python 22 0 0 100.0% 0.0%
18279 root dd 41088 41022 0 50.0% 50.0%
通过 cachetop
的输出,我们得出以下结论和分析:
- 命中率与未命中缓存页:
-
dd
进程的缓存命中率:50%。- 未命中缓存页数(MISSES) :41022 页。
- 这表明,运行的
dd
命令导致了大量缓冲区的占用,从而引发缓冲区使用升高。
- 缓冲区与 Swap 的关系:
-
- 缓冲区占用了系统绝大部分内存,属于可回收内存。
- 按照直观理解,当内存不足时,系统应优先回收缓冲区,而不是增加 Swap 的使用。
- 进一步分析的必要性:
-
- 为了理解 Swap 使用升高的原因,需要通过
/proc/zoneinfo
查看:
- 为了理解 Swap 使用升高的原因,需要通过
-
-
- 剩余内存(pages_free) 。
- 内存阈值(pages_min、pages_low、pages_high) 。
- 匿名页(活跃和非活跃) 。
- 文件页(活跃和非活跃) 。
-
css
# -d 表示高亮变化的字段
# -A 表示仅显示Normal行以及之后的15行输出
$ watch -d grep -A 15 'Normal' /proc/zoneinfo
Node 0, zone Normal
pages free 21328
min 14896
low 18620
high 22344
spanned 1835008
present 1835008
managed 1796710
protection: (0, 0, 0, 0, 0)
nr_free_pages 21328
nr_zone_inactive_anon 79776
nr_zone_active_anon 206854
nr_zone_inactive_file 918561
nr_zone_active_file 496695
nr_zone_unevictable 2251
nr_zone_write_pending 0
通过分析剩余内存(pages_free
)的波动变化,可以发现以下规律:
- 内存波动原因:
-
- 当剩余内存小于页低阈值(
pages_low
)时,系统会启动内存回收机制,释放一些缓存和匿名内存,使剩余内存迅速增加到高于页高阈值(pages_high
)。 - 回收过程中:
- 当剩余内存小于页低阈值(
-
-
- 缓存的回收导致缓冲区(
buffers
)减少。 - 匿名内存的回收导致 Swap 的使用量增加。
- 缓存的回收导致缓冲区(
-
- 循环变化:
-
- 在内存回收后,由于新的写入操作(如
dd
命令),剩余内存会被重新分配给缓存,导致剩余内存再次减少,缓冲区增大。 - 这一过程会循环往复,形成一个动态平衡。
- 在内存回收后,由于新的写入操作(如
- Swap 和缓冲区的波动:
-
- 在多次运行
dd
和观察sar
数据时,有时会发现 Swap 使用较多,而缓冲区波动较小;有时则相反。 - 这表明系统在回收内存时,对回收文件页和匿名页的倾向不明显。
- 在多次运行
- 影响因素:
-
- 系统内存回收策略受
swappiness
参数的影响。该参数可以调整文件页和匿名页回收的优先级,从而影响内存波动的具体表现。
- 系统内存回收策略受
显然,当前的Swap使用比较激进,这跟swappiness有关
ruby
root@calvin:~# cat /proc/sys/vm/swappiness
60
那么如何知道Swap换入换出的是哪些程序呢?建议还是通过/proc文件系统排查。
shell
# 按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
dockerd 2226 10728 kB
docker-containe 2251 8516 kB
snapd 936 4020 kB
networkd-dispat 911 836 kB
polkitd 1004 44 kB
proc文件系统的好处是通用的,任何操作系统都可以使用,若有条件,还有一个更简单的工具,smem
bash
smem --sort swap
Ubuntu安装如下
bash
apt update
apt-get install smem -y
#安装不成功建议源码安装,地址如下:
https://www.selenic.com/smem/download/
tar xvf smem-1.4.tar.gz
cp smem /usr/local/bin/
3. 小结
在内存资源紧张的情况下,Linux 会通过 Swap 将不常访问的匿名页换出到磁盘,以释放内存。当需要访问这些页时,再从磁盘换入到内存。以下是相关配置和方法的总结:
关键配置项:
- 调整内存回收阈值:
-
/proc/sys/vm/min_free_kbytes
:设置内存回收的最低阈值,控制系统定期回收内存的行为。
- 调整内存回收倾向:
-
/proc/sys/vm/swappiness
:调整文件页和匿名页的回收倾向。值越低,倾向于回收文件页;值越高,倾向于回收匿名页。
定位 Swap 升高的原因:
- 使用工具和文件:
-
sar
:查看系统整体的内存和 Swap 使用变化。/proc/zoneinfo
:观察内存剩余情况和内存域的活跃信息。/proc/pid/status
:分析具体进程的内存使用情况。
- 目标:通过这些工具找出 Swap 升高的根源,以及受影响的进程。
降低 Swap 使用的方法:
- 禁用 Swap:
-
- 如果服务器内存充足,可以禁用 Swap,避免不必要的内存换出。
- 许多云平台默认禁用 Swap。
- 降低 swappiness 值:
-
- 适当降低 swappiness,减少系统内存回收时对 Swap 的使用倾向。
- 锁定关键应用的内存:
-
- 对于响应延迟敏感的应用,可以使用库函数
mlock()
或mlockall()
锁定其内存,防止被换出。
- 对于响应延迟敏感的应用,可以使用库函数
合理配置 min_free_kbytes 和 swappiness,以及针对性地分析和优化 Swap 使用,不仅能降低 Swap 的使用,还能提高系统整体性能。