解决HttpClient连接未释放导致的新请求失败的问题

问题描述

实现了一个发送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

file

可以看到,线程处于waiting(parking)状态:
“- locked <0x0000000747534770> (a org.apache.http.pool.AbstractConnPool$2)”
说明线程被locked。然后根据:
“at org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking(AbstractConnPool.java:380)”
找到AbstractConnPool的第380行:

file

第三步:分析AbstractConnPool
第380行的代码是: this.condition.await();

file

第四步:debug
通过连接远程服务器进行debug,发现发起的请求的确走到AbstractConnPool的第380行就停止了

第五步:继续分析AbstractConnPool
既然线程由于condition对象调用await()方法而进入等待
那么就需要condition调用signal()或者signalAll()方法唤醒线程
如下图,发现有两处调用signalAll()的地方
file
第一个是在lease()方法中:

另外一个是在release方法中:
file

第六步:分析自定义的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状态:

file

发布者:CoolQA,转转请注明出处:https://www.amwalle.com/more/working/20210401-%e8%a7%a3%e5%86%b3httpclient%e8%bf%9e%e6%8e%a5%e6%9c%aa%e9%87%8a%e6%94%be%e5%af%bc%e8%87%b4%e7%9a%84%e6%96%b0%e8%af%b7%e6%b1%82%e5%a4%b1%e8%b4%a5%e7%9a%84%e9%97%ae%e9%a2%98.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
CoolQA的头像CoolQA
上一篇 2021年3月14日 15:20
下一篇 2021年4月26日 08:55

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理