记一次生产httpclient导致的tomcat假死事件

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/baomw/article/details/84070428

写在前面:现在负责的项目组中,有一个客户内部的现场管理系统,由于系统年份较久,之前一直部署在ibm小型机上,九月份进行了一次大规模的系统迁移工作。迁移具体实施这里不做细述。迁移完成之后系统改成在X86资源池上使用tomcat集群部署,使用F5做负载。
在系统运行一周以后,会经常出现域名无法访问,外围系统单点无法进入系统的现象。考虑可能是其中一台机器挂死,单独通过ip+port访问两台应用,发现其中一台访问无法接入。但是域名也访问不了,说明f5负载还是连接到了这台挂死的机器。
ps -ef|grep tomcat查看系统进程,进程正常
查看系统日志,未发现有报错日志,只是从某一个时间点之后,Catalina.out日志已不再记录

分析:结合现象分析可能是资源未释放导致应用程序线程挂死。线程池被占满导致的所有的http请求无法接入

1,登录挂死应用部署机器执行:netstat -n | awk ‘/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}’ 查看是否存在阻塞链接
在这里插入图片描述

未见异常。
关于命令相关状态描述如下:
CLOSED:无连接是活动的或正在进行
LISTEN:服务器在等待进入呼叫
SYN_RECV:一个连接请求已经到达,等待确认
SYN_SENT:应用已经开始,打开一个连接
ESTABLISHED:正常数据传输状态
FIN_WAIT1:应用说它已经完成
FIN_WAIT2:另一边已同意释放
ITMED_WAIT:等待所有分组死掉
CLOSING:两边同时尝试关闭
TIME_WAIT表示处理完毕,等待超时结束的请求数。
LAST_ACK:等待所有分组死掉

2,采用jstack分析线程堆栈信息
关于jstack使用,比较简单:
1),在操作系统配置相关jdk环境变量即可
修改/etc/profile,增加如下配置即可:

export JAVA_HOME=/app/jdk1.8.0_161
export PATH=$JAVA_HOME/bin:$PATH 
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar

输入java -version,能正确获取jdk版本信息即可
在这里插入图片描述
2)具体jstack分析步骤如下(由于没有生产故障时的截图,一下截图均为测试环境正常时的截图,不影响实际操作效果)
a)ps -ef|grep java 找到应用java进程号:pid 在这里插入图片描述
b)ps -mp -o THREAD,tid,time | sort -k2r 看进程cpu占用比例的排序 在这里插入图片描述
我们可以找cup占用率较高的线程,找到线程号:tid
c)printf “%x \n” 将cup占有率较高的进程的线程号转换成16进制
在这里插入图片描述
d)jstack -l <进程号> | grep <16进制的线程号> -A 10 查看线程占用情况(如下为故障时生线程泄漏日志)
在这里插入图片描述

由此分析可以得知存在线程池溢出,线程挂死。但是该日志未得出具体线程挂死应用,这时想起了另一个jdk监控工具:jconsole,但是生产系统实施未配置jconsole,导致不能用这个进行监控,具体配置如下(其他用法这里不做缀述):

JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=9999"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.authenticate=true"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.ssl=false"

3,由于无法获取更多信息,只能从最近上线内容和代码层考虑,联想到上线前手底下一开发写了个httpclient进行单点登录解析用户名的实现。猜想可能是代码不规范导致http连接未释放导致线程池被沾满,连接无法登入
检查代码后发现,未有释放连接的release操作,这里不规范代码不截图出来了,将HttpClient正确的写法整理出来如下:

 //连接池对象
	PoolingClientConnectionManager poolingClientConnectionManager = new PoolingClientConnectionManager();
	//设置最大连接数
	poolingClientConnectionManager.setMaxTotal(200);
	// 创建httpclient对象
	HttpClient client = new DefaultHttpClient(poolingClientConnectionManager);
	// 设置超时时间
	client.getParams().setIntParameter("http.socket.timeout", 30000);
	// 创建post方式请求对象
	HttpPost httpPost = new HttpPost(url);
	// 设置header信息
	httpPost.setHeader("Content-Type", "application/json;charset=UTF-8");
	// 执行请求操作,获取响应消息
	HttpResponse response  = null;
	try {
		//在http1.1中,client和server都是默认对方支持长链接的, 如果client使用http1.1协议,但又不希望使用长链接,
		//则需要在header中指明connection的值为close;如果server方也不想支持长链接,
		//则在response中也需要明确说明connection的值为close.
		httpPost.setHeader(HttpHeaders.CONNECTION, "close");
		
		response = client.execute(httpPost);
		<处理其他业务操作>
		} catch (Exception e) {
		// 记录日志
		resultFlag = "ERROR";
		e.printStackTrace();
	} finally {
		 if(response != null) {
	         EntityUtils.consumeQuietly(response.getEntity());
	     }
	     if(httpPost != null) {
	    	 httpPost.releaseConnection();
	     }
	   <记录日志等其他业务操作>
	}

此处代码关键点有二
a)httpPost.setHeader(HttpHeaders.CONNECTION, “close”); 设置不使用长连接
b) if(response != null) {
EntityUtils.consumeQuietly(response.getEntity());
}
if(httpPost != null) {
httpPost.releaseConnection();
}
连接使用完之后,及时httpPost.releaseConnection();

及时将该处代码改进后部署生产,后来运行至今未出现线程挂死现象,问题正常解决!

以上为我实际工作过程中遇到的一个小问题,按上述方法得到了正确的解决,希望对大家以后的工作中有一点帮助。同时希望各位大神要是有更好的解决方法可以联系告知一下。谢谢

写的不合理处,望各位大神指正!

猜你喜欢

转载自blog.csdn.net/baomw/article/details/84070428