关于JAVA HttpClient请求夯住问题排查

一、问题表现
系统自9月底正式运行,运行至今近半年时间,于2018年1月15日第一次出现HttpClient请求夯住现象,造成整个系统访问非常慢,甚至不可访问,如下为某一时刻截图:


在这也解释一下何为http请求夯住,夯住(Hang)是指程序仍在运行,卡在某个方法调用上,没有返回也没有异常抛出;卡住时间从几秒到几小时不等。

另外系统使用Tomcat中间件,对于Tomcat来说,每一个进来的请求(request)都需要一个线程,直到该请求结束。如果同时进来的请求多于当前可用的请求处理线程数,额外的线程就会被创建,直到到达配置的最大线程数(maxThreads属性值)。如果仍就同时接收到更多请求,这些来不及处理的请求就会在Connector创建的ServerSocket中堆积起来,直到到达最大的配置值(acceptCount属性值)。至此,任何再来的请求将会收到connection refused错误,直到有可用的资源来处理它们。

这里我们使用的Tomcat并没有做调优,所以maxThreads默认值为200,acceptCount默认值为100。(为什么不做优化?因为默认的配置足够应对生产环境的情况)

二、问题分析

造成在生产环境上系统缓慢,甚至不能使用是非常着急的,第一反应最短的时间恢复用户的正常使用,最简单粗暴的方式——重启应用。事实上这种简单粗暴的方式并没有解决问题,在系统重启短短十几秒内http请求的线程瞬间暴增,系统依然缓慢。

接下来以我当时几点思考,来阐述分析过程。面对如此多的request请求并且没有一个回收释放肯定是不正常的。

思考点1:这些请求都是从哪里来的?

(1) 面对瞬间生成的100多个http请求线程,是不是受到外部攻击(因为这台服务器早些时候受到过攻击)

(2) 与数字城管系统有交互的其它系统和内部HttpClient请求,是否出现多并发请求或者有轮询操作请求我们,其它系统主要有市级数字城管系统、前置交换系统、城管通服务端系统,其中市级数字城管系统是第三方排查不便,另外两个是我们自己维护方便排查。

思考点2:这些请求状态都是什么?

Java程序发生夯时,应该首先使用 jstack 把java进程的堆栈信息保存下来 ,供后继分析使用,jstack -l > js.txt 可以把pid的堆栈信息保存到文件js.txt中,如下图某时刻的堆栈信息:

发现线程得状态都是TIMED_WAITING(线程得各种状态在此不详细说,网上很多),Java文档官方定义TIMED_WAITING状态为:“一个线程在一个特定的等待时间内等待另一个线程完成一个动作会在这个状态”。
真实生活例子:尽管充满戏剧性,你在面试中做的非常好,惊艳了所有人并获得了高薪工作。(祝贺你!)你回家告诉你的邻居你的新工作并表达你激动的心情。你的朋友告诉你他也在同一个办公楼里工作。他建议你坐他的车去上班。你想这不错。所以第一天,你走到他的房子。在他的房子前停好你的车。你等了10分钟,但你的邻居没有出现。你继续开自己的车去上班,这样你不会在第一天就迟到。这就是TIMED_WAITING。

三、 进一步分析
接下来怎么办呢?
(1) 市级数字城管系统有多并发或者轮询操作?由于第三方系统,无法具体断言,而且运行一段时间以来,也未出现,基本上可以排除,也就只能调侃一下说是他们的原因。
(2) 受外部网络攻击,不断有请求进来?网络技术并不是我们的强项,往这方面走明显得不到根本解决,也可以借助一些杀毒软件查看网络请求来源。
(3) 接下来也就剩下内部的HttpClient请求和前置交换系统、城管通服务端系统,会发送请求进来,由于这些都是我们自己维护,排查起来自由方便。

由于代码也有些年头,前置交换系统与城管通系统与数字城管系统交换使用Apache HttpClient,HttpClient使用3.1版本,也并未使用HttpClient连接池,以及未关闭无效的连接。大部分人使用HttpClient都是使用类似下面的事例代码,包括Apache官方的例子也是类似如此。在性能测试过程中,使用HttpClient一次循环发起大量请求到服务器会使TCP连接被大量占用。


于是,我将HttpClient升级为4.5版本,并使用PoolingClientConnectionManager连接池管理器,HttpClient可以通过多个执行线程同时执行多个请求。PoolingClientConnectionManager将会根据其配置分配连接,如果某个路由的所有连接都已经被分配出去了,新进来的请求将会阻塞直到某个连接被释放回连接池,你可以通过配置http.conn-manager.timeout 这个参数来配置新的请求进来时阻塞的超时时间,从而避免无限期等待,如果在给定时间内连接没有被获取到,那么将会抛出ConnectionPoolTimeoutException异常。



将所有老代码的HttpClient请求升级换成PoolingClientConnectionManager连接池来管理时,初步感觉已经解决了HttpClient夯的问题,一波三折系统重启之后,问题依旧存在。这时候已经很抓狂!!!
这个时候问题没有得到根本解决是很容易否定之前所做的工作,往往回归到问题本身又是一个突破口。本着这个理念花了点时间理解了HttpClient连接回收策略。
HttpClient连接回收策略
经典阻塞I/O模型的一个主要缺点就是网络socket只有在I/O操作阻塞的情况下才会对I/O事件作出反应。当连接释放回管理器时,它虽然能够保持存活,但是它无法监控socket的状态也无法对任何I/O事件作出反应。如果连接在Server端被关闭,client端连接无法侦测到连接状态的改变并且作出适当的回应。
HttpClient尝试通过测试连接是否'stale'来缓解这个问题,stale的意思是连接不再有效因为在执行Http请求之前其已经被服务端关闭。过期连接检查不是100%可靠的,唯一可行解决方案是提供一个专用监控线程用于回收那些长时间内不活动的连接,而该解决方案不会影响到一个socket一个线程的空闲连接模型。监控线程可以定期调用 ClientConnectionManager#closeExpiredConnections()方法来关闭所有过期的连接,同时从连接池中回收已经被关闭的连接,也可以有选择性的调用 ClientConnectionManager#closeIdleConnection()方法去关闭那些在给定时间范围内空闲的连接。

通过增加一个独立线程专门回收不活动的连接,如下示例代码:



至此,系统HttpClient夯住问题基本解决。

四、 总结
a) 理解一下HttpClient这样设计的理由: socket重用,keepAlive协议的支持等,保证上一次数据不会对新的请求有影响。
b) Thread.interrpt()处理,只会在Thread处于sleep或者wait状态才会被唤醒(api的描述)。而且该方法的调用并不自动产生InterruptedException异常,一般是需要自己判断Thread.isInterrupted(),然后throw异常。 我们目前使用的一些jdk cocurrent类比如future.cancel也是类似处理。
c) OkHttp也许逐步替代Apache的HttpClient,未来可以尝试使用。
优点
支持SPDY, 可以合并多个到同一个主机的请,使用连接池技术减少请求的延迟(如果SPDY是可用的话) ,
使用GZIP压缩减少传输的数据量,缓存响应避免重复的网络请求、拦截器等等。
缺点
第一缺点是消息回来需要切到主线程,主线程要自己去写,第二传入调用比较复杂。

你可能感兴趣的:(关于JAVA HttpClient请求夯住问题排查)