解决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

在这里插入图片描述
可以看到,线程处于waiting(parking)状态:
“- locked <0x0000000747534770> (a org.apache.http.pool.AbstractConnPool$2)”
说明线程被locked。然后根据:
“at org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking(AbstractConnPool.java:380)”
找到AbstractConnPool的第380行:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210401203537480.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjUzNDk0MA==,size_16,color_FFFFFF,t_70

第三步:分析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状态:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_42534940/article/details/115362062