这一章在做什么
回答两个问题:内存是不是真的不够,如果不够或一直在涨,是谁、哪段代码在用。先分清「用掉了」和「被缓存占着、随时能让出来」,再看内存紧张的信号:换页、页扫描、PSI、OOM 杀进程。然后落到进程:VSZ 和 RSS 的区别,缺页为什么就是 RSS 在长,free 之后 RSS 为什么不降。最后是几个调优旋钮:swappiness、overcommit、大页、NUMA 绑定和 cgroup 限额。
什么时候翻这一章
- 服务的内存一直涨,不知道是泄漏还是正常先问「它本来就该这样吗」:缓存预热、分配器不还内存,都会让 RSS 只涨不落。确认在涨,再用缺页采样或 memleak 找代码路径。§7.4.6§7.3.3场景手册 →
- ARM 板内存小,进程被 OOM 杀掉没有 swap 的机器没有「先变慢」的缓冲期,内存一用完 OOM killer 就出手。先从 dmesg 里看清杀了谁、它当时占多少。§7.3.2场景手册 →
- free 显示内存快没了多半是页缓存占着,看 available 那一列。真正要担心的是换页和页扫描。§7.2.6§7.4.2
- 机器突然变慢,swap 在动vmstat 的 si、so 持续不为零就是在换匿名页,应用访问被换出的页要等磁盘。§7.2.2§7.5.1
- 容器里的进程被杀,宿主机内存还很多是容器的 cgroup 限额用完了,内存紧张要按限额算。§7.3.2§7.6.5场景手册 →
- 多路服务器上,同一个程序时快时慢可能是内存分到了另一个插槽上(远程 NUMA 节点),每次访问多绕一跳。§7.5.6§7.6.4
CPU 缓存和 TLB 的硬件细节在第 6 章;页缓存(文件系统缓存)和 free 命令在第 8 章(§8.6.2);容器的内存限额在第 11 章。这一章的方法框架来自第 2 章的 USE 方法。
先用中文过一遍
虚拟内存和按需分页:VSZ 大不等于占内存
每个进程看到的是自己的一大片线性地址空间,这就是虚拟内存(virtual memory)。它不是真的内存:malloc 或 mmap 只是在地址空间里划出一段,内核这时一页物理内存都没给。等程序第一次写这一页,MMU 查不到映射,触发缺页(page fault),内核才找一页空闲的物理内存挂上去。这叫按需分页(demand paging)(§7.2.3)。
所以进程有两个「大小」。VSZ(虚拟内存大小)是划出来的全部地址,碰没碰过都算;RSS(驻留集大小)是此刻真正在物理内存里的页。缺页分两种:能直接从内存满足的叫次缺页(minor fault),比如给新页分配内存;要读磁盘的叫主缺页(major fault),比如访问被换出到 swap 的页、或者还没读进来的映射文件。对排查最有用的一条推论是:次缺页的次数就是 RSS 增长的足迹,匿名内存大致每 4 KB 一次。想知道内存是被哪段代码长起来的,就去采缺页的调用栈(§7.5.10)。
VSZ 和 RSS 差得远是常态:线程栈、分配器给各线程预留的区域、映射了却没读的文件,都先占地址不占内存。Linux 还允许过量分配(overcommit):申请的总量可以超过物理内存加 swap,赌的是程序不会把申请的全用上(§7.2.4)。代价是到真的用不起时,malloc 不会失败,而是由 OOM killer 收场。
内存紧张时,内核按什么顺序腾地方
空闲页从空闲链表(free list)里拿。空闲页低于某条水位线,后台的 kswapd 被唤醒,开始扫描 LRU 链表找能回收的页(页扫描,page scanning),回收到更高的一条水位才停。最先腾的是页缓存:干净的文件页直接丢,脏页要先写回磁盘。再不够,有 swap 的话就把不常用的匿名页换出去,Linux 把这叫 swapping(§7.3.2)。vm.swappiness(默认 60;书里写的范围是 0 到 100,新内核允许设到 200)决定更偏向回收页缓存还是换出应用内存。
如果分配来得比 kswapd 回收得快,空闲页掉到最低水位,申请内存的线程就得自己动手回收,这叫直接回收(direct reclaim):线程在分配内存时被卡住,延迟直接算在请求上。最后一步是 OOM killer:挑一个进程杀掉,系统日志里留下「Out of memory」。这几步每一步都是内存饱和的信号,对应的指标也不同。
vm.min_free_kbytes 等参数决定(§7.6.1)。有没有 swap 决定了内存一直涨的服务怎么出事。有 swap:先是换页、变慢,你有时间在现场查;没有 swap(很多云主机和 ARM 板):没有缓冲期,用完就被杀,问题可能要跑几个小时才冒出来。书里提到 Netflix 的云主机就不配 swap,宁可让一台实例被杀、流量转走,也不要它慢慢换页。哪种好要看你的部署:一块独自干活的板子,没有「别的实例」可以接流量。
堆只涨不落:泄漏、增长和分配器
用户态程序的 malloc 和 free 不直接对着内核,中间隔着分配器(glibc malloc、jemalloc、TCMalloc)。分配器向内核要来大块内存,切小了发给程序;程序 free 之后,内存多半回到分配器自己的空闲链表里留着下次用,不一定还给内核。所以 RSS 只涨不落,可能完全正常(§7.3.3)。
书里把「内存一直涨」分成两种:泄漏(leak),内存不再用了却没释放,是代码的 bug;增长(growth),程序正常地用内存,只是用得比你想要的多,比如缓存设得太大、还在预热(§7.4.6)。两种的修法不同,后者常常被误判成前者。glibc 的规矩大致是:大块分配(默认从 128 KB 起,会动态调高)直接用 mmap,free 时立刻 munmap 还给内核;小块从堆里切,堆顶空出来足够多时才用 brk 缩回去。只要堆顶还有一块在用,下面空了再多也缩不回去。
malloc_trim 会把各个 arena 里空闲块中的整页都还回去,不只是堆顶(出处:malloc_trim(3) 手册)。别的分配器有自己的办法,书里提到 TCMalloc 会定期把内存收回中央堆(§7.3.3)。大页和 NUMA:两种和硬件有关的调优
MMU 把虚拟地址翻译成物理地址,结果缓存在 TLB 里。TLB 条目不多,页越大,同样多的条目能覆盖越多的内存,TLB 未命中就越少。Linux 的大页(huge pages)常见是 2 MB,可以预先配好给程序用,也可以交给透明大页(THP)自动合并(§7.6.2)。多插槽的服务器还有 NUMA:每颗 CPU 挂着自己的内存,访问别的插槽上的内存要经过互连,延迟更高(§7.3.1)。内核会尽量就近分配,numastat 看它做得怎么样,numactl 可以把进程和它的内存绑在一个节点上(§7.6.4)。单插槽的机器和多数 ARM 板只有一个节点,这一段可以先跳过。
| 英文 | 中文 | 一句话 |
|---|---|---|
| virtual memory | 虚拟内存 | 每个进程自己的一片地址空间,不是真内存,按需映射到物理页 §7.2.1 术语页 |
| RSS (resident set size) | 驻留集大小 | 此刻在物理内存里的页;含共享库,各进程相加会超过总内存 §7.2.3 术语页 |
| VSZ (virtual memory size) | 虚拟内存大小 | 划出来的全部地址,没碰过的也算,常常远大于 RSS §7.2.3 |
| PSS (proportional set size) | 按比例分摊的驻留大小 | 私有页加上共享页除以共享它的进程数,相加不会重复计算 §7.2.9 术语页 |
| page fault (minor / major) | 缺页(次缺页 / 主缺页) | 访问还没映射的页;不读盘是次缺页,读盘是主缺页 §7.2.3 术语页 |
| anonymous memory | 匿名内存 | 没有对应文件的内存:堆和栈;被回收时只能换出到 swap §7.1 |
| swapping | 换出(Linux 的用法) | Linux 指把匿名页换到 swap;传统 Unix 指把整个进程换出 §7.1 术语页 |
| overcommit | 过量分配 | 允许申请超过物理内存加 swap 的总量,靠按需分页兜着 §7.2.4 术语页 |
| OOM killer | OOM 杀手 | 内存实在腾不出来时,内核挑一个进程杀掉 §7.3.2 术语页 |
| kswapd / page scanning | 换页守护进程 / 页扫描 | 空闲页低于水位时在后台扫描 LRU 链表回收页 §7.3.2 |
| direct reclaim | 直接回收 | 空闲页太少时,申请内存的线程自己回收,被阻塞 §7.3.2 术语页 |
| WSS (working set size) | 工作集大小 | 进程经常访问的那部分内存;工具一般只给 RSS,量它要专门的办法 §7.2.10 术语页 |
| slab / SLUB | slab 分配器 | 内核按对象大小缓存内存块,slabtop 看它占了多少 §7.3.3 |
| huge pages / THP | 大页 / 透明大页 | 2 MB 等大尺寸的页,扩大 TLB 覆盖范围 §7.6.2 术语页 |
| NUMA | 非一致内存访问 | 多插槽机器上,访问本地内存比远程内存快 §7.3.1 术语页 |
方法:按什么顺序查
书里列了十种内存的方法,排查时作者建议先做三样:性能监控、USE 方法、刻画内存用量(§7.4)。USE 里又建议先查饱和度,因为持续的饱和就是内存出了问题的标志(§7.4.2)。本站把它们排成下面几步:前三步回答「够不够」,后三步回答「谁、为什么」。
- 内存真的不够吗?
free -m看available,不看free;cat /proc/pressure/memory看有没有任务在等内存。available 还多、PSI 是 0,就不是内存的事。§7.2.6§7.5.2 - 有没有饱和:换页、扫描、OOM?
vmstat 1的si、so持续不为零是在换页;sar -B 1的pgscand/s不为零说明有线程在直接回收;dmesg | grep -i 'out of memory'查有没有杀过进程。§7.4.2§7.5.4 - 有没有限额先到了?
容器和 systemd 服务都在 cgroup 里:看所在组的
memory.max、memory.current、memory.events。宿主机再空,组内到了限额照样回收、照样杀。§7.4.2§7.6.5 - 谁在用?
ps aux --sort=-rss | head或top里按M。几十个进程共享同一批库时,RSS 相加会超过总内存,按 PSS 看(/proc/PID/smaps_rollup、pmap -X)。内核自己用的看/proc/meminfo的Slab、slabtop。§7.5.7§7.5.9§7.5.5 - 它在涨吗?涨的是哪一段?
pidstat -r -p PID 60长期记 RSS;pmap -x PID隔一段时间比一次,看是堆、匿名映射还是文件映射在长。先问「它本来就该这样吗」:看配置里的缓存大小,看涨到某个值会不会停。§7.4.5§7.4.6 - 为什么涨:哪段代码?
缺页就是 RSS 在长:
perf record -e page-faults -g采缺页时的调用栈,画成火焰图。疑似泄漏用 BCC 的memleak看哪些分配没释放(开销不小)。也可以先做普通的 CPU 剖析,找分配函数所在的路径,最便宜。§7.5.10§7.4.6 - 配置对不对?
overcommit、swappiness、THP、有没有 swap、NUMA 怎么分布、应用自己配了多少缓存。这些在出事之前就能查。§7.4.7§7.6.1
工具速查
| 想知道 | 敲什么 | 看哪里 | 节 |
|---|---|---|---|
| 还剩多少能用 | free -mgrep MemAvailable /proc/meminfo | available;free 列小是正常的。used 的算法随版本变:procps-ng 4.0.1 起是 total − available,更早是 total − free − buff/cache | §7.4.2 |
| 有没有任务在等内存 | cat /proc/pressure/memory | some 是有任务卡住的时间比例,full 是所有可运行任务都卡住;avg10 比 avg300 高说明在变糟 | §7.5.2 |
| 在不在换页 | vmstat 1 | si、so 持续不为零;swpd 大但 si、so 为零,说明以前换出过、现在没在换 | §7.5.1 |
| 配了多少 swap | swapon --showcat /proc/swaps | 没有输出就是没配 swap,内存用完直接 OOM | §7.5.3 |
| 页扫描和直接回收 | sar -B 1 | pgscank/s 后台扫描,pgscand/s 直接回收(不为零就有线程被卡);%vmeff 低于 30% 说明回收很吃力 | §7.5.4 |
| 杀过谁 | dmesg -T | grep -iE 'out of memory|oom-kill'grep oom_kill /proc/vmstat | 被杀的进程名、anon-rss(它自己的匿名内存);计数在涨说明还在杀 | §7.4.1 |
| 谁占得最多 | ps aux --sort=-rss | headtop,按 M | RSS(top 里是 RES);VSZ / VIRT 大不说明问题 | §7.5.7 |
| 某个进程在长吗 | pidstat -r -p PID 10 | RSS 的走向;minflt/s 不为零就是在长新页,majflt/s 不为零是在等磁盘 | §7.4.5 |
| 进程的内存分在哪 | pmap -x PIDcat /proc/PID/smaps_rollup | 哪段映射的 RSS 最大,[ anon ] 多半是堆;Pss 是扣掉共享后的份额 | §7.5.9 |
| 内核 slab 占了多少 | slabtop -scgrep -E 'Slab|SUnreclaim' /proc/meminfo | 哪个缓存最大、在不在一直涨;SUnreclaim 一直涨可能是内核里的泄漏 | §7.5.5 |
| NUMA 分得好不好 | numastatnumactl --hardware | numa_miss、numa_foreign 相对 numa_hit 的比例;只有一个 node 就不用看 | §7.5.6 |
| 哪段代码让 RSS 长 | perf record -e page-faults -g -p PID -- sleep 30 | 缺页最多的调用栈;栈断在 libc 里时改用 --call-graph dwarf | §7.5.10 |
| 疑似泄漏 | memleak-bpfcc -p PID 30 | 一段时间里分配了没释放的调用栈和字节数;分不清泄漏和正常增长,要你读代码判断 | §7.4.6 |
| 直接回收卡了多久 | drsnoop-bpfcc -T | 每次直接回收的进程和 LAT(ms);量出内存紧张对应用的实际影响 | §7.5.11 |
| 容器的限额 | cat /sys/fs/cgroup/组路径/memory.maxcat …/memory.events | memory.current 离 memory.max 多远;memory.events 的 oom_kill 计数 | §7.6.5 |
free、vmstat、ps、top、pmap、slabtop 在 procps 包里;pidstat、sar 在 sysstat;numastat 在 numactl 包;perf 在 linux-perf;带 -bpfcc 后缀的在 bpfcc-tools。slabtop 要读 /proc/slabinfo,要 root;BCC 工具都要 root。工具名链接到本站的工具卡:free、vmstat、sar、ps、pidstat、pmap、slabtop、numastat、perf、memleak、drsnoop。
动手:在自己的机器上试
分配了不碰,再一点点碰:看 VSZ、RSS 和缺页
先猜:malloc(200 MB) 返回之后,RSS 涨多少?每写 50 MB,会发生多少次缺页?下面这个程序自己打印 /proc/self/status 里的两个数和累计的次缺页数,每一步停两秒,方便你从外面看。整个实验最多用 200 MB 内存,不会引起换页。
// grow.c:先要 200 MB 不碰,再每两秒碰 50 MB,最后释放
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/resource.h>
#define MB (1024L * 1024)
static void show(const char *what) {
char line[128]; long vsz = 0, rss = 0;
FILE *f = fopen("/proc/self/status", "r");
while (fgets(line, sizeof line, f)) {
sscanf(line, "VmSize: %ld", &vsz);
sscanf(line, "VmRSS: %ld", &rss);
}
fclose(f);
struct rusage ru; getrusage(RUSAGE_SELF, &ru);
printf("%-16s VSZ %4ld MB RSS %4ld MB minflt %6ld\n",
what, vsz / 1024, rss / 1024, ru.ru_minflt);
}
int main(void) {
printf("pid %d\n", getpid());
show("start");
char *p = malloc(200 * MB);
show("malloc 200 MB");
sleep(2);
for (int i = 0; i < 4; i++) {
memset(p + i * 50 * MB, 1, 50 * MB); // 第一次写,才真正分到物理页
show("touch +50 MB");
sleep(2);
}
free(p);
show("free");
sleep(2);
return 0;
}
$ gcc -O2 -g -fno-omit-frame-pointer grow.c -o grow $ ./grow # 另开一个终端,在它跑的时候看 $ pidstat -r -p $(pgrep -n grow) 1 $ ps -o pid,vsz,rss,min_flt,maj_flt,comm -p $(pgrep -n grow) $ pmap -x $(pgrep -n grow) | grep -E 'Address|anon'
本站在 WSL2 上看到的(只留了关键几行):
start VSZ 2 MB RSS 1 MB minflt 88 malloc 200 MB VSZ 202 MB RSS 1 MB minflt 89 touch +50 MB VSZ 202 MB RSS 51 MB minflt 12891 touch +50 MB VSZ 202 MB RSS 101 MB minflt 25691 touch +50 MB VSZ 202 MB RSS 201 MB minflt 51291 free VSZ 2 MB RSS 1 MB minflt 51291 pidstat: minflt/s majflt/s VSZ RSS Command 12800.00 0.00 207364 52736 grow pmap: Kbytes RSS Dirty Mode Mapping 204816 51212 51212 rw--- [ anon ]
malloc之后 VSZ 涨了 200 MB,RSS 一点没动:只划了地址,没给物理页。- 每写 50 MB,次缺页加 12800 次左右:50 MB ÷ 4 KB = 12800,一页一次。pidstat 的
minflt/s在写的那一秒正好是 12800,majflt/s是 0(没碰磁盘)。 - pmap 里那一段
[ anon ]有 200 MB 的地址,只有 50 MB 驻留:这就是一个正在「长」的进程在 pmap 里的样子。 free之后 VSZ 和 RSS 一起回落:200 MB 远超 glibc 的 mmap 阈值,这块是单独mmap来的,free直接还给了内核。下一个实验看小块为什么不一样。
再用 perf 数缺页、找代码。-g(帧指针)在这里栈会断:缺页发生在 libc 的 memset 里,perf 顺着帧指针走不出 libc(本站这台机器上的 libc 没有保留帧指针),回不到 main。改用 DWARF 回溯就看得到:
$ perf stat -e page-faults,minor-faults,major-faults ./grow $ perf record -e page-faults --call-graph dwarf ./grow $ perf report --stdio --no-children
51,261 page-faults:u
51,261 minor-faults:u
0 major-faults:u
99.85% grow libc.so.6 [.] 0x000000000017dc40
---0x754872844c40
main
__libc_start_main
把 memset 换成你的服务,这就是书里说的缺页火焰图的原料:哪条调用栈触发的缺页多,RSS 就是被谁长起来的(§7.5.10)。注意它只记「第一次碰」,已经驻留的内存被反复使用不会出现在这里。
- WSL2
- 本站实测过上面所有命令(Debian 13,glibc 2.41,perf 6.12),不用 root。本机的透明大页设置是
madvise(看/sys/kernel/mm/),所以每 4 KB 一次缺页;设成transparent_hugepage/ enabled always的机器上,大块匿名内存可能按 2 MB 一次缺页,次数会少很多。 - 服务器
- 对正在跑的服务,
pidstat -r -p PID 60挂一天,看 RSS 的走向比看一眼准。perf record -e page-faults -g -p PID -- sleep 30要 root 或调低kernel.perf_event_paranoid;DWARF 回溯生成的数据文件大,采样时间别太长。 - ARM 板
- 程序本身只依赖
/proc/self/status和getrusage,交叉编译就能跑。没有 pidstat 时,隔几秒读一次/proc/PID/status的VmRSS,和/proc/PID/stat的第 10、12 个字段(累计的次缺页、主缺页)。busybox 的ps、top显示哪些内存列,看它的编译选项。
free 之后 RSS 为什么不降
这是「服务内存只涨不落」最常见的样子。程序分配 20 万个 1000 字节的小块(约 200 MB),释放掉除了最后一块以外的全部,再调一次 malloc_trim(0)。先猜:释放之后 RSS 是多少?
// frag.c:20 万个 1000 字节的小块,释放掉除最后一个以外的全部
#include <malloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define N 200000
static long rss_mb(void) {
char line[128]; long rss = 0;
FILE *f = fopen("/proc/self/status", "r");
while (fgets(line, sizeof line, f)) sscanf(line, "VmRSS: %ld", &rss);
fclose(f);
return rss / 1024;
}
int main(void) {
static char *blk[N];
for (int i = 0; i < N; i++) { blk[i] = malloc(1000); memset(blk[i], 1, 1000); }
printf("allocated RSS %4ld MB\n", rss_mb());
for (int i = 0; i < N - 1; i++) free(blk[i]); // 只留最后一块
printf("freed all but 1 RSS %4ld MB\n", rss_mb());
malloc_trim(0);
printf("malloc_trim(0) RSS %4ld MB\n", rss_mb());
return 0;
}
$ gcc -O2 frag.c -o frag $ ./frag
allocated RSS 194 MB freed all but 1 RSS 195 MB malloc_trim(0) RSS 2 MB
- 释放了 99.99% 的块,RSS 一点没降:这些小块是从堆里切的,最后一块压在堆顶,
brk退不回去,空出来的内存留在 glibc 手里等下次分配。从外面看,这和泄漏一模一样。 malloc_trim(0)之后 RSS 回到 2 MB:它把空闲块里的整页还给了内核,不只是堆顶(glibc 2.8 以后的行为,见 malloc_trim(3) 手册)。- 所以看到 RSS 不回落,先别下结论。下一步是看它会不会继续涨:涨到某个值就平了,多半是分配器留着;一直涨,才去找泄漏(memleak、valgrind、分配器的调试模式)。
- 把最后一块也释放了再试试:在本站的 glibc 2.41 上 RSS 仍然停在 195 MB,关掉每线程缓存(tcache)也一样;本站没有追查原因。分配器的细节会随版本变,结论要自己量,别靠推理。
- WSL2
- 本站实测过(glibc 2.41),不用 root,峰值约 200 MB。
- 服务器
- 服务里可以在空闲时调
malloc_trim(0)(它本身要遍历空闲块,有开销),或者换 jemalloc、TCMalloc:书里说可以用LD_PRELOAD在启动时换,不用改代码(§7.6.3)。换之前用这个程序对比一下行为。 - ARM 板
- 很多 BSP 用的不是 glibc,而是 musl 或 uClibc,它们的分配器策略不同,有没有
malloc_trim也要查自己的 C 库。把这个程序交叉编译过去跑一遍,就知道你板子上的分配器会不会把内存还回去。
成书之后
内核里只剩 SLUB 一种 slab 分配器
- 书里
- 介绍了 slab 和 SLUB 两种内核分配器,说 Linux 后来把 SLUB 作为选项或默认(§7.3.3)。
- 今天
- SLAB 在 Linux 6.5 被标为弃用,6.8 从内核里删掉了,SLUB 成了唯一的通用对象分配器。
/proc/slabinfo和 slabtop 照常能用,看的就是 SLUB 的缓存。
内存限额改用 cgroup v2 的文件
- 书里
- 列的是 cgroup v1 内存控制器的文件:
memory.limit_in_bytes、memory.oom_control等(§7.6.5)。 - 今天
- Debian 11 起 systemd 默认用 cgroup v2(统一层级)。对应的文件是
memory.max(硬上限,到了会在组内触发 OOM)、memory.high(软上限,超过就被限流、加紧回收,不触发 OOM)、memory.events(含oom_kill计数)、memory.pressure(这个组自己的 PSI)。本站在 WSL2 的 Debian 13 上看到的就是这一套,路径用cat /proc/self/cgroup找。
分节地图
这一章的骨架和第 6 章一样:背景(§7.1–7.2)、架构(§7.3)、方法(§7.4)、工具(§7.5)、调优(§7.6)。上手只需要概念里讲按需分页、换页、缓存的几节,§7.3.2 里内核怎么腾内存(含 OOM),方法里的 USE 和泄漏检测,加上 vmstat、PSI、ps、pmap 四个工具。硬件、各个跟踪工具和调优参数用到时再查。
- System main memory stores application and…约 300 词开篇内存不够的两种后果:换页变慢、进程被杀。扫一眼当目录。
- Terminology约 300 词术语swapping 在 Linux 和传统 Unix 里意思不同,读工具输出前看一眼。
- Concepts约 100 词概念一句引子。
- Virtual Memory约 300 词虚拟内存虚拟内存为什么能超过物理内存;「虚拟内存耗尽」是什么意思。
- Paging约 500 词换页文件页换页和匿名页换页,为什么后者才伤性能。
- Demand Paging约 400 词按需分页缺页怎么发生,次缺页和主缺页,RSS 和虚拟大小的定义。
- Overcommit约 100 词过量分配为什么 malloc 很少失败,代价落在哪里。
- Process Swapping约 200 词整进程换出历史背景,Linux 根本不这样做。
- File System Cache Usage约 100 词文件系统缓存占用开机后空闲内存越来越少为什么是正常的。
- Utilization and Saturation约 100 词利用率和饱和度缓存算不算「用了」;哪些动作说明内存饱和。
- Allocators约 100 词分配器分配器能让程序变快,也能因碎片浪费内存。
- Shared Memory约 100 词共享内存共享库算谁的;PSS 怎么分摊。
- Working Set Size约 100 词工作集大小真正常用的那部分内存,概念有用但难量。
- Word Size约 200 词字长32 位地址空间的限制;做 32 位 ARM 的读者知道 3 GB 用户空间这回事即可。
- Architecture约 100 词架构硬件和软件两部分的引子。
- Hardware约 1200 词硬件DRAM、UMA 和 NUMA、DDR 带宽、MMU 和 TLB。查 NUMA、大页时回来看。
- Software约 1200 词软件内存不够时内核按什么顺序腾地方:页缓存、换出、收缩、OOM;kswapd 和水位。这一章最该精读的一节。
- Process Virtual Address Space约 1100 词进程虚拟地址空间各段的用途;堆为什么只涨不落;slab、glibc、jemalloc 等分配器。
- Methodology约 100 词方法十种方法的总表和作者建议先做的三种。
- Tools Method约 300 词工具法在 Linux 上挨个看哪些工具、看哪个数。
- USE Method约 400 词USE 方法内存的利用率、饱和度、错误各看什么;为什么先看饱和度;限额环境怎么算。
- Characterizing Usage约 400 词刻画内存用量用几句话描述内存去了哪;一张深入分析时可以照着问的清单。
- Cycle Analysis约 100 词周期分析从 IPC 看负载有多依赖内存,指向第 6 章。
- Performance Monitoring约 100 词性能监控长期记哪几个数;按进程记能看出泄漏的速度。
- Leak Detection约 300 词泄漏检测泄漏和增长的区别,先问哪个问题,memleak 能做什么不能做什么。
- Static Performance Tuning约 200 词静态性能调优一张检查内存配置的问题清单。
- Resource Controls约 100 词资源控制内存和虚拟内存的上限,细节在 §7.6。
- Micro-Benchmarking约 100 词微基准测试测内存速度和缓存大小,排查内存用量时用不到。
- Memory Shrinking约 100 词内存收缩实验故意压缩内存估计工作集,作者自己说不要在生产上用。
- Observability Tools约 100 词观测工具工具总表,从全系统到进程再到跟踪。
- vmstat约 300 词vmstatswpd、free、buff、cache、si、so 各是什么,哪一列持续不为零要警惕。
- PSI约 100 词PSI内存压力的 some 和 full 两行怎么读,看趋势。
- swapon约 100 词swapon配了哪些 swap、用了多少。
- sar约 300 词sar换页、页扫描、大页、swap 的统计;%vmeff 和 pgscand 两个最有用。
- slabtop约 100 词slabtop内核各个 slab 缓存占了多少。
- numastat约 200 词numastat多插槽机器上内存分配有多少落在了本地节点。
- ps约 200 词ps%MEM、RSS、VSZ;为什么 RSS 相加会超过总内存。
- top约 200 词top按内存排序,各个内存列的含义去看手册。
- pmap约 200 词pmap进程的每段映射占多少,PSS 从哪里看。
- perf约 500 词perf缺页采样和缺页火焰图:找出 RSS 是哪段代码长起来的。
- drsnoop约 100 词drsnoop量每次直接回收让进程等了多久。
- wss约 300 词wss用页表的访问位估计工作集;实验性质,先读警告。
- bpftrace约 600 词bpftrace跟踪 malloc、brk、缺页、回收;跟踪 malloc 开销很大。
- Other Tools约 300 词其他工具memleak、oomkill 等 BPF 工具,dmesg、valgrind、/proc 下的几个文件。
- Tuning约 100 词调优最重要的调优是让应用待在内存里、不换页。
- Tunable Parameters约 400 词可调参数脏页、水位、overcommit、swappiness、NUMA 平衡等参数。改之前读。
- Multiple Page Sizes约 200 词多种页大小配置大页的几种方式和透明大页。
- Allocators约 100 词分配器用 LD_PRELOAD 换分配器。
- NUMA Binding约 100 词NUMA 绑定numactl 把进程的内存和 CPU 绑在一个节点。
- Resource Controls约 100 词资源控制ulimit 和 cgroup 的内存限额(书里是 v1 的文件名)。
- Exercises约 300 词习题习题;动手部分本页已经给了替代。
词与短语
只收先读和用到再查两档的节。斜体那一行是原句里连续的几个词,在电子书里搜它就能找到那一句。
§0 开篇
§7.1 Terminology
§7.2.1 Virtual Memory
§7.2.2 Paging
§7.2.4 Overcommit
§7.2.6 File System Cache Usage
§7.2.7 Utilization and Saturation
§7.3.2 Software
§7.3.3 Process Virtual Address Space
§7.4.1 Tools Method
§7.4.2 USE Method
§7.4.6 Leak Detection
§7.5.7 ps
§7.5.9 pmap
§7.5.12 wss
§7.5.13 bpftrace
§7.6.1 Tunable Parameters
§7.6.2 Multiple Page Sizes
诊断题:换成你来查
场景和输出都是本站编的。先自己判断,再点选项看解释。
场景:一台 8 GB 的服务器跑你的 C++ 日志收集服务,一直在读写大量文件。
$ free -m
total used free shared buff/cache available
Mem: 7821 2411 312 18 5369 5410
Swap: 0 0 0一个一直在读写文件的服务,空闲内存被页缓存占掉是正常的,书里说空闲内存在开机后缩到接近零会吓到不明就里的用户,但不影响应用(§7.2.6)。available 才是不换页就能拿到的估计值。清缓存只会让后面的读重新打到磁盘上,反而变慢。页缓存涨不是泄漏,泄漏看的是某个进程的 RSS 或内核的不可回收部分。报警该改成基于 available 或 PSI。
场景:一个 C++ 写的网关,平时 RSS 约 300 MB。周一流量高峰时涨到 900 MB,之后流量回落,RSS 停在 900 MB 不下来。接下来一周它一直是 900 MB 左右,没有再涨。
pidstat -r(流量回落后):
minflt/s majflt/s RSS Command
0.40 0.00 921344 gateway泄漏的特征是一直涨;这里涨到一个值就平了,次缺页接近零说明它也没在长新页。分配器(这里是 glibc)在 free 之后不一定把内存还给内核,进程的驻留内存因此只涨不落;书里说,对简单的分配器这是正常的(§7.3.3)。要它降下来,可以在空闲时 malloc_trim(0),或者试别的分配器。如果一周后发现它每天都多几十 MB,那才去用 memleak 或缺页采样找代码路径。page cache 不算在进程的 RSS 里(mmap 的文件页除外)。
场景:一块 512 MB 内存、没有 swap 的 ARM 板,camd 跑了大约两天后消失。dmesg 里有:
Out of memory: Killed process 812 (camd) total-vm:402112kB, anon-rss:318420kB
anon-rss 是进程自己的匿名内存(堆、栈、匿名 mmap),318 MB 在 512 MB 的板子上占了六成多。跑两天才出事,像是缓慢增长或泄漏,先每隔几分钟读一次 /proc/PID/status 的 VmRSS,画出走向。免杀只是让内核去杀别的进程,内存照样不够,下一个被杀的可能是更要紧的服务。清页缓存腾不出匿名内存。关掉 overcommit 后 malloc 会更早失败,camd 要是不检查返回值照样崩,只是换了一种死法。没有 swap 就没有先变慢的缓冲期(§7.3.2),所以要靠监控提前发现。
场景:一台 16 GB 的服务器,配了 4 GB swap,同时跑你的服务和一个新上的批处理任务。
vmstat 1: swpd free si so us sy id wa 812044 98120 2310 4120 8 12 41 39 /proc/pressure/memory: full avg10=18.40 avg60=11.02
si、so 每秒几 MB(vmstat 默认以 KiB 为单位),持续不为零就是在换匿名页,书里说这是内存压力的明确信号(§7.5.1)。线程访问被换出的页要等磁盘,wa 高是这个后果,不是原因,换更快的盘只能缓解。PSI 的 full 不为零更严重:那段时间所有可运行的任务都在等内存(§7.5.2)。下一步 ps aux --sort=-rss | head 看是不是批处理吃掉了内存,限它的内存或者错开时间。
场景:一个 64 线程的 C++ 服务,跑在 16 GB 的机器上,一切正常。
$ ps -o pid,vsz,rss,comm -p 2741 PID VSZ RSS COMMAND 2741 37748736 624380 server
按需分页让「分配」和「占用」分开:地址划出来不碰就不占物理页(§7.2.3)。线程多的程序 VSZ 动辄几十 GB,这在 Linux 上很常见,overcommit 也允许这样。被换出的页确实算在 VSZ 里,但一台 16 GB 的机器很少配几十 GB 的 swap,服务又一切正常,用 vmstat 看一眼 si、so 就能排除。top 的 VIRT 和 ps 的 VSZ 是同一个数。要看真实占用,看 RSS;多进程共享库多的,看 PSS。
值得停下来的句子
Memory growth issues are often misidentified as memory leaks. The first question to ask is: Is it supposed to do that?— §7.4.6。本站译:内存增长的问题常常被误判成内存泄漏。第一个要问的问题是:它本来就该这样吗?
「内存一直涨」报到你这里时,最省时间的一步不是开 valgrind,而是去看配置:缓存上限设了多少、连接池多大、队列有没有上限。很多「泄漏」是缓存在按设计填满,或者分配器留着已经 free 的内存。把这个问题问在前面,排查就分成两条清楚的路:设计如此,就改配置或改用法;不该如此,才去找没释放的代码路径。
With swap, this is likely to first become a performance issue due to paging, which is an opportunity to debug the issue live.— §7.3.2。本站译:有 swap 时,这类问题多半先表现为换页带来的性能问题,这让你有机会在现场调试。
这句话说的是内存一直涨的应用。对 ARM 板的读者,它解释了为什么问题总是「跑了两天突然没了」:没有 swap,就没有那段慢下来的预警期,等你看到时进程已经被杀、现场也没了。能做的是把预警挪到前面:定时记关键进程的 RSS、盯 /proc/pressure/memory,或者在 OOM 之前由自己的看门狗按阈值重启并留下记录。要不要为此配 swap(比如 zram),要看你的存储寿命和延迟要求,书里没有替你决定。
带走的几条
一句话:先看换页和压力判断够不够,再看 RSS 的走向判断是不是在涨,最后用缺页采样找是谁长起来的。
free 列小是缓存在干活。内存真不够的信号是 si / so 不为零、页扫描、PSI 在涨、dmesg 里有 OOM。
§7.4.2malloc 只划地址,第一次写才给物理页。占内存看 RSS,共享库多时看 PSS(smaps_rollup、pmap -X)。
§7.2.3匿名内存大致每 4 KB 一次。pidstat -r 的 minflt/s 看速度,perf record -e page-faults 看是哪条调用栈。
§7.5.10分配器 free 后不一定还给内核。看它会不会继续涨:平了多半是分配器留着,一直涨才找泄漏。
§7.4.6板子和很多云主机不配 swap,内存用完直接 OOM。靠定时记 RSS 和 PSI 提前发现。
§7.3.2宿主机内存再多,cgroup 到了 memory.max 照样回收、照样杀。看 memory.events 的 oom_kill。
§7.6.5