java相关|一次线上http连接被拒绝问题的排查

线上环境连第三方的HTTP服务时报连接被拒绝,单独通过curl命令发现也是返回连接被拒绝,把问题反馈给第三方后得到的答复是他们有设置最大连接数为300,要我方自查代码。
赶紧自查代码后发现确实代码有问题:

public static CloseableHttpClient newInstance(String ip_port, String userName, String password) {。。。。。此处省略无关代码 CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm) .setDefaultRequestConfig(defaultRequestConfig) .setDefaultCredentialsProvider(provider).build(); return httpClient; } }

上述代码在每次调用newInstance时都创建了一个 httpClient ,如果并发高的话会导致创建大量的连接请求,最终导致第三方拒绝请求,问题找到后,赶紧将httClient修改成了单例模式,一顿操作操作猛如虎,发布到SIT环境信心满满进行测试,结果连接数量似乎是控制住了,但是多刷几次页面后又报了Timeout waiting for connection from pool,看到这个错误后马上想到是池里的连接被占用没有释放出来,于是又自查代码如下:
try { 。。。此处省略无关代码 String responseXml = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); 。。。此处省略无关代码 } catch (Exception e) { throw e; }

使用了EntityUtils.toString对返回的body进行处理,追踪进入EntityUtils.toString的源码后,发现其有在finally中关闭流,到此似乎有点懵了,线索断了。只好再次深翻错误日志,发现在抛出Timeout waiting for connection from pool错误之前已经有抛出自定义的异常了,然后根据异常堆栈信息追查到代码处后豁然开朗:抛出异常后不会走上述代码的EntityUtils.toString自然也就没有释放连接了,最终修改如下:
try { 。。。此处省略无关代码 String responseXml = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); 。。。此处省略无关代码 } catch (Exception e) { throw e; }finally { EntityUtils.consumeQuietly(response.getEntity()); }

再次测试OK。
总结一下:
在实际项目中如果没有考虑到对HttpClient或者CloseableHttpClient进行正确的释放连接操作,则会带来生产问题,后面的请求一直无法获取到连接资源,导致并发起不来。很多同学可能并没有遇到过这个问题是因为HttpClient或者CloseableHttpClient并不是单例的,而是每次使用时都创建新的实例,在正常情况下你可能正确地释放了连接,但一定要考虑到异常情况下的连接释放,正确的做法是确保释放连接的操作是在finally块中。
另外对于HttpClient或者CloseableHttpClient的创建还是不要太随意,尽量使用一个全局的(除非你有迫不得已的原因),同时设置好连接池等相关优化配置,这里着重解释下连接池配置的两个重要参数,MaxTotal表示池里最大的连接数量,DefaultMaxPerRoute按字面理解是每个路由的最大连接数(其实就是你对同一个目标服务器请求的最大连接数),此处千万不要被MaxTotal所迷惑,一定要结合DefaultMaxPerRoute参数来判断最大连数是多少。
另外CloseableHttpClient中的execute()有多个重载方法,而实际调用后能释放资源的execute必须是包含ResponseHandler这个属性的,否则和原有HttpClient方式是一样的。在使用EntityUtils.toString()方法时,虽然其底层调用会执行InputStream关闭流,但是这里也可能是坑之所在,当你请求第三方超时或其他异常时,会直接跳过,并没有执行上面的toString()方法,那么就不会释放连接资源。
针对CloseableHttpClient有下述两种释放连接的方案:
1、确保释放连接的操作是在finally块中。
2、使用包含ResponseHandler这个属性的execute方法
【java相关|一次线上http连接被拒绝问题的排查】

    推荐阅读