1. 程式人生 > >HttpClient 4.3連接池參數配置及源碼解讀

HttpClient 4.3連接池參數配置及源碼解讀

efault sgid adapt 讀取輸入 lag 原則 機器 syn 因此

目前所在公司使用HttpClient 4.3.3版本發送Rest請求,調用接口。最近出現了調用查詢接口服務慢的生產問題,在排查整個調用鏈可能存在的問題時(從客戶端發起Http請求->ESB->服務端處理請求,查詢數據並返回),發現原本的HttpClient連接池中的一些參數配置可能存在問題,如defaultMaxPerRoute、一些timeout時間的設置等,雖不能確定是由於此連接池導致接口查詢慢,但確實存在可優化的地方,故花時間做一些研究。本文主要涉及HttpClient連接池、請求的參數配置,使用及源碼解讀。 以下是本文的目錄大綱: 一、HttpClient連接池、請求參數含義 二、執行原理及源碼解讀 1、創建HttpClient,執行request 2、連接池管理 2.1、連接池結構 2.2、分配連接 & 建立連接 2.3、回收連接 & 保持連接 2.4、instream.close()、response.close()、httpclient.close()的區別 2.5、過期和空閑連接清理 三、如何設置合理的參數

一、HttpClient連接池、請求參數含義

import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.UnknownHostException;
import java.nio.charset.CodingErrorAction;
import javax.net.ssl.SSLException;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.config.MessageConstraints;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.HttpContext;
 
public class HttpClientParamTest {
    public static void main(String[] args) {
        /**
         * 創建連接管理器,並設置相關參數
         */
        //連接管理器,使用無慘構造
        PoolingHttpClientConnectionManager connManager
                                    = new PoolingHttpClientConnectionManager();
         
        /**
         * 連接數相關設置
         */
        //最大連接數
        connManager.setMaxTotal(200);
        //默認的每個路由的最大連接數
        connManager.setDefaultMaxPerRoute(100);
        //設置到某個路由的最大連接數,會覆蓋defaultMaxPerRoute
        connManager.setMaxPerRoute(new HttpRoute(new HttpHost("somehost", 80)), 150);
         
        /**
         * socket配置(默認配置 和 某個host的配置)
         */
        SocketConfig socketConfig = SocketConfig.custom()
                .setTcpNoDelay(true)     //是否立即發送數據,設置為true會關閉Socket緩沖,默認為false
                .setSoReuseAddress(true) //是否可以在一個進程關閉Socket後,即使它還沒有釋放端口,其它進程還可以立即重用端口
                .setSoTimeout(500)       //接收數據的等待超時時間,單位ms
                .setSoLinger(60)         //關閉Socket時,要麽發送完所有數據,要麽等待60s後,就關閉連接,此時socket.close()是阻塞的
                .setSoKeepAlive(true)    //開啟監視TCP連接是否有效
                .build();
        connManager.setDefaultSocketConfig(socketConfig);
        connManager.setSocketConfig(new HttpHost("somehost", 80), socketConfig);
         
        /**
         * HTTP connection相關配置(默認配置 和 某個host的配置)
         * 一般不修改HTTP connection相關配置,故不設置
         */
        //消息約束
        MessageConstraints messageConstraints = MessageConstraints.custom()
                .setMaxHeaderCount(200)
                .setMaxLineLength(2000)
                .build();
        //Http connection相關配置
        ConnectionConfig connectionConfig = ConnectionConfig.custom()
                .setMalformedInputAction(CodingErrorAction.IGNORE)
                .setUnmappableInputAction(CodingErrorAction.IGNORE)
                .setCharset(Consts.UTF_8)
                .setMessageConstraints(messageConstraints)
                .build();
        //一般不修改HTTP connection相關配置,故不設置
        //connManager.setDefaultConnectionConfig(connectionConfig);
        //connManager.setConnectionConfig(new HttpHost("somehost", 80), ConnectionConfig.DEFAULT);
         
        /**
         * request請求相關配置
         */
        RequestConfig defaultRequestConfig = RequestConfig.custom()
                .setConnectTimeout(2 * 1000)         //連接超時時間
                .setSocketTimeout(2 * 1000)          //讀超時時間(等待數據超時時間)
                .setConnectionRequestTimeout(500)    //從池中獲取連接超時時間
                .setStaleConnectionCheckEnabled(true)//檢查是否為陳舊的連接,默認為true,類似testOnBorrow
                .build();
         
        /**
         * 重試處理
         * 默認是重試3次
         */
        //禁用重試(參數:retryCount、requestSentRetryEnabled)
        HttpRequestRetryHandler requestRetryHandler = new DefaultHttpRequestRetryHandler(0, false);
        //自定義重試策略
        HttpRequestRetryHandler myRetryHandler = new HttpRequestRetryHandler() {
 
            public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
                //Do not retry if over max retry count
                if (executionCount >= 3) {
                    return false;
                }
                //Timeout
                if (exception instanceof InterruptedIOException) {
                    return false;
                }
                //Unknown host
                if (exception instanceof UnknownHostException) {
                    return false;
                }
                //Connection refused
                if (exception instanceof ConnectTimeoutException) {
                    return false;
                }
                //SSL handshake exception
                if (exception instanceof SSLException) {
                    return false;
                }
                 
                HttpClientContext clientContext = HttpClientContext.adapt(context);
                HttpRequest request = clientContext.getRequest();
                boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
                //Retry if the request is considered idempotent
                //如果請求類型不是HttpEntityEnclosingRequest,被認為是冪等的,那麽就重試
                //HttpEntityEnclosingRequest指的是有請求體的request,比HttpRequest多一個Entity屬性
                //而常用的GET請求是沒有請求體的,POST、PUT都是有請求體的
                //Rest一般用GET請求獲取數據,故冪等,POST用於新增數據,故不冪等
                if (idempotent) {
                    return true;
                }
                 
                return false;
            }
        };
         
        /**
         * 創建httpClient
         */
        CloseableHttpClient httpclient = HttpClients.custom()
                .setConnectionManager(connManager)             //連接管理器
                .setProxy(new HttpHost("myproxy", 8080))       //設置代理
                .setDefaultRequestConfig(defaultRequestConfig) //默認請求配置
                .setRetryHandler(myRetryHandler)               //重試策略
                .build();
         
        //創建一個Get請求,並重新設置請求參數,覆蓋默認
        HttpGet httpget = new HttpGet("http://www.somehost.com/");
        RequestConfig requestConfig = RequestConfig.copy(defaultRequestConfig)
            .setSocketTimeout(5000)
            .setConnectTimeout(5000)
            .setConnectionRequestTimeout(5000)
            .setProxy(new HttpHost("myotherproxy", 8080))
            .build();
        httpget.setConfig(requestConfig);
         
        CloseableHttpResponse response = null;
        try {
            //執行請求
            response = httpclient.execute(httpget);
             
            HttpEntity entity = response.getEntity();
             
            // If the response does not enclose an entity, there is no need
            // to bother about connection release
            if (entity != null) {
                InputStream instream = entity.getContent();
                try {
                    instream.read();
                    // do something useful with the response
                }
                catch (IOException ex) {
                    // In case of an IOException the connection will be released
                    // back to the connection manager automatically
                    throw ex;
                }
                finally {
                    // Closing the input stream will trigger connection release
                    // 釋放連接回到連接池
                    instream.close();
                }
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        finally{
            if(response != null){
                try {
                    //關閉連接(如果已經釋放連接回連接池,則什麽也不做)
                    response.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
             
            if(httpclient != null){
                try {
                    //關閉連接管理器,並會關閉其管理的連接
                    httpclient.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

  

上面的代碼參考httpClient 4.3.x的官方樣例,其實官方樣例中可配置的更多,我只將一些覺得平時常用的摘了出來,其實我們在實際使用中也是使用默認的 socketConfig 和 connectionConfig。具體參數含義請看註釋。 個人感覺在實際應用中連接數相關配置(如maxTotal、maxPerRoute),還有請求相關的超時時間設置(如connectionTimeout、socketTimeout、connectionRequestTimeout)是比較重要的。 連接數配置有問題就可能產生總的y 連接數不夠 或者 到某個路由的連接數太小 的問題,我們公司一些項目總連接數800,而defaultMaxPerRoute僅為20,這樣導致真正需要比較多連接數,訪問量比較大的路由也僅能從連接池中獲取最大20個連接,應該在默認的基礎上,針對訪問量大的路由單獨設置。 連接超時時間,讀超時時間,從池中獲取連接的超時時間如果不設置或者設置的太大,可能導致當業務高峰時,服務端響應較慢 或 連接池中確實沒有空閑連接時,不能夠及時將timeout異常拋出來,導致等待讀取數據的,或者等待從池中獲取連接的越積越多,像滾雪球一樣,導致相關業務都開始變得緩慢,而如果配置合理的超時時間就可以及時拋出異常,發現問題。 後面會盡量去闡述這些重要參數的原理以及如何配置一個合適的值。

二、執行原理及源碼解讀

1、創建HttpClient,執行request
/**
 * 創建httpClient
 */
CloseableHttpClient httpclient = HttpClients.custom()
                                 .setConnectionManager(connManager)             //連接管理器
                                 .setDefaultRequestConfig(defaultRequestConfig) //默認請求配置
                                 .setRetryHandler(myRetryHandler)               //重試策略
                                 .build();

  

創建HttpClient的過程就是在設置了“連接管理器”、“請求相關配置”、“重試策略”後,調用 HttpClientBuilder.build()。 build()方法會根據設置的屬性不同,創建不同的Executor執行器,如設置了retryHandler就會 new RetryExec(execChain, retryHandler),相當於retry Executor。當然有些Executor是必須創建的,如MainClientExec、ProtocolExec。最後new InternalHttpClient(execChain, connManager, routePlanner …)並返回。
CloseableHttpResponse httpResponse = httpClient.execute(httpUriRequest);
HttpClient使用了責任鏈模式,所有Executor都實現了ClientExecChain接口的execute()方法,每個Executor都持有下一個要執行的Executor的引用,這樣就會形成一個Executor的執行鏈條,請求在這個鏈條上傳遞。按照上面的方式構造的httpClient形成的執行鏈條為:
HttpRequestExecutor                              //發送請求報文,並接收響應信息
MainClientExec(requestExec, connManager, ...)    //main Executor,負責連接管理相關
ProtocolExec(execChain, httpprocessor)           //HTTP協議封裝
RetryExec(execChain, retryHandler)               //重試策略
RedirectExec(execChain, routePlanner, redirectStrategy)   //重定向
請求執行是按照從下到上的順序(即每個下面的Executor都持有上面一個Executor的引用),每一個執行器都會負責請求過程中的一部分工作,最終返回response。 2、連接池管理 2.1、連接池結構 連接池結構圖如下: 技術分享圖片
PoolEntry<HttpRoute, ManagedHttpClientConnection> -- 連接池中的實體 包含ManagedHttpClientConnection連接; 連接的route路由信息; 以及連接存活時間相隔信息,如created(創建時間),updated(更新時間,釋放連接回連接池時會更新),validUnit(用於初始化expiry過期時間,規則是如果timeToLive>0,則為created+timeToLive,否則為Long.MAX_VALUE),expiry(過期時間,人為規定的連接池可以保有連接的時間,除了初始化時等於validUnit,每次釋放連接時也會更新,但是從newExpiry和validUnit取最小值)。timeToLive是在構造連接池時指定的連接存活時間,默認構造的timeToLive=-1。 ManagedHttpClientConnection是httpClient連接,真正建立連接後,其會bind綁定一個socket,用於傳輸HTTP報文。 LinkedList<PoolEntry> available -- 存放可用連接 使用完後所有可重用的連接回被放到available鏈表頭部,之後再獲取連接時優先從available鏈表頭部叠代可用的連接。 之所以使用LinkedList是利用了其隊列的特性,即可以在隊首和隊尾分別插入、刪除。入available鏈表時都是addFirst()放入頭部,獲取時都是從頭部依次叠代可用的連接,這樣可以獲取到最新放入鏈表的連接,其離過期時間更遠(這種策略可以盡量保證獲取到的連接沒有過期,而從隊尾獲取連接是可以做到在連接過期前盡量使用,但獲取到過期連接的風險就大了),刪除available鏈表中連接時是從隊尾開始,即先刪除最可能快要過期的連接。 HashSet<PoolEntry> leased -- 存放被租用的連接 所有正在被使用的連接存放的集合,只涉及 add() 和 remove() 操作。 maxTotal限制的是外層httpConnPool中leased集合和available隊列的總和的大小,leased和available的大小沒有單獨限制。 LinkedList<PoolEntryFuture> pending -- 存放等待獲取連接的線程的Future 當從池中獲取連接時,如果available鏈表沒有現成可用的連接,且當前路由或連接池已經達到了最大數量的限制,也不能創建連接了,此時不會阻塞整個連接池,而是將當前線程用於獲取連接的Future放入pending鏈表的末尾,之後當前線程調用await(),釋放持有的鎖,並等待被喚醒。 當有連接被release()釋放回連接池時,會從pending鏈表頭獲取future,並喚醒其線程繼續獲取連接,做到了先進先出。 routeToPool -- 每個路由對應的pool 也有針對當前路由的available、leased、pending集合,與整個池的隔離。 maxPerRoute限制的是routeToPool中leased集合和available隊列的總和的大小。 2.2、分配連接 & 建立連接 分配連接 分配連接指的是從連接池獲取可用的PoolEntry,大致過程為: 1、獲取route對應連接池routeToPool中可用的連接,有則返回該連接,若沒有則轉入下一步; 2、若routeToPool和外層HttpConnPool連接池均還有可用的空間,則新建連接,並將該連接作為可用連接返回,否則進行下一步; 3、掛起當前線程,將當前線程的Future放入pending隊列,等待後續喚醒執行; 整個分配連接的過程采用了異步操作,只在前兩步時鎖住連接池,一旦發現無法獲取連接則釋放鎖,等待後續繼續獲取連接。 建立連接 當分配到PoolEntry連接實體後,會調用establishRoute(),建立socket連接並與conn綁定。 2.3、回收連接 & 保持連接 回收連接 連接用完之後連接池需要進行回收(AbstractConnPool#release()),具體流程如下: 1、若當前連接標記為重用,則將該連接從routeToPool中的leased集合刪除,並添加至available隊首,同樣的將該請求從外層httpConnPool的leased集合刪除,並添加至其available隊首。同時喚醒該routeToPool的pending隊列的第一個PoolEntryFuture,將其從pending隊列刪除,並將其從外層httpConnPool的pending隊列中刪除。 2、若連接沒有標記為重用,則分別從routeToPool和外層httpConnPool中刪除該連接,並關閉該連接。 保持連接 MainClientExec#execute()是負責連接管理的,在執行完後續調用鏈,並得到response後,會調用保持連接的邏輯,如下:
// The connection is in or can be brought to a re-usable state.
// 根據response頭中的信息判斷是否保持連接
if (reuseStrategy.keepAlive(response, context)) {
    // Set the idle duration of this connection
    // 根據response頭中的keep-alive中的timeout屬性,得到連接可以保持的時間(ms)
    final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
    if (this.log.isDebugEnabled()) {
        final String s;
        if (duration > 0) {
            s = "for " + duration + " " + TimeUnit.MILLISECONDS;
        } else {
            s = "indefinitely";
        }
        this.log.debug("Connection can be kept alive " + s);
    }
    //設置連接保持時間,最終是調用 PoolEntry#updateExpiry
    connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);
    connHolder.markReusable(); //設置連接reuse=true
}
else {
    connHolder.markNonReusable();
}

  

連接是否保持 客戶端如果希望保持長連接,應該在發起請求時告訴服務器希望服務器保持長連接(http 1.0設置connection字段為keep-alive,http 1.1字段默認保持)。根據服務器的響應來確定是否保持長連接,判斷原則如下:

1、檢查返回response報文頭的Transfer-Encoding字段,若該字段值存在且不為chunked,則連接不保持,直接關閉。其他情況進入下一步; 2、檢查返回的response報文頭的Content-Length字段,若該字段值為空或者格式不正確(多個長度,值不是整數)或者小於0,則連接不保持,直接關閉。其他情況進入下一步 3、檢查返回的response報文頭的connection字段(若該字段不存在,則為Proxy-Connection字段)值,如果字段存在,若字段值為close 則連接不保持,直接關閉,若字段值為keep-alive則連接標記為保持。如果這倆字段都不存在,則http 1.1版本默認為保持,將連接標記為保持, 1.0版本默認為連接不保持,直接關閉。 連接保持時間 連接交還至連接池時,若連接標記為保持reuse=true,則將由連接管理器保持一段時間;若連接沒有標記為保持,則直接從連接池中刪除並關閉entry。 連接保持時,會更新PoolEntry的expiry到期時間,計算邏輯為: 1、如果response頭中的keep-alive字段中timeout屬性值存在且為正值:newExpiry = 連接歸還至連接池時間System.currentTimeMillis() + timeout; 2、如timeout屬性值不存在或為負值:newExpiry = Long.MAX_VALUE(無窮) 3、最後會和PoolEntry原本的expiry到期時間比較,選出一個最小值作為新的到期時間。 2.4、instream.close()、response.close()、httpclient.close()的區別
/**
 * This example demonstrates the recommended way of using API to make sure
 * the underlying connection gets released back to the connection manager.
 */
public class ClientConnectionRelease {
 
    public final static void main(String[] args) throws Exception {
        CloseableHttpClient httpclient = HttpClients.createDefault();
        try {
            HttpGet httpget = new HttpGet("http://localhost/");
 
            System.out.println("Executing request " + httpget.getRequestLine());
            CloseableHttpResponse response = httpclient.execute(httpget);
            try {
                System.out.println("----------------------------------------");
                System.out.println(response.getStatusLine());
 
                // Get hold of the response entity
                HttpEntity entity = response.getEntity();
 
                // If the response does not enclose an entity, there is no need
                // to bother about connection release
                if (entity != null) {
                    InputStream instream = entity.getContent();
                    try {
                        instream.read();
                        // do something useful with the response
                    } catch (IOException ex) {
                        // In case of an IOException the connection will be released
                        // back to the connection manager automatically
                        throw ex;
                    } finally {
                        // Closing the input stream will trigger connection release
                        instream.close();
                    }
                }
            } finally {
                response.close();
            }
        } finally {
            httpclient.close();
        }
    }
}

  

在HttpClient Manual connection release的例子中可以看到,從內層依次調用的是instream.close()、response.close()、httpClient.close(),那麽它們有什麽區別呢? instream.close() 在主動操作輸入流,或者調用EntityUtils.toString(httpResponse.getEntity())時會調用instream.read()、instream.close()等方法。instream的實現類為org.apache.http.conn.EofSensorInputStream。 在每次通過instream.read()讀取數據流後,都會判斷流是否讀取結束
@Override
public int read(final byte[] b, final int off, final int len) throws IOException { 
    int l = -1;
    if (isReadAllowed()) {
        try {
            l = wrappedStream.read(b,  off,  len);
            checkEOF(l);
        } catch (final IOException ex) {
            checkAbort();
            throw ex;
        }
    }
    return l;
}

  

在EofSensorInputStream#checkEOF()方法中如果eof=-1,流已經讀完,如果連接可重用,就會嘗試釋放連接,否則關閉連接。
protected void checkEOF(final int eof) throws IOException {
    if ((wrappedStream != null) && (eof < 0)) {
        try {
            boolean scws = true; // should close wrapped stream? 
            if (eofWatcher != null) {
                scws = eofWatcher.eofDetected(wrappedStream);
            }
            if (scws) {
                wrappedStream.close();
            }
        } finally {
            wrappedStream = null;
        }
    }
}

  

ResponseEntityWrapper#eofDetected
public boolean eofDetected(final InputStream wrapped) throws IOException { 
    try {
        // there may be some cleanup required, such as
        // reading trailers after the response body:
        wrapped.close();
        releaseConnection(); //釋放連接 或 關閉連接
    } finally {
        cleanup();
    }
    return false;
}

  

ConnectionHolder#releaseConnection
public void releaseConnection() {
    synchronized (this.managedConn) {
        //如果連接已經釋放,直接返回
        if (this.released) {
            return;
        }
         
        this.released = true;
        //連接可重用,釋放回連接池
        if (this.reusable) {
            this.manager.releaseConnection(this.managedConn,
                    this.state, this.validDuration, this.tunit);
        }
        //不可重用,關閉連接
        else {
            try {
                this.managedConn.close();
                log.debug("Connection discarded");
            } catch (final IOException ex) {
                if (this.log.isDebugEnabled()) {
                    this.log.debug(ex.getMessage(), ex);
                }
            } finally {
                this.manager.releaseConnection(
                        this.managedConn, null, 0, TimeUnit.MILLISECONDS); 
            }
        }
    }
}

  

如果沒有instream.read()讀取數據,在instream.close()時會調用EofSensorInputStream#checkClose(),也會有類似上面的邏輯。 所以就如官方例子註釋的一樣,在正常操作輸入流後,會釋放連接。 response.close() 最終是調用ConnectionHolder#abortConnection()
public void abortConnection() {
    synchronized (this.managedConn) {
        //如果連接已經釋放,直接返回
        if (this.released) {
            return;
        }
        this.released = true;
        try {
            //關閉連接
            this.managedConn.shutdown();
            log.debug("Connection discarded");
        } catch (final IOException ex) {
            if (this.log.isDebugEnabled()) {
                this.log.debug(ex.getMessage(), ex);
            }
        } finally {
            this.manager.releaseConnection(
                    this.managedConn, null, 0, TimeUnit.MILLISECONDS); 
        }
    }
}

  

所以,如果在調用response.close()之前,沒有讀取過輸入流,也沒有關閉輸入流,那麽連接沒有被釋放,released=false,就會關閉連接。 httpClient.close() 最終調用的是InternalHttpClient#close(),會關閉整個連接管理器,並關閉連接池中所有連接。
public void close() {
    this.connManager.shutdown();
    if (this.closeables != null) {
        for (final Closeable closeable: this.closeables) { 
            try {
                closeable.close();
            } catch (final IOException ex) {
                this.log.error(ex.getMessage(), ex);
            }
        }
    }
}
總結: 1、使用連接池時,要正確釋放連接需要通過讀取輸入流 或者 instream.close()方式; 2、如果已經釋放連接,response.close()直接返回,否則會關閉連接; 3、httpClient.close()會關閉連接管理器,並關閉其中所有連接,謹慎使用。 2.5、過期和空閑連接清理 在連接池保持連接的這段時間,可能出現兩種導致連接過期或失效的情況:

1、連接保持時間到期

每個連接對象PoolEntry都有expiry到期時間,在創建和釋放歸還連接是都會為expiry到期時間賦值,在連接池保持連接的這段時間,連接已經到了過期時間(註意,這個過期時間是為了管理連接所設定的,並不是指的TCP連接真的不能使用了)。 對於這種情況,在每次從連接池獲取連接時,都會從routeToPool的available隊列獲取Entry並檢測此時Entry是否已關閉或者已過期,若是則關閉並分別從routeToPool、httpConnPool的available隊列移除該Entry,之後再次嘗試獲取連接。代碼如下
/**AbstractConnPool#getPoolEntryBlocking()*/
for (;;) {
    //從availabe鏈表頭叠代查找符合state的entry
    entry = pool.getFree(state);
    //找不到entry,跳出
    if (entry == null) {
        break;
    }
    //如果entry已關閉或已過期,關閉entry,並從routeToPool、httpConnPool的available隊列移除
    if (entry.isClosed() || entry.isExpired(System.currentTimeMillis())) {
        entry.close();
        this.available.remove(entry);
        pool.free(entry, false);
    }
    else {  //找到可用連接
        break;
    }
}

2、底層連接已被關閉

在連接池保持連接的時候,可能會出現連接已經被服務端關閉的情況,而此時連接的客戶端並沒有阻塞著去接收服務端的數據,所以客戶端不知道連接已關閉,無法關閉自身的socket。 對於這種情況,在從連接池獲取可用連接時無法知曉,在獲取到可用連接後,如果連接是打開的,會有判斷連接是否陳舊的邏輯,如下
/**MainClientExec#execute()*/
if (config.isStaleConnectionCheckEnabled()) {
    // validate connection
    if (managedConn.isOpen()) {
        this.log.debug("Stale connection check");
        if (managedConn.isStale()) {
            this.log.debug("Stale connection detected");
            managedConn.close();
        }
    }
}

  

isOpen()會通過連接的狀態判斷連接是否是open狀態; isStale()會通過socket輸入流嘗試讀取數據,在讀取前暫時將soTimeout設置為1ms,如果讀取到的字節數小於0,即已經讀到了輸入流的末尾,或者發生了IOException,可能連接已經關閉,那麽isStale()返回true,需要關閉連接;如果讀到的字節數大於0,或者發生了SocketTimeoutException,可能是讀超時,isStale()返回false,連接還可用。
/**BHttpConnectionBase#isStale()*/
public boolean isStale() {
    if (!isOpen()) {
        return true;
    }
    try {
        final int bytesRead = fillInputBuffer(1);
        return bytesRead < 0;
    } catch (final SocketTimeoutException ex) { 
        return false;
    } catch (final IOException ex) {
        return true;
    }
}

  

如果在整個判斷過程中發現連接是陳舊的,就會關閉連接,那麽這個從連接池獲取的連接就是不可用的,後面的代碼邏輯裏會重建當前PoolEntry的socket連接,繼續後續請求邏輯。

後臺監控線程檢查連接

上述過程是在從連接池獲取連接後,檢查連接是否可用,如不可用需重新建立socket連接,建立連接的過程是比較耗時的,可能導致性能問題,也失去了連接池的意義,針對這種情況,HttpClient采取一個策略,通過一個後臺的監控線程定時的去檢查連接池中連接是否還“新鮮”,如果過期了,或者空閑了一定時間則就將其從連接池裏刪除掉。 ClientConnectionManager提供了 closeExpiredConnections()和closeIdleConnections()兩個方法,關閉過期或空閑了一段時間的連接,並從連接池刪除。

closeExpiredConnections() 該方法關閉超過連接保持時間的連接,並從池中移除。 closeIdleConnections(timeout,tunit) 該方法關閉空閑時間超過timeout的連接,空閑時間從交還給連接池時開始,不管是否已過期,超過空閑時間則關閉。 下面是httpClient官方給出的清理過期、空閑連接的例子
public static class IdleConnectionMonitorThread extends Thread {
     
    private final ClientConnectionManager connMgr;
    private volatile boolean shutdown;
     
    public IdleConnectionMonitorThread(ClientConnectionManager connMgr) {
        super();
        this.connMgr = connMgr;
    }
 
    @Override
    public void run() {
        try {
            while (!shutdown) {
                synchronized (this) {
                    wait(5000);
                    // Close expired connections
                    connMgr.closeExpiredConnections();
                    // Optionally, close connections
                    // that have been idle longer than 30 sec
                    connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
                }
            }
        } catch (InterruptedException ex) {
            // terminate
        }
    }
     
    public void shutdown() {
        shutdown = true;
        synchronized (this) {
            notifyAll();
        }
    }
}

  

三、如何設置合理的參數

關於設置合理的參數,這個說起來真的不是一個簡單的話題,需要考慮的方面也聽到,是需要一定經驗的,這裏先簡單的說一下自己的理解,歡迎各位批評指教。 這裏主要涉及兩部分參數:連接數相關參數、超時時間相關參數

1、連接數相關參數

根據“利爾特法則”可以得到簡單的公式: 技術分享圖片
簡單地說,利特爾法則解釋了這三種變量的關系:L—系統裏的請求數量、λ—請求到達的速率、W—每個請求的處理時間。例如,如果每秒10個請求到達,處理一個請求需要1秒,那麽系統在每個時刻都有10個請求在處理。如果處理每個請求的時間翻倍,那麽系統每時刻需要處理的請求數也翻倍為20,因此需要20個線程。連接池的大小可以參考 L。 qps指標可以作為“λ—請求到達的速率”,由於httpClient是作為http客戶端,故需要通過一些監控手段得到服務端集群訪問量較高時的qps,如客戶端集群為4臺,服務端集群為2臺,監控到每臺服務端機器的qps為100,如果每個請求處理時間為1秒,那麽2臺服務端每個時刻總共有 100 * 2 * 1s = 200 個請求訪問,平均到4臺客戶端機器,每臺要負責50,即每臺客戶端的連接池大小可以設置為50。 當然實際的情況是更復雜的,上面的請求平均處理時間1秒只是一種業務的,實際情況的業務情況更多,評估請求平均處理時間更復雜。所以在設置連接數後,最好通過比較充分性能測試驗證是否可以滿足要求。 還有一些Linux系統級的配置需要考慮,如單個進程能夠打開的最大文件描述符數量open files默認為1024,每個與服務端建立的連接都需要占用一個文件描述符,如果open files值太小會影響建立連接。 還要註意,連接數主要包含maxTotal-連接總數、maxPerRoute-路由最大連接數,尤其是maxPerRoute默認值為2,很小,設置不好的話即使maxTotal再大也無法充分利用連接池。

2、超時時間相關參數

connectTimeout -- 連接超時時間 根據網絡情況,內網、外網等,可設置連接超時時間為2秒,具體根據業務調整 socketTimeout -- 讀超時時間(等待數據超時時間) 需要根據具體請求的業務而定,如請求的API接口從接到請求到返回數據的平均處理時間為1秒,那麽讀超時時間可以設置為2秒,考慮並發量較大的情況,也可以通過性能測試得到一個相對靠譜的值。 socketTimeout有默認值,也可以針對每個請求單獨設置。 connectionRequestTimeout -- 從池中獲取連接超時時間 建議設置500ms即可,不要設置太大,這樣可以使連接池連接不夠時不用等待太久去獲取連接,不要讓大量請求堆積在獲取連接處,盡快拋出異常,發現問題。 參考資料: httpClient 4.3.x configuration 官方樣例 使用httpclient必須知道的參數設置及代碼寫法、存在的風險 HttpClient連接池的連接保持、超時和失效機制 HttpClient連接池原理及一次連接時序圖

HttpClient 4.3連接池參數配置及源碼解讀