问题描述
实现了一个发送HTTP请求,然后获取其中的JSON数据的方法,如下所示。但是发现发送了一些请求成功返回之后,再发起请求,一直没有数据返回,也没有任何报错日志。
public class HttpUtil {
private final static Logger logger = LoggerFactory.getLogger(HttpUtil.class);
private static CloseableHttpClient httpClient;
static {
PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
connManager.setMaxTotal(200);
connManager.setDefaultMaxPerRoute(40);
httpClient = HttpClientBuilder.create().setConnectionManager(connManager).build();
}
public static ResultBase<JSON> doGetJSON(String url) {
ResultBase<JSON> resultBase = new ResultBase<>();
if (StringUtils.isEmpty(url)) {
logger.error("doGet with a null url");
return resultBase.setErrorMsgReturn("doGet with a null url");
}
HttpGet httpGet = new HttpGet(url);
HttpResponse response;
try {
response = httpClient.execute(httpGet);
Header contentType = response.getFirstHeader("Content-Type");
if (contentType != null && contentType.getValue().contains("application/json")) {
HttpEntity httpEntity = response.getEntity();
InputStream inputStream = httpEntity.getContent();
resultBase.setRightValueReturn(JSON.parseObject(inputStream, StandardCharsets.UTF_8, JSON.class));
return resultBase;
}
} catch (IOException e) {
logger.error(e.getMessage());
resultBase.setErrorMsgReturn("Do get error! Error message: " + e.getMessage());
return resultBase;
}
return resultBase.setErrorMsgReturn("Http get result is not json!");
}
}
问题分析
从问题现象上看,像是发生了死锁。所以先分析线程dump日志
dump日志分析方法的参考: https://www.cnblogs.com/z-sm/p/6745375.html
第一步:生成dump文件
./jcmd 50557 Thread.print > ~/regulus/logs/dump.log
** 50557 是我的应用的进程ID
第二步:查找HttpClient相关的内容
vim ~/regulus/logs/dump.log
可以看到,线程处于waiting(parking)状态:
“- locked <0x0000000747534770> (a org.apache.http.pool.AbstractConnPool$2)”
说明线程被locked。然后根据:
“at org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking(AbstractConnPool.java:380)”
找到AbstractConnPool的第380行:
第三步:分析AbstractConnPool
第380行的代码是: this.condition.await();
第四步:debug
通过连接远程服务器进行debug,发现发起的请求的确走到AbstractConnPool的第380行就停止了
第五步:继续分析AbstractConnPool
既然线程由于condition对象调用await()方法而进入等待
那么就需要condition调用signal()或者signalAll()方法唤醒线程
如下图,发现有两处调用signalAll()的地方
第一个是在lease()方法中:
另外一个是在release方法中:
第六步:分析自定义的HttpClient发送请求
由上一步,可以推断是由于连接没有关闭导致了release方法未被调用。所以要做的就是关闭http连接。
参考网上的连接关闭方式:https://stackoverflow.com/questions/30889984/whats-the-difference-between-closeablehttpresponse-close-and-httppost-release
然后,通过查看CloseableHttpResponse的实现类HttpResponseProxy的close()方法,然后一直往下执行,可以看到最终会调用到AbstractConnPool.release()方法
解决办法
使用CloseableHttpResponse代替HttpResponse,然后用“try (CloseableHttpResponse response = httpClient.execute(httpGet))”的方式使用连接资源。
public static ResultBase<JSON> doGetJSON(String url) {
ResultBase<JSON> resultBase = new ResultBase<>();
if (StringUtils.isEmpty(url)) {
logger.error("doGet with a null url");
return resultBase.setErrorMsgReturn("doGet with a null url");
}
HttpGet httpGet = new HttpGet(url);
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
Header contentType = response.getFirstHeader("Content-Type");
if (contentType != null && contentType.getValue().contains("application/json")) {
HttpEntity httpEntity = response.getEntity();
InputStream inputStream = httpEntity.getContent();
resultBase.setRightValueReturn(JSON.parseObject(inputStream, StandardCharsets.UTF_8, JSON.class));
return resultBase;
}
} catch (IOException e) {
logger.error(e.getMessage());
resultBase.setErrorMsgReturn("Do get error! Error message: " + e.getMessage());
return resultBase;
}
return resultBase.setErrorMsgReturn("Http get result is not json!");
}
代码修改完成并提交之后,机器重新部署完,发现请求还是无法正常返回。
通过查看端口链接状态,发现仍然有大量的端口处于CLOSE_WAIT状态(连接未正常关闭)
netstat -anp | grep 50557
……
tcp 0 0 0.0.0.0:60000 0.0.0.0:* LISTEN 50557/java
tcp 0 0 0.0.0.0:60100 0.0.0.0:* LISTEN 50557/java
tcp 0 0 0.0.0.0:7001 0.0.0.0:* LISTEN 50557/java
tcp 0 0 0.0.0.0:7002 0.0.0.0:* LISTEN 50557/java
tcp 1 0 127.0.0.1:7001 127.0.0.1:60230 CLOSE_WAIT 50557/java
tcp 1 0 127.0.0.1:7001 127.0.0.1:42800 CLOSE_WAIT 50557/java
tcp 1 0 127.0.0.1:7001 127.0.0.1:53916 CLOSE_WAIT 50557/java
tcp 1 0 127.0.0.1:7001 127.0.0.1:60622 CLOSE_WAIT 50557/java
tcp 1 0 127.0.0.1:7001 127.0.0.1:38762 CLOSE_WAIT 50557/java
tcp 1 0 127.0.0.1:7001 127.0.0.1:49438 CLOSE_WAIT
……
网上查找资料,也没有好的方法来释放这些端口。只能杀掉占用这些端口的应用进程:
kill -KILL 50557
然后重新部署,生成新的进程id, 再发送请求就正常了,查看端口连接状态,也都正常。
重新生成线程dump记录如下,线程是RUNNABLE状态: