nodetool netstats 排查死锁的完整过程拆开来聊聊,带你一步步揪出问题根源。

前阵子压测Cassandra时,发现一个节点死活停在STARTING状态转不过去。打开nodetool netstats一看,这哥们一直在原地踏步,怎么都没动静。搞了半天才意识到,内部可能出了死锁。下面就把排查死锁的完整过程拆开来聊聊,带你一步步揪出问题根源。 第一步先检查状态。图上很清楚显示,节点就卡在STARTING这一步了。给进程发个jstack,主线程赫然在WAITING呢,正死死抱着AbstractCommitLogSegmentManager.awaitAvailableSegment()这行代码不放。这就很奇怪了,主线程在等什么资源呢?查源码才知道,原来是在等COMMIT-LOG-ALLOCATOR线程去新建一个segment。 为了找这个线程的位置,我又去翻jstack日志。结果发现这哥们状态是RUNNABLE,但栈信息却一直在某一行(133行)打转。这难道不矛盾吗?明明是Runnable状态,却不干活。用cat命令看了下系统状态,果然发现线程卡在了133行,而且CPU使用率低得可怜。 点开133行代码一看,只是一个静态方法调用:CommitLog.handleCommitError(...)。这里面也没看见明显的锁操作啊,死锁是咋回事?这时候得往上看类初始化的逻辑了。 用pstack命令终于让我看清了真相:线程其实是卡在了JVM的InstanceKlass::initialize处,在等类加载完成。回到133行那条调用链看看,肯定要用到CommitLog类;而主线程此时正好把CommitLog类的锁给占住了。这样一来死锁就成闭环了: 主线程想要生产资源,就得调用CommitLog静态方法; COMMIT-LOG-ALLOCATOR线程想生产资源,也得通过CommitLog.handleCommitError这个方法; 这时候主线程和COMMIT-LOG-ALLOCATOR都在等对方释放锁,谁也动弹不得。 其实RUNNABLE状态并不代表真的在运行。jstack看到的只是Java层面的状态,可能因为内核调度、IO或者锁之类的原因让它看起来在跑其实啥也没干。类初始化死锁通常都是因为静态字段或者初始化块互相引用导致顺序乱套了。Cassandra这种依赖链复杂的系统最容易踩坑。 以后遇到死锁,可以先抓个jstack看现场,再用pstack看看JVM到底在等什么,最后回代码去确认锁粒度和类加载顺序。遇到问题的时候一定要把所有相关的类、静态方法和字段画出来标清楚顺序,避免出现循环依赖的坑。 这次的经历已经同步到了社区的CASSANDRA-15295里,后续版本应该会优化一下这方面的时序。要是想要个稳定的服务可以直接用云原生的方案;如果你对社区版感兴趣的话也欢迎去GitHub仓库里聊聊。