服务进程半夜凭空消失:一次 Linux OOM Killer 排查复盘

一个 Java 服务隔三差五进程凭空消失,ps 找不到,应用日志最后一行戛然而止没有任何异常。排查梳理:进程无声消失且日志没遗言极可能是被 SIGKILL 杀的,真凶是内核内存耗尽时的 OOM Killer;dmesg -T 与 journalctl -k 找 killed process 铁证、读懂 OOM 时的进程内存排行表;oom_score 决定杀谁、oom_score_adj 给关键进程减刑;free 看内存认准 available 列、buff/cache 是可让出的缓存;区分内存泄漏与正常吃满,限堆 加 swap cgroup MemoryMax 根治,以及一套 OOM 排查纪律。

2024 年,我维护的一个 Java 服务,开始玩一种诡异的"失踪"游戏。早上来上班,监控告警一片红,服务挂了。我登上去一看,进程确实没了——不是僵死,是干干净净地消失了,ps 里翻不到一丝痕迹。我把它重启,它又活蹦乱跳地跑起来,一整天都好好的。可第二天早上,同样的剧本再演一遍:进程又没了。最让我头疼的是,它走得悄无声息——服务自己的日志,一翻到底,最后一条记录之后干干净净,没有异常、没有报错、没有任何"我要退出了"的迹象,像是有人在它毫无防备的时候,直接拔了它的电源。一个进程,既没崩溃报错,也没收到我发的任何关闭指令,怎么会凭空蒸发?我盯着那截戛然而止的日志想了很久,最后意识到:既然进程自己什么都没说,那一定是【别人】动的手。而在一台 Linux 机器上,有资格不打招呼就弄死一个进程的,只有内核。这件事逼着我把 Linux 的内存管理、OOM Killer、内核日志这一整套彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7,4G 内存,跑一个 Java 服务 + 若干辅助进程
事故现象:
- 服务进程隔三差五"消失",ps 里完全找不到
- ★ 服务自己的日志没有任何异常,最后一行戛然而止
- 重启就好,能撑大半天,然后又没了

现场排查:
# 1. 进程确实不在了
$ ps -ef | grep myapp
(什么都没有)                              # ★ 进程凭空消失

# 2. 服务自己的日志 —— 干干净净,没有遗言
$ tail -50 /opt/myapp/logs/app.log
... 2024-06-01 03:11:20 处理请求完成        # ★ 最后一行,然后就没了
# 没有 Exception、没有 shutdown、什么都没有。

# 3. ★ 进程没留遗言 -> 一定是别人弄死的 -> 查内核日志
$ dmesg | grep -i 'killed process'
[12345.6789] Out of memory: Killed process 9001 (java)   # ★ 真相!
[12345.6789] oom-kill:...,task=java,pid=9001,...

# 4. ★ 内核日志里完整的 OOM 记录
$ dmesg | grep -i -A2 'out of memory'
Out of memory: Killed process 9001 (java)
  total-vm:5242880kB, anon-rss:3801200kB, ...    # ★ 这进程吃了 3.8G
# 内核因为内存耗尽,主动选了 java 进程,把它杀了。

根因(后来想清楚的):
1. ★ 进程不是自己崩的,是被【内核的 OOM Killer】杀的。
2. OOM = Out Of Memory。当整机内存即将耗尽、
   内核自己都快没内存可用时,它会启动一个"自救"机制:
   ★ 主动挑一个进程杀掉,把内存抢回来,保住整个系统。
3. 这台机器只有 4G 内存,Java 服务跑久了堆涨到 3.8G,
   加上别的进程,内存被榨干 -> 内核触发 OOM。
4. ★ OOM Killer 挑中了 java(它占内存最多),
   一个 SIGKILL 直接弄死 —— SIGKILL 没法被捕获,
   所以进程【来不及留任何遗言】,日志才那么干净。
5. 重启能撑半天,是因为 Java 堆要慢慢涨,涨到顶才再次 OOM。
进程"无声消失"+ 应用日志没异常,几乎一定要去查 OOM。

修复 1:进程凭空消失的真相——内核的 OOM Killer

# === ★ 先建立认知:一个进程的死法,不止"自己崩"一种 ===

# === 死法 A:进程自己崩(它会留下痕迹)===
# 程序抛异常、段错误、自己 exit ——
# ★ 这种死法,应用日志里【通常有迹象】:异常栈、错误码。

# === ★ 死法 B:被别人用信号杀死(它可能来不及留话)===
# 别的进程 / 内核,给它发一个信号把它干掉。
# 其中最狠的是 SIGKILL(信号 9):
# ★ SIGKILL【无法被捕获、无法被忽略】,进程收到就立刻死,
#   连"我要退出了"这句话都来不及写 —— 日志自然是干净的。

# === ★ 谁有资格不打招呼就杀进程:内核的 OOM Killer ===
# OOM Killer = Out-Of-Memory Killer,内核的一个自救机制。
# 触发场景:整机内存即将彻底耗尽,内核自己都快没内存了。
# 此时内核面临一个抉择:要么整个系统卡死/崩溃,
#   要么 —— ★ 牺牲一个进程,把它的内存抢回来,保住系统。
# 内核选了后者:它挑一个进程,送它一个 SIGKILL。

# === ★ 于是"无声消失"这个现象,本身就是线索 ===
# 进程消失了 + 应用日志没有任何异常和遗言
#   -> 极可能是被 SIGKILL 杀的(它没机会留话)
#   -> 而最常见的 SIGKILL 来源,就是 OOM Killer。
# ★ 排查"进程神秘失踪",第一站就该是内核日志,不是应用日志。

# === 一句话抓住本质 ===
# 应用日志记的是"进程自己想说的话";
# ★ 内核日志记的是"系统对进程做了什么"。
# 进程被外力弄死,真相只在【内核日志】里。

修复 2:从内核日志里找 OOM 的铁证

# === ★ 确认是不是 OOM,去内核日志里找铁证 ===

# === dmesg:看内核环形缓冲区的日志 ===
$ dmesg | grep -i 'out of memory'
Out of memory: Killed process 9001 (java) ...
$ dmesg | grep -i 'killed process'
Killed process 9001 (java) total-vm:5242880kB, anon-rss:3801200kB
# ★ 看到 "Out of memory" + "Killed process" = OOM 实锤。

# === ★ dmesg 加 -T,把时间戳变成人能读的 ===
$ dmesg -T | grep -i 'killed process'
[Sat Jun  1 03:15:22 2024] Killed process 9001 (java) ...
# ★ 默认 dmesg 时间是"开机后秒数",-T 转成日期,
#   能直接和监控告警的时间对上。

# === ★ 更可靠:OOM 记录也会进系统日志,且能查历史 ===
$ journalctl -k | grep -i 'out of memory'         # -k 只看内核消息
$ journalctl -k --since "2024-06-01" --until "2024-06-02"
$ grep -i 'out of memory' /var/log/messages         # CentOS 的系统日志
# ★ dmesg 的环形缓冲区会被新日志冲掉,旧的 OOM 记录可能没了;
#   journalctl / /var/log/messages 是落盘的,查几天前的 OOM 靠它。

# === ★ 读懂 OOM 那一段完整日志 ===
$ dmesg -T | grep -i -B1 -A3 'out of memory'
# OOM 发生时,内核会打印一大段,关键几行:
#  - "Out of memory: Killed process PID (名字)" -> 谁被杀了
#  - total-vm  -> 该进程申请的虚拟内存
#  - anon-rss  -> ★ 该进程实际占用的物理内存(最该看的)
#  - 上面通常还有一张【进程内存排行表】,列出当时
#    每个进程吃了多少 —— 能看清"内存到底被谁吃光的"。

# === 那张进程排行表长这样 ===
$ dmesg | grep -A20 'Out of memory'
#  pid     rss   name
#  9001  950300  java       <- ★ rss 那列,数字最大的就是吃内存大户
#  8800   12000  nginx
# ★ 被杀的不一定是 rss 最大的,但 rss 最大的通常是元凶。

修复 3:OOM Killer 凭什么选中它——oom_score

# === ★ 搞清楚:内存爆了,内核【怎么决定杀哪个进程】===

# === 内核给每个进程算一个"该死分":oom_score ===
$ cat /proc/9001/oom_score
820
# ★ oom_score 越高 = 内核越倾向于在 OOM 时杀它。
# 这个分主要看:进程【占了多少内存】—— 占得越多,分越高。
# 内核的逻辑很朴素:杀一个占内存最多的,回收效果最好。

# === 看所有进程的 oom_score,找"最危险"的那个 ===
$ for p in /proc/[0-9]*; do \
    printf "%s %s\n" "$(cat $p/oom_score 2>/dev/null)" \
    "$(cat $p/comm 2>/dev/null)"; done | sort -rn | head
820 java                          # ★ 分最高,OOM 时第一个被杀
130 mysqld
# ★ 提前看这个排行,就知道下次 OOM 内核会先动谁。

# === ★ 人为干预:oom_score_adj 调整某进程的"该死分" ===
$ cat /proc/9001/oom_score_adj
0
# oom_score_adj 范围 -1000 ~ +1000,会叠加进 oom_score:
#  调成 -1000 -> ★ 几乎"免死",内核 OOM 时尽量不杀它
#  调成 +1000 -> 优先杀它(适合"可牺牲"的进程)

# === 给关键进程"减刑",别让它在 OOM 时第一个挨刀 ===
$ echo -800 > /proc/9001/oom_score_adj
# ★ 比如数据库、核心服务,可以调低它的 oom_score_adj,
#   让 OOM Killer 优先去杀那些不重要的、可重启的进程。

# === ★ 但这只是"换个进程死",不是"不死" ===
# 改 oom_score_adj 只是改变"杀谁"的顺序 ——
#   内存真不够时,总得有进程死。
# ★ 它是应急/止损手段,不是根治。根治是下一节:
#   要么真的省内存,要么真的加内存。

# === systemd 服务里持久化这个设置 ===
# [Service]
# OOMScoreAdjust=-800
# ★ 写进 unit 文件,服务每次启动都带上这个"减刑",
#   比手动 echo 到 /proc(重启就没了)靠谱。

修复 4:内存到底去哪了——free 与 cache 的误会

# === ★ 确认 OOM 后,要搞清"内存是被正常吃光,还是异常" ===

# === free -h:看内存的大盘 ===
$ free -h
              total   used   free   shared  buff/cache  available
Mem:           3.7G   2.1G   180M     50M      1.4G        1.3G
Swap:            0B     0B     0B
# ★ 新手最容易看错的一行:别只盯 free(真正空闲)那列!
# - free       :纯空闲,通常很小,★ 看它会吓自己一跳
# - buff/cache :被【缓存】占用的 —— 这部分是"可让出来"的
# - ★ available:内核估算的"真正还能用的内存" —— 看这个!

# === ★ buff/cache 不是"被占用",是"被借用" ===
# Linux 会拿空闲内存去缓存磁盘数据,加速读写。
# 这部分内存,一旦进程要用,内核会【立刻让出来】。
# ★ 所以 free 小不代表内存紧张 —— available 小才是真紧张。
# 判断内存够不够,认准 available 这一列。

# === 看是哪个进程在吃内存 ===
$ ps aux --sort=-rss | head                # 按物理内存(RSS)排序
USER  PID  %MEM   RSS    COMMAND
app   9001 24.0  950300  java              # ★ RSS 列 = 实际占的物理内存
$ top                                       # 交互式,按 M 键按内存排序

# === ★ 区分两种"内存吃光" ===
# 情况 1:业务量就这么大,内存是被【正常】用满的
#   -> 解法:加内存,或把服务的内存用量调小(如 JVM 堆)
# 情况 2:★ 内存泄漏 —— 某进程的 RSS 一直涨、从不回落
#   -> 解法:必须修程序的 bug,加内存只是拖延爆炸时间
$ watch -n5 'ps -o rss= -p 9001'           # 持续盯一个进程的 RSS 走势
# ★ RSS 曲线只涨不跌 = 泄漏;涨到一个值后平稳 = 正常吃满。

# === 看某进程内存的细致构成 ===
$ cat /proc/9001/status | grep -i vm
VmRSS:    950300 kB                        # 实际物理内存
VmSwap:        0 kB
$ pmap -x 9001 | tail -1                   # 该进程内存映射的总计

修复 5:怎么根治——给内存上规矩

# === ★ 找到原因后,根治 OOM 的几个方向 ===

# === 方向 1:把"吃内存大户"自己的用量限制住 ===
# 我这次是 Java:它的内存大头是【堆】,由 JVM 参数控制。
$ java -Xmx2g -Xms2g -jar myapp.jar
# ★ -Xmx2g:堆最大 2G。机器 4G 内存,堆别配到 3.8G ——
#   要给操作系统、非堆内存、其他进程留出余量。
# 经典坑:堆配得过大 -> 堆涨满时整机就 OOM。

# === 方向 2:加 swap,给内存一个"缓冲垫" ===
$ free -h                                  # 我这台 Swap 是 0 —— 太极端
# 没有 swap,物理内存一满就直接 OOM,没有任何缓冲。
$ dd if=/dev/zero of=/swapfile bs=1M count=2048   # 建 2G swap 文件
$ chmod 600 /swapfile
$ mkswap /swapfile && swapon /swapfile
$ echo '/swapfile swap swap defaults 0 0' >> /etc/fstab   # 开机自动挂
# ★ swap 不能替代物理内存(它慢),但能在内存峰值时
#   兜一下,把"直接 OOM"变成"变慢",争取喘息空间。

# === 方向 3:★ 用 cgroup 限制单个服务的内存上限 ===
# systemd 服务可以直接配内存上限:
# [Service]
# MemoryMax=2G
# ★ 这样这个服务最多用 2G,它自己撑爆时,
#   触发的是【它自己范围内】的 OOM,不会把【整机】拖垮 ——
#   把"全局事故"缩小成"单服务事故",非常关键。
$ systemctl show myapp -p MemoryMax        # 确认限制生效

# === 方向 4:理解并谨慎对待 overcommit ===
$ cat /proc/sys/vm/overcommit_memory
0
# Linux 默认允许"内存超额承诺":进程申请内存时,
#   内核常常先答应下来(因为进程未必真用那么多)。
# ★ 这是 OOM 存在的根源之一 —— 答应得太满,真要用时
#   就兑现不了,只能 OOM。一般保持默认,别乱调。

# === ★ 治理思路总结 ===
# 先分清:是内存泄漏(治程序),还是真不够用(治容量)。
# 泄漏 -> 修 bug;真不够 -> 加内存 / 限堆 / 加 swap /
#   用 cgroup 把每个服务关进笼子,避免一个拖垮全部。

修复 6:OOM 与内存排查纪律

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

# === 1. ★ 进程"无声消失"+应用日志没异常,先查 OOM ===
$ dmesg -T | grep -i 'killed process'
$ journalctl -k | grep -i 'out of memory'

# === 2. ★ 进程被外力杀死,真相在内核日志,不在应用日志 ===
# 应用日志记"它想说的话",内核日志记"系统对它做的事"。

# === 3. 看内存够不够,认准 free 的 available 列 ===
$ free -h          # 别被很小的 free 列吓到,buff/cache 可让出

# === 4. ★ 区分内存泄漏和正常吃满 ===
$ watch -n5 'ps -o rss= -p PID'   # RSS 只涨不跌 = 泄漏

# === 5. 关键进程调低 oom_score_adj,但那只是换谁死 ===
$ echo -800 > /proc/PID/oom_score_adj   # 应急,非根治

# === 6. ★ 用 cgroup/systemd MemoryMax 把服务关进笼子 ===
# 让单服务撑爆只影响自己,不拖垮整机。

# === 7. 排查 OOM 问题的命令链 ===
$ dmesg -T | grep -i 'killed process'    # ① 确认是不是 OOM
$ dmesg | grep -A20 'Out of memory'      # ② 看当时进程内存排行
$ free -h                                # ③ 看内存大盘
$ ps aux --sort=-rss | head              # ④ 谁在吃内存
$ watch 'ps -o rss= -p PID'              # ⑤ 是泄漏还是正常吃满
# 按这个顺序,OOM 问题基本能定位。

命令速查

需求                        命令
=============================================================
看是不是 OOM 杀的进程       dmesg -T | grep -i 'killed process'
查历史 OOM 记录             journalctl -k | grep -i 'out of memory'
看 OOM 时进程内存排行       dmesg | grep -A20 'Out of memory'
看内存大盘                  free -h
按物理内存排序看进程        ps aux --sort=-rss | head
持续盯一个进程的内存        watch -n5 'ps -o rss= -p PID'
看进程的 oom_score          cat /proc/PID/oom_score
给进程减刑(少被 OOM 杀)   echo -800 > /proc/PID/oom_score_adj
看某进程内存细节            cat /proc/PID/status | grep -i vm
限制服务内存上限            systemd unit 配 MemoryMax=2G

口诀:进程无声消失先 dmesg 查 OOM,看内存认准 free 的 available
      RSS 只涨不跌是泄漏,真不够用就限堆加内存,cgroup 关笼子

避坑清单

  1. 进程凭空消失且应用日志无异常,极可能是被 SIGKILL 杀的,优先查 OOM
  2. OOM Killer 是内核内存耗尽时的自救机制,会主动杀进程把内存抢回来
  3. SIGKILL 无法被捕获,进程来不及留遗言,所以应用日志才那么干净
  4. 进程被外力杀死的真相在内核日志,dmesg 和 journalctl -k 才查得到
  5. dmesg 加 -T 把开机秒数转成日期,才能和监控告警时间对上
  6. dmesg 环形缓冲区会被冲掉,查几天前的 OOM 要用 journalctl 或 messages
  7. free 的 free 列是纯空闲常常很小,判断内存够不够要看 available 列
  8. buff/cache 是被借用的缓存,进程要用时内核会立刻让出,不算真占用
  9. 进程 RSS 只涨不跌是内存泄漏要修程序,涨到平稳是正常吃满
  10. oom_score_adj 只改变杀谁的顺序不能根治,根治靠限堆加内存或 cgroup 限制

总结

这次"Java 进程半夜无声失踪"的事故,纠正了我一个排查问题时几乎是本能的、却严重狭隘的习惯——一个进程出了状况,我会下意识地、几乎是唯一地,扑向它自己的日志。在我过去的认知里,一个服务就是一个能自己说话、能自己负责的主体:它要是出了事,它一定会在自己的日志里留下点什么——一段异常栈,一个错误码,哪怕只是一句"我要退出了"。日志,就是进程的"遗言",也是我排查故障雷打不动的第一现场,甚至是唯一现场。正是这个根深蒂固的习惯,让我这次彻底卡死:我把那份服务日志从头到尾翻了无数遍,可它干净得近乎诡异——最后一条正常记录之后,什么都没有,没有异常,没有挣扎,没有告别。一个进程,既没有崩溃的迹象,又没留下任何遗言,就这么凭空蒸发了。在"进程出事必有遗言"这个假设里,这件事根本无法解释。可正是这份"无法解释",最后反过来点醒了我:日志的这份诡异的干净,本身就是最大的一条线索。复盘到根上,我才真正想明白一件以前从没认真想过的事:一个进程的死,并不总是它"自己的事"。它当然可能是自己崩的——那种死法,它有充分的时间和机会留下遗言,日志里也确实会有迹象。但它也可能是被【外力】弄死的。在一台 Linux 机器上,进程并不是一个个绝对自治的孤岛,它们之上还盘踞着一个拥有生杀大权的存在——内核。当整台机器的内存即将彻底耗尽,内核自己都快揭不开锅的时候,它不会坐视整个系统一起陪葬,它会启动一套冷酷的自救逻辑:从现有进程里挑一个出来,送它一发 SIGKILL,用它的死,换回一整片内存,保住系统。而 SIGKILL 这一刀的特殊之处就在于——它是一记任何进程都无法招架、无法拦截、甚至无法感知的死刑。进程收到它的瞬间就停止了一切,它根本没有那一丝一毫的机会,去日志里写下哪怕一个字。于是事情就全说通了:我那个 Java 进程日志的干净,不是因为它走得安详,恰恰相反,是因为它死得太突然、太彻底——它是被内核在毫无预兆中一刀毙命的,它想留遗言,可它连"想"的时间都没有。它的死因,从来就不可能出现在它自己的日志里;它的死因,记在了那个动手者的账本上——内核日志。这次最大的收获,是我给自己脑子里那张"排查地图",郑重地添上了过去完全空白的一块。一个进程出了事,现场不止一个。应用日志,记的是这个进程"自己想说的话";而内核日志,记的是"系统这个更高的存在,对这个进程做了什么"。当一个进程死得无声无息、自己的日志里找不到半点说法时,我不该再对着那份干净的日志一遍遍空转、怀疑自己漏看了什么——那份干净本身就在告诉我:答案不在它那里。我该立刻抬起头,去问那个真正动了手的角色。进程的世界里,从来不是只有进程自己;它头顶上,始终还站着一个内核。

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

日志时间差了 8 小时:一次 Linux 系统时间与时区排查复盘

2026-5-20 20:17:17

Linux教程

一屏幕僵尸进程,kill -9 都杀不掉:一次 Linux 僵尸进程排查复盘

2026-5-20 20:25:53

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