被 kill -9 坑惨了!Java 进程被强制杀掉后,ShutdownHook 没执行导致的“脏数据”惨案


前言:消失的 5000 条日志

“生产环境数据对不上了!数据库里少了一批订单状态流转记录!”

昨晚 11 点,正在排位的我被测试疯狂 Call 醒。这批数据涉及到资金结算,少一条都是 P0 级事故。

我连夜爬起来排查,代码逻辑查了三遍,没毛病;数据库事务查了,也正常。最后去翻应用日志,发现了一个极其诡异的现象:日志在某个时间点突然截断了,没有任何报错,没有任何异常堆栈,就像应用“凭空蒸发”了一样。

我把目光转向了发版记录。果然,那个时间点,运维正在进行灰度发布。

我冲到运维工位(群里)问:“你刚才发版脚本里怎么停服务的?”
运维理直气壮地回了一句截图:kill -9 $PID

“因为之前老是停不掉,我就直接 -9 强杀了,快一点。”

我当时的血压就上来了。就是这一脚油门“快一点”,导致了 Java 的 ShutdownHook(停机钩子)根本没机会执行,内存里积压的 5000 多条异步日志和监控数据,还没来得及刷盘,就直接陪葬了。

今天必须给所有运维兄弟(和写烂代码的开发)上一课:kill -9 是核武器,不是让你用来杀鸡的!


科普:Java 的“临终遗言”——ShutdownHook

在 Java 应用中,我们通常会注册一个 ShutdownHook(停机钩子)。这是 JVM 提供的一种“临终关怀”机制。

当应用正常关闭(kill -15System.exit())时,JVM 会在退出前启动一个线程,专门去执行这个 Hook 里的代码。

我们通常在这里做什么?

  1. 注销服务:告诉 Nacos/Eureka “我要下线了”,别再给我发请求了(防止流量打到死节点)。
  2. 释放资源:关闭数据库连接池、释放 Redis 分布式锁。
  3. 刷盘数据:把内存里积压的 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(异步日志)。

  1. 业务线程把日志写到内存队列(Buffer)里,就直接返回了。
  2. 后台有一个 Worker 线程慢慢把 Buffer 里的日志写到磁盘文件。
  3. 运维执行 kill -9
  4. JVM 瞬间暴毙。
  5. 内存 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?因为你们写的代码经常停不掉

  1. ShutdownHook 不要执行太久:别在这里面做重 IO 操作,超时了运维照样杀。
  2. 线程池记得 shutdownNow:非守护线程(User Thread)如果不结束,JVM 是不会退出的。这也是导致 kill -15 无效的主要原因。
  3. 不要过度依赖 Hook:对于资金级的数据,不能只靠内存缓存+Hook刷盘。关键数据必须同步落库,或者使用 WAL(预写日志)机制。

结语

kill -9 是最后的手段,不是常规操作。

一次优雅的停机(Graceful Shutdown),是微服务高可用的基石。

下次再看到谁上来就 kill -9,请把这篇文章甩给他,并告诉他:
“你杀掉的不仅是进程,还有可能是年底的年终奖。”


博主留言:
你们公司的发布脚本是 -9 还是 -15?有没有遇到过因为强杀导致的数据诡异丢失?欢迎在评论区吐槽!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值