核心服务凌晨被处决:一次 Linux 内存与 OOM Killer 排查复盘

核心服务凌晨 4 点凭空消失,应用日志却干干净净。排查梳理:dmesg 里的 OOM Killer 实锤、free 各列与该看的 available、OOM Killer 按 oom_score 选受害者、用 ps --sort=-%mem 揪出内存真凶、swap 与 swappiness、用 systemd MemoryMax 给每个服务设内存上限,以及一套内存排查纪律。

2024 年一个清晨,我被告警叫醒:一台服务器上的核心服务,在凌晨四点多突然没了。我爬起来登上机器,服务进程确实不在了,可它自己的应用日志,翻到最后一行也干干净净,没有任何报错、没有任何异常堆栈,就像它好端端地跑着,然后凭空消失了一样。一个进程,不会无缘无故地自己消失。我顺着这个念头去翻内核日志,在 dmesg 里看到了那行让我脊背发凉的话:Out of memory: Killed process 6500 (myapp)。我的服务不是自己崩的——它是被 Linux 内核,亲手"处决"的。更让我想不通的是:被杀的 myapp,根本不是那台机器上最吃内存的进程。这件事逼着我把 Linux 的内存、OOM Killer 这一整套彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7,8GB 内存,跑着核心服务 myapp 和若干别的进程
事故现象:
- 凌晨 4 点多,核心服务 myapp 进程消失
- myapp 自己的应用日志没有任何报错
- 服务进程就那样"凭空消失"了

现场排查:
# 1. 应用日志没线索,翻内核日志
$ dmesg -T | grep -i -A2 'out of memory'
[Wed May 15 04:12:33] Out of memory: Killed process 6500 (myapp)
                      total-vm:2100000kB, anon-rss:1800000kB
# ★ 真相:myapp 是被内核的 OOM Killer 杀掉的

# 2. 看 OOM 发生前后,内核挑选"受害者"的过程
$ dmesg -T | grep -i -B20 'Killed process' | grep -E 'oom|rss'
[...] [ pid ]   uid  ...   rss  ... name
[...] [ 6500]     0  ...450000  ... myapp        # rss 450MB
[...] [ 7200]     0  ...620000  ... batch_job    # rss 620MB ← 更大
# ★ 疑点:batch_job 比 myapp 还吃内存,为什么杀的是 myapp?

# 3. 看是谁在那个时间点把内存吃光的
$ grep -i 'oom' /var/log/messages | head
# 关联到:凌晨 4 点有个 batch_job 任务在跑,内存持续上涨

# 4. 看 myapp 的 oom_score(它被杀的"原因分")
$ cat /proc/$(pgrep myapp)/oom_score
# (事后复现时看到)myapp 的 oom_score 偏高

根因(后来想清楚的):
1. 凌晨的 batch_job 任务有内存泄漏,内存一路上涨,
   最终把这台 8GB 机器的物理内存 + swap 全部吃光。
2. 内存彻底耗尽时,内核无法再分配内存,为了不让
   【整个系统】死掉,它启动 OOM Killer ——
   主动选一个进程杀掉,腾出内存自救。
3. ★ OOM Killer 选谁,不是简单地"谁占内存最多杀谁",
   它算一个 oom_score(综合占用、运行时长、oom_score_adj
   等因素)。myapp 常驻、占用也不小,score 算下来比
   batch_job 还高,于是【替罪羊】成了 myapp。
4. 真正该死的内存泄漏者 batch_job 反而活了下来。
进程不是自己崩的,是内存被别人吃光后,被内核选中处决的。

修复 1:看懂内存——free 各列与 available

# === free 是看内存的第一个命令,但很多人看错了 ===
$ free -h
              total   used    free   shared  buff/cache  available
Mem:           7.6G   3.2G    300M     180M        4.1G        3.9G
Swap:          2.0G   1.8G    200M

# === 逐列解释 ===
# total       物理内存总量
# used        已被进程实际占用的
# free        ★ 完全没被用的 —— 这个数小,【不代表】内存紧张!
# buff/cache  ★ 被内核拿去做磁盘缓存/缓冲的内存
# available   ★★ 真正该看的数:当前"可立即拿来用"的内存

# === ★ 最大的误区:看到 free 很小就以为内存不够 ===
# Linux 有个哲学:空闲的内存就是浪费。
# 内核会主动把暂时没人用的内存,拿去缓存磁盘文件(buff/cache),
# 让下次读文件更快。
# 所以一台健康的机器,跑一阵子之后 free 几乎总是很小 ——
# 因为内存都被拿去做缓存了。这【完全正常】。
# 一旦有进程需要内存,内核会立刻【回收】这些缓存还给它。

# === ★ 真正要盯的是 available ===
# available = free + 可被回收的那部分 buff/cache
# 它表示:不动用 swap、不杀进程,现在还能拿出多少内存。
# available 充足 -> 内存健康(哪怕 free 很小)
# available 很低 -> 内存才是真的紧张

# === 看内存的其他途径 ===
$ cat /proc/meminfo            # 最详尽的内存信息
$ vmstat -s                    # 内存的汇总统计
$ top                          # 进入后按 M,按内存占用排序进程

修复 2:OOM Killer 机制——这次的根因

# === ★ OOM Killer 是什么 ===
# OOM = Out Of Memory。当物理内存 + swap 全部耗尽,
# 内核连一点内存都拿不出来时,系统面临死锁的危险。
# 内核的选择是:与其让整个系统卡死,不如【主动牺牲一个进程】——
# 选一个进程杀掉,把它的内存抢回来,让系统活下去。
# 干这件事的内核机制,就叫 OOM Killer。

# === OOM Killer 怎么选"受害者" ===
# 它给每个进程算一个 oom_score(分越高越该死)。
# 这个分,综合考虑:
#   - 进程占用的内存(占得多,分高)—— 但这【只是因素之一】
#   - 进程的 oom_score_adj 调整值(见修复 5)
#   - 一些其他因素
# ★ 所以"杀的不一定是占内存最多的那个" ——
#   这正是这次 myapp 当替罪羊的原因。

# === 查看进程的 OOM 相关数值 ===
$ cat /proc//oom_score        # 当前算出来的分数
$ cat /proc//oom_score_adj    # 人为调整值,-1000 ~ 1000
# oom_score_adj 默认 0;调成负数 = 更不容易被杀;
# 调成正数 = 更容易被杀;-1000 = 几乎豁免。

# === ★ 确认一次"进程消失"是不是 OOM 干的 ===
$ dmesg -T | grep -i 'killed process'
$ dmesg -T | grep -i 'out of memory'
$ grep -i 'oom' /var/log/messages
$ journalctl -k | grep -i 'out of memory'
# 看到 "Out of memory: Killed process" = 实锤 OOM。
# ★ 应用日志干净、进程却没了 —— 第一反应就该是查 dmesg 看 OOM。

# === OOM 日志里还会打印一份"进程内存快照" ===
$ dmesg -T | grep -A30 'Out of memory' | grep -E 'rss| name'
# 它会列出 OOM 发生那一刻,各进程的 rss(实际物理内存)——
# 这份快照能帮你找出"到底是谁把内存吃光的"。

修复 3:揪出内存元凶进程

# === OOM 之后,核心问题:是谁把内存吃光的 ===
# 注意:被杀的进程 ≠ 吃内存的进程(这次就是)。
# 要找的是那个"内存一路上涨"的真凶。

# === 按内存占用排序所有进程 ===
$ ps aux --sort=-%mem | head
USER  PID  %MEM    RSS  COMMAND
root 7200  22.0 1750000 batch_job        # ★ RSS 1.7GB,真凶
root 6500   8.0  640000 myapp
# ★ 看 RSS(Resident Set Size)= 进程实际占用的物理内存。
#   %MEM 是 RSS 占总内存的百分比。

# === top 里实时看 ===
$ top                          # 进入后按 M(按内存排序)
# 重点看 RES 列(等同 RSS)。

# === 看单个进程的内存构成 ===
$ pmap -x 7200 | tail -1       # 进程的内存映射汇总
$ cat /proc/7200/status | grep -E 'VmRSS|VmSwap'
VmRSS:   1750000 kB           # 实际物理内存
VmSwap:   200000 kB           # 被换到 swap 的部分

# === ★ 判断是不是"内存泄漏":看趋势,不是看某一刻 ===
# 内存泄漏的特征是 RSS 【随时间持续单调上涨】,不回落。
# 隔一段时间采一次,对比:
$ while true; do \
    date; ps -o rss= -p 7200; sleep 60; \
  done | paste - -
# 如果这个数字几小时里只涨不跌 —— 基本就是内存泄漏。

# === RSS / VSZ 别搞混 ===
# VSZ(虚拟内存):进程"申请"的地址空间,通常远大于实际占用,
#               VSZ 大不代表真占了那么多内存。
# RSS(物理内存):进程【实际占用】的物理内存 —— 看这个。

# === 看内核自己用掉的内存(slab)===
$ slabtop                      # 内核数据结构占的内存
# 如果进程都没占多少、内存却紧张,可能是内核 slab 泄漏。

修复 4:swap 的作用与 swappiness

# === swap:磁盘上的一块空间,当"内存的备胎" ===
# 物理内存吃紧时,内核把一些"暂时用不到"的内存页,
# 挪到磁盘上的 swap 里,腾出物理内存给更急的需求。
# swap 是 OOM 之前的【最后一道缓冲】。

# === 看 swap 现状 ===
$ free -h | grep Swap
$ swapon --show                # 看 swap 分区/文件、用量
$ cat /proc/swaps

# === 加一块 swap(用文件,最简单)===
$ dd if=/dev/zero of=/swapfile bs=1M count=2048   # 建 2GB 文件
$ chmod 600 /swapfile          # ★ 权限必须 600,否则 swapon 拒绝
$ mkswap /swapfile             # 格式化成 swap
$ swapon /swapfile             # 启用
$ swapon --show                # 确认生效
# 持久化:写进 /etc/fstab
#   /swapfile  none  swap  defaults  0 0

# === ★ swappiness:内核"多积极地用 swap" ===
$ cat /proc/sys/vm/swappiness
60                             # 默认 60(0~100)
# 值越高,内核越倾向于把内存页换到 swap;
# 值越低,越倾向于保留在物理内存里、宁可回收缓存。
$ sysctl vm.swappiness=10      # 临时调低(服务器常用 10~20)
# 永久:写进 /etc/sysctl.conf -> vm.swappiness = 10

# === ★ swap 的两面性,要拎清 ===
# 好处:它是 OOM 前的缓冲,能扛过内存的短时尖峰。
# 坏处:进程的活跃内存一旦被换进 swap,访问它就要读磁盘,
#       慢几个数量级 —— 表现为系统"卡死般"地慢。
$ vmstat 1                     # si/so 持续不为 0 = 正在频繁换页
# ★ 没有 swap:内存一满就直接 OOM 杀进程,干脆但凶险。
#   有 swap:多一层缓冲,但换页太频繁会拖垮性能。
#   生产服务器通常配【适量】swap + 调低 swappiness。

修复 5:保护关键进程与限制内存

# === 思路一:让 OOM Killer "别杀我的核心服务" ===
# 调低核心进程的 oom_score_adj,让它更不容易被选中。
$ echo -800 > /proc/$(pgrep myapp)/oom_score_adj
# 范围 -1000~1000,越小越安全,-1000 接近豁免。
# ★ 但这是"治标":它只是把刀引向别的进程,
#   内存被吃光的根本问题还在。

# === systemd 服务里持久配置 OOM 保护 ===
$ vim /etc/systemd/system/myapp.service
[Service]
OOMScoreAdjust=-800            # 开机起来就带上这个保护值
$ systemctl daemon-reload && systemctl restart myapp

# === ★ 思路二(治本):用 cgroup 给进程内存设上限 ===
# 与其等某个进程把全机内存吃光,不如【从一开始就限制】
# 每个服务最多能用多少内存 —— 它撑爆的只是自己。
$ vim /etc/systemd/system/batch_job.service
[Service]
MemoryMax=1G                   # ★ 这个服务最多用 1G 内存
MemoryHigh=800M                # 超过 800M 就开始施压回收
$ systemctl daemon-reload && systemctl restart batch_job
# 这样 batch_job 内存泄漏时,它自己先撞到 1G 上限被处理,
# 而不会蔓延到吃光整机内存、连累 myapp 被误杀。

# === 验证 cgroup 限制生效 ===
$ systemctl show batch_job -p MemoryMax
$ cat /sys/fs/cgroup/memory/.../memory.limit_in_bytes   # cgroup v1
$ systemctl status batch_job   # 看 Memory: 当前用量

# === 思路三:让会泄漏的任务"用完即走" ===
# 像 batch_job 这种批处理,跑完就该退出,
# 把它做成"一次性任务"而不是常驻进程 ——
# 进程一退出,它占的内存全部归还,泄漏也就无从累积。

# === 内存预警:别等 OOM 才知道 ===
$ free -m | awk '/Mem:/ {if ($7 < 500) print "available 内存不足 500M!"}'
# 把 available 接进监控,低于阈值就告警,
# 在 OOM 发生【之前】就介入。

修复 6:内存排查纪律

# === 这次事故暴露的内存认知盲区,定几条纪律 ===

# === 1. ★ 进程"凭空消失"且应用日志干净,先查 OOM ===
$ dmesg -T | grep -i 'killed process'
# 这是排查"进程莫名消失"的第一动作,不是最后动作。

# === 2. 看内存看 available,不看 free ===
# free 小是正常的(内存被拿去做缓存了)。
# available 低,才是内存真的紧张。

# === 3. 被 OOM 杀的进程,不一定是吃内存的真凶 ===
# OOM Killer 按 oom_score 选,不是按占用量。
# 要从 dmesg 的进程快照 + ps --sort=-%mem 里找【真正的真凶】。

# === 4. 区分"内存占用高"和"内存泄漏" ===
# 占用高:RSS 高但稳定 —— 可能就是它该用这么多。
# 泄漏:RSS 随时间只涨不跌 —— 这才是要修的 bug。
# 判断要看【趋势】,采样对比,别看一个瞬间值。

# === 5. ★ 治本是限制,不是调 oom_score ===
# 给核心服务调低 oom_score_adj 只是"换个替罪羊"。
# 真正的治本:用 systemd 的 MemoryMax 给每个服务设内存上限,
# 让失控的进程撑爆的只是它自己,波及不到别人。

# === 6. 给服务器配适量 swap + 调低 swappiness ===
$ sysctl vm.swappiness=10
# swap 是 OOM 前的缓冲;swappiness 调低,减少不必要的换页。

# === 7. 内存指标接进监控,在 OOM 前介入 ===
# available 内存、swap 用量、各核心进程的 RSS 趋势 ——
# 都该常驻监控面板。OOM 是结果,趋势才是预警。

# === 排查内存问题的命令链 ===
$ free -h                      # ① available 够不够
$ dmesg -T | grep -i oom       # ② 近期有没有发生过 OOM
$ ps aux --sort=-%mem | head   # ③ 谁占内存最多
$ vmstat 1 5                   # ④ 是否在频繁 swap(si/so)
# 按这个顺序,内存问题基本能定位。

命令速查

需求                        命令
=============================================================
看内存(重点看 available)  free -h
确认是否发生过 OOM          dmesg -T | grep -i 'killed process'
按内存排序进程              ps aux --sort=-%mem | head
看进程实际物理内存          cat /proc/PID/status | grep VmRSS
看进程内存映射汇总          pmap -x PID
看 swap 用量                swapon --show / free -h
调 swap 倾向                sysctl vm.swappiness=10
保护进程不被 OOM 杀         echo -800 > /proc/PID/oom_score_adj
给服务设内存上限            systemd 配 MemoryMax=1G
看是否在频繁换页            vmstat 1(看 si/so)

口诀:进程消失先 dmesg 查 OOM -> 被杀的不一定是真凶
      -> ps 按内存排序找真凶 -> 治本是 MemoryMax 限制每个服务

避坑清单

  1. 进程凭空消失且应用日志干净,第一动作是 dmesg 查 OOM Killer
  2. free 列小是正常的,内存被拿去做磁盘缓存,要看 available
  3. available 才是真正可立即使用的内存,free 小不等于内存紧张
  4. OOM Killer 按 oom_score 选受害者,不是单纯杀占内存最多的
  5. 被 OOM 杀掉的进程往往不是吃光内存的真凶,要单独找真凶
  6. 区分内存占用高和内存泄漏:泄漏的特征是 RSS 随时间只涨不跌
  7. 看进程内存看 RSS(实际物理内存),VSZ 是虚拟地址空间会虚高
  8. swap 是 OOM 前的缓冲,但活跃内存换进 swap 会让系统极慢
  9. 调低核心进程 oom_score_adj 只是换替罪羊,治本是用 MemoryMax 限制
  10. 把 available 内存和进程 RSS 趋势接进监控,在 OOM 发生前介入

总结

这次"核心服务凌晨被处决"的事故,纠正了我两个一直没想清楚的认知。第一个,是我对一个进程"消失"的下意识反应。过去,只要一个进程不见了,我的第一念头永远是去翻它自己的应用日志,找它崩溃前最后吐出的那段报错堆栈。可这一次,myapp 的应用日志干干净净,翻到最后一行都风平浪静——这恰恰是因为,它根本不是"自己崩"的。它是被 Linux 内核从外部、用一个信号、瞬间地、强制地杀掉的,它自己没有机会、也没有理由,在日志里留下任何"我要死了"的话。复盘让我牢牢记住了一条:当一个进程凭空消失、而它自己的应用日志却异常干净时,我该做的第一件事,不是在应用日志里继续大海捞针,而是立刻去敲 dmesg,看看是不是内核的 OOM Killer 出手了。第二个、也是更深的一个认知纠正,是我终于搞懂了 OOM Killer 这套机制的逻辑,以及它那个最反直觉的地方。当一台机器的物理内存连同 swap 全部被耗尽,内核连一星半点内存都再也拿不出来时,整个系统就走到了死锁的悬崖边上。内核此刻面临一个残酷的抉择:是让整个系统就此卡死、所有进程同归于尽,还是当机立断,主动选一个进程"处决"掉,把它占的内存抢回来,让系统其余的部分活下去?内核选择了后者,这个负责挑选并执行的机制,就是 OOM Killer。而它最反直觉的地方,恰恰在于它"挑人"的方式——它并不是简单地、粗暴地"谁占内存最多就杀谁"。它会为每个进程计算一个综合的 oom_score,这个分数固然把内存占用量算了进去,但内存占用仅仅是其中一个因素而已,它还会掺入进程的运行时长、还有一个可以人为干预的 oom_score_adj 调整值等等。正因如此,最终被推上断头台的,完全可能不是那个真正把内存吃光的元凶。我那台机器上的真凶,是一个凌晨运行的、有内存泄漏的 batch_job 任务,是它一路疯涨,把 8GB 内存连同 swap 一点点蚕食干净的。可当 OOM Killer 启动、开始算分时,我那个常驻的、占用也不算小的核心服务 myapp,综合分数竟然比 batch_job 还要高——于是,那个真正闯了祸的 batch_job 安然无恙地活了下来,而无辜的 myapp 替它做了替罪羊。这个认知对排查的指导意义极大:从此以后,我看到 OOM 日志里"Killed process"后面跟着的那个进程名,绝不会再想当然地把它当成"内存问题的元凶";那只是"受害者",真正的真凶,我必须另外去 ps 的内存排序、去 dmesg 打印的那份进程内存快照里,把它单独揪出来。这次事故最终也让我想明白了什么才是真正的"治本"。一个看似顺手的办法,是把 myapp 这个核心服务的 oom_score_adj 调成一个很低的负数,让 OOM Killer 下次别再选中它——但我很快意识到,这只是"治标",它做的不过是把那把刀从 myapp 头上,引向了另一个无辜的进程,而"内存会被某个泄漏的进程吃光"这个根本病灶,丝毫没有被触动。真正的治本之策,是换一个思路:与其被动地等待某个失控的进程把全机内存吃光、再去祈祷 OOM Killer 别杀错人,不如从一开始就用 cgroup、用 systemd 服务配置里的 MemoryMax,给每一个服务都划定一个内存使用的上限。这样一来,当 batch_job 再次内存泄漏时,它会先一头撞上自己那 1GB 的天花板、被单独处理掉,它的失控被牢牢地圈在了它自己的边界之内,再也无法蔓延出去、连累整台机器、害得 myapp 被误杀。这次从一个凭空消失的进程出发,我最大的收获,是不再把内存当成一片可以任由各个进程自由争抢的公共草地——而是认识到,在生产服务器上,必须主动地为每一个服务,圈出它自己的那一块、并且只属于它的内存边界。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

负载飙到 50 而 CPU 却空闲:一次 Linux 系统负载排查复盘

2026-5-20 18:31:40

Linux教程

磁盘满了空间却找不到:一次 Linux 磁盘空间排查复盘

2026-5-20 18:39:13

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索