HttpClient 4.3連線池引數配置及原始碼解讀
目前所在公司使用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連線池、請求引數含義
上面的程式碼參考httpClient 4.3.x的官方樣例,其實官方樣例中可配置的更多,我只將一些覺得平時常用的摘了出來,其實我們在實際使用中也是使用預設的 socketConfig 和 connectionConfig。具體引數含義請看註釋。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(); } } } } }
個人感覺在實際應用中連線數相關配置(如maxTotal、maxPerRoute),還有請求相關的超時時間設定(如connectionTimeout、socketTimeout、connectionRequestTimeout)是比較重要的。
連線數配置有問題就可能產生總的 連線數不夠 或者 到某個路由的連線數太小 的問題,我們公司一些專案總連線數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連線池原理及一次連線時序圖