订阅更新

free -h 里的 available 和 free 差了20G——Linux内存到底怎么看?OOM killer杀谁?

Linux手册 第 3 题 约 14 分钟读完

本文目录
  1. 先说结论
  2. 业务场景:Hologres 节点频繁 OOM killer 告警
  3. 手段一:free -h 每个字段到底是什么
  4. 手段二:/proc/meminfo 看内存结构细节
  5. 手段三:OOM killer 的评分机制
  6. 手段四:cgroup 内存限制(容器和大数据生产必备)
  7. 手段五:swap 和 swappiness
  8. 内存相关命令对比
  9. 面试高频追问
  10. 实战踩坑:Hologres FE 进程莫名其妙被 OOM killer 杀
  11. 总结

先说结论

free 不是真的'空闲',available 才是。Linux 会把尽量多的内存用作 page cache 做磁盘缓存,这部分算在 `used` 里但其实随时可以回收。看内存够不够,永远看 `available`,不看 `free`。

OOM killer 不是随机杀进程,它按 `oom_score` 打分,分最高的被杀。oom_score 跟进程占用的物理内存、运行时长、oom_score_adj 有关。保护关键进程(DB、网关)要调 `/proc//oom_score_adj` 到 -1000。

容器内看内存别信 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/meminfoMemAvailable / AnonPages / Slab
按进程排序ps aux --sort=-rss | headRSS 是真实占用
看谁被 OOMdmesg -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 VM.native_memory summary`(需要启动参数加 `-XX:NativeMemoryTracking=summary`),能看到 JVM 各个区域的 native 内存分配。Direct Memory 泄漏在 Flink、Spark 里特别常见,通常是 Netty 的 ByteBuf 没释放。

追问二: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年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器