死锁,通常都是别人的故事,从未想过会发生在自己身上。
今天不光遇见了死锁,还第一次享受了JVM界顶级大佬R大(RednaxelaFX, 知乎问题:R大是谁 ) 的手把手指导,特别有意义,所以给各位看官一记。
感受之一: R大乐于助人的程度,中美互联网加起来也没有几个能比得上,应该每月收无数的好人卡。
感受之二: R大的脑容量和打字速度都是别人的七八倍,所以有人直接认为R大其实是代码成精变的。
感受之三: 在基础架构部这种地方,瞎蒙的运气,强大的观察能力,全面专业的技能,都那么重要。
今天把JMeter里的Netty也换成Netty Native模式,然后压测就停在那不动了。。。。。。
第一时间jstack,得到如下输出,看到Netty在加载Native库时, Object.wait() [0x00007fb274057000]被锁住了。但因为已经进入Native部分,所以线程还是显示Runnable。 同时,有一个在JMX里注册Metrics的线程成为了受害者,Netty的线程锁住了它所需要的”java.lang.Runtime<0x00000000ecee76e0>"
那第一时间,就想看看这个Object.wait() [0x00007fb274057000]是啥。
同事建议用jstack -m pid再看看,结果那条线程的输出如下,还是没看出来所以然。
此时就只好群里找R大求救。。。。R大在大洋彼岸晚饭中,让我用Sun的JDK自带的CLHSDB看看。
于是一边搜索到R大的文章借HSDB来探索HotSpot VM的运行时数据, http://rednaxelafx.iteye.com/blog/1847971 ,一边 生成CoreDump (先ulimit -c unlimited,再运行jmeter,再kill -7 pid, 就会在程序目录里有一个core.xxx文件)。
可惜真的不会用CLHSDB,whatis 0x00007fb274057000 说无效地址。
于是决定蒙一下,虽然JMX那条线程在jstack看起来只是受害者,但还是尝试把它禁止掉。翻代码翻到一个启动参数,感谢那个留了后门的同事,加上去一试,不锁了。。。。
赶紧把这个结论告诉负责Metrics模块的同事,然后,他很快就找到了原因,仔细一看,注册JMX那线程的栈里有一句
at sun.nio.ch.FileChannelImpl.(FileChannelImpl.java:1171)
然后, Netty的epoll代码 里也有一句,应该就是这两句之间锁了。
jclass fileChannelCls = (*env)->FindClass(env, "sun/nio/ch/FileChannelImpl")
问同事怎么这么快定位到FileChannelImpl头上的,同事笑而不语。
把结论告诉R大,此时R大已经有空了,就开始手把手教学--如何用gdb来找到这个问题,避免下次还要靠瞎蒙与肉眼观察来解决。
gdb [path of Java] core.xxx
因为要用一模一样的executable(java),所以core.xxx拿回家玩没用,必须就地打开。
jstack里的线程号是16进制的,先转回10进制,再对应回gdb里的线程号,这里Netty的线程号是17, jmx是56。
(gdb) info threads 56 Thread 0x7fac9c851700 (LWP 35389) 0x00007fad2932b98e in pthread_cond_timedwait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 (JMX) 17 Thread 0x7fac9ce2c700 (LWP 35366) 0x00007fad2932b5bc in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 (Netty)
(gdb) thread 17 [Switching to thread 17 (Thread 0x7fac9ce2c700 (LWP 35366))]#0 0x00007fad2932b5bc in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
jstack -m 里看到的在这里展开了。
(gdb) frame 7 #7 0x00007fac9c404b23 in JNI_OnLoad () from /apps/svr/jmeter/native/libnetty-transport-native-epoll3448157366930317836.so
厉害的R大这里继续让我输入
(gdb) p (char*) 0x7fac9c4057ac $2 = 0x7fac9c4057ac "sun/nio/ch/FileChannelImpl"
证明的确是"sun/nio/ch/FileChannelImpl”这个类锁了。
一头雾水的问,怎么知道要打印0x7fac9c4057ac??
R大解析,有个箭头的地方表明指令执行到这里,方法调用前是传参的语句,”0xc8f(%rip),%rsi 这句“把参数推进了rsi寄存器,所以# 0x7fac9c4057ac的注释就是char*的地址,遂打印之。
收获满满,下次遇到Natvie里的ClassLoader死锁问题也能GDB之了,少年时用过那么一下gdb,多少年没碰过它了。
原来的程序是Netty异步建立连接,然后注册一个线程池Metrics的MBean,此时Netty因为要加载Native Lib,hold住了java.runtime,然后在JNI代码里尝试去加载FileChannelImpl。同时,JMX注册的进程先加载了FileChannelImpl,然后到了某一步又尝试去获得java.runtime,传说中的死锁就这样发生了。
改进的方法很简单,尽早把JMX的MBean Server简单加载一下就好。
再次感谢R大和我的同事国忠。