free -h 里的 available 和 free 差了20G——Linux内存到底怎么看?OOM killer杀谁?
本文目录
先说结论
free 不是真的'空闲',available 才是。Linux 会把尽量多的内存用作 page cache 做磁盘缓存,这部分算在 `used` 里但其实随时可以回收。看内存够不够,永远看 `available`,不看 `free`。
OOM killer 不是随机杀进程,它按 `oom_score` 打分,分最高的被杀。oom_score 跟进程占用的物理内存、运行时长、oom_score_adj 有关。保护关键进程(DB、网关)要调 `/proc/
容器内看内存别信 free,free 拿的是宿主机数据。容器看真实限制用 `cat /sys/fs/cgroup/memory/memory.limit_in_bytes`(cgroup v1)或 `memory.max`(v2)。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
内存题是 Linux 面试区分度最高的题之一。新人一看到 `free -h` 里 `free` 只剩 500M 就说 '内存不够了',其实是还没搞懂 `free` 和 `available` 的区别。能说清楚 `buff/cache` 为什么能算可用内存、OOM killer 根据什么挑进程杀、怎么保护关键进程不被杀——这些都不是背概念能答出来的,是在线上内存告警群里被炸过之后才懂的。
业务场景:Hologres 节点频繁 OOM killer 告警
我在某短剧出海公司,有段时间 Hologres 一个 FE 节点每天被 OOM killer 杀 3-5 次,运维群天天告警。当班值班的同事查了两天,每次都是重启完事,没定位到根因。我接手的时候做了三件事:`dmesg -T | grep -i kill` 查历史 OOM 记录、`/proc/meminfo` 看内存结构、给 FE 进程调 `oom_score_adj` 做保护。最后定位是监控 agent 内存泄漏到 8G,比 FE 进程还大,被 killer 选中代替 FE 被杀的风险反而更高(讽刺的是它吃那么多但 oom_score_adj 默认 0)。最终解决是给 agent 加 cgroup 限制 + 修掉泄漏。
# 查 dmesg 里的 OOM killer 记录(-T 带时间戳)
dmesg -T | grep -i -E 'killed process|out of memory|oom'
# 输出示例:
# [Mon Mar 18 02:13:45 2024] Out of memory: Killed process 23451
# (hologres_fe) total-vm:9876543kB, anon-rss:8234567kB, file-rss:0kB
# score 892
# 查看所有进程的 oom_score(分最高的最先被杀)
for pid in $(ls /proc | grep -E '^[0-9]+$'); do
name=$(cat /proc/$pid/comm 2>/dev/null)
score=$(cat /proc/$pid/oom_score 2>/dev/null)
printf '%s\t%s\t%s\n' "$score" "$pid" "$name"
done | sort -rn | head -10
手段一:free -h 每个字段到底是什么
先把 free 的输出彻底搞懂。这是面试必问,而且很多工作多年的人都说不清楚。
free -h
# total used free shared buff/cache available
# Mem: 64G 42G 3G 200M 19G 21G
# Swap: 8G 0B 8G
# total = 总物理内存
# used = 已使用(不包括 buff/cache)
# free = 真正空闲(Linux 偏好把内存都用掉,这个值通常很小)
# shared = tmpfs/shmem 占用
# buff/cache = page cache + buffer,磁盘缓存,可回收
# available = 预估真正可用(free + 大部分可回收的 buff/cache)
# 关键公式(粗略):
# available ≈ free + 可回收的 cache(不是全部 cache)
# used ≠ total - free - buff/cache,老版本 free 的 used 包含 cache
常见误区:'free 只剩 500M,内存不够'——错的。free 很小是正常的,Linux 会尽量把闲置内存用作 page cache 加速磁盘访问。看 `available`,如果 available 也只剩 500M,才是真的不够。
手段二:/proc/meminfo 看内存结构细节
free 只是对 /proc/meminfo 的摘要显示,排查时要直接看 /proc/meminfo,里面的字段比 free 详细多了。
cat /proc/meminfo
# 关键字段(按重要性):
# MemTotal - 总内存
# MemFree - 真正空闲(= free 的 free)
# MemAvailable - 内核算的真正可用(= free 的 available)
# Buffers - 块设备缓存(读写裸设备用的)
# Cached - page cache(文件内容缓存)
# SReclaimable - 可回收的 slab(目录项、inode 缓存)
# SUnreclaim - 不可回收的 slab
# Shmem - tmpfs/共享内存
# AnonPages - 匿名页(进程的堆、栈)
# Mapped - 被 mmap 的文件
# Dirty - 脏页(还没写回磁盘的)
# Writeback - 正在写回的脏页
# 排查思路:
# 1. MemAvailable 低,找大户:按 RSS 排序看哪个进程占得多
ps aux --sort=-rss | head
# 2. AnonPages 高 = 应用进程吃内存
# 3. Cached 高但 MemAvailable 还可以 = 正常
# 4. Slab (SUnreclaim) 异常高 = 内核泄漏,很少见但致命
手段三:OOM killer 的评分机制
面试官最爱问:'Linux OOM killer 会杀谁?' 不是杀占内存最多的,也不是杀最新的,是按 `oom_score` 打分。
# 每个进程都有 3 个 OOM 相关文件
cat /proc/<pid>/oom_score # 当前评分(0-1000,越大越可能被杀)
cat /proc/<pid>/oom_score_adj # 调整值(-1000 到 +1000)
cat /proc/<pid>/oom_adj # 老接口(-17 到 +15),兼容保留
# oom_score 计算(粗略)
# oom_score = (进程 RSS / 总物理内存) * 1000 + oom_score_adj
# 值越大越先被杀。所以:
# - 占内存多的进程优先被杀
# - oom_score_adj = -1000 基本免疫
# - oom_score_adj = +1000 一定先被杀
# 保护关键进程(比如 Hologres FE)
echo -800 > /proc/23451/oom_score_adj
# 测试:触发一个进程先被杀
echo 1000 > /proc/$$/oom_score_adj # 把当前 shell 设为最先被杀
# 永久生效:通过 systemd 配置
# [Service]
# OOMScoreAdjust=-800
手段四:cgroup 内存限制(容器和大数据生产必备)
生产环境的进程都应该被 cgroup 限制住内存,否则一个进程内存泄漏会拖垮整机。容器、YARN NodeManager、K8s 都是用 cgroup 做的隔离。
# cgroup v1 看容器的内存限制
cat /sys/fs/cgroup/memory/docker/<cid>/memory.limit_in_bytes
cat /sys/fs/cgroup/memory/docker/<cid>/memory.usage_in_bytes
cat /sys/fs/cgroup/memory/docker/<cid>/memory.stat
# cgroup v2
cat /sys/fs/cgroup/<path>/memory.max
cat /sys/fs/cgroup/<path>/memory.current
# 容器内看自己的限制(不要看 free!)
# 方案一:直接读 cgroup
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
# 方案二:JDK 10+ 默认认 cgroup
java -XX:+PrintFlagsFinal -version | grep MaxHeap
大坑:cgroup 的 OOM 只杀 cgroup 内的进程,不是杀整机最大的进程。所以一个 container 被 OOM 杀,宿主机上其他进程都还好好的,但容器内的业务挂了——这就是为什么大数据作业偶发 '正常执行中突然挂' 却查不到原因。
手段五:swap 和 swappiness
生产环境的大数据机器,一般都建议关掉 swap 或者 swappiness 设很低,避免 JVM 被换到磁盘导致性能雪崩。
# 看 swap 使用
swapon --show
free -h | grep Swap
# swappiness:0-100,越高越倾向于用 swap
# 数据库服务器推荐 1,大数据推荐 1-10
cat /proc/sys/vm/swappiness
sysctl vm.swappiness=1
# 永久生效
echo 'vm.swappiness=1' >> /etc/sysctl.conf
# 临时关闭 swap(紧急情况)
swapoff -a
# 哪些进程用了 swap
for pid in $(ls /proc | grep -E '^[0-9]+$'); do
s=$(awk '/VmSwap/ {print $2}' /proc/$pid/status 2>/dev/null)
[ -n "$s" ] && [ "$s" != 0 ] && echo "$s KB pid=$pid $(cat /proc/$pid/comm 2>/dev/null)"
done | sort -rn | head
内存相关命令对比
| 场景 | 命令 | 要点 |
|---|---|---|
| 快速看全局 | free -h | 看 available 不看 free |
| 看详细结构 | cat /proc/meminfo | MemAvailable / AnonPages / Slab |
| 按进程排序 | ps aux --sort=-rss | head | RSS 是真实占用 |
| 看谁被 OOM | dmesg -T | grep -i kill | 历史 OOM 记录都在 dmesg |
| 容器限制 | /sys/fs/cgroup/memory/... | 容器内千万别信 free |
| 保护进程 | /proc/pid/oom_score_adj | 设 -800 到 -1000 |
面试高频追问
追问一:Java 进程 heap 配了 8G,但 top 看 RES 已经 12G 了,多出来的 4G 去哪了?
JVM 进程的 RSS 不只是 heap。完整构成:heap + Metaspace + Code Cache + Direct Memory + Thread Stack + JNI Native Memory + JVM 本身开销。常见吃内存大户是 Direct Memory(Netty、NIO)、Metaspace(动态类太多)、线程栈(线程数×栈大小,每个默认 1M)。排查思路:`jcmd
追问二:swap 到底该不该开?
看场景。纯服务器(尤其是数据库、大数据组件)建议关 swap 或 swappiness=1,因为 swap 上去性能断崖式下跌(IO 慢 100 倍),宁可让 OOM killer 杀进程也不要让它苟延残喘。桌面机、开发机该开,防止误操作卡死。Kubernetes 默认就要求关 swap,开了 kubelet 起不来。我在所有生产机器上都是 swapoff -a + vm.swappiness=1 双保险。
追问三:cgroup 给容器限了 4G,但容器里 JVM -Xmx8g,会怎么样?
两种结果:一是 JVM 启动时就失败,因为 cgroup 不让你 malloc 那么多;二是启动成功但一跑大数据就被 OOM killer 杀,还是 cgroup 内的 killer,杀 container 里内存最大的进程。典型症状是作业日志无异常,dmesg 里有 'Memory cgroup out of memory'。所以容器内的 JVM 参数必须和 cgroup 匹配:JDK 10+ 直接信 cgroup,JDK 8 要加 `-XX:+UseContainerSupport -XX:MaxRAMPercentage=75`。
实战踩坑:Hologres FE 进程莫名其妙被 OOM killer 杀
某短剧出海公司的 Hologres 有一段时间 FE 节点频繁被 OOM killer 杀,每天 3-5 次。第一次定位思路:`dmesg -T | grep -i kill` 拉历史记录,看被杀的是不是 FE 进程。结果有意思,被杀的既有 FE,也有一个我没怎么注意的监控 agent。
# 查所有被杀进程的历史
dmesg -T | grep -i 'Killed process' | awk -F'(' '{print $2}' | awk '{print $1}' | sort | uniq -c
# 输出:
# 12 hologres_fe
# 18 metric_agent <-- 吃内存最多的实际上是这个
# 查 metric_agent 的 RSS 趋势
watch -n 10 'ps -p $(pgrep metric_agent) -o pid,rss,vsz,cmd'
# 启动后 500M → 运行 6 小时后 8G,明显泄漏
# 查它的 oom_score
cat /proc/$(pgrep metric_agent)/oom_score # 780
cat /proc/$(pgrep metric_agent)/oom_score_adj # 0
# FE 的分数
cat /proc/$(pgrep hologres_fe)/oom_score # 820
cat /proc/$(pgrep hologres_fe)/oom_score_adj # 0
根因链条:监控 agent 有内存泄漏(RocksDB 后台线程没释放),慢慢吃到 8G,触发整机内存紧张,OOM killer 两个大户(FE 和 agent)之间挑,因为 FE 连接数多 RSS 稍大,score 820 比 agent 780 高,所以 FE 先被杀。修复两步:① 给 FE 加 `oom_score_adj=-800` 做保护;② 把监控 agent 的内存泄漏 bug 提给 infra 修掉,同时给 agent 加 cgroup 限制到 2G。做完之后 OOM 告警再没响过。
总结
Linux 内存题考的是你能不能说清楚:① free 里的 available 才是真的可用;② OOM killer 按 oom_score 打分挑进程;③ 容器里看内存必须走 cgroup 不能信 free;④ Java 进程的 RSS ≠ heap,还有 Direct Memory 等大户。能顺着 dmesg → /proc/meminfo → oom_score → cgroup 这条线讲一个完整的排查故事,基本就是线上背锅的资深同学了。
下一篇聊另一个磁盘经典题:df 报 100%,rm 完文件空间还是没回来——inode、lsof deleted、/tmp 那些坑。
你们生产机器的 swappiness 设了多少?评论区聊聊~
关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长