一、为什么要写这一篇
做运维的几乎每个季度都要面对一次"内存相关"的故障:
- 数据库服务器 swap 长期 100%,慢查询莫名其妙。
- Java 应用 -Xmx 设置 4G,宿主机才 8G 内存却没 OOM。
free 看 available 还很大,应用却报"Cannot allocate memory"。
这些故障的根因都不在"内存不够"这四个字上,而在内存子系统的具体机制:虚拟内存、页表、缺页、回收、Swap、cgroup、NUMA、oom_score。
这一篇按"原理 → 命令 → 案例 → 调优"的结构,把这些机制与运维操作对上。
二、阅读这一篇你能解决什么
- 看懂
free、/proc/meminfo、vmstat、/proc/$PID/status 等内存相关输出。 - 区分 RSS / VSZ / PSS / USS 的差别,能在内存泄漏排查中用上。
- 解释为什么
free 显示还有很多但 OOM 还是发生了。 - 区分容器 OOM 与节点 OOM,能从 dmesg、kubelet 日志定位。
- 调整 swap、swappiness、overcommit 等参数并理解副作用。
- 处理 Java 应用、Redis、MySQL、Elasticsearch 等典型场景的内存问题。
三、本文约定
- 操作系统以 RHEL 7/8、CentOS 7/8 Stream、AlmaLinux 8/9、Ubuntu 20.04/22.04 为主。
- 内核版本以 3.10 - 6.6 区间常见版本为主。
- 命令需要 root 权限,请用
sudo -i 或切到 root。
四、内存机制总览
4.1 总图
+---------------------------------------------------+| 应用 || (malloc / mmap / Java Heap / Python PyObject) |+--------------------+------------------------------+ | v+---------------------------------------------------+| 虚拟内存 (mm_struct) || - VMA: text/data/heap/stack/shared lib/anon mmap || - 每个 VMA 有自己的权限 (rwx) || - 虚拟地址 -> 物理地址: 页表 |+--------------------+------------------------------+ | v+---------------------------------------------------+| 内核页子系统 || - 物理页分配器 (Buddy / Slab) || - 页面回收 (LRU + kswapd) || - Swap (anon page -> disk) || - 页错误 (minor fault / major fault) |+--------------------+------------------------------+ | v+---------------------------------------------------+| 物理内存 || ZONE_DMA | ZONE_NORMAL | ZONE_MOVABLE |+---------------------------------------------------+
4.2 关键术语
| |
|---|
| 每个进程看到的"假"地址空间,由内核映射到物理内存或磁盘 |
| |
| |
| TLB (Translation Lookaside Buffer) | |
| |
| |
| |
| |
| |
| PSS (Proportional Set Size) | |
| |
| |
| |
| |
| |
| |
| |
五、虚拟内存全景
5.1 进程的虚拟地址空间
每个进程都以为自己独占一整块虚拟地址空间。以 64 位 Linux 为例:
0x0000000000000000 - 0x0000555555555000 : 用户空间代码段0x0000555555555000 - 0x00007fffffffffff : 用户空间堆 / mmap / 栈0xffff800000000000 - 0xffffffffffffffff : 内核空间(不同内核映射位置不同)
虚拟内存通过 VMA(Virtual Memory Area)组织:
5.2 VMA 的查看
# 看进程 VMAcat /proc/$PID/maps# 看 VMA 详细信息cat /proc/$PID/smaps# 看 VMA 摘要pmap -X $PID
输出示例:
00400000-00401000 r-xp 00000000 fd:01 12345 /usr/bin/myapp00600000-00601000 r--p 00000000 fd:01 12345 /usr/bin/myapp00601000-00602000 rw-p 00001000 fd:01 12345 /usr/bin/myapp7f1234000000-7f1234021000 rw-p 00000000 00:00 0 7f1234021000-7f1234200000 r--p 00000000 fd:01 67890 /usr/lib/x86_64-linux-gnu/libc.so.6
字段含义:
00400000-00401000:虚拟地址范围。r-xp:权限(r 读 / w 写 / x 执行 / s 共享 / p 私有)。/usr/bin/myapp:文件路径(匿名映射是空)。
5.3 虚拟地址到物理地址
CPU 访问虚拟地址时,会经过 MMU(内存管理单元)查页表,找到对应的物理页。Linux 默认 4 级页表:
PGD (Page Global Directory) -> PUD (Page Upper Directory) -> PMD (Page Middle Directory) -> PTE (Page Table Entry)
PTE 包含:
- 标志位:R/W、U/S、A (Accessed)、D (Dirty)
每多一级页表,访问就多一次内存读,所以 TLB(CPU 内)缓存"虚拟 → 物理"映射,命中 TLB 时几乎是零开销。
5.4 缺页中断
CPU 访问一个虚拟地址,但 PTE 标志位显示:
- Present 位 = 0(页不在物理内存):内核处理缺页,从文件 / Swap / zero page 加载。
- Present 位 = 1:正常访问,更新 A/D 位。
缺页分为:
- minor fault:页已经在物理内存(例如 fork 后子进程第一次访问,页是写时复制的祖先页)。
- major fault:需要从磁盘读(Swap in 或 file-backed page 第一次读)。
major fault 慢,因为 IO。vmstat 的 si/so 列就是 swap in / swap out 的速率。
# 看 major fault 速率vmstat 1# bi: blocks in from disk# bo: blocks out to disk
或者:
cat /proc/vmstat | grep -E 'pgfault|pgmajfault'
5.5 写时复制 (Copy-on-Write, COW)
fork 时,子进程不立即复制父进程的物理页,只共享同一份物理页 + PTE 标记为只读。一旦父 / 子任何一方写,触发 COW 缺页,内核才分配新物理页。
这是 fork 比想象中便宜的原因,也是 docker commit / docker build 镜像层叠加性能好的原因(容器里的进程也是 COW)。
六、物理内存与页分配
6.1 ZONE 划分
x86_64 下典型 ZONE 划分:
- ZONE_DMA:0-16MB,给老设备 DMA 用。
- ZONE_DMA32:0-4GB,32 位 DMA。
- ZONE_MOVABLE:可移动页,用于内存热插拔。
- ZONE_DEVICE:pmem / DAX 设备内存。
绝大多数分配走 ZONE_NORMAL。
6.2 Buddy Allocator
物理页按"伙伴系统"管理:每 2^n 页组成一个 block,按大小分组。
cat /proc/buddyinfo
示例输出:
Node 0, zone Normal 12 345 200 150 100 50 20 10 5 1 0
每列表示 1/2/4/8/16/32/64/128/256/512/1024 页大小的 free block 数。
如果某列一直是 0,说明该尺寸的 free block 紧缺,分配可能要 fallback 到更小的尺寸或触发回收。
6.3 Slab / Slub
内核自己的小对象(inode、dentry、file 等)由 Slab 分配器管理。
cat /proc/slabinfo# 或者用 slabtopslabtop
slabtop 实时显示 Slab 占用排行。Slab 占用过高常见原因:
七、/proc/meminfo 字段详解
/proc/meminfo 是理解 Linux 内存的核心入口,下面按"一眼要看的"分组讲解。
cat /proc/meminfo
7.1 容量字段
| |
|---|
| |
| |
| 应用可用内存估算(free + reclaimable) |
7.2 缓存字段
7.3 Swap 字段
7.4 内存回收指标
7.5 OOM 相关
| |
|---|
| |
| |
| |
| |
| |
| HugePages_Total / Free / Rsvd | |
7.6 关键判断
MemAvailable 才是"应用还能用多少内存"的真实值,比 MemFree 准确。Buffers + Cached 很大不代表浪费,它们随时可以被回收。Dirty 持续增长且 Writeback 不动 → IO 写入有瓶颈。
八、free / top / htop / vmstat / sar 实战
8.1 free
free -h
输出示例:
total used free shared buff/cache availableMem: 15Gi 3.2Gi 1.0Gi 200Mi 11Gi 11GiSwap: 4.0Gi 0.5Gi 3.5Gi
解读:
used = total - free - buffers - cached(部分版本不同)。Swap 有内容不代表出问题,关注"持续增长 + IO 压力大"。
8.2 top / htop
top# 进入 top 后按 E 切换单位;按 M 按内存排序;按 1 切多 CPU;按 c 看命令路径htop# 鼠标友好,F6 按字段排序,F5 树状视图
top 关键字段:
VIRT:虚拟内存总量(包括 mmap 但未实际使用的部分)。
8.3 vmstat
vmstat 1
输出列:
si / so:swap in / swap out(KB/s)。us / sy / id / wa / st:CPU 时间占比。
si/so 持续 > 0 表示内存压力;wa 持续 > 10% 表示 IO 瓶颈。
8.4 sar -r
sar -r 1
输出关键字段:
kbmemfree / kbmemused / kbbuffers / kbcached。kbactive / kbinact / kbdirty。
8.5 pidstat -r
pidstat -r -p $PID 1
看指定进程的内存变化:
九、/proc/$PID/status 与 smaps
9.1 status 关键字段
cat /proc/$PID/status
9.2 smaps 与 PSS / USS
cat /proc/$PID/smaps_rollup
输出关键字段(每个 VMA 一组):
KernelPageSize / MMUPageSize。Shared_Clean / Shared_Dirty / Private_Clean / Private_Dirty。
PSS 是排查内存泄漏的关键指标,因为它扣除了共享部分,能反映"这个进程独占了多少内存"。
USS 是 Private_Clean + Private_Dirty,等于该进程独占且不可被其它进程共享的内存。
9.3 pmap
pmap -X $PID
输出每段 VMA 的详细信息,类似 smaps。
# 看 RSS Top 10pmap -X $PID | sort -k3 -nr | head -10
十、页面回收机制
10.1 LRU 链表
内核为每个 zone 维护多个 LRU 链表:
匿名页(anon)和文件页(file)分别管理。匿名页只能被 swap out,文件页可以 write back 或 drop(如果没脏)。
10.2 kswapd
kswapd 是内核的页面回收线程,当 zone 的水位(watermark)低于 low 时被唤醒,回收页直到水位高于 high。
水位:
WMARK_HIGH:高水位,kswapd 停止。
# 看 watermarkcat /proc/zoneinfo | grep -E 'pages_free|min|low|high|node'
10.3 direct reclaim
分配页时如果 free < min,水位跌破,会进行 direct reclaim:同步回收页。direct reclaim 会让分配路径变慢,导致分配延迟上升。
direct reclaim 触发频率可以通过 pgscan_direct_* 监控。
10.4 回收策略
文件页回收代价:
- clean file page:直接 drop,无需 IO。
- dirty file page:write back 到磁盘,再回收。
匿名页回收代价:
页回收优先级:内核按 page type 选择优先顺序,可通过 /proc/sys/vm/swappiness 调整(参见下文)。
10.5 refault
被回收的页很快被重新访问,叫 refault。refault 说明 working set 偏大,回收策略过激进。
cat /proc/vmstat | grep -E 'pgrefill|pgactivate|pgdeactivate'
十一、Swap 机制
11.1 什么是 Swap
Swap 是把匿名页换出到磁盘的机制。本质是用磁盘空间换内存空间。当内存不足时,内核把冷的匿名页写盘;访问时再 swap in 回来。
代价:
11.2 配置 Swap
# 看当前 Swapswapon --showcat /proc/swaps# 创建 Swap 文件(演示用,生产用真分区更好)fallocate -l 4G /swapfilechmod 600 /swapfilemkswap /swapfileswapon /swapfile# 永久生效:写入 /etc/fstabecho '/swapfile none swap sw 0 0' >> /etc/fstab# 关闭 Swapswapoff /swapfile
风险提示:
swapoff 会触发大量 swap in,可能让机器 IO 撑爆。- 不要在内存吃紧时关闭 Swap,可能立刻触发 OOM。
11.3 swappiness
/proc/sys/vm/swappiness 控制内核倾向 swap 的程度。范围 0-200(Linux 4.x+),默认 60。
- swappiness = 0:禁用 swap(仅在 OOM 时 swap)。
- swappiness = 100:匿名页和文件页同等对待。
- swappiness = 200:积极 swap。
# 临时调整sysctl -w vm.swappiness=10# 持久化echo 'vm.swappiness = 10' >> /etc/sysctl.d/99-swap.confsysctl -p /etc/sysctl.d/99-swap.conf
数据库服务器通常建议:
- PostgreSQL:swappiness=1-10。
- Elasticsearch:swappiness=1。
11.4 overcommit
/proc/sys/vm/overcommit_memory:
/proc/sys/vm/overcommit_ratio:仅在 overcommit_memory=2 时生效。
数据库和 JVM 场景一般不开 overcommit,依赖应用层内存管理。
11.5 swap vs no swap
主流观点:
- 数据库 / Redis:建议保留少量 Swap,但把 swappiness 调到很低。
- Java 应用:Swap 是性能杀手,应该关闭或调到极低。
- 容器化部署:合理设置 cgroup limit 而不是依赖 Swap。
十二、OOM Killer 详解
12.1 触发条件
内核在 __alloc_pages_may_oom 路径下决定 OOM:
12.2 选择受害进程
每个进程有一个 oom_score(0-1000),内核选 oom_score 最高的进程杀掉。算法(简化版):
oom_score = (RSS / total_RAM) * 1000 + oom_score_adj/1000 * 1000
oomscore_adj 是用户调整权重,-1000(永不杀)~ +1000(必杀)。
12.3 oom_score_adj
# 看当前进程的 oom_score 和 oom_score_adjcat /proc/$PID/oom_scorecat /proc/$PID/oom_score_adj# 调整(需要相同或更高权限)echo -500 > /proc/$PID/oom_score_adj# 设置为 -1000 表示永不 OOMecho -1000 > /proc/$PID/oom_score_adj
注意:
oom_score_adj 是新版本(Linux 2.6.36+),oom_adj 已废弃但仍可读。- 设置 -1000 不会"保护"进程逃过 SIGKILL,只是不在 OOM 计算中被选中;如果系统真的无内存可分配(cgroup 限制),还是会被杀。
12.4 dmesg 看 OOM 日志
dmesg -T | grep -i -E 'oom|killed|invoked oom'
典型日志:
[Thu Jan 01 10:00:00 2026] myapp invoked oom-killer(gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0)[Thu Jan 01 10:00:00 2026] CPU: 0 PID: 1234 Comm: myapp Tainted: G E 6.6.0[Thu Jan 01 10:00:00 2026] memory: avail 1024 kB[Thu Jan 01 10:00:00 2026] Out of memory: Killed process 1234 (myapp) total-vm:4194304kB, anon-rss:2097152kB, file-rss:0kB, shmem-rss:0kB, UID:0 pgtables:2048kB oom_score_adj:0
解析:
avail 1024 kB:系统可用内存接近 0。shmem-rss:tmpfs / shmem 占用的 RSS。
12.5 容器 OOM
容器场景下 OOM 分为两类:
- container OOM:cgroup 内存超限,容器内进程被 cgroup 杀掉,dmesg 可能看不到 OOM Killer 记录(取决于 cgroup v1 / v2)。
- node OOM:宿主整机内存耗尽,OOM Killer 选进程杀。
判断方法:
# 看 cgroup 内存水位cat /sys/fs/cgroup/memory/memory.events# oom 字段不为 0 表示 cgroup 触发过 OOM# 看具体容器ls /sys/fs/cgroup/memory/kubepods/...cat memory.events
# cgroup v2cat /sys/fs/cgroup/system.slice/*.service/memory.events# oom_kill 字段
Kubelet 会把 OOM 记到 Pod status:
kubectl describe pod mypod# Last State: Terminated, Reason: OOMKilled
容器内看到的 OOMKilled 多半是 cgroup OOM,不是 node OOM。
十三、cgroup 内存子系统
13.1 cgroup v1
# 看 cgroup mountmount | grep cgroup# 容器 cgroup 路径示例/sys/fs/cgroup/memory/kubepods/burstable/pod-xxx/container-yyy/
关键文件:
memory.limit_in_bytes:硬上限。memory.soft_limit_in_bytes:软上限。memory.usage_in_bytes:当前使用(含 cache)。memory.memsw.usage_in_bytes:内存 + swap 使用。memory.failcnt:触发 limit 的次数。memory.max_usage_in_bytes:使用峰值。memory.events:事件计数(low/high/max/oom)。
# 看容器实际内存使用cat /sys/fs/cgroup/memory/.../memory.usage_in_bytes# 看是否触发过 OOMgrep oom /sys/fs/cgroup/memory/.../memory.events
13.2 cgroup v2
mount | grep cgroup2
cgroup v2 文件:
memory.events:低/高/最大/OOM 事件。memory.swap.high / memory.swap.max:swap 限制。memory.pressure:PSI(pressure stall information)。
# 看 cgroup v2 OOM 事件cat /sys/fs/cgroup/system.slice/.../memory.events# oom_kill 0
13.3 Kubernetes 内存配置
apiVersion: v1kind: Podmetadata:name: mypodspec:containers:- name: appimage: my-app:1.0resources:requests:memory: "256Mi"limits:memory: "512Mi"
limits 是 cgroup v2 的 memory.max,超出会触发 OOMKilled。- 当
requests == limits 时 Pod 处于 Guaranteed QoS 类。
13.4 JVM 与 cgroup
Java 10+ 已经能感知 cgroup 限制(-XX:+UseContainerSupport,默认开启)。8u191+ 也支持。
JVM 启动时读 memory.max 计算最大堆:
# 默认最大堆是 cgroup limit 的 1/4java -XX:+PrintFlagsFinal -version | grep MaxHeapSize
风险点:
- 如果容器内存限制 512MB,JVM 默认最大堆约 128MB。期望堆更大需要
-Xmx 显式设置,但不要超过 cgroup limit。 - JVM 元数据、线程栈、CodeCache、GC 开销等都算在 cgroup 内。
- 设置
-Xmx 与 cgroup limit 的差值应 ≥ 256MB,避免元空间被打爆。
十四、透明大页(THP)
14.1 概念
透明大页(Transparent HugePages, THP)让内核自动把 4KB 物理页合并成 2MB 大页,减少页表项数量,提升 TLB 命中率。
14.2 模式
madvise:只在 madvise(MADV_HUGEPAGE) 的 VMA 启用。
cat /sys/kernel/mm/transparent_hugepage/enabled# always madvise [never]
14.3 性能影响
- 大型内存扫描、数据库等场景,THP 能减少缺页开销。
- 某些场景下(fork 密集、NUMA 不均衡、某些 JVM GC 模式),THP 反而引起延迟尖刺。
典型坑:
- 启用 THP 的进程在 fork 时(如 Redis BGSAVE、MySQL fork)会触发 khugepaged 大页整理,短时间内 CPU 飙升、延迟波动。
- 容器内启用 THP 可能与某些 cgroup 行为冲突。
14.4 推荐设置
# 数据库 / Redis 推荐 neverecho never > /sys/kernel/mm/transparent_hugepage/enabledecho never > /sys/kernel/mm/transparent_hugepage/defrag# 持久化(CentOS 7 / RHEL 7)echo 'transparent_hugepage=never' >> /etc/default/grubgrub2-mkconfig -o /boot/grub2/grub.cfgreboot
十五、NUMA
15.1 概念
NUMA(Non-Uniform Memory Access):多插槽服务器内存分节点分布,CPU 访问本地节点内存比远端节点快。
numactl --hardware# available: 2 nodes (0-1)# node 0 cpus: 0 1 2 3 4 5 6 7# node 1 cpus: 8 9 10 11 12 13 14 15# node 0 size: 32768 MB# node 1 size: 32768 MB# node distances:# node 0 1# 0: 10 20# 1: 20 10
15.2 NUMA 跨节点访问问题
应用进程跑在 node 0,但分配了大量 node 1 的内存,跨节点访问导致性能下降。
# 看进程在哪个节点cat /proc/$PID/numa_maps# ...# 7f1234000000 default anon=10 dirty=10 N0=5 N1=5# 看 CPU 节点绑定numactl --cpunodebind=0 --membind=0 myapp
15.3 优化建议
# 让应用绑定到特定节点numactl --cpunodebind=0 --membind=0 ./myapp# 或者使用 interleaved,所有节点平分内存numactl --interleave=all ./myapp# MySQL 配置innodb_numa_interleave = 1
十六、案例 1:Java 应用 OOM Killed
16.1 现象
Java 应用(Spring Boot)跑在容器内,内存限制 1GB,启动一段时间后 OOMKilled。
16.2 命令检查
# 1) 看 Pod 事件kubectl describe pod mypod# Last State: Terminated, Reason: OOMKilled# 2) 看 cgroup 事件cat /sys/fs/cgroup/memory/.../memory.events# oom_kill 1# 3) 看 JVM 启动参数kubectl logs mypod | head -50# -Xmx512m -Xms512m# 4) 看 dmesg(宿主机)dmesg -T | grep -i oom | tail
16.3 关键指标
memory.usage_in_bytes 接近 memory.limit_in_bytes。- JVM 堆使用 400MB,但 cgroup 显示 1GB。
16.4 根因定位
JVM 堆设置 512MB,加上 Metaspace、CodeCache、线程栈、GC overhead,实际 cgroup 内占用接近 1GB。元空间不断扩张(动态类加载 / CGLIB 代理),最终触发 OOM。
16.5 修复方案
短期:
# 调高容器内存限制resources: limits: memory: "1.5Gi"
中期:
# 限制 JVM 元空间java -XX:MaxMetaspaceSize=256m ...# 用 G1GC 替代 CMSjava -XX:+UseG1GC ...
长期:
- 排查 Metaspace 泄漏(动态类加载器泄漏)。
16.6 验证
# 24 小时观察watch -n 60 "kubectl top pod mypod"# 内存稳定不超限
十七、案例 2:数据库服务器 swap 长期 100%
17.1 现象
MySQL 服务器慢查询突增,free 显示 swap 100% 使用,磁盘 IO 高。
17.2 命令检查
# 1) swap 总量swapon --showfree -h# 2) swappinesssysctl vm.swappiness# 3) 谁在用 swapfor f in /proc/*/status; do awk '/^Name:/{name=$2} /VmSwap:/{if ($2+0 > 0) print $2, name}' "$f"done | sort -rn | head# 4) 内存分布ps -eo pid,rss,cmd --sort=-rss | head# 5) NUMAnumactl --hardware
17.3 关键指标
- swappiness 默认 60,匿名页频繁换出。
17.4 根因定位
数据库 InnoDB Buffer Pool 假设占用 70% 内存,但 swappiness=60 导致部分匿名页(连接 session、临时表)频繁换出。Buffer Pool 是文件页(file-backed),swappiness 对它影响小,但 anon 页面回收的代价比文件页高。
17.5 修复方案
# 降低 swappinesssysctl -w vm.swappiness=1# 减少 Swap 使用倾向sysctl -w vm.overcommit_memory=0
数据库参数:
# my.cnfinnodb_buffer_pool_size = 12G # 占内存的 50-70%innodb_numa_interleave = 1
17.6 验证
# 看 swap 换出速率vmstat 1# si / so 应接近 0# 看慢查询是否减少cat /var/log/mysqld-slow.log | wc -l
十八、案例 3:容器内存限制与 JVM 不匹配
18.1 现象
容器内存限制 512MB,应用设了 -Xmx512m。运行时 OOMKilled。
18.2 根因
JVM 最大堆 512MB 已逼近容器上限。元空间、线程栈、CodeCache、JIT 编译产物的开销没地方放。
18.3 修复方案
# 1) 调低 -Xmx,让出元空间java -Xmx384m ...# 2) 调高容器内存resources: limits: memory: "1Gi"# 3) 让 JVM 自动感知 cgroupjava -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0
MaxRAMPercentage 是 Java 10+ 的新参数,建议在容器化部署里使用。
18.4 验证
# 看 JVM 是否感知 cgroupjcmd $PID VM.flags | grep -E 'MaxRAM|MaxHeap'
十九、案例 4:THP 引发延迟尖刺
19.1 现象
Redis 跑在 Linux 主机上,启用 THP。BGSAVE 或 AOF rewrite 时出现 ms 级延迟,业务 P99 抖动。
19.2 命令检查
# 看 THP 是否启用cat /sys/kernel/mm/transparent_hugepage/enabled# 看 khugepaged 是否在跑ps -eo pid,comm,pcpu | grep khuge
19.3 根因
Redis fork 子进程做 BGSAVE 时,内核尝试把 4KB 页合并成 2MB 大页,khugepaged 占用大量 CPU,且 fork 后写时复制大页代价高。
19.4 修复方案
echo never > /sys/kernel/mm/transparent_hugepage/enabledecho never > /sys/kernel/mm/transparent_hugepage/defrag
持久化通过 grub 或 systemd。
19.5 验证
# 看 P99 抖动是否消失redis-cli --latency-history -i 1
二十、案例 5:RSS 不高但 cgroup 触发 OOM
20.1 现象
容器内存限制 1GB,进程 RSS 仅 200MB,但 cgroup 仍 OOMKilled。
20.2 排查
# 看 cgroup 详细使用cat /sys/fs/cgroup/memory/.../memory.stat# cache 524288000 # 大量 page cache# rss 209715200# mapped 52428800# ...
20.3 根因
cgroup v1 的 memory.usage_in_bytes = RSS + Cache。memory.limit_in_bytes 不区分 RSS 和 cache。当容器内做大量 IO,page cache 占用也会算进 cgroup usage。
20.4 修复方案
- 升级到 cgroup v2,使用
memory.high 区分硬 / 软限制。 - 业务侧减少 IO(例如用 stream 而非 readall)。
二十一、内核参数调优速查
| | | |
|---|
| | | |
| | | |
| | | |
| | | |
| vm.dirty_background_ratio | | | |
| vm.dirty_expire_centisecs | | | |
| vm.dirty_writeback_centisecs | | | |
| | | |
| | | |
| | | |
21.1 修改流程
# 备份cp -a /etc/sysctl.d/99-sysctl.conf /tmp/# 写配置cat > /etc/sysctl.d/99-memory-tuning.conf <<'EOF'vm.swappiness = 10vm.dirty_ratio = 15vm.dirty_background_ratio = 5vm.vfs_cache_pressure = 200EOF# 应用sysctl -p /etc/sysctl.d/99-memory-tuning.conf# 验证sysctl vm.swappiness
21.2 回滚
mv /etc/sysctl.d/99-memory-tuning.conf /etc/sysctl.d/99-memory-tuning.conf.disabledsysctl -p
二十二、监控指标建议
22.1 必备
| |
|---|
| node_memory_MemTotal_bytes | |
| node_memory_MemAvailable_bytes | |
| node_memory_Buffers_bytes | |
| |
| node_memory_SwapTotal_bytes | |
| node_memory_SwapFree_bytes | |
| |
| node_memory_Writeback_bytes | |
| |
| |
| |
| |
| |
| |
22.2 进程级
- container_memory_rss:容器 RSS。
- container_memory_cache:容器 page cache。
- container_memory_swap:容器 swap。
- container_memory_failures_total:cgroup 触发限制计数。
- container_oom_events_total:OOM 事件。
22.3 自定义告警
groups:- name: memoryrules:- alert: HighSwapUsageexpr: (node_memory_SwapTotal_bytes - node_memory_SwapFree_bytes) / node_memory_SwapTotal_bytes > 0.8for: 5mlabels:severity: warning- alert: OOMKilledexpr: increase(container_oom_events_total[5m]) > 0for: 1mlabels:severity: critical- alert: LowMemAvailableexpr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.1for: 5mlabels:severity: warning
二十三、内存泄漏排查方法
23.1 识别内存增长
# 持续观察进程 RSSwhile true; do ps -p $PID -o rss= >> /tmp/rss.logsleep 60done# 看 RSS 是否单调上升gnuplot -e 'plot "/tmp/rss.log"' -
23.2 smem
# 看 PSS Top 进程smem -t -k -s pss# 看 USS(独有内存)smem -t -k -s uss# 看指定进程smem -P myapp
23.3 valgrind / asan
# valgrind(运行时开销大,不适合生产)valgrind --tool=massif ./myappms_print massif.out.*# AddressSanitizer(编译时开启)gcc -fsanitize=address -g myapp.c -o myapp./myapp
23.4 bcc / bpftrace
# 安装 bccyum install -y bcc# 用 memleak 跟踪分配但未释放的内存/usr/share/bcc/tools/memleak -p $PID
23.5 火焰图
# perf 采样perf record -F 99 -p $PID -g -- sleep 30perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg
火焰图能直观看到哪个函数分配最多内存。
23.6 通用排查流程
1. 观察 RSS / PSS 是否单调上升2. 看堆 / 元空间 / CodeCache 各自的占用3. 看线程数(线程越多栈越大)4. 用 jemalloc / tcmalloc 的 profile 接口5. 用 bcc memleak / valgrind massif 抓泄漏6. 业务侧对照代码,定位可疑对象
二十四、容器化深度专题
24.1 Java 容器内存估算
容器内存 = JVM 堆 + 元空间 + CodeCache + 线程栈 + JIT + GC + 直接内存
经验公式:
容器内存 = MaxHeap * 1.5 ~ 2
即 -Xmx=2GB 时,容器内存限制至少 3-4GB。
24.2 Redis 容器内存
# 配置 maxmemory 时要预留# maxmemory + fork 开销 + 客户端缓冲区 + AOF buffer# 经验: 容器内存 = maxmemory * 1.5
24.3 MySQL 容器内存
innodb_buffer_pool_size = 12Gmax_connections = 200# 容器内存 >= innodb_buffer_pool_size * 1.2
24.4 多容器共存
同一节点跑多个容器时,内存规划:
- kubelet eviction 阈值默认 100Mi(硬)。
二十五、特殊场景
25.1 大页 (HugePages)
# 预留 1GB 大页echo 512 > /proc/sys/vm/nr_hugepages# 看大页使用cat /proc/meminfo | grep -E 'HugePages'
数据库场景(Oracle / MySQL)使用大页可以提升 TLB 命中率,但需要配置合理。
25.2 tmpfs 与内存
mount -t tmpfs -o size=512M tmpfs /tmp
tmpfs 占内存。容器内的 /tmp 常常是 tmpfs,大文件临时写入会占内存。
25.3 Memory Cgroup 限制进程
# 把进程放进 cgroupmkdir -p /sys/fs/cgroup/memory/mygroupecho $PID > /sys/fs/cgroup/memory/mygroup/tasksecho 512M > /sys/fs/cgroup/memory/mygroup/memory.limit_in_bytes
25.4 PSI(Pressure Stall Information)
cat /proc/pressure/memory# some avg10=0.00 avg60=0.00 avg300=0.00 total=0# full avg10=0.00 avg60=0.00 avg300=0.00 total=0
PSI 反映"等待内存的进程所占时间比例",可作为高级监控指标。
二十六、生产实践清单
26.1 上线前
- 数据库 / Redis 关闭或调低 swappiness。
- 容器内 JVM 设置
-XX:MaxRAMPercentage=75.0。
26.2 上线后
- 监控 RSS / cache / swap / OOM 计数。
- 设置告警:可用内存 < 10%、swap > 80%、container OOM。
26.3 故障响应
- OOM 时立即看 dmesg / kubectl describe / cgroup memory.events。
- 根因分析:smem / bcc memleak / 代码层。
26.4 回滚
二十七、常见问答
Q1:free 显示还有很多内存为什么还会 OOM?
free 的可用内存不准确,可能被 cache 占满。OOM 的真正触发点是"分配新页失败",可能因为:
Q2:Swap 设多大合适?
经验法则:
- 4GB < 内存 ≤ 16GB:Swap = 内存。
- 内存 > 16GB:Swap = 8-16GB 即可。
数据库 / Redis 场景建议保留小 Swap(4-8GB)作为兜底。
Q3:怎么区分 container OOM 与 node OOM?
- container OOM:cgroup memory.events 中
oom_kill > 0,Pod status OOMKilled。 - node OOM:dmesg 中有
Out of memory: Killed process,被杀的进程不一定在容器内。
Q4:为什么 -Xmx 设置后没生效?
可能是:
- JVM 启动时识别 cgroup(UseContainerSupport)。
Q5:怎么快速知道系统在 OOM 的边缘?
# 看 PSIcat /proc/pressure/memory# some 列 > 30% 表示内存紧张# 看 kswapd 是否在跑ps -eo pid,comm | grep kswapd# kswapd0 在跑不代表内存紧,CPU 占用高才代表# 看 direct reclaimcat /proc/vmstat | grep pgscan_direct
Q6:可以禁止 OOM 杀某个进程吗?
echo -1000 > /proc/$PID/oom_score_adj
但如果整机真的无内存分配(cgroup / node),还是会触发 SIGKILL。根本上还是要给足够内存。
Q7:JVM 堆外内存泄漏怎么查?
-XX:MaxMetaspaceSize + -XX:NativeMemoryTracking=summary。- jcmd
NativeMemoryTracking 看细分。 - bcc memleak 跟踪 native malloc。
Q8:swap 突然全部用完怎么办?
# 1) 找大内存进程ps -eo pid,cmd --sort=-rss | head# 2) 关闭 swap(紧急)swapoff -a# 3) 重启大进程释放内存# 4) 扩内存或调整业务
风险提示:swapoff 会触发大量 swap in,可能 IO 撑爆机器,慎用。
二十八、命令速查
# 内存全景free -hcat /proc/meminfovmstat 1slabtop# 进程视角cat /proc/$PID/statuscat /proc/$PID/smaps_rolluppmap -X $PID | head# Swapswapon --showcat /proc/swapssysctl vm.swappiness# OOMdmesg -T | grep -i oomcat /sys/fs/cgroup/memory/.../memory.eventscat /proc/$PID/oom_scorecat /proc/$PID/oom_score_adj# 回收cat /proc/zoneinfo | grep -E 'min|low|high'cat /proc/vmstat | grep -E 'pgscan|pgsteal|pgrefill'# NUMAnumactl --hardwarenumastat -m# THPcat /sys/kernel/mm/transparent_hugepage/enabled# 监控nstat -az | grep -E 'pgfault|migrate'sar -r 1pidstat -r -p $PID 1
二十九、总结与最佳实践
- 看 MemAvailable 不要看 MemFree:MemAvailable 才是应用可用内存的真实值。
- Swap 是兜底不是依赖:避免依赖 Swap 解决内存问题,必要时降级性能也要避免频繁 swap。
- 数据库关 swappiness:MySQL / Redis / PG 的 swappiness 调到 1-10。
- 容器化用 cgroup v2:v2 的
memory.high 行为更合理,统计更准确。 - JVM 用 MaxRAMPercentage:让 JVM 自动感知 cgroup 限制,避免硬编码 -Xmx。
- THP 谨慎启用:数据库 / Redis 关 THP,应用可保留 madvise。
- 多指标交叉验证:单看 free 没意义,结合 vmstat / PSI / cgroup 一起判断。
- 维护 oom_score_adj 列表:核心业务进程设 -500 ~ -900,关键运维工具设 -900。
- 监控告警覆盖 PSI:Pressure Stall Information 是内存压力最直接的指标。
- 测试时做内存压测:模拟峰值负载下的内存表现,提前发现 OOM 风险。
- 保留 dump 现场:OOMKilled 时保留 core dump、jmap heap dump、bcc memleak 数据。
- 把内存当稀缺资源:内存不是"用了就好",而是"精确分配 + 主动回收"。
内存问题是 Linux 上最难定位的一类问题,因为症状在应用层、根因在系统层、修复要兼顾多层。掌握这一篇的体系结构与命令清单,至少能把 80% 的内存类故障定位到正确的层级。剩下的 20% 需要更深入的 eBPF / perf / 内核追踪,那是另一篇文章要讲的内容了。
文末福利
网络监控是保障网络系统和数据安全的重要手段,能够帮助运维人员及时发现并应对各种问题,及时发现并解决,从而确保网络的顺畅运行。
谢谢一路支持,给大家分享6款开源免费的网络监控工具,并准备了对应的资料文档,建议运维工程师收藏(文末一键领取)。
100%免费领取
一、zabbix
二、Prometheus
内容较多,6款常用网络监控工具(zabbix、Prometheus、Cacti、Grafana、OpenNMS、Nagios)不再一一介绍, 需要的朋友扫码备注【监控合集】,即可100%免费领取。
以上所有资料获取请扫码
100%免费领取
(后台不再回复,扫码一键领取)