1. 程式人生 > >HttpClient連線池的一些思考

HttpClient連線池的一些思考

前言

使用apache的httpclient進行http的互動處理已經很長時間了,而httpclient例項則使用了http連線池,想必大家也沒有關心過連線池的管理。事實上,通過分析httpclient原始碼,發現它很優雅地隱藏了所有的連線池管理細節,開發者完全不用花太多時間去思考連線池的問題。

Apache官網例子

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
    HttpEntity entity = response.getEntity();
    if (entity != null) {
        long len = entity.getContentLength();
        if (len != -1 && len < 2048) {
            System.out.println(EntityUtils.toString(entity));
        } else {
            // Stream content out
        }
    }
} finally {
    response.close();
}

HttpClient及其連線池配置

  • 整個執行緒池中最大連線數 MAX_CONNECTION_TOTAL = 800
  • 路由到某臺主機最大併發數,是MAX_CONNECTION_TOTAL(整個執行緒池中最大連線數)的一個細分 ROUTE_MAX_COUNT = 500
  • 重試次數,防止失敗情況 RETRY_COUNT = 3
  • 客戶端和伺服器建立連線的超時時間 CONNECTION_TIME_OUT = 5000
  • 客戶端從伺服器讀取資料的超時時間 READ_TIME_OUT = 7000
  • 從連線池中獲取連線的超時時間 CONNECTION_REQUEST_TIME_OUT = 5000
  • 連線空閒超時,清楚閒置的連線 CONNECTION_IDLE_TIME_OUT = 5000
  • 連線保持存活時間 DEFAULT_KEEP_ALIVE_TIME_MILLIS = 20 * 1000

MaxtTotal和DefaultMaxPerRoute的區別

  • MaxtTotal是整個池子的大小;
  • DefaultMaxPerRoute是根據連線到的主機對MaxTotal的一個細分;

比如:MaxtTotal=400,DefaultMaxPerRoute=200,而我只連線到http://hjzgg.com時,到這個主機的併發最多隻有200;而不是400;而我連線到http://qyxjj.com 和 http://httls.com時,到每個主機的併發最多隻有200;即加起來是400(但不能超過400)。所以起作用的設定是DefaultMaxPerRoute。

HttpClient連線池模型

HttpClient從連線池中獲取連線分析

org.apache.http.pool.AbstractConnPool

private E getPoolEntryBlocking(
        final T route, final Object state,
        final long timeout, final TimeUnit tunit,
        final PoolEntryFuture<E> future)
            throws IOException, InterruptedException, TimeoutException {


    Date deadline = null;
    if (timeout > 0) {
        deadline = new Date
            (System.currentTimeMillis() + tunit.toMillis(timeout));
    }


    this.lock.lock();
    try {
        final RouteSpecificPool<T, C, E> pool = getPool(route);//這是每一個路由細分出來的連線池
        E entry = null;
        while (entry == null) {
            Asserts.check(!this.isShutDown, "Connection pool shut down");
            //從池子中獲取一個可用連線並返回
            for (;;) {
                entry = pool.getFree(state);
                if (entry == null) {
                    break;
                }
                if (entry.isExpired(System.currentTimeMillis())) {
                    entry.close();
                } else if (this.validateAfterInactivity > 0) {
                    if (entry.getUpdated() + this.validateAfterInactivity <= System.currentTimeMillis()) {
                        if (!validate(entry)) {
                            entry.close();
                        }
                    }
                }
                if (entry.isClosed()) {
                    this.available.remove(entry);
                    pool.free(entry, false);
                } else {
                    break;
                }
            }
            if (entry != null) {
                this.available.remove(entry);
                this.leased.add(entry);
                onReuse(entry);
                return entry;
            }

            //建立新的連線
            // New connection is needed
            final int maxPerRoute = getMax(route);//獲取當前路由最大併發數
            // Shrink the pool prior to allocating a new connection
            final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute);
            if (excess > 0) {//如果當前路由對應的連線池的連線超過最大路由併發數,獲取到最後使用的一次連線,釋放掉
                for (int i = 0; i < excess; i++) {
                    final E lastUsed = pool.getLastUsed();
                    if (lastUsed == null) {
                        break;
                    }
                    lastUsed.close();
                    this.available.remove(lastUsed);
                    pool.remove(lastUsed);
                }
            }

            //嘗試建立新的連線 
            if (pool.getAllocatedCount() < maxPerRoute) {//當前路由對應的連線池可用空閒連線數+當前路由對應的連線池已用連線數 < 當前路由對應的連線池最大併發數
                final int totalUsed = this.leased.size();
                final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0);
                if (freeCapacity > 0) {
                    final int totalAvailable = this.available.size();
                    if (totalAvailable > freeCapacity - 1) {//執行緒池中可用空閒連線數 > (執行緒池中最大連線數 - 執行緒池中已用連線數 - 1)
                        if (!this.available.isEmpty()) {
                            final E lastUsed = this.available.removeLast();
                            lastUsed.close();
                            final RouteSpecificPool<T, C, E> otherpool = getPool(lastUsed.getRoute());
                            otherpool.remove(lastUsed);
                        }
                    }
                    final C conn = this.connFactory.create(route);
                    entry = pool.add(conn);
                    this.leased.add(entry);
                    return entry;
                }
            }


            boolean success = false;
            try {
                pool.queue(future);
                this.pending.add(future);
                success = future.await(deadline);
            } finally {
                // In case of 'success', we were woken up by the
                // connection pool and should now have a connection
                // waiting for us, or else we're shutting down.
                // Just continue in the loop, both cases are checked.
                pool.unqueue(future);
                this.pending.remove(future);
            }
            // check for spurious wakeup vs. timeout
            if (!success && (deadline != null) &&
                (deadline.getTime() <= System.currentTimeMillis())) {
                break;
            }
        }
        throw new TimeoutException("Timeout waiting for connection");
    } finally {
        this.lock.unlock();
    }
}

連線重用和保持策略

http的長連線複用, 其判定規則主要分兩類。
  1. http協議支援+請求/響應header指定
  2. 一次互動處理的完整性(響應內容消費乾淨)
  對於前者, httpclient引入了ConnectionReuseStrategy來處理, 預設的採用如下的約定:

  • HTTP/1.0通過在Header中新增Connection:Keep-Alive來表示支援長連線。
  • HTTP/1.1預設支援長連線, 除非在Header中顯式指定Connection:Close, 才被視為短連線模式。

HttpClientBuilder建立MainClientExec

ConnectionReuseStrategy(連線重用策略) 

org.apache.http.impl.client.DefaultClientConnectionReuseStrategy

MainClientExec處理連線

處理完請求後,獲取到response,通過ConnectionReuseStrategy判斷連線是否可重用,如果是通過ConnectionKeepAliveStrategy獲取到連線最長有效時間,並設定連線可重用標記。

連線重用判斷邏輯

  • request首部中包含Connection:Close,不復用
  • response中Content-Length長度設定不正確,不復用
  • response首部包含Connection:Close,不復用
  • reponse首部包含Connection:Keep-Alive,複用
  • 都沒命中的情況下,如果HTTP版本高於1.0則複用

更多參考:https://www.cnblogs.com/mumuxinfei/p/9121829.html

連線釋放原理分析

HttpClientBuilder會構建一個InternalHttpClient例項,也是CloseableHttpClient例項。InternalHttpClient的doExecute方法來完成一次request的執行。

會繼續呼叫MainClientExec的execute方法,通過連線池管理者獲取連線(HttpClientConnection)。

構建ConnectionHolder型別物件,傳遞連線池管理者物件和當前連線物件。

請求執行完返回HttpResponse型別物件,然後包裝成HttpResponseProxy物件(是CloseableHttpResponse例項)返回。

CloseableHttpClient類其中一個execute方法如下,finally方法中會呼叫HttpResponseProxy物件的close方法釋放連線。

最終呼叫ConnectionHolder的releaseConnection方法釋放連線。

CloseableHttpClient類另一個execute方法如下,返回一個HttpResponseProxy物件(是CloseableHttpResponse例項)。 

這種情況下呼叫者獲取了HttpResponseProxy物件,可以直接拿到HttpEntity物件。大家關心的就是操作完HttpEntity物件,使用完InputStream到底需不需要手動關閉流呢?

其實呼叫者不需要手動關閉流,因為HttpResponseProxy構造方法裡有增強HttpEntity的處理方法,如下。

呼叫者最終拿到的HttpEntity物件是ResponseEntityProxy例項。

ResponseEntityProxy重寫了獲取InputStream的方法,返回的是EofSensorInputStream型別的InputStream物件。

EofSensorInputStream物件每次讀取都會呼叫checkEOF方法,判斷是否已經讀取完畢。

checkEOF方法會呼叫ResponseEntityProxy(實現了EofSensorWatcher介面)物件的eofDetected方法。

EofSensorWatcher#eofDetected方法中會釋放連線並關閉流。

 

綜上,通過CloseableHttpClient例項處理請求,無需呼叫者手動釋放連線。

HttpClient在Spring中應用

建立ClientHttpRequestFactory

@Bean
public ClientHttpRequestFactory clientHttpRequestFactory()
        throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
    HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
    SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, (arg0, arg1) -> true).build();

    httpClientBuilder.setSSLContext(sslContext)
            .setMaxConnTotal(MAX_CONNECTION_TOTAL)
            .setMaxConnPerRoute(ROUTE_MAX_COUNT)
            .evictIdleConnections(CONNECTION_IDLE_TIME_OUT, TimeUnit.MILLISECONDS);

    httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(RETRY_COUNT, true));
    httpClientBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());
    CloseableHttpClient client = httpClientBuilder.build();

    HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(client);
    clientHttpRequestFactory.setConnectTimeout(CONNECTION_TIME_OUT);
    clientHttpRequestFactory.setReadTimeout(READ_TIME_OUT);
    clientHttpRequestFactory.setConnectionRequestTimeout(CONNECTION_REQUEST_TIME_OUT);
    clientHttpRequestFactory.setBufferRequestBody(false);
    return clientHttpRequestFactory;
}

建立RestTemplate

@Bean
public RestTemplate restTemplate()  {
   RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory());
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler());
        // 修改StringHttpMessageConverter內容轉換器
        restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
        return restTemplate;
}

 Spring官網例子

@SpringBootApplication
public class Application {

    private static final Logger log = LoggerFactory.getLogger(Application.class);

    public static void main(String args[]) {
        SpringApplication.run(Application.class);
    }

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder.build();
    }

    @Bean
    public CommandLineRunner run(RestTemplate restTemplate) throws Exception {
        return args -> {
            Quote quote = restTemplate.getForObject(
                    "https://gturnquist-quoters.cfapps.io/api/random", Quote.class);
            log.info(quote.toString());
        };
    }
}

總結

Apache的HttpClient元件可謂良心之作,細細的品味一下原始碼可以學到很多設計模式和比編碼規範。不過在閱讀原始碼之前最好了解一下不同版本的HTTP協議,尤其是HTTP協議的Keep-Alive模式。使用Keep-Alive模式(又稱持久連線、連線重用)時,Keep-Alive功能使客戶端到服 務器端的連線持續有效,當出現對伺服器的後繼請求時,Keep-Alive功能避免了建立或者重新建立連線。這裡推薦一篇參考連結:https://www.jianshu.com/p/49551bda6619。