内存问题是生产系统中最难排查的故障类型之一。症状可能表现为 OOM 崩溃、响应延迟飙升、Swap 风暴,也可能是长达数天才显现的缓慢内存泄漏。本文从内核数据结构出发,系统讲解 /proc/meminfo、/proc/vmstat 的每个字段含义,结合五大实战案例与 bpftrace 诊断脚本,构建一套完整的内存诊断与调优方法论。
一、/proc/meminfo 完整解析 /proc/meminfo 是内存诊断的第一入口,由内核函数 meminfo_proc_show()(fs/proc/meminfo.c)填充,数据来源于全局变量 totalram_pages、zone->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。
MemFree :NR_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。现代系统中该值通常较小。
Cached :NR_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_ANON、LRU_INACTIVE_ANON、LRU_ACTIVE_FILE、LRU_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
AnonHugePages 与 HugePages_Total 的区别:前者是 THP(内核自动管理的 2MB 匿名透明大页),后者是通过 hugetlbfs 或 mmap(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 pgsteal_direct 234567 pgscan_kswapd 15234567 pgscan_direct 289034 pgmajfault 45678 pgfault 987654321
关键比率:
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 $ watch -n 5 'free -h'
used 在 buff/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 VmPin: 0 kB VmHWM: 5021952 kB VmRSS: 5021952 kB VmAnon: 4987904 kB VmFile: 34048 kB VmExe: 12288 kB VmLib: 98304 kB VmPTE: 12288 kB VmSwap: 204800 kB
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 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 $ 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 --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_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 $ 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 [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 $ cat /proc/5678/oom_score_adj 0 $ echo -500 > /proc/$(pgrep -f "critical-service" )/oom_score_adj [Service] OOMScoreAdjust=-500 $ echo -1000 > /proc/1/oom_score_adj
Step 3:cgroup 内存限制防止 OOM 蔓延
1 2 3 4 5 6 7 8 9 $ 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 $ echo 5678 > /sys/fs/cgroup/memory/java-app/tasks $ 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 vm.overcommit_ratio = 50 $ grep -E 'CommitLimit|Committed_AS' /proc/meminfo CommitLimit: 20971520 kB Committed_AS: 18874368 kB
案例三:高内存压力 / 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 $ sysctl vm.swappiness vm.swappiness = 60 $ 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 $ 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 $ zcat /proc/config.gz | grep ZSWAP CONFIG_ZSWAP=y CONFIG_ZSWAP_COMPRESSOR_DEFAULT_LZ4=y $ echo 1 > /sys/module/zswap/parameters/enabled $ echo lz4 > /sys/module/zswap/parameters/compressor $ echo zsmalloc > /sys/module/zswap/parameters/zpool $ echo 20 > /sys/module/zswap/parameters/max_pool_percent $ 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 $ 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
Step 4:extfrag_threshold 调整
1 2 3 4 5 6 7 8 9 10 11 $ sysctl vm.extfrag_threshold vm.extfrag_threshold = 500 $ sysctl -w vm.extfrag_threshold = 200 $ 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 $ cat /proc/slabinfo | awk 'NR<=2 || $1=="dentry"' 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 $ mount | grep cgroup2 cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime) $ CGPATH="/sys/fs/cgroup/kubepods/burstable/pod<uid>/<container-id>" $ echo "2147483648" > $CGPATH /memory.max $ echo "1932735283" > $CGPATH /memory.high $ echo "104857600" > $CGPATH /memory.swap.max
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 kernel 67108864 kernel_stack 8388608 pagetables 4194304 percpu 1048576 sock 16777216 shmem 0 file_mapped 134217728 file_dirty 2097152 file_writeback 0 anon_thp 268435456 inactive_anon 536870912 active_anon 536870912 inactive_file 268435456 active_file 268435456 unevictable 0 pgfault 9876543 pgmajfault 1234 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 $ cat /sys/fs/cgroup/kubepods/burstable/pod<uid>/<cid>/memory.events low 0 high 1234 max 89 oom 5 oom_kill 3 oom_group_kill 0 $ 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 $ cat /sys/fs/cgroup/memory.max 2147483648
四、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 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)); } }
脚本二: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 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 (); }
脚本三:缺页异常来源分析 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 kprobe:handle_mm_fault { @fault_comm[comm ] = count(); @fault_pid[pid, comm ] = count(); } kprobe:do_swap_page { @swap_faults[comm ] = count(); } kprobe:do_anonymous_page { @anon_faults[comm ] = count(); } kprobe:filemap_fault { @file_faults[comm ] = count(); } 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); }
脚本四: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 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) { @anon_mmap[comm ] += args->len; } else { @file_mmap[comm ] += args->len; } if ($flags & 0x10) { @fixed_mmap[comm ] = count(); } } tracepoint:syscalls:sys_exit_mmap { if (args->ret == (uint64)-1) { @mmap_fail[comm ] = count(); } } 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 // 持续追踪各进程的页面分配情况 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); }
五、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 groups: - name: linux_memory_alerts rules: - alert: MemoryAvailableLow expr: | node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.10 for: 5m labels: severity: warning annotations: summary: "可用内存低于 10%(当前: {{ $value | humanizePercentage }} )" - alert: MemoryAvailableCritical expr: | node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.05 for: 2m labels: severity: critical annotations: summary: "可用内存严重不足(当前: {{ $value | humanizePercentage }} )" - 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%,存在内存压力" - alert: MajorPageFaultHigh expr: | rate(node_vmstat_pgmajfault[5m]) > 100 for: 5m labels: severity: warning annotations: summary: "Major page fault 速率 {{ $value | humanize }} /s,可能存在 swap thrashing" - alert: OOMKillDetected expr: | increase(node_vmstat_oom_kill[5m]) > 0 labels: severity: critical annotations: summary: "检测到 OOM Kill 事件,过去 5 分钟发生 {{ $value }} 次" - 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 膨胀" - 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%,内存压力严重" - 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 echo "===============================" echo " Linux Memory Health Check" echo " $(date '+%Y-%m-%d %H:%M:%S') " echo "===============================" echo -e "\n[1] Memory Overview" free -h 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%!" 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=$(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!" echo -e "\n[2] Page Fault Rate (10s sample)" MAJFAULT1=$(awk '/pgmajfault/ {print $2}' /proc/vmstat) sleep 10MAJFAULT2=$(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/buddyinfoecho -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 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 阻塞风险)
总结 内存诊断的核心方法论可以归纳为”三步定位法”:
全局视图 :从 free -h、/proc/meminfo、vmstat 1 入手,判断是内存不足、泄漏还是碎片化。
计数器驱动 :通过 /proc/vmstat 的 pgmajfault、pswpout、compact_stall 等速率指标,确定内存压力的类型和严重程度。
精确定位 :使用 smaps_rollup、bpftrace 脚本、kmemleak 等工具,将问题聚焦到具体进程、内核子系统或调用路径。
在参数调优上,没有万能配方——vm.swappiness=10 对数据库是合理的,对内存密集型批处理任务可能反而有害。理解每个参数背后的内核机制,结合具体工作负载的 vmstat 和 sar 数据,才能做出正确决策。
本系列的下一篇将深入探讨 NUMA 架构下的内存管理优化,以及 numactl、mbind 等工具在多 CPU 服务器上的实践应用。