很多技术故障,困住我们的从来不是技术难度,而是「思维定式」。
十几年前的互联网,没有K8s、没有云原生、没有全链路监控。
所有服务跑在物理机、虚拟机上,架构简单、工具原始,但也正因如此,真正底层、隐蔽、颠覆认知的玄学故障,最容易被所有人误诊。
我刚入职的一家公司,就撞上了一个折磨团队整整一个月有余的顶级疑难线上Bug。
放在今天来看,依然是教科书级别的复盘:现象100%像Java死锁,全员专家定性错误,最后根因颠覆所有人认知——根本不是代码问题。
当年线上有几百个Java应用实例,出现了一个极其诡异的故障:
每周,随机一批应用突然原地Hang死。
没有崩溃、没有退出、没有重启、无报错日志、无OOM、无GC抖动。
表现极其统一:
进程活着、端口正常监听
完全不打印任何新日志
无法接收任何新请求,接口全部超时
服务彻底僵死,一动不动
运维同学排查了一个月,束手无策。
每次故障只能暴力重启恢复,唯一能做的止损操作:保留卡死现场、隔离实例,等待排查。
当时团队所有资深开发、架构师统一给出结论:这是Logback日志锁导致的线程死锁。
甚至给出了解决方案:把同步日志改成异步日志,彻底解决卡死问题。
所有人都深信不疑。
直到我接手现场,拉取线程快照,彻底推翻了整个团队的结论。
我登陆隔离的故障机器,没有看日志、没有看监控。
在那个没有Arthas、没有可视化监控的年代,排查Hang死唯一的王牌:jstack 线程快照。
执行经典命令:
jstack [pid] > thread_dump.log
打开线程文件,真相一目了然,画面极其诡异:
Tomcat 200个业务工作线程:
199个线程:BLOCKED 阻塞状态
仅1个线程:RUNNABLE 运行状态
所有阻塞线程,全部在等待同一把锁——Logback日志输出锁。
唯一那处在 RUNNABLE 的线程,正在执行Logback写文件逻辑,持有日志锁。
所有人看到这个堆栈,第一反应都是:妥妥的死锁!
但我多做了一步关键操作——间隔几十分钟,多次打印线程栈。
结果惊人:
每一次线程栈,状态完全一模一样,没有任何变化。
持有锁的RUNNABLE线程,几十分钟没有任何执行进度、没有释放锁、没有报错、没有退出。
这里我瞬间意识到:这绝对不是死锁!
这里纠正一个绝大多数开发者的致命误区(也是当年团队集体踩坑的根源):
真正的 Java 死锁,一定是「互相持有对方锁、循环等待」。
而本次场景:
只有一把锁、一个线程持有、其他线程排队等待。
这在Java层面,根本不构成死锁!
正常业务逻辑中:
写日志拿到锁 → 执行文件写入 → 瞬间完成 → 释放锁 → 其他线程竞争执行。
哪怕磁盘慢、IO高,最多毫秒/秒级结束,绝无可能卡住几十分钟不返回。
更诡异的是:
Java线程状态是 RUNNABLE,JVM认为它正在正常执行,没有阻塞、没有等待、没有异常。
一个本该毫秒级完成的普通文件写入,彻底僵死、永不返回。
这已经超出了Java代码的问题范畴。
我陷入了短暂的困惑:
写文件而已,能有什么问题?
磁盘满了会抛异常、磁盘卡顿会超时、文件权限问题会报错。
什么情况下,一次普通文件写入会永久卡死、无报错、无退出?
我一直默认:程序写日志,写的是本地Linux文件系统。
只要是本地磁盘IO,绝对不可能出现这种玄学卡死。
就在我百思不得其解时,和运维同学的一句闲聊,直接点亮全局!
运维随口说:我们业务日志目录,挂载的不是本地盘,是 GlusterFS 分布式文件系统。
那一刻,我瞬间通透,所有诡异现象全部对上了!
十年前的 GlusterFS 还算热门新潮存储,很多公司批量落地。
我立刻检索官方 Issue、Release Note,精准命中高危已知Bug:
特定集群运维操作后,GlusterFS 分布式inode锁会永久卡死,FUSE客户端IO无限阻塞,上层应用进程彻底Hang死。
我立刻和运维核对操作记录:
一个多月前,集群确实执行过一次节点替换、Rebalance数据迁移操作。
正是这次操作,埋下了隐患!
Bug原理极其清晰:
GlusterFS 执行 Rebalance / Replace-brick 运维操作
集群内网轻微抖动、报文丢失,导致分布式文件锁未正常释放
锁处于「僵死持有状态」,无超时、无自动释放机制
Java写日志需要抢占文件锁,底层FUSE IO 永久阻塞在内核态
JVM 感知不到底层存储异常,线程持续 RUNNABLE 假死
日志锁无法释放,所有业务线程排队阻塞,应用彻底僵死
这就是为什么:看起来是Java卡死,实际上是分布式存储底层Bug。
这个问题困扰团队一个月,所有资深开发、架构师全部翻车,核心原因只有一个:
基础知识不扎实,思维高度固化。
1. 看到「多线程等同一把锁」,就无脑定性死锁,不懂区分Java死锁 vs 底层IO假死
2. 默认日志一定写本地盘,完全忽略分布式挂载场景
3. 不懂 Linux 内核 D 状态、FUSE 同步阻塞机制,以为JVM线程状态就是全部真相
4. 妄图用「改异步日志」解决底层存储卡死问题,治标不治本,完全无效
事后复盘:就算改成异步日志,只要底层GlusterFS IO僵死,异步线程一样会卡死,故障依旧存在。
根因明确后,运维快速落地根治方案,彻底解决周期性Hang死问题:
临时恢复:重新挂载Gluster目录、清理僵死文件锁,恢复业务
运维规范:Rebalance、节点替换等高风险操作,避开业务高峰,必要时停业务执行
架构优化:业务日志、临时高频IO,剥离分布式存储,落地本地物理盘
长期规避:老旧GlusterFS集群逐步迭代,规避已知锁死高危Bug
这场新人入职首战的经典故障,让我受用至今:
1、现象一致,不代表根因一致。
看似线程死锁,未必是代码问题。上层应用只是「背锅侠」,底层中间件、存储、网络,才是真正元凶。
2、JVM 线程状态,会欺骗开发者。
RUNNABLE 不代表真的在跑,大概率是卡死在 Native 内核 IO。
3、周期性玄学故障,90%是底层运维、底层组件Bug,不是业务Bug。
4、技术最大的坑,是经验主义固化。
基础概念模糊,就会把所有相似现象,强行套自己熟悉的结论,越修越错。
真正的高级排查能力,不是会用工具,而是敢于推翻经验、看透表象、直击底层。
你线上遇到过哪些「全员误诊」的玄学故障?评论区聊聊你的踩坑经历。
本文复盘干货建议收藏,下次遇到Java应用Hang死,直接对照排查!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。