kill -9 用习惯了搞坏了数据:一次 Linux 信号与优雅关闭排查复盘

每次 kill -9 重启服务,监控就准时冒出一小撮请求失败,还留下状态卡在处理中的脏数据。排查梳理:kill 不是杀而是发信号、SIGTERM 与 SIGKILL 的天壤之别、优雅关闭就是捕获 TERM 处理完在途请求再退、kill -9 不可捕获让进程暴毙、D 状态和僵尸进程为何 -9 也无用、systemd 先 TERM 后 KILL 的停止流程,以及一套信号排查纪律。

2024 年,我们团队有个心照不宣的"重启服务"习惯动作:kill -9。发版要重启,kill -9;服务卡了要重启,kill -9;反正 -9 一上,没有杀不掉的进程,干净利落。直到有一次,运营找过来说,每次我们发版的那个时间点,监控上总会准时冒出一小撮"订单提交失败"的用户投诉,而且偶尔还能查到几笔状态卡在"处理中"、再也不动的脏数据。我一开始根本没往重启上想——重启而已,几秒钟的事,怎么会搞坏数据?可一次次发版、一次次对时间点,那个规律清晰得让人无法回避:问题就出在 kill -9 这一下。一个我用了好几年、自以为无比可靠的"重启"动作,竟然是在制造故障。这件事逼着我把 Linux 的信号机制、SIGTERM 与 SIGKILL、优雅关闭这一整套彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7,一个处理订单的 Java 服务
事故现象:
- 每次发版重启服务,监控准时出现一小撮请求失败
- 偶尔产生状态卡在"处理中"的脏数据
- 重启方式:kill -9 ,一直这么干的

现场排查:
# 1. 复盘发版动作,我们一直是这么"重启"的
$ kill -9 8123                  # ★ 直接 -9,从不含糊

# 2. 看服务日志,重启那一刻是怎么结束的
$ tail -100 /var/log/app/app.log
... 正在处理订单 ORD20240518xxx
... 正在写入数据库 ...
(日志到这里【戛然而止】,没有任何"关闭"相关的行)
# ★ 进程是被【当场打断】的,不是正常退出的

# 3. 对比一个用 kill(默认 SIGTERM)停的服务,日志是这样
... received SIGTERM, shutting down gracefully
... waiting for 3 in-flight requests to finish
... all requests done, closing db pool
... bye
# ★ 收到 TERM 的进程,有一段【收尾过程】

根因(后来想清楚的):
1. ★ kill -9 发的是 SIGKILL 信号,它【不可被捕获、
   不可被忽略】—— 内核直接把进程干掉,进程【没有任何
   机会】执行自己的关闭逻辑。
2. 我们的服务本来写了优雅关闭:收到关闭信号后,会
   先处理完手里在途的请求、刷盘、关连接,再退出。
3. ★ 但 kill -9 根本不给它这个机会 —— 进程在"订单
   写一半"的瞬间被瞬间抹掉。
4. 于是:在途请求 -> 客户端报失败;写一半的事务 ->
   留下"处理中"的脏数据。
5. ★ 正解是 kill(默认 SIGTERM):给进程发"请关闭"
   的信号,让它自己优雅收尾。-9 只该是最后手段。
kill 不是"杀",是"发信号"。-9 和默认,天差地别。

修复 1:kill 不是"杀"——它是"发信号"

# === ★ 第一个要纠正的认知:kill 这个命令名,起得有误导性 ===
# kill 干的事,根本不是"杀死",而是【给进程发送一个信号】。
# "杀死"只是某些信号【碰巧】会导致的【默认结果】而已。

# === 信号是什么 ===
# 信号是内核提供的一种【异步通知机制】——
# 用一个小小的编号,通知一个进程"发生了某件事"。
# 进程收到信号后,可以:
#   1. 执行该信号的【默认动作】(终止 / 忽略 / 暂停…)
#   2. 【捕获】它,运行自己注册的处理函数
#   3. 【忽略】它(部分信号可以)

# === 看系统支持哪些信号 ===
$ kill -l
 1) SIGHUP   2) SIGINT   3) SIGQUIT  6) SIGABRT
 9) SIGKILL 15) SIGTERM 17) SIGCHLD 18) SIGCONT
19) SIGSTOP 20) SIGTSTP ...
# 每个信号一个编号、一个名字。kill -9 就是发 9 号 = SIGKILL。

# === ★ kill 的几种写法,都是"发信号" ===
$ kill 8123                     # 不指定 -> 默认发 SIGTERM(15)
$ kill -15 8123                 # 显式发 SIGTERM
$ kill -TERM 8123               # 用名字,等价于 -15
$ kill -9 8123                  # 发 SIGKILL
$ kill -HUP 8123                # 发 SIGHUP,很多服务用它"重载配置"

# === 给一批进程发信号 ===
$ killall myapp                 # 按【进程名】发(默认 TERM)
$ pkill -f 'java.*myapp'        # 按命令行【模式】匹配着发
$ kill -- -8123                 # ★ PID 前加负号 = 发给整个【进程组】

# === ★ 关键结论 ===
# "kill 一个进程",准确说法是"给它发一个信号"。
# 发哪个信号,决定了进程是【优雅退场】还是【当场暴毙】。

修复 2:SIGTERM、SIGKILL、SIGINT——区别与该用哪个

# === ★ 停止一个进程,最该分清的三个信号 ===

# === SIGTERM(15)—— "请你关闭" 的【礼貌请求】 ===
$ kill 8123          # 或 kill -15 / kill -TERM
# - 默认动作:终止进程
# - ★ 但它【可以被进程捕获】—— 进程可以注册一个处理函数,
#   收到 TERM 时不立刻退,而是先做完收尾工作再退。
# - 这是【停止服务的标准、首选方式】。

# === SIGKILL(9)—— "立即处决",不容商量 ===
$ kill -9 8123
# - 默认动作:终止进程
# - ★ 它【无法被捕获、无法被忽略、无法被阻塞】——
#   信号根本不交给进程,由【内核直接】把进程干掉。
# - 后果:进程【没有一丝机会】执行任何清理逻辑。
#   在途请求、没刷的缓冲、没关的连接、没提交的事务,
#   全部【当场丢弃】。这就是我们这次踩的坑。

# === SIGINT(2)—— 你按 Ctrl+C 时发的就是它 ===
$ kill -2 8123       # 等价于在前台按 Ctrl+C
# - 默认动作:终止;也【可被捕获】。
# - 语义上是"用户请求中断",前台程序常用它来优雅退出。

# === ★ 一张表记住差异 ===
# 信号       能否被捕获   进程有机会清理吗   该什么时候用
# SIGTERM    ★ 能         ★ 有              停服务的【首选】
# SIGINT     能           有                前台程序 Ctrl+C
# SIGKILL    ★ 不能       ★ 没有            ★ 万不得已的【最后手段】

# === ★ 正确的停服务姿势:先礼后兵 ===
$ kill 8123                          # ① 先发 TERM,给它优雅退出的机会
$ sleep 10                           # ② 给它一点时间收尾
$ kill -0 8123 2>/dev/null && kill -9 8123   # ③ 还活着,才动用 -9
# kill -0 :不发真信号,只用来【探测进程是否还存在】。
# ★ 这个"先 TERM、等一会、还赖着不走才 KILL"的套路,
#   就是 systemd 停服务时内部干的事。

修复 3:优雅关闭——进程收到 SIGTERM 该做什么

# === ★ "优雅关闭",本质就是:进程【捕获】SIGTERM,
#     收到后不立刻死,而是先把手里的活儿干完再退 ===

# === Shell 脚本里用 trap 捕获信号 ===
#!/bin/bash
cleanup() {
    echo "收到关闭信号,正在清理..."
    rm -f /tmp/myapp.lock        # 释放锁
    echo "清理完成,退出"
    exit 0
}
trap cleanup SIGTERM SIGINT      # ★ 捕获 TERM 和 INT,交给 cleanup
# ... 主逻辑 ...
while true; do work; sleep 1; done
# ★ 这个脚本被 kill(TERM)时,会先跑 cleanup 再退;
#   被 kill -9 时,trap 完全无效 —— cleanup 不会执行。

# === 一个服务"优雅关闭"通常要做的几件事 ===
# 1. ★ 停止接收【新】请求(从负载均衡摘掉 / 关监听端口)
# 2. ★ 把【已经在处理】的请求处理完(in-flight requests)
# 3. 刷新缓冲区,确保数据落盘
# 4. 提交或回滚未完成的事务
# 5. 关闭数据库连接池、消息队列连接
# 6. 删除 pid 文件 / 锁文件,然后退出

# === 各语言捕获 SIGTERM 的入口(概念示意)===
# Java   : Runtime.getRuntime().addShutdownHook(...)
# Go     : signal.Notify(ch, syscall.SIGTERM) 然后 <-ch
# Python : signal.signal(signal.SIGTERM, handler)
# Node   : process.on('SIGTERM', () => {...})
# ★ 它们捕获的都是 SIGTERM —— 这正是为什么停服务要用
#   kill(TERM)而不是 kill -9:-9 让以上所有钩子统统失效。

# === ★ 优雅关闭要配一个【超时】,不能无限期等 ===
# 万一某个请求卡死了,优雅关闭不能就一直等下去。
# 通行做法:给收尾过程设一个上限(比如 30 秒),
#   到点还没收完,就放弃剩下的、强制退出。
# systemd 的 TimeoutStopSec 干的就是这件事(见修复 5)。

# === 验证你的服务到底有没有优雅关闭 ===
$ kill 8123 ; tail -f /var/log/app/app.log
# ★ 看日志里有没有 "shutting down / 收尾 / closing" 这类行。
#   有 -> 优雅关闭生效了;
#   没有、日志直接断掉 -> 要么没写优雅关闭,要么你发的是 -9。

修复 4:kill -9 为什么是最后手段

# === ★ 把 kill -9 当默认动作,是这次事故的总根源 ===

# === -9 的代价:进程"暴毙",留下一地未收拾的烂摊子 ===
# - 在途请求:客户端那头直接收到连接中断 -> 报错
# - 写缓冲区:还在内存没落盘的数据 -> 丢失
# - 数据库事务:写了一半没 commit -> 留下中间态脏数据
# - 锁文件 / pid 文件:没人删 -> 下次启动可能被"已在运行"挡住
# - 子进程:可能变成没人收养的孤儿进程

# === ★ 那 kill -9 什么时候才该用 ===
# 只有一种情况:你先发了 SIGTERM,给了它合理的时间,
# 它【就是不退】—— 这时候才轮到 -9 出场。
$ kill 8123 ; sleep 15
$ ps -p 8123 && kill -9 8123    # 等了 15 秒还在,才 -9

# === ★ 一个 -9 都杀不掉的进程:D 状态(不可中断睡眠)===
$ ps -eo pid,stat,comm | grep 8123
8123 D    myapp                 # ★ STAT 是 D
# D 状态的进程,正陷在内核里等一个不可被打断的操作(通常是磁盘 IO)。
# ★ 此时连 SIGKILL 都得【排队等它从内核返回】才能生效 ——
#   所以你会看到"kill -9 了,进程却还在"。
# 别跟它死磕,去查它在等的那个 IO(见负载排查相关文章)。

# === 僵尸进程(Z 状态):kill 它没有任何意义 ===
$ ps -eo pid,stat,comm | awk '$2 ~ /Z/'
# 僵尸进程【已经死了】,只是父进程还没 wait() 回收它。
# ★ 你 kill(无论几号信号)一个僵尸都是徒劳 —— 它已经没了。
#   要清掉它,得去处理它的【父进程】。

# === 找出一个进程到底在响应哪些信号 ===
$ cat /proc/8123/status | grep -i sig
SigCgt: 0000000000014002    # ★ SigCgt = 进程"捕获"了哪些信号
# 这个掩码能看出进程有没有注册 SIGTERM 处理函数。

修复 5:systemd 是怎么优雅停服务的

# === ★ 与其自己 kill,不如把服务交给 systemd 管 ===
# systemd stop 一个服务时,内部就是一套标准的"先礼后兵"。

# === systemctl stop 背后的完整流程 ===
$ systemctl stop myapp
# systemd 实际做的:
# 1. 给主进程发 SIGTERM(可由 KillSignal= 改)
# 2. ★ 等待,最多等 TimeoutStopSec 这么久
# 3. 进程在超时前退出  -> 完美,优雅停止
# 4. 超时还没退        -> 发 SIGKILL 强制干掉
# ★ 这正是修复 2 那个套路的"产品化"版本。

# === service 单元里和"停止"相关的关键配置 ===
$ cat /etc/systemd/system/myapp.service
[Service]
ExecStart=/opt/myapp/start.sh
KillSignal=SIGTERM            # ★ 停止时先发的信号(默认就是 TERM)
TimeoutStopSec=30             # ★ 给优雅关闭 30 秒,超时才 KILL
KillMode=control-group        # ★ 杀谁:control-group=整个 cgroup 里的进程
Restart=on-failure

# === ★ KillMode 几个取值,决定"停服务"波及谁 ===
# control-group  停掉该服务 cgroup 内的【所有】进程(默认,最干净)
# mixed          主进程发 TERM,其余进程发 KILL
# process        ★ 只停【主进程】—— 它 fork 的子进程可能变孤儿
# none           systemd 不发任何信号(极少用)

# === 服务优雅关闭需要更久?调大超时 ===
$ systemctl edit myapp          # 改 TimeoutStopSec
# 比如批处理服务,收尾要 2 分钟,就设 TimeoutStopSec=120。
# ★ 设太小,systemd 会在服务还没收完尾时就 -9 它 —— 等于又踩坑。

# === 看一次 stop 到底花了多久、怎么结束的 ===
$ systemctl stop myapp ; journalctl -u myapp -n 30
# 日志里能看到 "Stopping..." 到 "Stopped" 之间的过程,
# 以及 systemd 有没有因为超时而动用 SIGKILL。

# === ★ 结论:能交给 systemd 管的服务,就别手动 kill ===
# systemd 的停止流程,天然就是"先 TERM、给时间、超时才 KILL",
# 比人肉 kill -9 安全得多。

修复 6:信号与进程停止排查纪律

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

# === 1. ★ 停服务的默认动作是 kill(SIGTERM),不是 kill -9 ===
$ kill                     # 先发 TERM,让它优雅退出
# kill -9 是"最后手段",不是"顺手就来"。

# === 2. ★ 先礼后兵:TERM -> 等一会 -> 还在才 KILL ===
$ kill ; sleep 10; ps -p  && kill -9 

# === 3. SIGKILL 不可捕获,进程没机会清理 ===
# -9 会让所有 shutdown hook / trap 失效,
# 在途请求、未刷数据、未提交事务全部丢。

# === 4. 服务要写优雅关闭:捕获 TERM,处理完在途请求再退 ===
# 并给收尾过程配一个超时,别无限期等。

# === 5. kill -9 都杀不掉,先看进程状态 ===
$ ps -eo pid,stat,comm | grep 
# D 状态:在等 IO,-9 也要排队 -> 去查 IO
# Z 状态:僵尸,已经死了 -> 去处理它的父进程

# === 6. 能交给 systemd 管就别手动 kill ===
# 配好 KillSignal / TimeoutStopSec / KillMode,
# systemctl stop 天然就是安全的"先礼后兵"。

# === 7. 排查"进程停不掉 / 停出问题"的命令链 ===
$ kill -l                       # ① 信号编号速查
$ ps -eo pid,stat,comm | grep   # ② 进程在什么状态(D/Z?)
$ cat /proc//status        # ③ 它捕获了哪些信号(SigCgt)
$ kill  && tail -f 日志    # ④ 发 TERM,看有没有优雅收尾
$ journalctl -u 服务 -n 30      # ⑤ systemd 管的服务看停止过程
# 按这个顺序,信号与进程停止的问题基本能定位。

命令速查

需求                        命令
=============================================================
看所有信号编号              kill -l
优雅停止(发 SIGTERM)       kill   或  kill -15 
强制杀死(最后手段)         kill -9 
探测进程是否还活着          kill -0 
按进程名发信号              killall 
按命令行模式发信号          pkill -f '模式'
给整个进程组发信号          kill -- -
看进程捕获了哪些信号        cat /proc//status | grep SigCgt
让服务重载配置              kill -HUP 
优雅停止 systemd 服务       systemctl stop <服务>

口诀:停服务先发 TERM 给它优雅退出 -> 等一会还在才动 -9
      kill -9 不可捕获 = 进程暴毙 -> 在途请求和数据全丢

避坑清单

  1. kill 不是"杀死",是"给进程发信号",杀死只是某些信号的默认动作
  2. kill 不带参数默认发 SIGTERM(15),不是 SIGKILL(9)
  3. SIGTERM 可被进程捕获,进程能借机优雅收尾;SIGKILL 不可捕获
  4. kill -9 由内核直接干掉进程,在途请求、未刷数据、未提交事务全丢
  5. 停服务标准姿势是先礼后兵:TERM 等一会,还不退才 KILL
  6. 优雅关闭要捕获 TERM,处理完在途请求再退,并给收尾配超时
  7. kill -9 杀不掉的进程先看状态:D 是等 IO,Z 是僵尸已死
  8. 僵尸进程已经死了,kill 它无意义,要处理它的父进程
  9. 能交给 systemd 管的服务别手动 kill,配好 TimeoutStopSec 就是安全的
  10. systemd 的 TimeoutStopSec 设太小,会在服务没收完尾时就 -9 它

总结

这次"kill -9 重启搞坏数据"的事故,纠正了我一个用了好几年、从来没被质疑过的操作习惯背后的认知错误。在这次之前,我心里对 kill 这个命令的理解,简单粗暴到只有一个词:"杀"。kill 一个进程,就是把它杀掉;而 kill -9,在我心里是这把"杀"刀里最锋利、最可靠的那一把——别的写法或许偶尔会"杀不干净",但 -9 从不失手,一刀毙命。正因如此,我把 kill -9 当成了重启服务的默认动作,图的就是它的"可靠"。我从来没想过,这个我引以为傲的"可靠",恰恰是在制造故障。复盘到根上,我才真正理解,kill 这个命令名,起得是有误导性的——它干的事,根本不是"杀死",而是【向一个进程发送一个信号】。信号,是内核提供的一种异步通知机制,它用一个小小的编号,去通知进程"发生了某件事"。而"进程被终止",仅仅是某些信号会导致的【默认结果】,不是信号本身。想通这一层,SIGTERMSIGKILL 这两个我以前混为一谈的东西,差异就豁然清晰了。kill 不带参数时发的 SIGTERM(15 号),它的本质是一句【礼貌的请求】——"请你关闭一下"。它最关键的性质是:它【可以被进程捕获】。一个设计良好的服务,会注册一个处理函数专门"接住"这个 SIGTERM,收到它以后,进程不会立刻死,而是从容地开始收尾:先停止接收新请求,把手里那几个【已经在处理】的请求老老实实做完,把内存缓冲区刷到磁盘,提交或回滚未完成的事务,关闭数据库连接池,删掉锁文件——这一整套动作,就是所谓的"优雅关闭"。而 kill -9 发的 SIGKILL(9 号),则是完全不同的另一种东西:它【不可被捕获、不可被忽略、不可被阻塞】——这个信号根本不会被交到进程手里,而是由【内核直接】把进程从这个世界上抹掉。这意味着,进程【没有一丝一毫的机会】去执行它那套精心写好的优雅关闭逻辑。我们的订单服务,其实是写了优雅关闭的;可我那句习惯性的 kill -9,等于在它即将开始收尾的前一刻,直接拔掉了它的电源——进程在"一笔订单的数据库事务写了一半"的精确瞬间被瞬间冻结、抹除:那个在途的请求,客户端那头收到的是连接的猝然中断,于是报"提交失败";那个写了一半、还没 commit 的事务,在数据库里留下了一条状态卡在"处理中"、再也不会推进的脏数据。监控上那一小撮准时出现的投诉,全都是被我 -9 当场打断的"半成品"。想清楚这一切,正确的停服务姿势也就不言自明了——它是一套"先礼后兵":先发 SIGTERM,把"请关闭"这个请求递给进程,给它一段合理的时间去优雅地收尾;只有当它在这段时间之后,依然赖着不退,我才有理由动用 SIGKILL 这个最后手段。这个"先 TERM、给时间、超时才 KILL"的套路,其实并不需要我自己人肉去做——systemdsystemctl stop 一个服务时,内部干的恰恰就是这件事:先发 KillSignal(默认 SIGTERM),然后最多等待 TimeoutStopSec 那么久,进程在超时前退出就皆大欢喜,超时还没退才补上一刀 SIGKILL。这次从一个"重启怎么会搞坏数据"的困惑出发,我最大的收获,是把脑子里"kill = 杀,-9 = 最可靠的杀"这个粗暴的等式,彻底拆掉了,换成了一个准确的理解:kill 是"发信号",SIGTERM 是给进程留了一条体面退场的路、是停服务的首选,而 SIGKILL 是一种剥夺了进程所有善后权利的"暴力处决",它只配做万不得已时的最后手段。一个 -9 敲下去快是快,但它快的代价,是进程手里那些没来得及收拾的烂摊子——而那些烂摊子,最后都会变成监控上的告警和用户的投诉,加倍地还回来。

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

公钥明明在服务端却死活不认:一次 SSH 连接问题排查复盘

2026-5-20 19:17:29

Linux教程

挂个数据盘把服务器搞到开不了机:一次 /etc/fstab 排查复盘

2026-5-20 19:23:41

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