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

    目前所在公司使用HttpClient 4.3.3版本發送Rest請求,調用接口。最近出現了調用查詢接口服務慢的生產問題,在排查整個調用鏈可能存在的問題時(從客戶端發起Http請求->ESB->服務端處理請求,查詢數據並返回),發現本來的HttpClient鏈接池中的一些參數配置可能存在問題,如defaultMaxPerRoute、一些timeout時間的設置等,雖不能肯定是因爲此鏈接池致使接口查詢慢,但確實存在可優化的地方,故花時間作一些研究。本文主要涉及HttpClient鏈接池、請求的參數配置,使用及源碼解讀。html

 

    如下是本文的目錄大綱:java

    1、HttpClient鏈接池、請求參數含義apache

    2、執行原理及源碼解讀c#

        一、建立HttpClient,執行request服務器

        二、鏈接池管理網絡

            2.一、鏈接池結構併發

            2.二、分配鏈接 & 創建鏈接app

            2.三、回收鏈接 & 保持鏈接異步

            2.四、instream.close()、response.close()、httpclient.close()的區別socket

            2.五、過時和空閒鏈接清理

    3、如何設置合理的參數

 

1、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)是比較重要的。

    鏈接數配置有問題就可能產生總的 鏈接數不夠 或者 到某個路由的鏈接數過小 的問題,咱們公司一些項目總鏈接數800,而defaultMaxPerRoute僅爲20,這樣致使真正須要比較多鏈接數,訪問量比較大的路由也僅能從鏈接池中獲取最大20個鏈接,應該在默認的基礎上,針對訪問量大的路由單獨設置。

    鏈接超時時間,讀超時時間,從池中獲取鏈接的超時時間若是不設置或者設置的太大,可能致使當業務高峯時,服務端響應較慢 或 鏈接池中確實沒有空閒鏈接時,不可以及時將timeout異常拋出來,致使等待讀取數據的,或者等待從池中獲取鏈接的越積越多,像滾雪球同樣,致使相關業務都開始變得緩慢,而若是配置合理的超時時間就能夠及時拋出異常,發現問題。

    後面會盡可能去闡述這些重要參數的原理以及如何配置一個合適的值。

 

2、執行原理及源碼解讀

一、建立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.一、鏈接池結構

鏈接池結構圖以下:

6f3717d34737_thumb2

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.二、分配鏈接 & 創建鏈接

分配鏈接

分配鏈接指的是從鏈接池獲取可用的PoolEntry,大體過程爲:

一、獲取route對應鏈接池routeToPool中可用的鏈接,有則返回該鏈接,若沒有則轉入下一步;

二、若routeToPool和外層HttpConnPool鏈接池均還有可用的空間,則新建鏈接,並將該鏈接做爲可用鏈接返回,不然進行下一步;

三、掛起當前線程,將當前線程的Future放入pending隊列,等待後續喚醒執行;

整個分配鏈接的過程採用了異步操做,只在前兩步時鎖住鏈接池,一旦發現沒法獲取鏈接則釋放鎖,等待後續繼續獲取鏈接。

創建鏈接

當分配到PoolEntry鏈接實體後,會調用establishRoute(),創建socket鏈接並與conn綁定。

 

2.三、回收鏈接 & 保持鏈接

回收鏈接

鏈接用完以後鏈接池須要進行回收(AbstractConnPool#release()),具體流程以下:
一、若當前鏈接標記爲重用,則將該鏈接從routeToPool中的leased集合刪除,並添加至available隊首,一樣的將該請求從外層httpConnPool的leased集合刪除,並添加至其available隊首。同時喚醒該routeToPool的pending隊列的第一個PoolEntryFuture,將其從pending隊列刪除,並將其從外層httpConnPool的pending隊列中刪除。
二、若鏈接沒有標記爲重用,則分別從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字段默認保持)。根據服務器的響應來肯定是否保持長鏈接,判斷原則以下:

一、檢查返回response報文頭的Transfer-Encoding字段,若該字段值存在且不爲chunked,則鏈接不保持,直接關閉。其餘狀況進入下一步;
二、檢查返回的response報文頭的Content-Length字段,若該字段值爲空或者格式不正確(多個長度,值不是整數)或者小於0,則鏈接不保持,直接關閉。其餘狀況進入下一步
三、檢查返回的response報文頭的connection字段(若該字段不存在,則爲Proxy-Connection字段)值,若是字段存在,若字段值爲close 則鏈接不保持,直接關閉,若字段值爲keep-alive則鏈接標記爲保持。若是這倆字段都不存在,則http 1.1版本默認爲保持,將鏈接標記爲保持, 1.0版本默認爲鏈接不保持,直接關閉。

鏈接保持時間

鏈接交還至鏈接池時,若鏈接標記爲保持reuse=true,則將由鏈接管理器保持一段時間;若鏈接沒有標記爲保持,則直接從鏈接池中刪除並關閉entry。
鏈接保持時,會更新PoolEntry的expiry到期時間,計算邏輯爲:
一、若是response頭中的keep-alive字段中timeout屬性值存在且爲正值:newExpiry = 鏈接歸還至鏈接池時間System.currentTimeMillis() + timeout;
二、如timeout屬性值不存在或爲負值:newExpiry = Long.MAX_VALUE(無窮)
三、最後會和PoolEntry本來的expiry到期時間比較,選出一個最小值做爲新的到期時間。

 

2.四、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);
            }
        }
    }
}

 

總結:

一、使用鏈接池時,要正確釋放鏈接須要經過讀取輸入流 或者 instream.close()方式;

二、若是已經釋放鏈接,response.close()直接返回,不然會關閉鏈接;

三、httpClient.close()會關閉鏈接管理器,並關閉其中全部鏈接,謹慎使用。

 

2.五、過時和空閒鏈接清理

在鏈接池保持鏈接的這段時間,可能出現兩種致使鏈接過時或失效的狀況:

一、鏈接保持時間到期

每一個鏈接對象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;
    }
}

二、底層鏈接已被關閉

在鏈接池保持鏈接的時候,可能會出現鏈接已經被服務端關閉的狀況,而此時鏈接的客戶端並無阻塞着去接收服務端的數據,因此客戶端不知道鏈接已關閉,沒法關閉自身的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();
        }
    }
}

 

3、如何設置合理的參數

關於設置合理的參數,這個提及來真的不是一個簡單的話題,須要考慮的方面也聽到,是須要必定經驗的,這裏先簡單的說一下本身的理解,歡迎各位批評指教。

這裏主要涉及兩部分參數:鏈接數相關參數、超時時間相關參數

一、鏈接數相關參數

根據「利爾特法則」能夠獲得簡單的公式:

bb1dddfc6ee63

簡單地說,利特爾法則解釋了這三種變量的關係: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再大也沒法充分利用鏈接池。

二、超時時間相關參數

connectTimeout  --  鏈接超時時間

根據網絡狀況,內網、外網等,可設置鏈接超時時間爲2秒,具體根據業務調整

socketTimeout  --  讀超時時間(等待數據超時時間)

須要根據具體請求的業務而定,如請求的API接口從接到請求到返回數據的平均處理時間爲1秒,那麼讀超時時間能夠設置爲2秒,考慮併發量較大的狀況,也能夠經過性能測試獲得一個相對靠譜的值。

socketTimeout有默認值,也能夠針對每一個請求單獨設置。

connectionRequestTimeout  --  從池中獲取鏈接超時時間

建議設置500ms便可,不要設置太大,這樣可使鏈接池鏈接不夠時不用等待過久去獲取鏈接,不要讓大量請求堆積在獲取鏈接處,儘快拋出異常,發現問題。

 

參考資料:

httpClient 4.3.x configuration 官方樣例

使用httpclient必須知道的參數設置及代碼寫法、存在的風險

HttpClient鏈接池的鏈接保持、超時和失效機制

HttpClient鏈接池原理及一次鏈接時序圖

相關文章
相關標籤/搜索