来源关注Java爱好者社区 ,
作者东升的思考
.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);
} catch(final InterruptedException interrupted) {
}
这里的 timeout,即 connectionRequestTimeout,正是计算 deadline 时间的 timeout 值。印证了我们的猜测。
初始化 HttpClient 工具的初始配置参数,并没有配置 connectionRequestTimeout 这个参数的,该参数也是很关键的,如果没有设置,并且被 park 挂起的线程一直没有被 signal 唤醒,那么会一直等待下去。
所以,必须得设置这个参数。这里的 deadline 是个时间,不为空时,会调用 condition 的 awaitUtil(deadline) 方法,即使没有被 signal 唤醒,也会自动唤醒,去争抢锁,而不会导致未被唤醒就一直阻塞下去。
而且这个 awaitUtil(deadline) 方法跟 awaitNanos(long nanosTimeout) 方法里的 deadline 变量设计上异曲同工。
达到了设定的超时时间,并且没有 signal 过,终 success 变量为 false 不成功,直接 break 跳出循环,终会抛出 TimeoutException("Timeout waiting for connection") 异常。
抛出这个异常,系统错误日志中,也就明确了是因为无法获得连接导致的。同时,也避免了一直占用着线程。
5
再从堆栈中找到"罪魁祸首"
上一节,从段堆栈日志分析到了 Condition 并发底层源码细节。但是这还没完,因为我们统计 java.lang.Thread.State 中,仅分析完了WAITING (parking) 状态,问题原因也不一定是这个状态导致的。接下来继续分析另外的「异常」线程状态 WAITING (on object monitor) 。
在 java 堆栈中 第二段关键的日志 如下:
at java.net.InetAddress.checkLookupTable(InetAddress.java:1393) ``
这段代码调用引起,还是要去看下源码:
找到了是 lookupTable 对象,使用了同步块锁 synchronized,内部调用了 lookupTable 对象的 wait() 方法,就是在这里等不到通知,一直阻塞着。
这个问题代码排查一通,你是看不出什么问题来的,因为跟应用程序本身关系不大了,是因为 IPV6 导致的 JVM 线程死锁问题。
参考国外 zimbra 站点 wiki:https://wiki.zimbra.com/wiki/Configuring_for_IPv4
这里解释下问题产生的原因:
应用本身在 IPv4 环境下,如果尝试使用了 IPv6 会导致一些已知问题。
当调用了 Inet6AddressImpl.lookupAllHostAddr() 方法,因为 Java 与操作系统 libc 库之间存在一个bug,当特定的竞态条件发生时,将会导致查找 host 地址动作一直无限循环下去。这种情况发生的频率很低,但是一旦发生将会导致 JVM 死锁问题,进而导致 JVM 中所有线程会被阻塞住。
根据上述分析,在 jstack 堆栈中找到了 第三段关键的堆栈日志 如下:
如何判断操作系统是否启用了 IPv6 ?
介绍两种方式:
1)ifconfig
这个很明显就能看得出来,有 inet6 addr 字样说明启用了 IPv6。
2)lsmod
[root@BJ]# lsmod | grep ipv6
Module Size Used by
ipv6 335951 73 bridge
主要看 Used 这一列,数值 70+,不支持 IPv6 环境 Used 列是 1(不同服务器环境该值可能不一样)。
6
问题优化方案总结
经过对 java 堆栈中关键线程状态的分析,明确了问题原因,接下来说下问题解决方案。
个问题:
针对从 Http 连接池中获取不到连接时,可能使线程进入阻塞状态。
在 HttpClient 客户端初始化参数配置中增加 connectionRequestTimeout ,获取连接的超时时间,一般不建议过大,我们设置为 500ms。
设置后,就会调用底层的 condition#awaitUtil(deadline) 方法,当线程无法被 signal 唤醒,到达了 deadline 时间后,线程会自动从等待队列中被唤醒,加入到 AQS 同步队列争抢锁。
第二个问题:
针对 IPv6 导致的 JVM 进程死锁问题,有两种解决方案:
1)操作系统层面禁用 IPv6
编辑 /etc/sysctl.conf 文件,添加下面两行:
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
保存,执行 sysctl -p 使其生效。
运行操作系统中执行如下命令直接生效:
sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1
2)Java 应用程序层面
在应用 JVM 启动参数上添加 -Djava.net.preferIPv4Stack=true 。
从操作系统层面禁用 IPv6,如果服务器上还部署了其他应用,注意观察下,如果遇到一些问题可以借助搜索引擎查下。
我们有很多台服务器,都是运维来维护的,所以我采用了第二种方式,直接在 JVM 上增加参数,简单方便。
后的总结:
java 堆栈日志中两个关键的 WAITING 线程状态,先出现了 WAITING (on object monitor),因 IPv6 问题触发了 HttpClient 线程池所有线程阻塞。后出现了 WAITING (parking) ,Tomcat 线程接收转发请求,当请求调用到 HttpClient,因无法获得 Http 连接资源,且未设置获取连接的超时时间,造成了大量线程阻塞。
经过对上述两个问题的优化后,上线观察很长一段时间,也经历过比这次问题出现时更高的访问量,再没有出现过 JVM 线程阻塞问题。通过网络命令行统计,基本不会出现大量的 CLOSE_WAIT 网络连接状态。