Linux内存问题排查工具
工具
为了迅速定位内存问题,通常会先运行几个覆盖面比较大的性能工具,比如 free、top、vmstat、pidstat 等。具体的分析思路主要有这几步。
-
先用 free 和 top,查看系统整体的内存使用情况。
-
再用 vmstat 和 pidstat,查看一段时间的趋势,从而判断出内存问题的类型。
-
最后进行详细分析,比如内存分配分析、缓存 / 缓冲区分析、具体进程的内存使用分析等。
内存调优最重要的就是,保证应用程序的热点数据放到内存中,并尽量减少换页和交换。
常见的优化思路有这么几种。
-
最好禁止 Swap。如果必须开启 Swap,降低 swappiness 的值,减少内存回收时 Swap 的使用倾向。
-
减少内存的动态分配。比如,可以使用内存池、大页(HugePage)等。
-
尽量使用缓存和缓冲区来访问数据。比如,可以使用堆栈明确声明内存空间,来存储需要缓存的数据;或者用 Redis 这类的外部缓存组件,优化数据的访问。
-
使用 cgroups 等方式限制进程的内存使用情况。这样,可以确保系统内存不会被异常进程耗尽。
-
通过 /proc/pid/oom_adj ,调整核心应用的 oom_score。这样,可以保证即使内存紧张,核心应用也不会被 OOM 杀死。
oom_score
OOM(Out of Memory)其实是内核的一种保护机制。它监控进程的内存使用情况,并且使用 oom_score 为每个进程的内存使用情况进行评分:
-
一个进程消耗的内存越大,oom_score 就越大;
-
一个进程运行占用的 CPU 越多,oom_score 就越小。
进程的 oom_score 越大,代表消耗的内存越多,也就越容易被 OOM 杀死,从而可以更好保护系统。
管理员可以通过 /proc 文件系统,手动设置进程的 oom_adj ,从而调整进程的 oom_score。
oom_adj 的范围是 [-17, 15],数值越大,表示进程越容易被 OOM 杀死;数值越小,表示进程越不容易被 OOM 杀死,其中 -17 表示禁止 OOM。
比如用下面的命令,你就可以把 sshd 进程的 oom_adj 调小为 -16,这样, sshd 进程就不容易被 OOM 杀死。
echo -16 > /proc/$(pidof sshd)/oom_adj
free
free可以看系统整体内存使用情况
free
total used free shared buff/cache available
Mem: 3514764 940248 219344 73716 2355172 2219028
Swap: 0 0 0
free 输出的是一个表格,其中的数值都默认以字节为单位。表格总共有两行六列,这两行分别是物理内存 Mem 和交换分区 Swap 的使用情况,而六列中,每列数据的含义分别为:
-
第一列,total 是总内存大小;
-
第二列,used 是已使用内存的大小,包含了共享内存;
-
第三列,free 是未使用内存的大小;
-
第四列,shared 是共享内存的大小;
-
第五列,buff/cache 是缓存和缓冲区的大小;
-
最后一列,available 是新进程可用内存的大小。
这里尤其注意一下,最后一列的可用内存 available 。available 不仅包含未使用内存,还包括了可回收的缓存,所以一般会比未使用内存更大。不过,并不是所有缓存都可以回收,因为有些缓存可能正在使用中。
top
top 可以看进程内存使用情况
top
top - 09:12:28 up 367 days, 14:10, 0 users, load average: 0.04, 0.02, 0.00
Tasks: 132 total, 1 running, 87 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.7 us, 0.3 sy, 0.0 ni, 98.8 id, 0.0 wa, 0.0 hi, 0.2 si, 0.0 st
KiB Mem : 3514764 total, 368416 free, 943196 used, 2203152 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 2215896 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1962 ubuntu 20 0 956624 74288 36344 S 0.3 2.1 0:04.76 node
2063 ubuntu 20 0 976732 87552 38292 S 0.3 2.5 0:05.73 node
11399 root 20 0 1115012 150912 19764 S 0.3 4.3 560:46.93 YDService
30256 root 20 0 64552 11232 3632 S 0.3 0.3 44:22.12 barad_agent
1 root 20 0 225544 7596 4920 S 0.0 0.2 19:44.42 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:14.03 kthreadd
4 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/0:0H
6 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 mm_percpu_wq
7 root 20 0 0 0 0 S 0.0 0.0 3:12.92 ksoftirqd/0
8 root 20 0 0 0 0 I 0.0 0.0 35:31.52 rcu_sched
9 root 20 0 0 0 0 I 0.0 0.0 0:00.00 rcu_bh
10 root rt 0 0 0 0 S 0.0 0.0 1:06.76 migration/0
11 root rt 0 0 0 0 S 0.0 0.0 0:37.36 watchdog/0
12 root 20 0 0 0 0 S 0.0 0.0 0:00.00 cpuhp/0
-
VIRT 是进程虚拟内存的大小,只要是进程申请过的内存,即便还没有真正分配物理内存,也会计算在内。
-
RES 是常驻内存的大小,也就是进程实际使用的物理内存大小,但不包括 Swap 和共享内存。
-
SHR 是共享内存的大小,比如与其他进程共同使用的共享内存、加载的动态链接库以及程序的代码段等。
-
%MEM 是进程使用物理内存占系统总内存的百分比。
top 输出时,要注意两点。
第一,虚拟内存通常并不会全部分配物理内存。从上面的输出,你可以发现每个进程的虚拟内存都比常驻内存大得多。
第二,共享内存 SHR 并不一定是共享的,比方说,程序的代码段、非共享的动态链接库,也都算在 SHR 里。当然,SHR 也包括了进程间真正共享的内存。所以在计算多个进程的内存使用时,不要把所有进程的 SHR 直接相加得出结果。
Buffer & Cache
Buffer 是对磁盘数据的缓存
Cache 是文件数据的缓存
它们既会用在读请求中,也会用在写请求中。
磁盘是一个块设备,可以划分为不同的分区;在分区之上再创建文件系统,挂载到某个目录,之后才可以在这个目录中读写文件。
在读写普通文件时,会经过文件系统,由文件系统负责与磁盘交互;而读写磁盘或者分区时,就会跳过文件系统,也就是所谓的“裸I/O“。这两种读写方式所使用的缓存是不同的,也就是文中所讲的 Cache 和 Buffer 区别。
cachestat & cachetop
-
cachestat 提供了整个操作系统缓存的读写命中情况。
-
cachetop 提供了每个进程的缓存命中情况。
这两个工具都是 bcc 软件包的一部分,需要通过源码的方式进行安装,且LLVM需要11及以上。
cachestat 运行效果:
sudo python3 /usr/share/bcc/tools/cachestat
HITS MISSES DIRTIES HITRATIO BUFFERS_MB CACHED_MB
18102 0 60 100.00% 196 1440
7126 0 43 100.00% 196 1440
6883 0 40 100.00% 196 1440
14 0 13 100.00% 196 1440
3528 0 9 100.00% 196 1440
19 0 8 100.00% 196 1440
cachestat 的输出其实是一个表格。每行代表一组数据,而每一列代表不同的缓存统计指标。这些指标从左到右依次表示:
-
HITS ,表示缓存命中的次数;
-
MISSES ,表示缓存未命中的次数;
-
DIRTIES, 表示新增到缓存中的脏页数;
-
BUFFERS_MB 表示 Buffers 的大小,以 MB 为单位;
-
CACHED_MB 表示 Cache 的大小,以 MB 为单位。
cachetop 运行效果
sudo python3 /usr/share/bcc/tools/cachetop
12:14:36 Buffers MB: 315 / Cached MB: 1363 / Sort: HITS / Order: descending
PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT%
1608 ubuntu ps 929 0 0 100.0% 0.0%
1604 root awk 909 0 0 100.0% 0.0%
1610 ubuntu cpuUsage.sh 762 0 0 100.0% 0.0%
1615 ubuntu sed 642 0 0 100.0% 0.0%
1611 ubuntu sed 624 0 0 100.0% 0.0%
1603 root grep 596 0 0 100.0% 0.0%
1617 ubuntu cpuUsage.sh 540 0 0 100.0% 0.0%
1601 root barad_agent 529 0 0 100.0% 0.0%
1613 ubuntu sleep 515 0 0 100.0% 0.0%
pcstat
pcstat 可以指定文件在内存中的缓存大小,查看文件在内存中的缓存大小以及缓存比例。
pcstat 是一个基于 Go 语言开发的工具,所以安装它之前,你首先应该安装 Go 语言。安装完 Go 语言,再运行下面的命令安装 pcstat:
export GOPATH=~/go
export PATH=~/go/bin:$PATH
go install github.com/tobert/pcstat@latest
运行效果
pcstat /bin/ls
+---------+----------------+------------+-----------+---------+
| Name | Size (bytes) | Pages | Cached | Percent |
|---------+----------------+------------+-----------+---------|
| /bin/ls | 133792 | 33 | 33 | 100.000 |
+---------+----------------+------------+-----------+---------+
memleak
memleak 是 bcc 软件包中的一个工具,我们一开始就装好了,执行 /usr/share/bcc/tools/memleak 就可以运行它。比如,我们运行下面的命令:
/usr/share/bcc/tools/memleak -p $(pidof app) -a
Attaching to pid 12512, Ctrl+C to quit.
[03:00:41] Top 10 stacks with outstanding allocations:
addr = 7f8f70863220 size = 8192
addr = 7f8f70861210 size = 8192
addr = 7f8f7085b1e0 size = 8192
addr = 7f8f7085f200 size = 8192
addr = 7f8f7085d1f0 size = 8192
40960 bytes in 5 allocations from stack
fibonacci+0x1f [app]
child+0x4f [app]
start_thread+0xdb [libpthread-2.27.so]
swap 机制
在内存资源紧张时,Linux 通过直接内存回收和定期扫描的方式,来释放文件页和匿名页,以便把内存分配给更需要的进程使用。
文件页的回收会直接清空,或者把脏数据写回磁盘后再释放。而对匿名页的回收,需要通过 Swap 换出到磁盘中,下次访问时,再从磁盘换入到内存中。
kswapd0 定义了三个内存阈值(watermark,也称为水位),分别是页最小阈值(pages_min)、页低阈值(pages_low)和页高阈值(pages_high)。
kswapd0 定期扫描内存的使用情况,并根据剩余内存落在这三个阈值的空间位置,进行内存的回收操作。
-
剩余内存小于页最小阈值,说明进程可用内存都耗尽了,只有内核才可以分配内存。
-
剩余内存落在页最小阈值和页低阈值中间,说明内存压力比较大,剩余内存不多了。这时 kswapd0 会执行内存回收,直到剩余内存大于高阈值为止。
-
剩余内存落在页低阈值和页高阈值中间,说明内存有一定压力,但还可以满足新内存请求。
-
剩余内存大于页高阈值,说明剩余内存比较多,没有内存压力。
一旦剩余内存小于页低阈值,就会触发内存的回收。
这个页低阈值,其实可以通过内核选项 /proc/sys/vm/min_free_kbytes 来间接设置。
min_free_kbytes 设置了页最小阈值,而其他两个阈值,都是根据页最小阈值计算生成的,计算方法如下 :
pages_low = pages_min*5/4
pages_high = pages_min*3/2
还可以设置 /proc/sys/vm/swappiness,来调整文件页和匿名页的回收倾向。
swappiness 的范围是 0-100,数值越大,越积极使用 Swap,也就是更倾向于回收匿名页;数值越小,越消极使用 Swap,也就是更倾向于回收文件页。虽然 swappiness 的范围是 0-100,不过要注意,这并不是内存的百分比,而是调整 Swap 积极程度的权重,即使你把它设置成 0,当剩余内存 + 文件页小于页高阈值时,还是会发生 Swap。
在 NUMA 架构下,每个 Node 都有自己的本地内存空间,而当本地内存不足时,默认既可以从其他 Node 寻找空闲内存,也可以从本地内存回收。你可以设置 /proc/sys/vm/zone_reclaim_mode ,来调整 NUMA 本地内存的回收策略。
你可以通过 /proc/sys/vm/zone_reclaim_mode 来调整。它支持以下几个选项:默认的 0 ,表示既可以从其他 Node 寻找空闲内存,也可以从本地回收内存。1、2、4 都表示只回收本地内存,2 表示可以回写脏数据回收内存,4 表示可以用 Swap 方式回收内存。