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();
  }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章