Linux 内存管理深度剖析(七):内存诊断工具与性能调优实战

内存问题是生产系统中最难排查的故障类型之一。症状可能表现为 OOM 崩溃、响应延迟飙升、Swap 风暴,也可能是长达数天才显现的缓慢内存泄漏。本文从内核数据结构出发,系统讲解 /proc/meminfo/proc/vmstat 的每个字段含义,结合五大实战案例与 bpftrace 诊断脚本,构建一套完整的内存诊断与调优方法论。


一、/proc/meminfo 完整解析

/proc/meminfo 是内存诊断的第一入口,由内核函数 meminfo_proc_show()fs/proc/meminfo.c)填充,数据来源于全局变量 totalram_pageszone->vm_stat[] 以及各 per-CPU 计数器的聚合值。

1.1 基础可用内存三元组

1
2
3
4
$ cat /proc/meminfo
MemTotal: 32768000 kB # 系统可用物理内存总量
MemFree: 1024000 kB # 完全空闲(未被使用)的内存
MemAvailable: 12288000 kB # 实际可用于启动新应用的内存估算值

三者的本质区别:

  • MemTotal:内核启动时从 BIOS/UEFI 的 e820 内存映射中获取的可用 RAM 总量,减去内核自身代码、保留区域后的值,对应 totalram_pages << PAGE_SHIFT
  • MemFreeNR_FREE_PAGES 计数器的值,表示 Buddy System 中完全空闲的页面。该值低并不代表系统内存紧张,因为大量内存被用作文件缓存(可随时回收)。
  • MemAvailable:内核 3.14 引入(commit 34e431b0ae398),综合评估当前可回收的 page cache、部分 slab,以及当前 free 内存,减去不可驱逐的内存下限后得出的估算值。生产监控应以此字段为准,而非 MemFree。

1.2 Buffers、Cached 与 SwapCached

1
2
3
Buffers:          102400 kB   # 块设备原始 I/O 缓冲(裸块设备读写)
Cached: 8192000 kB # 文件系统 page cache(含 tmpfs)
SwapCached: 20480 kB # 已从 swap 换回但 swap 槽尚未释放的页面
  • Buffers:对应 NR_BUFFERS 计数器,主要用于文件系统元数据(superblock、inode、目录项)的裸块设备 I/O。现代系统中该值通常较小。
  • CachedNR_FILE_PAGES - SwapCached - Buffers 的近似值,是文件映射 page cache 的总量。这部分内存在内存压力下可被 kswapd 回收。
  • SwapCached:页面被 swap out 后又 swap in 时,其 swap 槽位暂时保留,以便在再次被驱逐时可直接写回同一槽位(跳过分配新槽位的开销)。这部分内存在 vm_stat 中对应 NR_SWAPCACHE

1.3 LRU 四象限:Active/Inactive × Anon/File

1
2
3
4
5
6
Active(anon):    2048000 kB   # 匿名页(堆/栈)活跃 LRU
Inactive(anon): 512000 kB # 匿名页非活跃 LRU(swap 候选)
Active(file): 4096000 kB # 文件映射页活跃 LRU
Inactive(file): 3072000 kB # 文件映射页非活跃 LRU(回收候选)
Unevictable: 65536 kB # 被 mlock 锁定,不可驱逐
Mlocked: 65536 kB # mlock/mlockall 锁定的页面子集

内核使用两个 LRU 链表(LRU_ACTIVE_ANONLRU_INACTIVE_ANONLRU_ACTIVE_FILELRU_INACTIVE_FILE)管理这四类页面,数据来源于 zone->vm_stat[NR_LRU_BASE + lru]

kswapd 的回收策略:优先回收 Inactive(file)(成本最低,只需丢弃缓存),其次 Inactive(anon)(需要 swap out),Active 链表中的页面被访问时 PTE_AF 位会被置位,下次 shrink_active_list 扫描时若 AF 位已清则降级到 Inactive。

1.4 回写状态字段

1
2
3
Dirty:            204800 kB   # 已修改但尚未写回磁盘的脏页
Writeback: 10240 kB # 正在写回磁盘的页面
WritebackTmp: 1024 kB # FUSE 文件系统临时写回缓冲
  • Dirty:超过 vm.dirty_ratio(默认 20% MemTotal)时,写入进程本身会被迫同步刷盘(throttle);超过 vm.dirty_background_ratio(默认 10%)时触发 pdflush/writeback 线程后台刷盘。
  • Writeback:对应 NR_WRITEBACK,值持续偏高说明存在 I/O 瓶颈(磁盘带宽饱和)。

1.5 匿名页与共享内存

1
2
3
AnonPages:       3584000 kB   # 用户态匿名映射(malloc、栈、mmap MAP_ANONYMOUS)
Mapped: 1024000 kB # 已被进程 mmap 映射的文件页(subset of Cached)
Shmem: 512000 kB # tmpfs / POSIX 共享内存 / SysV shm
  • AnonPages:对应 NR_ANON_MAPPED,与 Mapped 相互独立。MAP_ANONYMOUS|MAP_SHARED 的页面计入 Shmem 而非 AnonPages。
  • Shmem:包含所有 tmpfs 页面(含 /dev/shm、容器 overlay 等)以及 shmget 创建的 SysV 共享内存。容器环境中该值偏高时需关注 tmpfs 使用情况。

1.6 Slab 分配器统计

1
2
3
4
KReclaimable:     819200 kB   # 可回收内核内存(含 SReclaimable)
Slab: 1048576 kB # slab/slub 分配器总使用量
SReclaimable: 819200 kB # 可回收 slab(dentry cache、inode cache)
SUnreclaim: 229376 kB # 不可回收 slab(内核对象生命周期绑定)
  • SReclaimable:dcache(dentry)和 icache(inode)是最大的可回收 slab 消费者,在文件密集型工作负载(大量小文件遍历)下可能膨胀到数 GB。通过 echo 2 > /proc/sys/vm/drop_caches 可强制回收(生产慎用)。
  • SUnreclaim:包含 kmalloc 创建的长生命周期对象。该值持续增长是内核内存泄漏的信号。

1.7 页表与内核虚拟地址空间

1
2
3
4
5
6
7
PageTables:        81920 kB   # 进程页表本身占用的物理内存
SecPageTables: 4096 kB # 安全相关页表(如 AMD SEV)
NFS_Unstable: 0 kB # 已发往 NFS 但尚未提交的数据(旧版,现恒为 0)
Bounce: 0 kB # DMA bounce buffer(32 位 DMA 设备)
VmallocTotal: 34359738367 kB # vmalloc 地址空间总量(64 位系统几乎无限)
VmallocUsed: 204800 kB # 实际消耗的 vmalloc 空间
VmallocChunk: 0 kB # 最大连续 vmalloc 空闲块(5.x 内核已废弃)

PageTables 在进程数量多、内存映射复杂(如 JVM、大型 Redis 实例)时可能显著增大,每个进程最低消耗 4KB(一个 PGD 页)。

1.8 大页(HugePages)统计

1
2
3
4
5
6
7
8
HugePages_Total:     512   # 预分配的 2MB 静态大页总数
HugePages_Free: 128 # 空闲静态大页
HugePages_Rsvd: 64 # 已预留(承诺分配但未实际使用)
HugePages_Surp: 0 # 超出 nr_hugepages 的临时大页(来自 nr_overcommit)
Hugepagesize: 2048 kB
AnonHugePages: 524288 kB # THP(Transparent HugePages)匿名大页
ShmemHugePages: 0 kB # tmpfs/shmem 使用的 THP
FileHugePages: 0 kB # 文件映射使用的 THP

AnonHugePagesHugePages_Total 的区别:前者是 THP(内核自动管理的 2MB 匿名透明大页),后者是通过 hugetlbfsmmap(MAP_HUGETLB) 使用的静态大页。两者均能减少 TLB miss,但静态大页预留后即使不使用也无法被其他进程占用。


二、/proc/vmstat 关键指标解读

/proc/vmstat 包含数百个计数器,均为自系统启动以来的累计值(通过 /proc/vmstat 可读差值,vmstat 工具每行输出即为时间窗口内的差值)。

2.1 页面回收计数器

1
2
3
4
5
6
7
$ cat /proc/vmstat | grep -E 'pgsteal|pgscan|pgmaj'
pgsteal_kswapd 12845678 # kswapd 回收的文件页数
pgsteal_direct 234567 # 直接回收(进程上下文)文件页数
pgscan_kswapd 15234567 # kswapd 扫描的页面总数
pgscan_direct 289034 # 直接回收扫描的页面总数
pgmajfault 45678 # 主缺页(需要从磁盘读取)
pgfault 987654321 # 次缺页(含 CoW、匿名页分配等)

关键比率:

  • pgsteal / pgscan:回收效率,正常应 > 0.9。若比值持续偏低(< 0.5),说明大量页面被扫描但无法回收(被进程持续访问),内存压力极大。
  • pgmajfault:Major fault 需要磁盘 I/O,每次代价约为 10ms 级别。速率 > 1000/s 通常意味着严重的 swap thrashing 或文件缓存抖动。

2.2 Swap I/O 计数器

1
2
3
4
pswpin             12345   # swap in 的页面数(磁盘 -> 内存)
pswpout 56789 # swap out 的页面数(内存 -> 磁盘)
pgpgin 234567 # 从块设备读入的页面(含 swap in)
pgpgout 345678 # 写出到块设备的页面(含 swap out)

pswpout 速率持续 > 0 是内存压力的直接证据。区分 pgpgout(含文件回写)和 pswpout(纯 swap)很重要——前者是正常的文件系统写操作。

2.3 内存压缩计数器

1
2
3
4
5
6
compact_migrate_scanned    234567   # 压缩扫描的可迁移页面数
compact_free_scanned 345678 # 压缩扫描的空闲页面数
compact_isolated 12345 # 成功隔离待迁移的页面数
compact_stall 1234 # 因碎片化导致进程被迫等待压缩的次数
compact_fail 567 # 压缩失败次数
compact_success 678 # 压缩成功次数

compact_stall 增长说明系统频繁出现大阶内存分配失败,影响实时性。可通过 vm.compaction_proactiveness 调整主动压缩力度。

2.4 透明大页(THP)计数器

1
2
3
4
5
thp_fault_alloc           234567   # 缺页时成功分配 THP 的次数
thp_fault_fallback 5678 # 缺页时 THP 分配失败,回退到 4KB 页
thp_collapse_alloc 1234 # khugepaged 成功合并为 THP 的次数
thp_split_page 456 # THP 被拆分为普通页的次数(CoW、迁移等)
thp_zero_page_alloc 12 # 零页 THP 分配次数

thp_fault_fallback 高说明系统碎片化严重,THP 无法分配连续 2MB 物理内存,需关注 compact_* 相关指标。


三、五大实战案例

案例一:内存泄漏排查

场景:监控告警 MemAvailable 持续下降,系统运行 3 天后内存耗尽。

Step 1:确认内存持续增长

1
2
3
4
5
6
7
$ free -h
total used free shared buff/cache available
Mem: 31G 28G 512M 1.2G 2.5G 1.8G
Swap: 8G 5.2G 2.8G

# 每隔 5 秒观察趋势
$ watch -n 5 'free -h'

usedbuff/cache 稳定的情况下持续增长,排除缓存膨胀,指向进程内存泄漏。

Step 2:定位可疑进程

1
2
3
4
5
$ ps aux --sort=-%mem | head -10
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
www 1234 2.1 15.3 8421504 5021MB ? Sl Apr10 245:12 /opt/app/server
root 567 0.1 3.2 2048000 1042MB ? Ss Apr10 12:34 /usr/bin/kubelet
...

字段说明:VSZ(虚拟地址空间大小)和 RSS(常驻内存大小,即实际占用物理内存)。关注 RSS 持续增大的进程。

Step 3:查看进程详细内存状态

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cat /proc/1234/status | grep -E 'Vm|Threads'
VmPeak: 8421504 kB # 历史最高虚拟内存
VmSize: 8421504 kB # 当前虚拟内存
VmLck: 0 kB # mlock 锁定内存
VmPin: 0 kB # 不可迁移的 pinned 内存
VmHWM: 5021952 kB # 历史最高 RSS(High Water Mark)
VmRSS: 5021952 kB # 当前 RSS(物理内存占用)
VmAnon: 4987904 kB # 匿名映射(堆/栈)
VmFile: 34048 kB # 文件映射
VmExe: 12288 kB # 代码段
VmLib: 98304 kB # 动态库
VmPTE: 12288 kB # 页表大小
VmSwap: 204800 kB # 已换出到 swap 的大小

VmRSS 等于 VmHWM 说明内存从未被回收,泄漏可能性大。

Step 4:分析内存构成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ cat /proc/1234/smaps_rollup
55a3b2000000-7ffe9a4ce000 ---p 00000000 00:00 0
Size: 8421504 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 5021952 kB
Pss: 4998144 kB # 按共享比例分摊的 RSS
Pss_Dirty: 4850688 kB
Shared_Clean: 8192 kB
Shared_Dirty: 0 kB
Private_Clean: 26880 kB
Private_Dirty: 4986880 kB # 私有脏页——堆分配后写入的数据,无法释放
Anonymous: 4987904 kB # 高匿名内存 = 堆泄漏
LazyFree: 0 kB
AnonHugePages: 2097152 kB
Swap: 204800 kB
SwapPss: 204800 kB

Private_Dirty 接近 Anonymous 说明大量匿名内存被分配且无法共享,是堆泄漏的典型特征。

Step 5:bpftrace 追踪内核 kmem_cache 分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 追踪 kmem_cache_alloc,按 cache 名称统计分配次数
$ bpftrace -e '
kprobe:kmem_cache_alloc {
@alloc[((struct kmem_cache *)arg0)->name] = count();
}
interval:s:10 {
print(@alloc);
clear(@alloc);
}
'
# 输出示例:
@alloc[dentry]: 45678
@alloc[inode_cache]: 12345
@alloc[sock_inode_cache]: 8901
@alloc[TCP]: 234

若某个 cache 名称计数持续单调增长,且没有对应的 kmem_cache_free 平衡,则为内核泄漏点。

Step 6:用户态泄漏——Valgrind 和 AddressSanitizer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Valgrind(适用于测试环境,有显著性能开销)
$ valgrind --leak-check=full --show-leak-kinds=all \
--track-origins=yes --log-file=valgrind.log \
/opt/app/server --config /etc/app/config.yaml

# 输出示例:
==1234== 5,021,952 bytes in 2,048 blocks are definitely lost in loss record 1 of 3
==1234== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck.so)
==1234== at 0x10A234B: create_session (session.c:127)
==1234== at 0x10A1892: handle_request (handler.c:89)

# ASAN(编译时插桩,性能开销约 2x,适合 CI/预发布环境)
$ ASAN_OPTIONS=detect_leaks=1:log_path=/tmp/asan.log \
./server --config /etc/app/config.yaml

Step 7:kmemleak 内核泄漏检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 确认内核编译时启用了 CONFIG_DEBUG_KMEMLEAK
$ zcat /proc/config.gz | grep KMEMLEAK
CONFIG_DEBUG_KMEMLEAK=y

# 触发扫描
$ echo scan > /sys/kernel/debug/kmemleak

# 等待扫描完成后查看结果(通常需要几十秒)
$ cat /sys/kernel/debug/kmemleak
unreferenced object 0xffff88810a234560 (size 4096):
comm "kworker/0:1", pid 1234, jiffies 4295867295
hex dump (first 32 bytes):
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
backtrace:
[<0000000012345678>] kmalloc include/linux/slab.h:552
[<00000000abcdef01>] nf_ct_ext_add+0x3c/0x90

案例二:OOM 事件分析

场景:系统日志出现进程被杀,业务中断。

Step 1:解读 OOM Kill 日志

1
2
3
4
5
6
7
8
9
10
11
12
13
$ dmesg | grep -A 30 "Out of memory"
[1234567.890123] Out of memory: Kill process 5678 (java) score 892 or sacrifice child
[1234567.890456] Killed process 5678 (java) total-vm:16777216kB, anon-rss:14680064kB, file-rss:204800kB, shmem-rss:0kB
[1234567.890789] oom_reaper: reaped process 5678 (java), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

# OOM Killer 触发时的内存状态快照:
[1234567.891001] Mem-Info:
[1234567.891002] active_anon:3670016 inactive_anon:131072 isolated_anon:0
[1234567.891003] active_file:512 inactive_file:1024 isolated_file:0
[1234567.891004] unevictable:65536 dirty:0 writeback:0
[1234567.891005] slab_reclaimable:204800 slab_unreclaimable:229376
[1234567.891006] mapped:2048 shmem:16384 pagetables:20480 bounce:0
[1234567.891007] free:5120 free_pcp:256 free_cma:0

字段解读:

  • score 892:OOM 评分,越高越先被杀。评分基于进程 RSS 与内存总量的比例,加上 oom_score_adj 修正值。
  • total-vm:16777216kB:进程虚拟地址空间大小(不代表实际占用)。
  • anon-rss:实际占用的匿名物理内存(这才是 OOM Killer 关注的值)。
  • free:5120:触发 OOM 时仍有 5120 个 4KB 空闲页(20MB),但可能全是 order-0 的碎片,无法满足更大阶的分配请求。

Step 2:查看和调整 OOM 评分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查看当前评分
$ cat /proc/5678/oom_score
892

# 查看调整值(-1000 到 1000)
$ cat /proc/5678/oom_score_adj
0

# 保护关键进程(将其 OOM 评分调低)
$ echo -500 > /proc/$(pgrep -f "critical-service")/oom_score_adj

# 或通过 systemd 配置(推荐,重启后持久化)
# /etc/systemd/system/critical.service
[Service]
OOMScoreAdjust=-500

# 标记为绝对不杀(-1000 仅对 root 有效)
$ echo -1000 > /proc/1/oom_score_adj # init 进程永远不被杀

Step 3:cgroup 内存限制防止 OOM 蔓延

1
2
3
4
5
6
7
8
9
# cgroup v1:限制 Java 进程堆内存
$ mkdir -p /sys/fs/cgroup/memory/java-app
$ echo 8G > /sys/fs/cgroup/memory/java-app/memory.limit_in_bytes
$ echo 10G > /sys/fs/cgroup/memory/java-app/memory.memsw.limit_in_bytes # 含 swap
$ echo 5678 > /sys/fs/cgroup/memory/java-app/tasks

# cgroup v2(推荐)
$ echo "8589934592" > /sys/fs/cgroup/java-app/memory.max
$ echo "7516192768" > /sys/fs/cgroup/java-app/memory.high # 软限制,触发回收

Step 4:overcommit 策略调优

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 查看当前策略
$ sysctl vm.overcommit_memory vm.overcommit_ratio
vm.overcommit_memory = 0 # 0=启发式(默认), 1=总是允许, 2=精确限制
vm.overcommit_ratio = 50 # 当 overcommit_memory=2 时,物理内存的可承诺比例

# 策略说明:
# 0(默认):内核评估申请是否合理,拒绝明显过度的 malloc
# 1:永远返回成功(适合内存计算密集型,如某些科学计算场景)
# 2:CommitLimit = (RAM * overcommit_ratio/100) + Swap
# 超出 CommitLimit 的 mmap/malloc 会直接失败(ENOMEM)

# 查看 CommitLimit 和 Committed_AS
$ grep -E 'CommitLimit|Committed_AS' /proc/meminfo
CommitLimit: 20971520 kB # 允许承诺的最大内存
Committed_AS: 18874368 kB # 当前已承诺的内存(所有 mmap 的虚拟内存之和)

案例三:高内存压力 / Swap 风暴

场景:系统响应延迟从 5ms 飙升至 500ms,CPU idle 正常,磁盘 I/O wait 高。

Step 1:vmstat 观察 swap 活动

1
2
3
4
5
$ vmstat 1 10
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 4 5242880 524288 10240 819200 450 380 5890 420 1234 4567 8 5 42 45 0
3 5 5345280 409600 10240 786432 512 467 6234 512 1456 5012 9 6 38 47 0
  • si(swap in)和 so(swap out)同时 > 0:Swap 风暴,内存严重不足,系统在换入换出间来回折腾。
  • wa(I/O wait)高:印证 swap I/O 消耗了大量 CPU 等待时间。

Step 2:sar 分析页面回收行为

1
2
3
4
5
6
$ sar -B 1 10
Linux 6.4.0 (hostname) 04/14/2026 _x86_64_ (16 CPU)

15:00:01 pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgsteal/s %vmeff
15:00:02 5890.0 420.0 98765.0 450.0 34567.0 23456.0 20123.0 85.8
15:00:03 6234.0 512.0 102345.0 512.0 38901.0 28901.0 23456.0 81.1
  • pgscank/s:kswapd 每秒扫描页面数。
  • pgsteal/s:每秒成功回收页面数。
  • %vmeff = pgsteal/pgscank * 100:回收效率,低于 70% 说明回收困难。
  • majflt/s:每秒主缺页次数,直接反映 swap thrashing 程度。

Step 3:调整 vm.swappiness

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 查看当前值(内核 6.1+ 支持 0-200,之前版本 0-100)
$ sysctl vm.swappiness
vm.swappiness = 60 # 默认值

# 含义:控制 anon 页与 file 页的回收倾向比例
# 0:尽可能避免 swap,优先回收 file cache
# 60(默认):anon 和 file 相对平衡
# 100:同等对待 anon 和 file
# 200(内核 5.8+):积极使用 zswap,大幅减少实际磁盘 swap

# 数据库/低延迟服务推荐设置(避免 swap 换出导致延迟抖动)
$ sysctl -w vm.swappiness=10

# 持久化
$ echo "vm.swappiness = 10" >> /etc/sysctl.d/99-memory.conf

Step 4:vm.vfs_cache_pressure 调优

1
2
3
4
5
6
7
8
9
10
$ sysctl vm.vfs_cache_pressure
vm.vfs_cache_pressure = 100 # 默认值

# 含义:slab 缓存(dentry/inode)相对于匿名内存的回收力度
# < 100:倾向于保留 dcache/icache(适合文件访问密集型工作负载)
# = 100:公平回收
# > 100:更激进地回收 slab(适合内存压力大但 slab 膨胀的场景)

# 若 SReclaimable 占 MemTotal 的 20% 以上,可适当提高
$ sysctl -w vm.vfs_cache_pressure=150

Step 5:启用 zswap 压缩 swap 页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 检查 zswap 支持
$ zcat /proc/config.gz | grep ZSWAP
CONFIG_ZSWAP=y
CONFIG_ZSWAP_COMPRESSOR_DEFAULT_LZ4=y

# 启用 zswap(内核 6.x 默认可能已启用)
$ echo 1 > /sys/module/zswap/parameters/enabled
$ echo lz4 > /sys/module/zswap/parameters/compressor # LZ4 压缩(速度优先)
$ echo zsmalloc > /sys/module/zswap/parameters/zpool # 内存池类型
$ echo 20 > /sys/module/zswap/parameters/max_pool_percent # 最大使用 20% 物理内存

# 验证 zswap 状态
$ cat /sys/kernel/debug/zswap/pool_pages
123456 # 已压缩存储的页面数

$ cat /sys/kernel/debug/zswap/written_back_pages
1234 # 压缩后仍溢出写到磁盘的页面数(应保持低值)

zswap 的本质是在内存中维护一个压缩缓存层,将即将被换出的页面先压缩存储(典型压缩比 2:1 到 4:1),大幅减少磁盘 swap I/O。


案例四:内存碎片化

场景:系统运行数周后,分配大页(THP、DMA 缓冲区)失败,日志出现 alloc_pages failed order:9 类错误。

Step 1:观察 Buddy System 状态

1
2
3
4
$ cat /proc/buddyinfo
Node 0, zone DMA 1 0 0 0 0 0 0 0 1 1 3
Node 0, zone DMA32 2345 1234 567 234 123 56 23 12 5 2 1
Node 0, zone Normal 98765 45678 23456 8901 3456 1234 456 123 23 5 0

每列代表对应 order(0-10)的空闲块数量。order:10 对应 4MB 连续物理内存块(4KB * 2^10)。

上面的 Normal zone 中 order:10 为 0,说明没有 4MB 连续内存可用,高阶分配(THP、大 DMA 缓冲)将失败。

Step 2:pagetypeinfo 分析碎片分布

1
2
3
4
5
6
7
8
9
10
$ cat /proc/pagetypeinfo
Page block order: 9
Pages per block: 512

Free pages count per migrate type at order 0 1 2 3 4 5 6 7 8 9 10
Node 0, zone DMA, type Unmovable 1 0 0 1 1 0 1 0 1 1 3
Node 0, zone DMA, type Movable 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone Normal, type Unmovable 12345 5678 2345 890 345 123 45 12 4 1 0
Node 0, zone Normal, type Movable 86420 40000 21111 8011 3111 1111 411 111 19 4 0
Node 0, zone Normal, type Reclaimable 3456 1234 567 234 78 23 7 2 1 0 0

Unmovable 类型页面(内核分配的不可迁移页)分散在高地址会严重阻碍内存压缩。Movable 类型(用户态匿名页)可以通过迁移来整理。

Step 3:手动触发内存压缩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 触发全局内存压缩(将 Movable 页面迁移,整理出连续空闲内存)
$ echo 1 > /proc/sys/vm/compact_memory

# 观察压缩进度
$ watch -n 1 'cat /proc/vmstat | grep compact'
compact_migrate_scanned 234567
compact_free_scanned 345678
compact_success 1234 # 成功次数持续增加
compact_fail 56

# 压缩后再次验证
$ cat /proc/buddyinfo
Node 0, zone Normal 45678 23456 12345 5678 2345 890 234 56 12 4 2
# order:10 从 0 变为 2,压缩有效

Step 4:extfrag_threshold 调整

1
2
3
4
5
6
7
8
9
10
11
# 外碎片阈值:当碎片化指数超过此值时,kswapd 触发压缩
$ sysctl vm.extfrag_threshold
vm.extfrag_threshold = 500 # 默认值(0-1000)

# 降低阈值让内核更积极地压缩(减少大阶分配失败)
$ sysctl -w vm.extfrag_threshold = 200

# 查看当前各 zone 的碎片化指数(需要 CONFIG_COMPACTION)
$ cat /sys/kernel/debug/extfrag/extfrag_index
Node 0, zone DMA extfrag_index: 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000
Node 0, zone Normal extfrag_index: 0.000 0.001 0.012 0.045 0.156 0.378 0.589 0.734 0.891 0.956 0.998

extfrag_index 接近 1.0 表示该 order 的分配失败主要由碎片化引起(而非内存总量不足),压缩是有效手段。接近 0 则表示是内存总量不足,压缩无济于事。

Step 5:Slab 碎片分析

1
2
3
4
5
6
7
8
9
10
11
12
13
$ vmstat -m | sort -k3 -rn | head -15
Cache Num Total Size Pages
dentry 456789 456789 192 21
inode_cache 12345 12345 616 13
proc_inode_cache 5678 5678 680 11
filp 23456 23456 256 16
vm_area_struct 34567 34567 208 19
sock_inode_cache 890 890 1088 7

# 详细查看特定 cache 的 slab 统计
$ cat /proc/slabinfo | awk 'NR<=2 || $1=="dentry"'
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab>
dentry 456789 456789 192 21 1

案例五:容器内存隔离问题

场景:Kubernetes Pod 内存使用量与宿主机 /proc/meminfo 统计不一致,容器 OOM Kill 后宿主机显示内存充足。

Step 1:cgroup v2 内存控制器配置

1
2
3
4
5
6
7
8
9
10
11
# 确认 cgroup v2 已挂载
$ mount | grep cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

# 查看 Pod 对应的 cgroup 路径(以 Kubernetes 为例)
$ CGPATH="/sys/fs/cgroup/kubepods/burstable/pod<uid>/<container-id>"

# 配置内存限制
$ echo "2147483648" > $CGPATH/memory.max # 硬限制 2GB(OOM Kill)
$ echo "1932735283" > $CGPATH/memory.high # 软限制 1.8GB(触发回收,不 Kill)
$ echo "104857600" > $CGPATH/memory.swap.max # swap 限制 100MB

Step 2:memory.stat 字段解读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ cat /sys/fs/cgroup/kubepods/burstable/pod<uid>/<cid>/memory.stat
anon 1073741824 # 匿名内存(堆/栈)
file 536870912 # 文件缓存(page cache)
kernel 67108864 # 内核代码路径使用的内存(如 socket buffer)
kernel_stack 8388608 # 内核栈
pagetables 4194304 # 进程页表
percpu 1048576 # per-CPU 数据结构
sock 16777216 # socket 发送/接收缓冲区
shmem 0 # 共享内存/tmpfs
file_mapped 134217728 # 已映射的文件页
file_dirty 2097152 # 脏页
file_writeback 0 # 正在写回的页面
anon_thp 268435456 # THP 匿名大页
inactive_anon 536870912
active_anon 536870912
inactive_file 268435456
active_file 268435456
unevictable 0
pgfault 9876543
pgmajfault 1234 # 容器内的 major fault 次数
workingset_refault_anon 234
workingset_refault_file 12345

Step 3:memory.high vs memory.max 的区别

1
2
3
4
5
6
7
8
9
10
memory.high(软限制):
- 超过时内核立即对该 cgroup 施加 reclaim 压力
- 进程不会被 Kill,但会出现短暂内存分配延迟
- 适合设置为目标内存的 90%,提前触发回收
- 对应内核中的 memory_high_work 机制

memory.max(硬限制):
- 超过时触发 OOM Killer,Kill cgroup 内评分最高的进程
- 触发后在 memory.events 中记录 oom 和 oom_kill 计数
- 对应 Kubernetes 中的 resources.limits.memory

Step 4:容器 OOM 排查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 实时监控容器 OOM 事件
$ cat /sys/fs/cgroup/kubepods/burstable/pod<uid>/<cid>/memory.events
low 0 # 内存低于 memory.low 的次数(通常不配置)
high 1234 # 超过 memory.high 并触发回收的次数
max 89 # 达到 memory.max 触发 OOM 的次数
oom 5 # OOM Kill 发生次数
oom_kill 3 # 实际被 Kill 的进程数
oom_group_kill 0 # 整个 cgroup 组被 Kill 的次数

# 结合 dmesg 排查具体被 Kill 的进程
$ dmesg | grep -A 5 "oom-kill"
[987654.321] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=...,
mems_allowed=0,oom_memcg=/kubepods/burstable/pod.../...,
task_memcg=/kubepods/burstable/pod.../...,
task=java,pid=23456,uid=1000

Step 5:容器统计与宿主机 /proc/meminfo 的关系

关键认知:容器看到的 /proc/meminfo 在没有 pid namespace 隔离 proc 的情况下,显示的是宿主机的内存信息,而非容器自身的内存限制。

1
2
3
4
5
6
7
8
9
10
11
# 在容器内查看(看到的是宿主机总内存)
$ cat /proc/meminfo | grep MemTotal
MemTotal: 32768000 kB # 宿主机 32GB!

# 正确获取容器内存限制(cgroup v2)
$ cat /sys/fs/cgroup/memory.max
2147483648 # 容器实际限制 2GB

# 在应用层(如 JVM)应使用 cgroup 感知的内存计算
# JVM 8u191+ 支持:-XX:+UseContainerSupport(默认启用)
# 可用内存 = min(memory.max, 宿主机物理内存)

四、bpftrace 内存诊断脚本集

脚本一:追踪大块内存分配(> 1MB)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env bpftrace
# 文件:trace_large_alloc.bt
# 用途:追踪 order >= 8(1MB)的大页面分配,定位大内存分配来源

kprobe:__alloc_pages {
$order = arg1;
if ($order >= 8) {
printf("%-16s PID:%-6d order:%-3d size:%-8dKB stack:\n",
comm, pid, $order, (4 << $order));
print(kstack(5)); # 打印内核调用栈(最多 5 层)
}
}

# 使用方法:sudo bpftrace trace_large_alloc.bt
# 输出示例:
# nginx PID:12345 order:8 size:1024 KB stack:
# __alloc_pages+0x0
# alloc_pages_vma+0x9e
# do_huge_pmd_anonymous_page+0x105
# __handle_mm_fault+0xb1c
# handle_mm_fault+0x11c

脚本二:kmalloc 分配热点(按调用栈聚合)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/usr/bin/env bpftrace
# 文件:kmalloc_hotspot.bt
# 用途:统计内核 kmalloc 分配的大小分布,定位内核内存分配热点

kprobe:__kmalloc {
@alloc_size[ustack] = hist(arg0);
@total_by_comm[comm] += arg0;
}

kretprobe:__kmalloc {
if (retval == 0) {
@alloc_fail[comm] = count(); # 统计分配失败次数
}
}

interval:s:10 {
printf("\n=== kmalloc size distribution by call stack ===\n");
print(@alloc_size);
printf("\n=== total kmalloc bytes by process ===\n");
print(@total_by_comm);
printf("\n=== allocation failures ===\n");
print(@alloc_fail);
exit();
}

# 输出示例(@alloc_size 中的直方图):
# [16, 32) 1234 |@@@@@@@@@@@@@@@@@@@@@@@@@@|
# [32, 64) 5678 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
# [64, 128) 2345 |@@@@@@@@@@@@@@@@@@@@@@@|
# [1K, 2K) 234 |@@|
# [1M, 2M) 3 |

脚本三:缺页异常来源分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/usr/bin/env bpftrace
# 文件:page_fault_analysis.bt
# 用途:分析缺页异常的来源进程和类型,区分 minor/major/swap fault

kprobe:handle_mm_fault {
@fault_comm[comm] = count();
@fault_pid[pid, comm] = count();
}

kprobe:do_swap_page {
@swap_faults[comm] = count(); # swap in 触发的缺页
}

kprobe:do_anonymous_page {
@anon_faults[comm] = count(); # 匿名页首次访问(堆扩张)
}

kprobe:filemap_fault {
@file_faults[comm] = count(); # 文件映射缺页(page cache miss)
}

interval:s:5 {
printf("\n===== Page Fault Analysis (5s window) =====\n");
printf("--- Total faults by process ---\n");
print(@fault_comm);
printf("--- Swap-in faults (major) ---\n");
print(@swap_faults);
printf("--- Anonymous page faults ---\n");
print(@anon_faults);
printf("--- File mapping faults ---\n");
print(@file_faults);
clear(@fault_comm); clear(@swap_faults);
clear(@anon_faults); clear(@file_faults);
}

# 持续运行直到 Ctrl+C

脚本四:mmap 调用追踪与内存映射分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/env bpftrace
# 文件:mmap_trace.bt
# 用途:追踪进程 mmap 调用,统计各进程映射的内存总量

tracepoint:syscalls:sys_enter_mmap {
$flags = args->flags;
$prot = args->prot;

printf("%-16s PID:%-6d len:%-12d prot:0x%02x flags:0x%04x fd:%d\n",
comm, pid, args->len, $prot, $flags, args->fd);

@mmap_size[comm] += args->len;

# 分类统计
if ($flags & 0x20) { # MAP_ANONYMOUS
@anon_mmap[comm] += args->len;
} else {
@file_mmap[comm] += args->len;
}
if ($flags & 0x10) { # MAP_FIXED
@fixed_mmap[comm] = count();
}
}

tracepoint:syscalls:sys_exit_mmap {
if (args->ret == (uint64)-1) {
@mmap_fail[comm] = count(); # ENOMEM 或其他错误
}
}

interval:s:30 {
printf("\n=== mmap summary (30s) ===\n");
printf("Total mmap bytes:\n"); print(@mmap_size);
printf("Anonymous mmap:\n"); print(@anon_mmap);
printf("File-backed mmap:\n"); print(@file_mmap);
printf("MAP_FIXED calls:\n"); print(@fixed_mmap);
printf("Failed mmaps:\n"); print(@mmap_fail);
}

脚本五:OOM Kill 前进程内存快照

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/usr/bin/env bpftrace
# 文件:oom_snapshot.bt
# 用途:在 OOM Kill 发生时打印各进程的内存使用排名,辅助事后分析

// 持续追踪各进程的页面分配情况
kprobe:__alloc_pages_slowpath {
// slowpath 被触发说明内存压力较大
@process_rss[comm, pid] += 1;
}

// 追踪进程 RSS 增长(通过 rss_stat tracepoint)
tracepoint:kmem:rss_stat {
if (args->member == 0) { // MM_ANONPAGES
@anon_pages[comm, pid] = args->size;
}
}

// OOM Kill 即将发生时触发
kprobe:oom_kill_process {
printf("\n========================================\n");
printf("!!! OOM Kill Triggered !!!\n");
printf("Victim: %s (pid=%d)\n", comm, pid);
printf("Timestamp: %llu ns\n", nsecs);
printf("\n--- Top processes by alloc_pages_slowpath hits ---\n");
print(@process_rss);
printf("\n--- Anonymous pages by process ---\n");
print(@anon_pages);
printf("========================================\n");
}

// 每 60 秒清理一次历史数据,避免数据堆积
interval:s:60 {
clear(@process_rss);
}

# 使用方法:sudo bpftrace oom_snapshot.bt > /var/log/oom_trace.log 2>&1 &
# 后台运行,OOM 发生时自动记录快照

五、vm.* sysctl 参数速查表

参数 默认值 推荐值(通用服务器) 说明
vm.swappiness 60 10~20(低延迟)/ 80(内存密集) 控制 anon 页 swap 出的倾向性;0 不完全禁用 swap
vm.dirty_ratio 20 10~15 脏页占总内存比例上限,超过时写入进程被 throttle
vm.dirty_background_ratio 10 5 后台刷盘触发阈值,建议设为 dirty_ratio 的一半
vm.dirty_expire_centisecs 3000 1500~3000 脏页超过此时间(单位 10ms)必须写回
vm.dirty_writeback_centisecs 500 500 writeback 线程唤醒间隔(单位 10ms)
vm.vfs_cache_pressure 100 50~80(文件密集型)/ 150(内存压力大) slab 缓存回收压力;< 100 保留更多 dcache/icache
vm.overcommit_memory 0 0 或 2(严格场景) 内存超分配策略:0=启发式,1=总允许,2=精确限制
vm.overcommit_ratio 50 80~90 overcommit_memory=2 时的物理内存允诺比例(百分比)
vm.min_free_kbytes (自动计算) MemTotal * 0.5% ~ 1% 保留给内核紧急分配的最低空闲内存;过低导致 OOM,过高浪费
vm.watermark_scale_factor 10 125~200(NUMA 系统) kswapd 触发阈值的缩放因子,提高值使 kswapd 更早介入
vm.watermark_boost_factor 15000 15000 碎片化时临时提高水位线的倍数(基于 min_free_kbytes)
vm.zone_reclaim_mode 0 0(UMA)/ 1(NUMA 严格本地回收) NUMA 内存回收策略;1 在 NUMA 节点本地优先回收
vm.numa_balancing 1 1 NUMA 自动内存迁移到最近 CPU 节点
vm.compaction_proactiveness 20 0(低延迟)/ 20~40(THP 需求高) 主动内存压缩的积极程度(0 禁用,100 最积极)
vm.extfrag_threshold 500 200~300 外碎片化指数阈值,超过时触发压缩(0-1000)
vm.page-cluster 3 0(SSD)/ 3(HDD) swap in 时一次读取的页面数(2^n),SSD 无需预读
vm.stat_interval 1 1 vm_stat 更新间隔(秒),降低可减少 NUMA 抖动
vm.laptop_mode 0 0(服务器)/ 5(笔记本) 笔记本模式:延迟回写以节省磁盘转速
vm.drop_caches (write-only) 谨慎使用 1=page cache,2=slab,3=两者;生产环境慎用
vm.oom_kill_allocating_task 0 0 1=优先杀触发 OOM 的进程;0=按评分选择受害者
vm.panic_on_oom 0 0 1=OOM 时触发内核 panic(配合 kdump 使用)
vm.oom_dump_tasks 1 1 OOM 时在 dmesg 中打印所有进程内存信息
vm.mmap_min_addr 65536 65536 禁止 mmap 到低于此地址(防止空指针解引用利用)
vm.max_map_count 65530 262144~1048576(Java/Elasticsearch) 进程最大 VMA 数量,Java 应用通常需要调大
vm.percpu_pagelist_high_fraction 0 8~16(NUMA 系统) per-CPU page list 高水位,减少跨节点分配

六、内存监控告警指标

6.1 核心告警指标与阈值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# Prometheus alerting rules 示例
groups:
- name: linux_memory_alerts
rules:

# 可用内存低于 10%
- alert: MemoryAvailableLow
expr: |
node_memory_MemAvailable_bytes /
node_memory_MemTotal_bytes < 0.10
for: 5m
labels:
severity: warning
annotations:
summary: "可用内存低于 10%(当前: {{ $value | humanizePercentage }})"

# 可用内存低于 5%(紧急)
- alert: MemoryAvailableCritical
expr: |
node_memory_MemAvailable_bytes /
node_memory_MemTotal_bytes < 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "可用内存严重不足(当前: {{ $value | humanizePercentage }})"

# Swap 使用率超过 20%
- alert: SwapUsageHigh
expr: |
(node_memory_SwapTotal_bytes - node_memory_SwapFree_bytes) /
node_memory_SwapTotal_bytes > 0.20
for: 10m
labels:
severity: warning
annotations:
summary: "Swap 使用率超过 20%,存在内存压力"

# Major page fault 速率过高(滑动窗口)
- alert: MajorPageFaultHigh
expr: |
rate(node_vmstat_pgmajfault[5m]) > 100
for: 5m
labels:
severity: warning
annotations:
summary: "Major page fault 速率 {{ $value | humanize }}/s,可能存在 swap thrashing"

# OOM Kill 发生
- alert: OOMKillDetected
expr: |
increase(node_vmstat_oom_kill[5m]) > 0
labels:
severity: critical
annotations:
summary: "检测到 OOM Kill 事件,过去 5 分钟发生 {{ $value }} 次"

# Slab 内存占比过高(超过物理内存的 20%)
- alert: SlabMemoryHigh
expr: |
node_memory_Slab_bytes /
node_memory_MemTotal_bytes > 0.20
for: 15m
labels:
severity: warning
annotations:
summary: "Slab 内存占比 {{ $value | humanizePercentage }},可能存在 dcache/icache 膨胀"

# kswapd 回收效率低
- alert: KswapdEfficiencyLow
expr: |
rate(node_vmstat_pgsteal_kswapd[5m]) /
(rate(node_vmstat_pgscan_kswapd[5m]) + 1) < 0.5
for: 10m
labels:
severity: warning
annotations:
summary: "kswapd 回收效率低于 50%,内存压力严重"

# 大页分配失败(THP fallback 率过高)
- alert: THPFallbackHigh
expr: |
rate(node_vmstat_thp_fault_fallback[5m]) /
(rate(node_vmstat_thp_fault_alloc[5m]) + 1) > 0.30
for: 10m
labels:
severity: warning
annotations:
summary: "THP 分配失败率 {{ $value | humanizePercentage }},内存碎片化严重"

6.2 日常巡检脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#!/bin/bash
# 文件:/usr/local/bin/memory_health_check.sh
# 用途:每日内存健康检查,输出关键指标摘要

echo "==============================="
echo " Linux Memory Health Check"
echo " $(date '+%Y-%m-%d %H:%M:%S')"
echo "==============================="

# 基础内存统计
echo -e "\n[1] Memory Overview"
free -h

# MemAvailable 百分比
MEMTOTAL=$(awk '/MemTotal/ {print $2}' /proc/meminfo)
MEMAVAIL=$(awk '/MemAvailable/ {print $2}' /proc/meminfo)
AVAIL_PCT=$((MEMAVAIL * 100 / MEMTOTAL))
echo -e "\nMemAvailable: ${AVAIL_PCT}% (${MEMAVAIL} kB / ${MEMTOTAL} kB)"
[ $AVAIL_PCT -lt 10 ] && echo " [WARNING] Available memory < 10%!"

# Swap 使用率
SWAPTOTAL=$(awk '/SwapTotal/ {print $2}' /proc/meminfo)
SWAPFREE=$(awk '/SwapFree/ {print $2}' /proc/meminfo)
if [ $SWAPTOTAL -gt 0 ]; then
SWAPUSED=$((SWAPTOTAL - SWAPFREE))
SWAP_PCT=$((SWAPUSED * 100 / SWAPTOTAL))
echo "Swap usage: ${SWAP_PCT}% (${SWAPUSED} kB used)"
[ $SWAP_PCT -gt 20 ] && echo " [WARNING] Swap usage > 20%!"
fi

# Slab 占比
SLAB=$(awk '/^Slab:/ {print $2}' /proc/meminfo)
SLAB_PCT=$((SLAB * 100 / MEMTOTAL))
echo "Slab: ${SLAB_PCT}% (${SLAB} kB)"
[ $SLAB_PCT -gt 20 ] && echo " [WARNING] Slab > 20% of total memory!"

# pgmajfault 速率(采样 10 秒)
echo -e "\n[2] Page Fault Rate (10s sample)"
MAJFAULT1=$(awk '/pgmajfault/ {print $2}' /proc/vmstat)
sleep 10
MAJFAULT2=$(awk '/pgmajfault/ {print $2}' /proc/vmstat)
MAJFAULT_RATE=$(( (MAJFAULT2 - MAJFAULT1) / 10 ))
echo "Major page faults: ${MAJFAULT_RATE}/s"
[ $MAJFAULT_RATE -gt 100 ] && echo " [WARNING] High major fault rate!"

# 内存碎片状态
echo -e "\n[3] Memory Fragmentation (order >= 6, blocks > 0)"
awk '
/^Node/ {
split($0, a, ":"); zone=$0;
counts=a[2];
split(counts, c, " ");
# 检查 order 6-10
high=0;
for (i=7; i<=11; i++) if (c[i]+0 > 0) high=1;
if (!high) print " [WARNING] " zone " - no high-order free pages!";
}' /proc/buddyinfo

# 最近 OOM 事件
echo -e "\n[4] Recent OOM Events"
dmesg --time-format=reltime | grep -E "Out of memory|oom-kill" | tail -5
if dmesg | grep -q "Out of memory"; then
echo " [CRITICAL] OOM events found in dmesg!"
fi

# Top 5 内存消耗进程
echo -e "\n[5] Top 5 Memory Consumers (by RSS)"
ps aux --sort=-%mem | awk 'NR<=6 {printf " %-20s %s MB\n", $11, $6/1024}'

echo -e "\n==============================="
echo " Check Complete"
echo "==============================="

6.3 内存压力等级评估矩阵

指标 正常 警告 紧急
MemAvailable / MemTotal > 20% 10%~20% < 10%
Swap 使用率 < 10% 10%~30% > 30% 或持续增长
pgmajfault 速率 < 10/s 10~100/s > 100/s
kswapd CPU 占用 < 1% 1%~5% > 5%
compact_stall 增量 0 偶发 持续增长
OOM Kill 次数 0 任何一次 反复发生
Slab / MemTotal < 10% 10%~20% > 20%
Dirty / MemTotal < 5% 5%~15% > 15%(I/O 阻塞风险)

总结

内存诊断的核心方法论可以归纳为”三步定位法”:

  1. 全局视图:从 free -h/proc/meminfovmstat 1 入手,判断是内存不足、泄漏还是碎片化。
  2. 计数器驱动:通过 /proc/vmstatpgmajfaultpswpoutcompact_stall 等速率指标,确定内存压力的类型和严重程度。
  3. 精确定位:使用 smaps_rollup、bpftrace 脚本、kmemleak 等工具,将问题聚焦到具体进程、内核子系统或调用路径。

在参数调优上,没有万能配方——vm.swappiness=10 对数据库是合理的,对内存密集型批处理任务可能反而有害。理解每个参数背后的内核机制,结合具体工作负载的 vmstatsar 数据,才能做出正确决策。

本系列的下一篇将深入探讨 NUMA 架构下的内存管理优化,以及 numactlmbind 等工具在多 CPU 服务器上的实践应用。