HttpClient使用时Timeout waiting for connection from pool,maxConnTotal和maxConnPerRoute

一、为什么会报Timeout waiting for connection from pool

首先我们需要知道,HttpClient是不建议每次使用都创建的,因为它本身就带一个连接池。如果我们使用频繁的话,频繁创建HttpClient对象也不是明智的。

我在使用HttpClient的时候,在一个调用链中,只是依次使用了HttpClient调用了几次http接口,却发现在第5个的时候,报错了
org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool

这是我当时使用的配置:

  private static HttpClient client;

  static {
    RequestConfig requestConfig = RequestConfig.custom()
            .setConnectTimeout(5000)
            .setConnectionRequestTimeout(3000)
            .setSocketTimeout(5000).build();
    client = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig)
            .build();
  }

查了下才知道这个错,是因为连接池没有空闲连接,并且等待获取连接的时间达到了setConnectionRequestTimeout,我很疑惑为啥才一个单线程使用了几下就报错了呢?于是我开始了测试。

二、测试

这里先说结论

HttpClient默认连接池,最大连接是20个,最大同路由的是2个,也就是maxConnTotal是20,maxConnPerRoute是2。

maxConnTotal是同时间正在使用的最多的连接数
maxConnPerRoute是针对一个域名同时间正在使用的最多的连接数

看源码

可以看org.apache.http.impl.conn.PoolingHttpClientConnectionManager类,

public PoolingHttpClientConnectionManager(HttpClientConnectionOperator httpClientConnectionOperator, HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory, long timeToLive, TimeUnit timeUnit) {
    this.log = LogFactory.getLog(this.getClass());
    this.configData = new PoolingHttpClientConnectionManager.ConfigData();
    this.pool = new CPool(new PoolingHttpClientConnectionManager.InternalConnectionFactory(this.configData, connFactory), 2, 20, timeToLive, timeUnit);
    this.pool.setValidateAfterInactivity(2000);
    this.connectionOperator = (HttpClientConnectionOperator)Args.notNull(httpClientConnectionOperator, "HttpClientConnectionOperator");
    this.isShutDown = new AtomicBoolean(false);
  }

验证1

先前5个调用百度,后5次调用hao123

package com.zgd.demo.web.test;

import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.junit.jupiter.api.Test;

import java.io.IOException;

/**
 * @Author: zgd
 * @Date: 2019/8/15 00:04
 * @Description: 测试httpclient
 */
@Slf4j
public class MyHttpClientTest {

  private static HttpClient client;

  static {
    RequestConfig requestConfig = RequestConfig.custom()
            .setConnectTimeout(5000)
            .setConnectionRequestTimeout(3000)
            .setSocketTimeout(5000).build();
    client = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig)
            .build();
  }


  @Test
  public void fun01()  {
    for (int i = 0; i < 10; i++) {
      String url = "";
      if (i < 5){
        url = "http://www.baidu.com";
      }else {
        url = "http://www.hao123.com";
      }
      HttpGet httpGet = new HttpGet(url);
      log.info("------url:{}-----",url);
      HttpResponse res = null;
      try {
        res = client.execute(httpGet);
      } catch (IOException e) {
        log.error("异常: ",e);
        return;
      }
      int code = res.getStatusLine().getStatusCode();
      log.info("---url:{}\tcode:{}\ti:{}",url, code,i+1);

    }

  }

}

运行结果
在这里插入图片描述
在第3个,i=2的时候就跑这个异常了,在debug级别日志打印出total kept alive: 0; route allocated: 2 of 2; total allocated: 2 of 20,也就是这个route下一共2个连接,已经使用2个。整个pool有20个连接,使用了2个

验证2

依次调用百度和hao123


  @Test
  public void fun01()  {
    for (int i = 0; i < 10; i++) {
      String url = "";
      if (i % 2 == 0){
        url = "http://www.baidu.com";
      }else {
        url = "http://www.hao123.com";
      }
      HttpGet httpGet = new HttpGet(url);
      log.info("------url:{}-----",url);
      HttpResponse res = null;
      try {
        res = client.execute(httpGet);
      } catch (IOException e) {
        log.error("异常: ",e);
        return;
      }
      int code = res.getStatusLine().getStatusCode();
      log.info("---url:{}\tcode:{}\ti:{}",url, code,i+1);

    }

  }

在这里插入图片描述

验证3

我们修改代码如下

@Test
  public void fun01()  {
    for (int i = 0; i < 10; i++) {
      String url = "";
      if (i % 2 == 0){
        url = "http://www.baidu.com";
      }else {
        url = "http://www.hao123.com";
      }
      HttpGet httpGet = new HttpGet(url);
      log.info("------url:{}-----",url);
      HttpResponse res = null;
      try {
        res = client.execute(httpGet);
        int code = res.getStatusLine().getStatusCode();
        log.info("---url:{}\tcode:{}\ti:{}",url, code,i+1);
      } catch (IOException e) {
        log.error("异常: ",e);
        return;
      }finally {
        if (res != null){
          EntityUtils.consumeQuietly(res.getEntity());
        }
      }
    }
  }

不管是先百度后好123,还是依次调用,都完全ok了
这里使用了HttpClient提供的工具类EntityUtils来处理,EntityUtils的consume方法都对连接资源进行了释放。

 public static void consumeQuietly(HttpEntity entity) {
    try {
      consume(entity);
    } catch (IOException var2) {
    }

  }
...

 public static void consume(HttpEntity entity) throws IOException {
    if (entity != null) {
      if (entity.isStreaming()) {
        InputStream inStream = entity.getContent();
        if (inStream != null) {
          inStream.close();
        }
      }

    }
  }

三、结果

可以看出,出现Timeout waiting for connection from pool异常的原因主要有二:

  1. 连接没关闭,资源没释放。
  2. pool的连接数设置太小

可以看到第1条,是老生常谈的资源关闭问题。博主在开发的时候,正是因为不需要知道调用的返回结果,所以没有对Response进行处理,所以资源并没有释放,导致后面再使用HttpClient,就拿不到连接了

第2条,需要知道maxConnTotal和maxConnPerRoute的具体含义,如果是需要频繁调用同一个域名,那么即使maxConnTotal设置再大,还是受限制与maxConnPerRoute

根据业务实际情况合理配置连接池

 private static HttpClient client;

  static {
    RequestConfig requestConfig = RequestConfig.custom()
            .setConnectTimeout(5000)
            .setConnectionRequestTimeout(1000)
            .setSocketTimeout(5000).build();
    client = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig)
            .setMaxConnTotal(200)
            .setMaxConnPerRoute(50)
            .build();
  }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章