前言:消失的 5000 条日志
“生产环境数据对不上了!数据库里少了一批订单状态流转记录!”
昨晚 11 点,正在排位的我被测试疯狂 Call 醒。这批数据涉及到资金结算,少一条都是 P0 级事故。
我连夜爬起来排查,代码逻辑查了三遍,没毛病;数据库事务查了,也正常。最后去翻应用日志,发现了一个极其诡异的现象:日志在某个时间点突然截断了,没有任何报错,没有任何异常堆栈,就像应用“凭空蒸发”了一样。
我把目光转向了发版记录。果然,那个时间点,运维正在进行灰度发布。
我冲到运维工位(群里)问:“你刚才发版脚本里怎么停服务的?”
运维理直气壮地回了一句截图:kill -9 $PID。
“因为之前老是停不掉,我就直接 -9 强杀了,快一点。”
我当时的血压就上来了。就是这一脚油门“快一点”,导致了 Java 的 ShutdownHook(停机钩子)根本没机会执行,内存里积压的 5000 多条异步日志和监控数据,还没来得及刷盘,就直接陪葬了。
今天必须给所有运维兄弟(和写烂代码的开发)上一课:kill -9 是核武器,不是让你用来杀鸡的!
科普:Java 的“临终遗言”——ShutdownHook
在 Java 应用中,我们通常会注册一个 ShutdownHook(停机钩子)。这是 JVM 提供的一种“临终关怀”机制。
当应用正常关闭(kill -15、System.exit())时,JVM 会在退出前启动一个线程,专门去执行这个 Hook 里的代码。
我们通常在这里做什么?
- 注销服务:告诉 Nacos/Eureka “我要下线了”,别再给我发请求了(防止流量打到死节点)。
- 释放资源:关闭数据库连接池、释放 Redis 分布式锁。
- 刷盘数据:把内存里积压的 Log4j/Logback 日志强制写入硬盘;把 Prometheus 的监控数据最后推一次。
代码长这样:
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("⚠️ 应用正在停止,开始处理善后工作...");
// 1. 强制刷盘日志
// 2. 释放分布式锁
// 3. 优雅关闭线程池
System.out.println("✅ 善后完成,安心上路。");
}));
凶手:SIGKILL (9) vs SIGTERM (15)
在 Linux 中,kill 命令发的不是“杀气”,而是信号(Signal)。
1. kill -15 (SIGTERM):一种礼貌的劝退
这是默认的 kill 方式。它就像是老板走到你工位旁说:“兄弟,我们要裁员了,你把手头工作交接一下,把电脑文件删一删,然后走人吧。”
- JVM 的反应:收到信号,触发 ShutdownHook,执行清理逻辑,然后退出。这是优雅停机。
2. kill -9 (SIGKILL):一颗无情的子弹
这是内核级别的强制查杀。它就像是特种部队直接破门而入,一枪爆头。
- JVM 的反应:JVM 进程瞬间在操作系统层面被抹除。CPU 立即停止执行该进程指令,内存立即回收。
- 后果:ShutdownHook?根本没机会执行!finally 块?也不会执行!
- 内存里的日志 -> 丢了。
- 没提交的事务 -> 可能导致脏数据(虽然 DB 有回滚,但应用层状态可能不一致)。
- Nacos 里的服务 -> 变成“僵尸节点”,消费者还在不断请求,导致大量 502 报错。
灾难现场还原
回到开头那个事故。我们的日志框架为了性能,开启了 AsyncAppender(异步日志)。
- 业务线程把日志写到内存队列(Buffer)里,就直接返回了。
- 后台有一个 Worker 线程慢慢把 Buffer 里的日志写到磁盘文件。
- 运维执行
kill -9。 - JVM 瞬间暴毙。
- 内存 Buffer 里还没来得及写盘的那几千条关键日志,直接灰飞烟灭。
这就是为什么我们查不到任何报错的原因——报错信息本身也被“杀”掉了!
怎么解决?(开发和运维都要看)
不要一味指责运维,有时候开发的代码也确实烂。
给运维的建议:请手下留情
修改你的发布脚本,采用“先礼后兵”的策略。
不要上来就 -9,先给 -15,给应用几秒钟喘息和料理后事的时间。如果它真的赖着不走(死锁或资源泄露),再用 -9 送它一程。
推荐的 Shell 脚本逻辑:
PID=$(get_pid)
if [ -n "$PID" ]; then
echo "Sending SIGTERM to $PID..."
kill -15 $PID # 先礼貌劝退
# 等待 10 秒
for i in {1..10}; do
if ! check_pid_exists $PID; then
echo "Process stopped gracefully."
break
fi
sleep 1
done
# 如果还在,再强制击杀
if check_pid_exists $PID; then
echo "Process still alive, sending SIGKILL..."
kill -9 $PID
fi
fi
给开发的建议:别写“赖着不走”的代码
为什么运维喜欢用 -9?因为你们写的代码经常停不掉!
- ShutdownHook 不要执行太久:别在这里面做重 IO 操作,超时了运维照样杀。
- 线程池记得 shutdownNow:非守护线程(User Thread)如果不结束,JVM 是不会退出的。这也是导致
kill -15无效的主要原因。 - 不要过度依赖 Hook:对于资金级的数据,不能只靠内存缓存+Hook刷盘。关键数据必须同步落库,或者使用 WAL(预写日志)机制。
结语
kill -9 是最后的手段,不是常规操作。
一次优雅的停机(Graceful Shutdown),是微服务高可用的基石。
下次再看到谁上来就 kill -9,请把这篇文章甩给他,并告诉他:
“你杀掉的不仅是进程,还有可能是年底的年终奖。”
博主留言:
你们公司的发布脚本是 -9 还是 -15?有没有遇到过因为强杀导致的数据诡异丢失?欢迎在评论区吐槽!

被折叠的 条评论
为什么被折叠?



