服务进程隔几天就消失:一次 Linux 进程与信号排查的复盘

后台服务隔几天就凭空消失一次,应用日志最后一行却干干净净,没有任何报错。一个进程的死法远不止"自己崩溃":被信号杀、被 OOM Killer 处决、被父进程带走。排查梳理:ps/pstree 看进程、读懂 STAT 状态、信号与退出码 137、dmesg 找 OOM、systemd 自愈兜底。

2024 年我们有个后台服务,隔三差五就会"凭空消失"一次。运维早上来看,进程没了,服务也没了,但日志的最后一行干干净净,没有任何报错、没有异常堆栈,就好像它自己安安静静地走掉了。一开始我们都以为是程序自己崩了,翻遍了应用日志却一无所获。后来我才明白,一个 Linux 进程的"死法"远不止"自己崩溃"这一种——它可能是被信号杀掉的,可能是被内核的 OOM Killer 选中处决的,也可能是父进程退出后被连带带走的。这一次排查,逼着我把 Linux 进程与信号这套机制从头理了一遍。本文复盘这次实战。

问题背景

环境:CentOS 7,一个常驻的 Java 后台数据处理服务
事故现象:
- 服务进程隔几天就消失一次,毫无规律
- 应用日志最后一行很干净,没有任何报错或异常
- 进程没了,服务自然也停了,要人工重启
- top / ps 都找不到它,像凭空蒸发

现场排查(一开始的困惑):
# 1. 进程还在不在?
$ ps -ef | grep data-service
# —— 不在了,连个影子都没有

# 2. 翻应用日志,想找崩溃堆栈
$ tail -100 /var/log/data-service/app.log
# —— 日志正常结束,没有 Exception,没有 OOM 字样
#    应用层面看,它就是"正常地"停了

# 3. 关键转折:去看【内核日志】,而不是应用日志
$ dmesg | grep -i kill
# —— 一行刺眼的记录跳了出来

根因(后来定位到的):
这台机器上还跑着别的服务,某些时段总内存吃紧,
内核的 OOM Killer 触发了,
它挑中了占内存较多的这个 Java 进程,把它杀掉。
应用日志当然干净 —— 进程是被内核从外部"处决"的,
它根本没机会写下任何遗言。

修复 1:先看清进程——ps 与 pstree

# === 排查进程问题,先把"进程长什么样"看清楚 ===

# === ps:看进程快照,最常用的两种写法 ===
$ ps -ef
# -e 所有进程  -f 全格式(含 PPID 父进程、启动时间、命令)
$ ps aux
# a 所有用户  u 带 CPU/内存占用  x 含没有终端的进程
# 两种都常用,aux 多了 %CPU/%MEM,排查资源问题更顺手

# === 找特定进程 ===
$ ps -ef | grep data-service | grep -v grep
# grep -v grep:把 grep 自己这一行过滤掉
# 更省事的是 pgrep:
$ pgrep -a java        # -a 连命令行一起显示
$ pgrep -f data-service.jar   # -f 匹配整条命令行

# === pstree:看进程的【父子关系树】 ===
$ pstree -p 12345
# -p 显示 PID。能看清这个进程是谁的孩子、又有哪些孩子。
# 这一点排查时很重要 —— 父进程一死,
# 它的孩子可能也会跟着受影响。

# === 看进程的关键字段 ===
$ ps -o pid,ppid,stat,etime,rss,cmd -p 12345
#  PID   PPID STAT     ELAPSED   RSS CMD
# 12345     1 Sl    2-13:40:11 1820M java -jar ...
# PPID:父进程。PPID=1 表示父进程是 init/systemd
# STAT:进程状态(下一节细讲)
# etime:已经运行了多久
# rss:实际占用的物理内存(KB)

修复 2:看懂进程状态 STAT

=== ps 里的 STAT 列,每个字母都有含义 ===

主状态(第一个字母):
R  Running    : 正在运行,或在运行队列里等 CPU
S  Sleeping   : 可中断睡眠 —— 最常见,在等某个事件
                (等网络、等定时、等锁),能被信号唤醒
D  Disk sleep : 不可中断睡眠 —— ★ 通常在等磁盘 IO,
                这种状态【连 kill -9 都杀不动】,
                D 状态进程一多,往往是 IO 出了问题
T  Stopped    : 被暂停了(收到 STOP 信号,或被调试器停住)
Z  Zombie     : 僵尸进程 —— 进程已经死了,但它的
                父进程还没"收尸"(没调用 wait 回收)

后缀修饰(后面的字母):
s  : 是会话首进程(session leader)
l  : 是多线程进程
+  : 在前台进程组
N  : 优先级被调低了(nice 值为正)

=== 排查时最该警惕的两个状态 ===

【Z 僵尸进程】
僵尸本身不占 CPU、不占内存,它只是占着一个进程表项。
少量、短暂的僵尸是正常的。但如果僵尸越积越多,
说明【父进程有 BUG】—— 父进程 fork 了子进程,
却一直不去 wait() 回收它们。
解法:重启那个【父进程】。杀僵尸本身没用 ——
它已经死了,你杀不死一个死人。

【D 不可中断睡眠】
进程卡在内核态等一个无法被打断的操作(几乎都是磁盘 IO)。
大量 D 状态进程 = 磁盘 IO 严重阻塞。
而且 D 状态的进程 kill -9 也杀不掉,
只能去解决底层的 IO 问题。
# === 快速找出僵尸进程 ===
$ ps -ef | awk '$2 ~ /defunct/ || $0 ~ /defunct/'
# 或直接看 STAT 列带 Z 的
$ ps aux | awk '$8 ~ /Z/ {print}'

# === 找出僵尸进程的"父进程"(真正要处理的对象)===
$ ps -o ppid= -p <僵尸PID>
# 拿到 PPID,去重启 / 修复那个父进程

修复 3:信号——进程是被"谁"杀的

# === 进程间靠"信号"通信,kill 本质就是发信号 ===

# === 几个必须分清的信号 ===
# SIGTERM(15):礼貌地请进程退出 —— kill 默认发的就是它。
#   进程能捕获它,做完清理(关连接、刷盘)再优雅退出。
# SIGKILL(9) :强制处决 —— 不可被捕获、不可被忽略,
#   内核直接干掉进程,它没有任何机会做清理。
# SIGHUP(1)  :终端挂断 —— 常被服务用来表示"重载配置"。
# SIGINT(2)  :就是你按 Ctrl+C 发出的那个。
# SIGSTOP/SIGCONT:暂停 / 恢复进程。

# === 正确的杀进程姿势:先礼后兵 ===
$ kill 12345           # 先发 SIGTERM,给它优雅退出的机会
# 等几秒,还不退,再强制:
$ kill -9 12345        # 发 SIGKILL,强制
# 一上来就 kill -9 是坏习惯 —— 进程来不及刷盘、
# 来不及关连接,可能丢数据、留下脏状态。

# === 查所有信号 ===
$ kill -l

# === 关键问题:我的进程到底是被什么信号杀的 ===
# 如果服务用 systemd 管理,日志里会明确记录:
$ journalctl -u data-service --no-pager | tail -20
# 会看到类似:
#   data-service.service: Main process exited,
#   code=killed, status=9/KILL
# code=killed、status=9 —— 它是被 SIGKILL(9)杀的!
# 这就把"自己崩溃(code=exited)"和
# "被信号杀死(code=killed)"区分开了。

# === 进程退出码也能透露死因 ===
# 一个进程如果是被信号 N 杀掉的,
# 它的退出码通常是 128 + N。
# 128 + 9 = 137  -> 被 KILL 杀
# 128 + 15 = 143 -> 被 TERM 杀
# 在脚本 / docker logs / k8s 里看到 137,
# 第一反应就该是:它是被 KILL 杀的(常常就是 OOM)。

修复 4:OOM Killer——内核的"处决者"

# === 我们这次的真凶:OOM Killer ===
# 当系统内存严重不足、又腾不出来时,
# 内核为了自保不至于整个宕机,会启动 OOM Killer,
# 从现有进程里【挑一个】杀掉来释放内存。

# === 去内核日志里找它的处决记录 ===
$ dmesg -T | grep -i -E 'oom|kill'
[Tue May 14 03:21:08] Out of memory: Killed process 12345
   (java) total-vm:6291456kB, anon-rss:3984000kB...
# -T 把时间戳转成人能读的格式
# 真相大白:03:21,内核因为内存不足,杀了 PID 12345 的 java

# === 也可以查 messages 日志 ===
$ grep -i 'killed process' /var/log/messages

# === OOM Killer 是怎么挑"倒霉蛋"的 ===
# 内核给每个进程算一个 oom_score(分数越高越该死),
# 大致是"占内存越多,分越高"。可以查看:
$ cat /proc/12345/oom_score

# === 保护关键进程:调低它的 oom_score_adj ===
# oom_score_adj 范围 -1000 ~ 1000,值越小越不容易被选中
$ echo -800 > /proc/12345/oom_score_adj
# 设成 -1000 基本就是"豁免",但要谨慎用 ——
# 别让内核没得选,最后只能杀更要命的进程。

# === 根治 OOM 的几个方向 ===
# 1. 内存确实不够 -> 加内存,或把挤在一起的服务拆开
# 2. 某个进程内存泄漏 -> 修代码(治本)
# 3. 给 Java 这类进程设好上限(-Xmx),别让它无节制涨
# 4. systemd 里给服务设 MemoryMax,超了先在 cgroup 内
#    被限制,而不是惊动全局的 OOM Killer
# 我们这次:① 给 Java 设了合理的 -Xmx
#          ② 把抢内存的服务迁走 ③ 加了内存告警

修复 5:让服务"死了能自己爬起来"——systemd

# === 退一步:就算进程意外死了,也不该靠人手动重启 ===
# 用 systemd 托管服务,让它具备自愈能力。

# === 写一个 systemd service 单元 ===
$ cat /etc/systemd/system/data-service.service
[Unit]
Description=Data Processing Service
After=network.target

[Service]
Type=simple
User=appuser
ExecStart=/usr/bin/java -Xmx2g -jar /opt/app/data-service.jar
# ★ 关键:进程死了自动拉起
Restart=always
RestartSec=5
# 内存超过 3G,先在 cgroup 内被管控
MemoryMax=3G
# 标准输出/错误都进 journal,方便 journalctl 查
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

# === 让配置生效并开机自启 ===
$ systemctl daemon-reload          # 改了单元文件后必须执行
$ systemctl enable data-service    # 开机自启
$ systemctl start data-service     # 立即启动

# === 日常操作 ===
$ systemctl status data-service    # 看状态(运行中?重启过几次?)
$ systemctl restart data-service
$ journalctl -u data-service -f    # 实时跟该服务的日志
$ journalctl -u data-service --since "1 hour ago"

# === Restart=always 的价值 ===
# 配了它之后,哪怕进程再被 OOM 杀掉,
# systemd 也会在 5 秒后自动把它拉起来,
# 服务的"消失时间"从几小时缩短到几秒。
# 但记住:这是兜底,不是免死金牌 ——
# 它治的是"死了没人管",治不了"为什么会死"。

修复 6:别再让任务因终端关闭而被带走

# === 一个相关的常见坑:进程随终端关闭而消失 ===
# 你 SSH 上去,手动跑了个长任务,然后关掉了 SSH 窗口,
# 回来一看任务没了 —— 这也是"进程凭空消失"的一种。

# === 原因:SIGHUP ===
# 终端(会话)关闭时,会给它名下的进程发 SIGHUP 信号,
# 默认行为是终止。所以你的任务被"挂断"带走了。

# === 解法 1:nohup —— 让进程忽略 SIGHUP ===
$ nohup ./long_task.sh > task.log 2>&1 &
# nohup 让进程对 SIGHUP 免疫
# > task.log 2>&1 把输出重定向到文件(否则输出会丢)
# 末尾的 & 让它在后台跑

# === 解法 2:已经跑起来了才想起来,用 disown ===
$ ./long_task.sh &
$ disown -h %1     # 把后台作业 1 从当前 shell 的管理里摘掉
# 这样关掉终端也不会波及它

# === 解法 3(推荐长任务用):tmux / screen ===
$ tmux new -s mytask
# 在 tmux 里跑任务,然后 Ctrl+b d 脱离(detach)
# 终端关了,tmux 会话还在后台活着
$ tmux attach -t mytask    # 下次回来重新接上
# tmux 的好处:任务还在交互式会话里,
# 随时能回去看输出、继续操作,比 nohup 灵活得多

# === 验证进程是否真的脱离了终端 ===
$ ps -o pid,ppid,tpgid,cmd -p <pid>
# 看 PPID 是不是 1(被 init/systemd 收养了),
# 这说明它已经和你的终端脱钩了

命令速查

场景                        命令
=============================================================
看所有进程                  ps -ef  /  ps aux
找特定进程                  pgrep -af 关键字
看进程父子树                pstree -p <pid>
看进程详细字段              ps -o pid,ppid,stat,rss,cmd -p <pid>
找僵尸进程                  ps aux | awk '$8 ~ /Z/'
优雅杀进程                  kill <pid>        (SIGTERM)
强制杀进程                  kill -9 <pid>     (SIGKILL)
查进程被什么信号杀          journalctl -u 服务 | grep code=
查 OOM 处决记录             dmesg -T | grep -i oom
看进程 OOM 分数             cat /proc/<pid>/oom_score
保护进程不被 OOM            echo -800 > /proc/<pid>/oom_score_adj
服务自动重启                systemd 里设 Restart=always
跟服务日志                  journalctl -u 服务 -f
后台跑任务不被挂断          nohup cmd > log 2>&1 &  /  tmux

口诀:日志干净就查内核 -> dmesg 找 OOM -> 退出码 137 = 被 KILL
      根治内存 + systemd 自愈兜底

避坑清单

  1. 进程消失但应用日志干净,八成是被外部杀掉的,要去查内核日志和 systemd 日志
  2. ps -ef 看父子和命令,ps aux 看资源占用,pgrep -af 找进程比 grep 更省事
  3. STAT 列带 Z 是僵尸,根因是父进程没 wait 回收,要重启父进程而不是杀僵尸
  4. STAT 列带 D 是不可中断睡眠,通常卡在磁盘 IO,kill -9 都杀不动,要查 IO
  5. kill 默认发 SIGTERM 让进程优雅退出,别上来就 kill -9,会丢数据留脏状态
  6. 进程退出码 128+N 表示被信号 N 杀,看到 137 就是被 SIGKILL,常常意味着 OOM
  7. dmesg -T grep oom 能找到 OOM Killer 的处决记录,精确到时间和被杀进程
  8. OOM 治本要修内存:给 Java 设 -Xmx、迁走抢内存的服务、修内存泄漏
  9. 用 systemd 托管服务并设 Restart=always,让进程死了能在几秒内自愈
  10. 手动跑长任务用 nohup 或 tmux,否则终端一关 SIGHUP 会把任务带走

总结

这次"进程凭空消失"的排查,纠正了我一个根深蒂固的思维定式:进程没了,第一反应一定是去翻应用日志找崩溃堆栈。这个习惯在大多数时候没错,但它有一个致命的盲区——它默认了进程是"自己"死的。可一个 Linux 进程的死法,远不止"自己崩溃"这一种。它可能被另一个进程发信号杀掉,可能被内核的 OOM Killer 在内存危急时选中处决,可能因为父进程退出而被连带回收,也可能因为你关掉了 SSH 终端、被一个 SIGHUP 信号悄无声息地带走。这些死法有一个共同的特征:进程是被"外部"干掉的,它根本没有机会在自己的应用日志里写下任何一句遗言。所以这次排查真正的转折点,不是我在应用日志里找到了什么,而是我终于意识到该去看应用日志之外的地方——内核日志 dmesg 和 systemd 的 journal。这是我想记住的第一条:当一个进程消失了,而它的应用日志却干干净净、没有任何异常时,这种"干净"本身就是一条极重要的线索,它几乎是在明确地告诉你,这个进程不是自己死的,是被外部处决的,你该去查的是内核和 systemd,而不是继续在应用堆栈里打转。顺着这条线索,我重新认识了几样东西。一是进程的状态,ps 输出里那个不起眼的 STAT 列,其实信息量很大,尤其是 Z 和 D 这两个状态特别值得警惕:Z 是僵尸,它代表进程已经死了但父进程没去"收尸",僵尸本身无害,可它一旦越积越多,矛头就直指父进程的 BUG,而且你要处理的是那个父进程,你没法杀死一个已经死了的僵尸;D 是不可中断睡眠,它代表进程卡在内核态等一个打断不了的操作,几乎总是磁盘 IO,这种进程连 kill -9 都奈何不了它,大量的 D 状态就是磁盘 IO 阻塞的强烈信号。二是信号,kill 这个命令的本质是"发信号",而 SIGTERM 和 SIGKILL 是必须分清的两个:SIGTERM 是礼貌的请求,进程能捕获它、做完刷盘关连接的清理再优雅退出;SIGKILL 是强制处决,不可捕获、不可忽略,进程没有任何机会善后。一上来就 kill -9 是个坏习惯,它可能让进程来不及刷盘而丢数据。而信号还藏着一条破案线索:一个被信号 N 杀掉的进程,退出码通常是 128 加 N,所以当你在脚本里、在 docker logs 里、在 k8s 的事件里看到那个 137,你就该立刻反应过来——这是 128 加 9,它是被 SIGKILL 杀的,而能悄无声息发出 SIGKILL 的,十有八九就是 OOM Killer。OOM Killer 正是我们这次的真凶:当系统内存严重不足、又实在腾不出空间时,内核为了不让整台机器宕机,会启动这个机制,从现有进程里按内存占用高低算分,挑一个分最高的杀掉来自救,而我们那个吃内存较多的 Java 进程,就这样在某个内存吃紧的凌晨被选中处决了——它的应用日志当然干净,因为它是被内核从外部一击毙命的。明白了真凶,根治的方向就清楚了:OOM 的本质是内存不够,治本要么是给 Java 这类进程设好 -Xmx 上限别让它无节制地涨,要么是把挤在一台机器上互相抢内存的服务拆开,要么是去修真正的内存泄漏。但我还想强调最后一层:就算根因一时半会儿治不彻底,你也完全可以让"进程意外死亡"这件事的破坏力降到最低,办法就是用 systemd 托管服务并配上 Restart=always,这样哪怕进程再次被 OOM 杀掉,systemd 也会在几秒之内把它重新拉起来,服务的中断时间就从过去那种"几小时无人值守"压缩成了"几秒钟的自愈"。不过我也时刻提醒自己,Restart=always 是兜底,不是免死金牌,它治的是"进程死了没人管"这个运维问题,治不了"进程为什么会死"这个根本问题,这两件事必须同时做、缺一不可。从最初对着干净的应用日志百思不得其解,到最后理清"日志干净就转查内核、dmesg 锁定 OOM、看懂退出码 137、根治内存再加 systemd 自愈兜底"这样一条完整的链路,这次排查让我真正记住的是:排查进程消失,你首先要问的不是"它报了什么错",而是"它到底是怎么死的"——是自杀,还是他杀。问对了这个问题,你才会知道,答案往往不在应用日志里,而在你一开始压根没想到去看的那些地方。

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

接口偶发超时,竟是 TIME_WAIT 堆积:一次 Linux 网络排查的复盘

2026-5-20 17:20:35

Linux教程

十万行日志里捞线索:Linux grep/awk/sed 三剑客实战复盘

2026-5-20 17:26:17

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