Kyle's Notebook

Linux 内存原理与分析

Word count: 7.4kReading time: 29 min
2021/04/28

Linux 内存原理分析

  • 内存性能指标
    • 系统内存
      • 已用内存
      • 剩余内存
      • 可用内存
      • 缺页异常
        • 主缺页异常
        • 次缺页异常
      • 缓存/缓冲区
        • 使用量
        • 命中率
      • Slabs
    • 进程内存
      • 虚拟内存(VSS)
      • 常驻内存(RSS)
      • 按比例分配共享内存后的物理内存(PSS)
      • 独占内存(USS)
      • 共享内存
      • SWAP 内存
      • 缺页异常
        • 主缺页异常
        • 次缺页异常
    • SWAP
      • 已用空间
      • 剩余空间
      • 换入速度
      • 换出速度

通常使用命令 free 可以查看系统内存的总体情况:

1
2
3
4
5
6
free
# (包括共享内存) (包括可回收缓存)
# 总内存 已使用内存 未使用内存 共享内存 缓存和缓冲区 可用内存
# total used free shared buff/cache available
# Mem: 8169348 263524 6875352 668 1030472 7611064
# Swap: 0 0 0

使用命令 topps 查看进程的内存使用情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
top
# ...
# KiB Mem : 8169348 total, 6871440 free, 267096 used, 1030812 buff/cache
# KiB Swap: 0 total, 0 free, 0 used. 7607492 avail Mem
#
# 虚拟内存 常驻内存
# PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
# 430 root 19 -1 122360 35588 23748 S 0.0 0.4 0:32.17 systemd-journal
# 1075 root 20 0 771860 22744 11368 S 0.0 0.3 0:38.89 snapd
# 1048 root 20 0 170904 17292 9488 S 0.0 0.2 0:00.24 networkd-dispat
# 1 root 20 0 78020 9156 6644 S 0.0 0.1 0:22.92 systemd
# 12376 azure 20 0 76632 7456 6420 S 0.0 0.1 0:00.01 systemd
# 12374 root 20 0 107984 7312 6304 S 0.0 0.1 0:00.00 sshd
# ...

# 按下 M 切换到内存排序

其每列的含义:

  • VIRT:虚拟内存,只要是进程申请过的内存,即便还没有真正分配物理内存也会计算在内。

  • RES:常驻内存,进程实际使用的物理内存大小,不包括 Swap 和共享内存。

  • SHR:共享内存,比如与其他进程共同使用的共享内存、加载的动态链接库以及程序的代码段等。

  • %MEM:使用物理内存占系统总内存的百分比。

需要注意:

  • 虚拟内存通常不会全部分配物理内存(从上面的输出可见每个进程的虚拟内存都比常驻内存大得多)。

  • 在计算多个进程的内存使用时,不应把所有进程的 SHR 直接相加得出结果。共享内存 SHR 并不一定是共享的,比如程序的代码段、非共享的动态链接库都算在 SHR。

工作原理

内存映射

Linux 内核为每个进程提供独立的、连续的虚拟地址空间,以便应用程序访问内存。

其中虚拟地址空间又分为内核空间和用户空间,分别对应进程的内核态和用户态,进程在用户态时只能访问用户空间内存,只有进入内核态后才可以访问内核空间内存。其地址范围因 CPU 字长而异:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+-------------+ 0xFFFFFFFF
| 内核空间 1G |
+-------------+ 0xC0000000
| 用户空间 3G |
+-------------+ 0x0
(32 位地址空间)

+---------------+ 0xFFFFFFFFFFFFFFFF
| 内核空间 128T |
+---------------+ 0xFFFF800000000000
| 未定义 |
+---------------+ 0x00007FFFFFFFF000
| 用户空间 128T |
+---------------+ 0x0
(64 位地址空间)

用户空间和内核空间都对应同一块物理内存,但很显然物理内存的总大小是远小于进程的虚拟内存大小的,因此只有被实际使用的虚拟内存才会被分配物理内存,两者的关系被称为 内存映射,由 CPU 的 MMU(内存管理单元)管理:

  • MMU 以 页(Page) 为单位管理内存映射,一般为 4KB。但为了提高管理大内存块的效率,也提供 2MB、1GB 的大页 Huge Page)。

  • 内存页存放在多级页表中(可节省页表空间),每级的表项用于在下一级表中选择页,最后一个索引表示页内偏移,最终 以起始地址和偏移量构成物理地址

  • 每当应用程序访问的虚拟地址在页表中没有找到,就会触发 缺页异常。在内核空间分配物理内存、更新页表后再返回用户空间,才能继续执行。

img

其中缺页异常又分为两种场景:

  • 次缺页异常:可以直接从物理内存中分配。

  • 主缺页异常:需要磁盘 I/O 介入(比如 Swap)。

CPU 使用 TLB(转移后备缓冲区)提高地址翻译效率,内存映射翻译的结果存放在 TLB 中。因此减少进程的上下文切换,减少 TLB 刷新次数,就可以提高 TLB 缓存的利用率,从而提高 CPU 的内存访问性能。

虚拟内存布局

虚拟内存布局基本如下,其中用户空间内存被分为多个段,从低位到高位:

  • 只读段:包括代码和常量等。

  • 数据段:包括全局变量等。

  • :包括动态分配的内存,从低地址开始向上增长。

  • 文件映射段:动态分配,包括动态库、共享内存等,从高地址开始向下增长。

  • :包括局部变量,函数调用上下文等(大小固定,一般是 8MB)。

1
2
3
4
5
6
7
8
9
10
11
12
13
+----------+ 0xFFFFFFFF
| 内核空间 |
+----------+ 0xC0000000
| 栈 |
+----------+
| 文件映射 |
+----------+
| 堆 |
+----------+
| 数据段 |
+----------+
| 只读段 |
+----------+ 0x0

内存管理

内存分配malloc() 是 C 标准库提供的内存分配函数,对应到系统调用上有两种方式:

  • brk():小块内存(< 128K)通过移动堆顶的位置来分配,释放后会被缓存起来(用户空间),可重复使用。可以减少缺页异常的发生,提高内存访问效率。但由于内存没有归还系统,在内存工作繁忙时频繁的内存分配和释放会造成内存碎片。

  • mmap():大块内存(> 128K)使用内存映射分配,即在文件映射段找一块空闲内存分配出去。由于在释放时直接归还系统,每次 mmap 都会发生缺页异常。在内存工作繁忙时大量的缺页中断会使内核的管理负担增大。

无论采用何种调用分配内存,都只有在首次访问、发生缺页异常时才会进入内核分配。

内存碎片 是内存分配时会出现的现象,其分为:

  • 外部碎片:零散的内存块不足以满足一次内存分配请求,但其总和却大于请求分配的内存大小,则这些内存块为外部碎片。该情况可通过 伙伴系统(合并相邻的页)来解决。

  • 内部碎片:请求的内存比一个页更小,则分配的内存没有被充分利用,其中分配出去、但未被利用的部分称为内部碎片。对于小内存,则使用 slab 分配器 来分配和释放(可理解为伙伴系统的缓存)。

内存回收:在分配给应用程序的内存使用完毕后,应当调用 free()unmap() 函数来释放。同时操作系统也会限制进程使用内存,在内存紧张时:

  • 基于一定规则回收,比如 LRU(Least Recently Used),Second Chance 等。

  • 回收不常访问的内存,把不常用的内存通过 交换分区(Swap) 直接写到磁盘中(换出,需要访问时再从磁盘读取)。

  • 基于 zproject(zcache、zswap、zram 等)的 内存压缩(当需要访问时再解压,效果比落盘好)。

  • 基于 oom_score 为进程评分,分数越高则通过 OOM(Out of Memory)异常 直接杀死(消耗内存越大 oom_score 越大,占用 CPU 越多 oom_score 越小)。

也可以通过 /proc 手动设置 oom_adj(范围 [-17, 5],数值越大越容易被 OOM)手动设置进程的 oom_score。

比如不希望一个进程被杀死,可以:

echo -16 > /proc/$(pidof sshd)/oom_adj

Buffer 与 Cache

free 命令的输出中,有一列名为 buff/cache。其中 buffer 和 cache 的含义是(参考 man proc ):

  • Buffers:磁盘数据的缓存。内核缓冲区用到的内存,对应的是 /proc/meminfo 中的 Buffers 值。通常不会特别大(20MB 左右),内核把分散的写集中以统一优化磁盘的写入,比如把多次小写合并成单次大写等。

  • Cache:文件数据的缓存。包括内核 页缓存Slab 用到的内存(可回收缓存),对应的是 /proc/meminfo 中的 Cached 与 SReclaimable 之和。缓存从文件读取的数据,下次访问时直接从内存中获取,而不需要再次访问磁盘。

  • SReclaimable:Slab 分为两部分,其中的可回收部分,用 SReclaimable 记录;不可回收部分,用 SUnreclaim 记录。

太长不看版:总的来说,Buffer 和 Cache 分别缓存磁盘和文件系统的读写数据。

  • 写:不仅优化磁盘和文件的写入,对于应用程序而言在数据真正落盘前,就可以返回去做其他工作。

  • 读:既可以加速读取那些需要频繁访问的数据,也降低了频繁 I/O 对磁盘的压力。

实际上其包括的内容因环境而异,要理解实际的含义需要进一步分析。

写操作

使用 dd 生成读取随机设备生成大文件或写入磁盘分区:

1
2
3
4
5
6
7
dd if=/dev/urandom of=/tmp/file bs=1M count=500

# 清理缓存
# echo 3 > /proc/sys/vm/drop_caches ; sync

# 向磁盘分区 /dev/sdb1 写入 2G 数据(请确保 /dev/sdb1 未使用)
# dd if=/dev/urandom of=/dev/sdb1 bs=1M count=2048

使用 vmstat 监控内存和 I/O:

1
2
3
4
5
6
7
8
9
10
11
12
vmstat 1
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa st
# 0 0 0 7499460 1344 230484 0 0 0 0 29 145 0 0 100 0 0
# 1 0 0 7338088 1752 390512 0 0 488 0 39 558 0 47 53 0 0 bi 只出现了一次 488 KB/s,bo 则只有一次 4KB
# 0 0 0 6977448 1752 752072 0 0 0 0 29 138 0 0 100 0 0
# 0 0 0 6977440 1760 752080 0 0 0 152 42 212 0 1 99 1 0
# ...
# 0 1 0 6977216 1768 752104 0 0 4 122880 33 234 0 1 51 49 0 一段时间后,才会出现大量的块设备写
# 0 1 0 6977440 1768 752108 0 0 0 10240 38 196 0 0 50 50 0

# 重点关注内存部分的 buff 和 cache(即 Buffers 和 Cache,单位 KB)、io 部分的 bi 和 bo(表示块设备读取和写入的大小,单位 1KB/s,因为 Linux 中块的大小是 1KB)。

以写文件为例,在 Cache 刚开始增长时块设备 I/O 很少。一段时间后才出现大量的块设备写;dd 命令结束后 Cache 不再增长,但块设备写还会持续一段时间,多次 I/O 写的结果加起来才是 dd 要写入的 500M 数据。

相反,如果是直接写入磁盘分区,可见写磁盘时 bo 大于 0,Buffer 和 Cache 都在增长,但 Buffer 的增长快得多。

因此写文件时用到 Cache 缓存数据,而写磁盘则会用到 Buffer 缓存数据。

读操作

清理缓存,从文件或磁盘分区中读取数据写入空设备:

1
2
3
4
5
6
7
8
9
10
11
# 清理缓存
echo 3 > /proc/sys/vm/drop_caches ; sync

# 读取文件数据
dd if=/tmp/file of=/dev/null

# 清理缓存
# echo 3 > /proc/sys/vm/drop_caches ; sync

# 读取磁盘
# dd if=/dev/sda1 of=/dev/null bs=1M count=1024

使用 vmstat 监控内存和 I/O:

1
2
3
4
5
6
7
vmstat 1
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa st
# 0 1 0 7724164 2380 110844 0 0 16576 0 62 360 2 2 76 21 0
# 0 1 0 7691544 2380 143472 0 0 32640 0 46 439 1 3 50 46 0
# 0 1 0 7658736 2380 176204 0 0 32640 0 54 407 1 4 50 46 0
# 0 1 0 7626052 2380 208908 0 0 32640 40 44 422 2 2 50 46 0

从文件中读取,可见读时(bi 大于 0 时)Buffer 保持不变,Cache 在不停增长。因此可印证 Cache 是对文件读的页缓存。

从磁盘分区中读取,可见读时(bi 大于 0)Buffer 和 Cache 都在增长,其中 Buffer 的增长快很多。说明读磁盘时数据缓存到了 Buffer 中。

缓存优化

缓存命中率越高,使用缓存带来收益越高,应用程序的性能越好。

基于 Linux 内核 eBPF(extended Berkeley Packet Filters)机制的 bcc 工具来跟踪内核中管理的缓存,可以查看缓存的使用和命中情况,安装:

1
2
3
4
5
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD
echo "deb https://repo.iovisor.org/apt/xenial xenial main" | sudo tee /etc/apt/sources.list.d/iovisor.list
sudo apt update
sudo apt install -y bcc-tools libbcc-examples linux-headers-$(uname -r)
export PATH=$PATH:/usr/share/bcc/tools

使用 cachestat 查看总体缓存的读写命中情况:

1
2
3
4
5
6
cachestat 1 3
# 总 I/O 未命中次数 命中次数 新增到缓存脏页数 Buffers(MB) Cache(MB)
# TOTAL MISSES HITS DIRTIES BUFFERS_MB CACHED_MB
# 2 0 2 1 17 279
# 2 0 2 1 17 279
# 2 0 2 1 17 279

使用 cachetop 查看每个进程的缓存命中情况:

1
2
3
4
5
6
7
cachetop
# 11:58:50 Buffers MB: 258 / Cached MB: 347 / Sort: HITS / Order: ascending
# 命中次数 未命中次数 新增到缓存脏页数 读命中率 写命中率
# PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT%
# 13029 root python 1 0 0 100.0% 0.0%

# 输出默认按缓存命中次数(HITS)排序

使用 pcstat 工具可以查看文件在内存中的缓存大小以及缓存比例,安装:

1
2
3
# 其依赖于 Golang
go get golang.org/x/sys/unix
go get github.com/tobert/pcstat/pcstat

查看文件缓存:

1
2
3
4
5
6
7
8
ls
pcstat /bin/ls
# 文件在缓存中的大小、百分比
# +---------+----------------+------------+-----------+---------+
# | Name | Size (bytes) | Pages | Cached | Percent |
# |---------+----------------+------------+-----------+---------|
# | /bin/ls | 133792 | 33 | 33 | 100.000 |
# +---------+----------------+------------+-----------+---------+

命中分析

测试使用 dd 对同一个文件进行多次读取。

先生成文件、清理缓存:

1
2
3
4
5
6
7
8
9
10
dd if=/dev/sda1 of=file bs=1M count=512
echo 3 > /proc/sys/vm/drop_caches ; sync

pcstat file
# 可见生成的文件不在缓存中:
# +-------+----------------+------------+-----------+---------+
# | Name | Size (bytes) | Pages | Cached | Percent |
# |-------+----------------+------------+-----------+---------|
# | file | 536870912 | 131072 | 0 | 000.000 |
# +-------+----------------+------------+-----------+---------+

使用 dd 测试文件读取速度:

1
2
3
4
5
6
dd if=file of=/dev/null bs=1M
# 512+0 records in
# 512+0 records out
# 536870912 bytes (537 MB, 512 MiB) copied, 16.0509 s, 33.4 MB/s

# 可见通过文件系统从磁盘中读取性能是 33.4 MB/s。

使用 cachetop 监控缓存命中情况:

1
2
3
4
cachetop 5
# PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT%
# ...
# 3264 root dd 37077 37330 0 49.8% 50.2%

读请求的缓存命中率只有为 49.8% ,此时在另一个终端执行 dd 读取同一个文件:

1
2
3
4
5
6
dd if=file of=/dev/null bs=1M
# 512+0 records in
# 512+0 records out
# 536870912 bytes (537 MB, 512 MiB) copied, 0.118415 s, 4.5 GB/s

# 可见读取性能是 4.5 GB/s。

查看监控缓存命中情况:

1
2
3
4
5
6
7
8
9
10
11
cachetop 5
# PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT%
# ...
# 2642 root dd 131637 0 0 100.0% 0.0%

pcstat file
# +-------+----------------+------------+-----------+---------+
# | Name | Size (bytes) | Pages | Cached | Percent |
# |-------+----------------+------------+-----------+---------|
# | file | 536870912 | 131072 | 131072 | 100.000 |
# +-------+----------------+------------+-----------+---------+

可见系统缓存对第二次 dd 操作有明显的加速效果,大大提高文件读取的性能。

如何监控?

监控读写性能时需要注意:

  • 使用缓存可以大幅提高读写性能,在用 dd 测试文件系统性能时,要注意由于缓存造成的测试结果失真。

  • 命中率只反映经过系统缓存的读写请求比例,不能反映实际的读写数据大小。比如使用直接 I/O 会绕过系统缓存,由于没有使用缓存,就谈不上缓存命中率了(cachetop 工具不会把直接 I/O 计算在内)。因此除了缓存命中率,还要结合实际的 I/O 大小(HITS,次数)综合分析缓存的使用情况。

内存泄漏

进程的内存空间:

  • :内存由系统自动分配和管理,程序运行超出局部变量的作用域,栈内存就会被系统自动回收,所以不会产生内存泄漏。

  • :内存由应用程序动态分配管理,比如使用 malloc() 分配,使用 free() 释放。如果没有正确释放堆内存,就会造成内存泄漏。

  • 只读段:包括只读的程序的代码和常量,不会产生内存泄漏。

  • 数据段:包括在定义时就已经确定大小的全局变量和静态变量,不会产生内存泄漏。

  • 内存映射段:包括动态链接库和共享内存,共享内存由程序动态分配和管理,没有正确回收会造成内存泄漏.

在内存紧张时系统会通过 OOM (Out of Memory)机制杀死进程,但在 OOM 前就可能造成严重的性能问题,比如:其他需要内存的进程,无法分配新的内存;内存不足触发系统的缓存回收以及 SWAP 机制,从而进一步导致 I/O 的性能问题等。

如何监控?

使用 vmstat 查看内存动态变化情况:

1
2
3
4
5
6
7
8
9
10
11
vmstat 3
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa st
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa st
# 0 0 0 6601824 97620 1098784 0 0 0 0 62 322 0 0 100 0 0
# 0 0 0 6601700 97620 1098788 0 0 0 0 57 251 0 0 100 0 0
# 0 0 0 6601320 97620 1098788 0 0 0 3 52 306 0 0 100 0 0
# 0 0 0 6601452 97628 1098788 0 0 0 27 63 326 0 0 100 0 0
# 2 0 0 6601328 97628 1098788 0 0 0 44 52 299 0 0 100 0 0
# 0 0 0 6601080 97628 1098792 0 0 0 0 56 285 0 0 100 0 0

可见 memory 的 free 一列不断下降,而 buff 和 cache 基本不变。说明系统使用内存在不断升高,要确定是否发生内存泄漏,还需要找到让内存增长的进程,并找到其内存增长的原因。

使用 bcc 的 memleak 工具可以跟踪系统或指定进程的内存分配、释放请求,然后定期输出未释放内存和相应调用栈的汇总情况(默认 5s)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# -a 显示每个内存分配请求的大小以及地址
docker cp app:/app /app
memleak -a -p $(pidof app)
# 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]

# 内存分配的调用栈中可见应用在不断分配内存,且没有回收。
# 假设修复代码中的 bug、继续监控可以看见以下正常输出:
memleak -a -p $(pidof app)
# Attaching to pid 18808, Ctrl+C to quit.
# [10:23:18] Top 10 stacks with outstanding allocations:
# [10:23:23] Top 10 stacks with outstanding allocations:

bcc-tools 需要内核版本为 4.1 或者更高,对于较久的版本,建议使用 valgrind。

内存泄漏的原因有很多,比如:

  • malloc()free() 通常并不是成对出现,需要注意在每个异常处理路径和成功路径上都释放内存 。

  • 在多线程程序中,一个线程中分配的内存,可能会在另一个线程中访问和释放。

  • 在第三方的库函数中,隐式分配的内存可能需要应用程序显式释放。

  • 对于 Java 应用,可参考 Java 虚拟机 ProcessOn Mind 的 “内存溢出异常(OOM)”。

交换空间

文件页(File-backed Page):经系统释放后可以回收的内存,包括缓存和缓冲区。其在内存管理中通常被称为文件页,大部分文件页都可以直接回收,需要时再从磁盘重新读取。被应用程序修改过且未落盘的数据称为内存 脏页,需要先落盘然后才能进行内存释放:

  • 应用程序通过系统调用 fsync 把脏页同步到磁盘中。

  • 交由系统内核线程 pdflush 负责脏页的刷新。

通过内存映射获取的文件映射页也是常见的文件页,也可以被释放掉、在下次访问时从文件重新读取。

匿名页(Anonymous Page):诸如应用程序动态分配的堆内存,不能被直接回收。但通过 Swap 机制 就能把它们暂时先存在磁盘、释放内存。

Swap 原理

Swap 即把磁盘空间或者本地文件当成内存使用,使服务器内存不足也可以运行大内存的应用程序。

其包括换出和换入两个过程:

  • 换出:把进程暂时不用的内存数据存储到磁盘中,并释放数据占用的内存。

  • 换入:在进程再次访问这些内存时,从磁盘读到内存。

应用场景:

  • 应用程序不希望被 OOM 杀死(即使内存不足时),而是缓一段时间、等待人工介入或系统自动释放其他进程内存,再分配给它。

  • 常见的电脑休眠和快速开机功能也是基于 Swap,省去应用程序的初始化过程,加快开机速度。

内存回收的时机:

  • 直接回收:有新的大块内存分配请求、但是剩余内存不足,系统回收一部分内存(比如缓存),尽可能满足新内存请求。

  • 定期回收:即 kswapd0。kswapd0 定义了三个内存阈值(watermark,也称为水位)用于衡量内存的使用情况,剩余内存使用 pages_free 表示:

    • 页最小阈值(pages_min)

    • 页低阈值(pages_\low)

    • 页高阈值(pages_high)

1
2
3
4
5
6
7
8
9
10

pages_free |
| 内存充足
+---------------- pages_high
| 内存分配正常
+---------------- pages_low
| 内存压力巨大
+---------------- pages_min
| 内存基本耗尽,
| 仅内核可分配内存

一旦剩余内存小于页低阈值就会触发内存的回收。页低阈值可通过内核选项 /proc/sys/vm/min_free_kbytes 间接设置页最小阈值,其他两个阈值根据页最小阈值计算生成的 :

1
2
pages_low = pages_min*5/4
pages_high = pages_min*3/2

NUMA 架构

有时系统有剩余内存,但 Swap 仍在升高。就是处理器 NUMA (Non-Uniform Memory Access)架构 导致。

在 NUMA 架构下多个处理器被划分到不同 Node 上,每个 Node 都拥有自己的本地内存空间。同一个 Node 内部的内存空间可进一步分为不同的内存域(Zone):

  • 直接内存访问区(DMA)

  • 普通内存区(NORMAL)

  • 伪内存区(MOVABLE)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Node Memory Node Memory
+---------------+ +---------------+
| ZONE_DMA | | ZONE_DMA |
+---------------+ +---------------+
| ZONE_DMA32 | | ZONE_DMA32 |
+---------------+ +---------------+
| ZONE_NORMAL | | ZONE_NORMAL |
+---------------+ +---------------+
| ZONE_MOVABLE | | ZONE_MOVABLE |
+--+---------------+--+ +--+---------------+--+
| | ZONE_DEVICE | | | | ZONE_DEVICE | |
| +---------------+ | | +---------------+ |
| +---+---+ | | +---+---+ |
| | 0 | 1 | Node 0 | | | 0 | 1 | Node 1 |
| +---+---+ | | +---+---+ |
| | 2 | 3 | | | | 2 | 3 | |
| +---+---+ | | +---+---+ |
+---------------------+ +---------------------+

使用 numactl 命令可查看处理器在 Node 的分布情况、每个 Node 的内存使用情况:

1
2
3
4
5
6
7
8
numactl --hardware
# available: 1 nodes (0)
# node 0 cpus: 0 1
# node 0 size: 7977 MB
# node 0 free: 4416 MB
# ...

# cpu0 和 cpu1 都在 Node0 上,Node0 内存为 7977MB,剩余内存 4416MB。

通过 /proc/zoneinfo 查看内存阈值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 活跃文件页数
# ...

# 可见剩余内存(free)远远大于页高阈值(22344),此时 kswapd0 不会回收内存。

某个 Node 内存不足时,系统可以从其他 Node 寻找空闲内存,也可以从本地内存中回收内存。

通过 /proc/sys/vm/zone_reclaim_mode 来调整模式:

  • 默认 0 即既可以从其他 Node 寻找空闲内存,也可以从本地回收内存。

  • 1、2、4 表示只回收本地内存,2 表示可以回写脏数据回收内存,4 表示可以用 Swap 方式回收内存。

因此内存的回收既包含文件页,也包含匿名页:

  • 对文件页的回收是直接回收缓存,或把脏页写回磁盘后再回收。

  • 对匿名页的回收,是通过 Swap 机制把它们写入磁盘后再释放内存。

通过 /proc/sys/vm/swappiness 选项可调整使用 Swap 的积极程度:范围 [0, 100],数值越大越积极使用 Swap(更倾向于回收匿名页);数值越小越消极使用 Swap(更倾向于回收文件页)。

如何监控?

配置 Swap(以 Swap 文件为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 创建 Swap 文件
fallocate -l 8G /mnt/swapfile

# 修改权限只有 root 用户可以访问
chmod 600 /mnt/swapfile

# 配置 Swap 文件
mkswap /mnt/swapfile

# 开启 Swap
swapon /mnt/swapfile

# 关闭 Swap(先关闭再打开就能清理 Swap)
# swapoff -a

使用 free 命令查看 Swap 使用情况:

1
2
3
4
free
# total used free shared buff/cache available
# Mem: 8169348 331668 6715972 696 1121708 7522896
# Swap: 8388604 0 8388604

使用 dd 命令模拟大文件读取:

1
dd if=/dev/sda1 of=/dev/null bs=1G count=2048

使用 sar 命令观察各项指标变化(内存、Swap):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

可见总内存使用率(%memused)在不断增长(23% -> 98%),且主要内存都被缓冲区(kbbuffers)占用:

  • 剩余内存(kbmemfree)不断减少,缓冲区(kbbuffers)不断增大,可知剩余内存不断分配给缓冲区。
  • 一段时间后剩余内存很小,而缓冲区占用大部分内存。此时 Swap 逐渐增大,缓冲区和剩余内存则只在小范围内波动。

使用 cachetop 命令观察缓存使用情况:

1
2
3
4
5
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%

可见 dd 进程只有 50% 的缓存命中率,未命中的缓存页数(MISSES)为 41022(单位是页),之前运行的 dd 使得缓冲区使用升高。动态观察剩余内存、内存阈值以及匿名页和文件页的活跃情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# -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)。结合用 sar 看到剩余内存和缓冲区的变化情况可以推导出,剩余内存和缓冲区的波动变化是由于内存回收和缓存再次分配的循环往复:

  • 当剩余内存小于页低阈值时,系统会回收一些缓存和匿名内存,使剩余内存增大。缓存的回收导致 sar 中的缓冲区减小,而匿名内存的回收导致了 Swap 的使用增大。

  • 由于 dd 还在继续,剩余内存重新分配给缓存,导致剩余内存减少、缓冲区增大。

如果多次运行 ddsar,可能会见到在多次的循环重复中,有时 Swap 时多时少,缓冲区的波动则更大。系统回收内存时有时候会回收更多的文件页,有时候回收更多的匿名页。系统回收不同类型内存的倾向不明显,可以查看 swappiness 配置:

1
2
cat /proc/sys/vm/swappiness
# 60

查看使用 Swap 最多的进程:

1
2
3
4
5
6
7
8

# 按 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

虽然缓存属于可回收内存,但在类似大文件拷贝的场景下,系统还是会用 Swap 机制回收匿名内存,而不仅回收占用绝大部分内存的文件页。

Swap 用来解决内存资源紧张的问题,但本质上还是磁盘操作,因此降低降低 Swap 的使用,可以提高系统的整体性能,因此建议:

  • 服务器内存足够大则直接禁止 Swap。

  • 大部分云平台中的虚拟机都默认禁止 Swap。如果实需要用 Swap,可尝试降低 swappiness,减少内存回收时 Swap 的使用倾向。

  • 对于响应延迟敏感的应用,如果在开启 Swap 的服务器中运行,可以用库函数 mlock() 或者 mlockall() 锁定内存、阻止其内存换出。

优化总结

基本优化思路:

  • 禁止 Swap,如果必须开启 Swap 则降低 swappiness 的值,减少内存回收时 Swap 的使用倾向。

  • 减少内存的动态分配。比如使用内存池、大页(HugePage)等。

  • 尽量使用缓存和缓冲区来访问数据。比如使用堆栈明确声明内存空间来存储需要缓存的数据;或者用 Redis 等外部缓存组件,优化数据的访问。

  • 使用 cgroups 等方式限制进程内存使用情况。可以确保系统内存不会被异常进程耗尽。

  • 通过 /proc/pid/oom_adj,调整核心应用的 oom_score。可保证即使内存紧张,核心应用也不会被 OOM 杀死。

了解内存相关概念与指标后,可通过下表找到合适的工具、分析问题。

img img

img

参考

分析案例可参考:

CATALOG
  1. 1. Linux 内存原理分析
    1. 1.1. 工作原理
      1. 1.1.1. 内存映射
      2. 1.1.2. 虚拟内存布局
      3. 1.1.3. 内存管理
      4. 1.1.4. Buffer 与 Cache
        1. 1.1.4.1. 写操作
        2. 1.1.4.2. 读操作
    2. 1.2. 缓存优化
      1. 1.2.1. 命中分析
      2. 1.2.2. 如何监控?
    3. 1.3. 内存泄漏
      1. 1.3.1. 如何监控?
    4. 1.4. 交换空间
      1. 1.4.1. Swap 原理
      2. 1.4.2. NUMA 架构
      3. 1.4.3. 如何监控?
    5. 1.5. 优化总结
    6. 1.6. 参考