绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
由一次线上故障来理解下TCP三握、四挥; Java堆栈分析到源码探秘
2019-12-06 11:51:35

来源关注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 网络连接状态。

分享好友

分享这个小栈给你的朋友们,一起进步吧。

JAVA玩具小屋
创建时间:2019-08-16 16:54:49
分享程序开发方面的小经验,思考一些比较简单易懂的技术问题
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

栈主、嘉宾

查看更多
  • Yios5092
    栈主

小栈成员

查看更多
  • 栈栈
  • coyan
  • 25minutes
  • ?
戳我,来吐槽~