目前所在公司使用HttpClient 4.3.3版本發(fā)送Rest請(qǐng)求,調(diào)用接口。最近出現(xiàn)了調(diào)用查詢接口服務(wù)慢的生產(chǎn)問題,在排查整個(gè)調(diào)用鏈可能存在的問題時(shí)(從客戶端發(fā)起Http請(qǐng)求->ESB->服務(wù)端處理請(qǐng)求,查詢數(shù)據(jù)并返回),發(fā)現(xiàn)原本的HttpClient連接池中的一些參數(shù)配置可能存在問題,如defaultMaxPerRoute、一些timeout時(shí)間的設(shè)置等,雖不能確定是由于此連接池導(dǎo)致接口查詢慢,但確實(shí)存在可優(yōu)化的地方,故花時(shí)間做一些研究。本文主要涉及HttpClient連接池、請(qǐng)求的參數(shù)配置,使用及源碼解讀。 以下是本文的目錄大綱: 一、HttpClient連接池、請(qǐng)求參數(shù)含義 二、執(zhí)行原理及源碼解讀 1、創(chuàng)建HttpClient,執(zhí)行request 2、連接池管理 2.1、連接池結(jié)構(gòu) 2.2、分配連接 & 建立連接 2.3、回收連接 & 保持連接 2.4、instream.close()、response.close()、httpclient.close()的區(qū)別 2.5、過期和空閑連接清理 三、如何設(shè)置合理的參數(shù) 一、HttpClient連接池、請(qǐng)求參數(shù)含義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) { * 創(chuàng)建連接管理器,并設(shè)置相關(guān)參數(shù) PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); * 連接數(shù)相關(guān)設(shè)置 connManager.setMaxTotal(200); //默認(rèn)的每個(gè)路由的最大連接數(shù) connManager.setDefaultMaxPerRoute(100); //設(shè)置到某個(gè)路由的最大連接數(shù),會(huì)覆蓋defaultMaxPerRoute connManager.setMaxPerRoute(new HttpRoute(new HttpHost("somehost", 80)), 150); * socket配置(默認(rèn)配置 和 某個(gè)host的配置) SocketConfig socketConfig = SocketConfig.custom() .setTcpNoDelay(true) //是否立即發(fā)送數(shù)據(jù),設(shè)置為true會(huì)關(guān)閉Socket緩沖,默認(rèn)為false .setSoReuseAddress(true) //是否可以在一個(gè)進(jìn)程關(guān)閉Socket后,即使它還沒有釋放端口,其它進(jìn)程還可以立即重用端口 .setSoTimeout(500) //接收數(shù)據(jù)的等待超時(shí)時(shí)間,單位ms .setSoLinger(60) //關(guān)閉Socket時(shí),要么發(fā)送完所有數(shù)據(jù),要么等待60s后,就關(guān)閉連接,此時(shí)socket.close()是阻塞的 .setSoKeepAlive(true) //開啟監(jiān)視TCP連接是否有效 connManager.setDefaultSocketConfig(socketConfig); connManager.setSocketConfig(new HttpHost("somehost", 80), socketConfig); * HTTP connection相關(guān)配置(默認(rèn)配置 和 某個(gè)host的配置) * 一般不修改HTTP connection相關(guān)配置,故不設(shè)置 MessageConstraints messageConstraints = MessageConstraints.custom() //Http connection相關(guān)配置 ConnectionConfig connectionConfig = ConnectionConfig.custom() .setMalformedInputAction(CodingErrorAction.IGNORE) .setUnmappableInputAction(CodingErrorAction.IGNORE) .setCharset(Consts.UTF_8) .setMessageConstraints(messageConstraints) //一般不修改HTTP connection相關(guān)配置,故不設(shè)置 //connManager.setDefaultConnectionConfig(connectionConfig); //connManager.setConnectionConfig(new HttpHost("somehost", 80), ConnectionConfig.DEFAULT); * request請(qǐng)求相關(guān)配置 RequestConfig defaultRequestConfig = RequestConfig.custom() .setConnectTimeout(2 * 1000) //連接超時(shí)時(shí)間 .setSocketTimeout(2 * 1000) //讀超時(shí)時(shí)間(等待數(shù)據(jù)超時(shí)時(shí)間) .setConnectionRequestTimeout(500) //從池中獲取連接超時(shí)時(shí)間 .setStaleConnectionCheckEnabled(true)//檢查是否為陳舊的連接,默認(rèn)為true,類似testOnBorrow //禁用重試(參數(shù):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) { if (exception instanceof InterruptedIOException) { if (exception instanceof UnknownHostException) { if (exception instanceof ConnectTimeoutException) { //SSL handshake exception if (exception instanceof SSLException) { HttpClientContext clientContext = HttpClientContext.adapt(context); HttpRequest request = clientContext.getRequest(); boolean idempotent = !(request instanceof HttpEntityEnclosingRequest); //Retry if the request is considered idempotent //如果請(qǐng)求類型不是HttpEntityEnclosingRequest,被認(rèn)為是冪等的,那么就重試 //HttpEntityEnclosingRequest指的是有請(qǐng)求體的request,比HttpRequest多一個(gè)Entity屬性 //而常用的GET請(qǐng)求是沒有請(qǐng)求體的,POST、PUT都是有請(qǐng)求體的 //Rest一般用GET請(qǐng)求獲取數(shù)據(jù),故冪等,POST用于新增數(shù)據(jù),故不冪等 CloseableHttpClient httpclient = HttpClients.custom() .setConnectionManager(connManager) //連接管理器 .setProxy(new HttpHost("myproxy", 8080)) //設(shè)置代理 .setDefaultRequestConfig(defaultRequestConfig) //默認(rèn)請(qǐng)求配置 .setRetryHandler(myRetryHandler) //重試策略 //創(chuàng)建一個(gè)Get請(qǐng)求,并重新設(shè)置請(qǐng)求參數(shù),覆蓋默認(rèn) HttpGet httpget = new HttpGet("http://www./"); RequestConfig requestConfig = RequestConfig.copy(defaultRequestConfig) .setConnectionRequestTimeout(5000) .setProxy(new HttpHost("myotherproxy", 8080)) httpget.setConfig(requestConfig); CloseableHttpResponse response = null; 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 InputStream instream = entity.getContent(); // do something useful with the response // In case of an IOException the connection will be released // back to the connection manager automatically // Closing the input stream will trigger connection release //關(guān)閉連接(如果已經(jīng)釋放連接回連接池,則什么也不做) } catch (IOException e) { //關(guān)閉連接管理器,并會(huì)關(guān)閉其管理的連接 } catch (IOException e) {
上面的代碼參考httpClient 4.3.x的官方樣例,其實(shí)官方樣例中可配置的更多,我只將一些覺得平時(shí)常用的摘了出來,其實(shí)我們?cè)趯?shí)際使用中也是使用默認(rèn)的 socketConfig 和 connectionConfig。具體參數(shù)含義請(qǐng)看注釋。 個(gè)人感覺在實(shí)際應(yīng)用中連接數(shù)相關(guān)配置(如maxTotal、maxPerRoute),還有請(qǐng)求相關(guān)的超時(shí)時(shí)間設(shè)置(如connectionTimeout、socketTimeout、connectionRequestTimeout)是比較重要的。 連接數(shù)配置有問題就可能產(chǎn)生總的 連接數(shù)不夠 或者 到某個(gè)路由的連接數(shù)太小 的問題,我們公司一些項(xiàng)目總連接數(shù)800,而defaultMaxPerRoute僅為20,這樣導(dǎo)致真正需要比較多連接數(shù),訪問量比較大的路由也僅能從連接池中獲取最大20個(gè)連接,應(yīng)該在默認(rèn)的基礎(chǔ)上,針對(duì)訪問量大的路由單獨(dú)設(shè)置。 連接超時(shí)時(shí)間,讀超時(shí)時(shí)間,從池中獲取連接的超時(shí)時(shí)間如果不設(shè)置或者設(shè)置的太大,可能導(dǎo)致當(dāng)業(yè)務(wù)高峰時(shí),服務(wù)端響應(yīng)較慢 或 連接池中確實(shí)沒有空閑連接時(shí),不能夠及時(shí)將timeout異常拋出來,導(dǎo)致等待讀取數(shù)據(jù)的,或者等待從池中獲取連接的越積越多,像滾雪球一樣,導(dǎo)致相關(guān)業(yè)務(wù)都開始變得緩慢,而如果配置合理的超時(shí)時(shí)間就可以及時(shí)拋出異常,發(fā)現(xiàn)問題。 后面會(huì)盡量去闡述這些重要參數(shù)的原理以及如何配置一個(gè)合適的值。 二、執(zhí)行原理及源碼解讀1、創(chuàng)建HttpClient,執(zhí)行request CloseableHttpClient httpclient = HttpClients.custom() .setConnectionManager(connManager) //連接管理器 .setDefaultRequestConfig(defaultRequestConfig) //默認(rèn)請(qǐng)求配置 .setRetryHandler(myRetryHandler) //重試策略
創(chuàng)建HttpClient的過程就是在設(shè)置了“連接管理器”、“請(qǐng)求相關(guān)配置”、“重試策略”后,調(diào)用 HttpClientBuilder.build()。 build()方法會(huì)根據(jù)設(shè)置的屬性不同,創(chuàng)建不同的Executor執(zhí)行器,如設(shè)置了retryHandler就會(huì) new RetryExec(execChain, retryHandler),相當(dāng)于retry Executor。當(dāng)然有些Executor是必須創(chuàng)建的,如MainClientExec、ProtocolExec。最后new InternalHttpClient(execChain, connManager, routePlanner …)并返回。 CloseableHttpResponse httpResponse = httpClient.execute(httpUriRequest);
HttpClient使用了責(zé)任鏈模式,所有Executor都實(shí)現(xiàn)了ClientExecChain接口的execute()方法,每個(gè)Executor都持有下一個(gè)要執(zhí)行的Executor的引用,這樣就會(huì)形成一個(gè)Executor的執(zhí)行鏈條,請(qǐng)求在這個(gè)鏈條上傳遞。按照上面的方式構(gòu)造的httpClient形成的執(zhí)行鏈條為: HttpRequestExecutor //發(fā)送請(qǐng)求報(bào)文,并接收響應(yīng)信息 MainClientExec(requestExec, connManager, ...) //main Executor,負(fù)責(zé)連接管理相關(guān) ProtocolExec(execChain, httpprocessor) //HTTP協(xié)議封裝 RetryExec(execChain, retryHandler) //重試策略 RedirectExec(execChain, routePlanner, redirectStrategy) //重定向
請(qǐng)求執(zhí)行是按照從下到上的順序(即每個(gè)下面的Executor都持有上面一個(gè)Executor的引用),每一個(gè)執(zhí)行器都會(huì)負(fù)責(zé)請(qǐng)求過程中的一部分工作,最終返回response。 2、連接池管理 2.1、連接池結(jié)構(gòu) 連接池結(jié)構(gòu)圖如下: 
PoolEntry<HttpRoute, ManagedHttpClientConnection> -- 連接池中的實(shí)體 包含ManagedHttpClientConnection連接; 連接的route路由信息; 以及連接存活時(shí)間相隔信息,如created(創(chuàng)建時(shí)間),updated(更新時(shí)間,釋放連接回連接池時(shí)會(huì)更新),validUnit(用于初始化expiry過期時(shí)間,規(guī)則是如果timeToLive>0,則為created+timeToLive,否則為Long.MAX_VALUE),expiry(過期時(shí)間,人為規(guī)定的連接池可以保有連接的時(shí)間,除了初始化時(shí)等于validUnit,每次釋放連接時(shí)也會(huì)更新,但是從newExpiry和validUnit取最小值)。timeToLive是在構(gòu)造連接池時(shí)指定的連接存活時(shí)間,默認(rèn)構(gòu)造的timeToLive=-1。 ManagedHttpClientConnection是httpClient連接,真正建立連接后,其會(huì)bind綁定一個(gè)socket,用于傳輸HTTP報(bào)文。 LinkedList<PoolEntry> available -- 存放可用連接 使用完后所有可重用的連接回被放到available鏈表頭部,之后再獲取連接時(shí)優(yōu)先從available鏈表頭部迭代可用的連接。 之所以使用LinkedList是利用了其隊(duì)列的特性,即可以在隊(duì)首和隊(duì)尾分別插入、刪除。入available鏈表時(shí)都是addFirst()放入頭部,獲取時(shí)都是從頭部依次迭代可用的連接,這樣可以獲取到最新放入鏈表的連接,其離過期時(shí)間更遠(yuǎn)(這種策略可以盡量保證獲取到的連接沒有過期,而從隊(duì)尾獲取連接是可以做到在連接過期前盡量使用,但獲取到過期連接的風(fēng)險(xiǎn)就大了),刪除available鏈表中連接時(shí)是從隊(duì)尾開始,即先刪除最可能快要過期的連接。 HashSet<PoolEntry> leased -- 存放被租用的連接 所有正在被使用的連接存放的集合,只涉及 add() 和 remove() 操作。 maxTotal限制的是外層httpConnPool中l(wèi)eased集合和available隊(duì)列的總和的大小,leased和available的大小沒有單獨(dú)限制。 LinkedList<PoolEntryFuture> pending -- 存放等待獲取連接的線程的Future 當(dāng)從池中獲取連接時(shí),如果available鏈表沒有現(xiàn)成可用的連接,且當(dāng)前路由或連接池已經(jīng)達(dá)到了最大數(shù)量的限制,也不能創(chuàng)建連接了,此時(shí)不會(huì)阻塞整個(gè)連接池,而是將當(dāng)前線程用于獲取連接的Future放入pending鏈表的末尾,之后當(dāng)前線程調(diào)用await(),釋放持有的鎖,并等待被喚醒。 當(dāng)有連接被release()釋放回連接池時(shí),會(huì)從pending鏈表頭獲取future,并喚醒其線程繼續(xù)獲取連接,做到了先進(jìn)先出。 routeToPool -- 每個(gè)路由對(duì)應(yīng)的pool 也有針對(duì)當(dāng)前路由的available、leased、pending集合,與整個(gè)池的隔離。 maxPerRoute限制的是routeToPool中l(wèi)eased集合和available隊(duì)列的總和的大小。 2.2、分配連接 & 建立連接 分配連接 分配連接指的是從連接池獲取可用的PoolEntry,大致過程為: 1、獲取route對(duì)應(yīng)連接池routeToPool中可用的連接,有則返回該連接,若沒有則轉(zhuǎn)入下一步; 2、若routeToPool和外層HttpConnPool連接池均還有可用的空間,則新建連接,并將該連接作為可用連接返回,否則進(jìn)行下一步; 3、掛起當(dāng)前線程,將當(dāng)前線程的Future放入pending隊(duì)列,等待后續(xù)喚醒執(zhí)行; 整個(gè)分配連接的過程采用了異步操作,只在前兩步時(shí)鎖住連接池,一旦發(fā)現(xiàn)無法獲取連接則釋放鎖,等待后續(xù)繼續(xù)獲取連接。 建立連接 當(dāng)分配到PoolEntry連接實(shí)體后,會(huì)調(diào)用establishRoute(),建立socket連接并與conn綁定。 2.3、回收連接 & 保持連接 回收連接 連接用完之后連接池需要進(jìn)行回收(AbstractConnPool#release()),具體流程如下: 1、若當(dāng)前連接標(biāo)記為重用,則將該連接從routeToPool中的leased集合刪除,并添加至available隊(duì)首,同樣的將該請(qǐng)求從外層httpConnPool的leased集合刪除,并添加至其available隊(duì)首。同時(shí)喚醒該routeToPool的pending隊(duì)列的第一個(gè)PoolEntryFuture,將其從pending隊(duì)列刪除,并將其從外層httpConnPool的pending隊(duì)列中刪除。 2、若連接沒有標(biāo)記為重用,則分別從routeToPool和外層httpConnPool中刪除該連接,并關(guān)閉該連接。 保持連接 MainClientExec#execute()是負(fù)責(zé)連接管理的,在執(zhí)行完后續(xù)調(diào)用鏈,并得到response后,會(huì)調(diào)用保持連接的邏輯,如下: // The connection is in or can be brought to a re-usable state. // 根據(jù)response頭中的信息判斷是否保持連接 if (reuseStrategy.keepAlive(response, context)) { // Set the idle duration of this connection // 根據(jù)response頭中的keep-alive中的timeout屬性,得到連接可以保持的時(shí)間(ms) final long duration = keepAliveStrategy.getKeepAliveDuration(response, context); if (this.log.isDebugEnabled()) { s = "for " + duration + " " + TimeUnit.MILLISECONDS; this.log.debug("Connection can be kept alive " + s); //設(shè)置連接保持時(shí)間,最終是調(diào)用 PoolEntry#updateExpiry connHolder.setValidFor(duration, TimeUnit.MILLISECONDS); connHolder.markReusable(); //設(shè)置連接reuse=true connHolder.markNonReusable();
連接是否保持 客戶端如果希望保持長連接,應(yīng)該在發(fā)起請(qǐng)求時(shí)告訴服務(wù)器希望服務(wù)器保持長連接(http 1.0設(shè)置connection字段為keep-alive,http 1.1字段默認(rèn)保持)。根據(jù)服務(wù)器的響應(yīng)來確定是否保持長連接,判斷原則如下: 1、檢查返回response報(bào)文頭的Transfer-Encoding字段,若該字段值存在且不為chunked,則連接不保持,直接關(guān)閉。其他情況進(jìn)入下一步; 2、檢查返回的response報(bào)文頭的Content-Length字段,若該字段值為空或者格式不正確(多個(gè)長度,值不是整數(shù))或者小于0,則連接不保持,直接關(guān)閉。其他情況進(jìn)入下一步 3、檢查返回的response報(bào)文頭的connection字段(若該字段不存在,則為Proxy-Connection字段)值,如果字段存在,若字段值為close 則連接不保持,直接關(guān)閉,若字段值為keep-alive則連接標(biāo)記為保持。如果這倆字段都不存在,則http 1.1版本默認(rèn)為保持,將連接標(biāo)記為保持, 1.0版本默認(rèn)為連接不保持,直接關(guān)閉。 連接保持時(shí)間 連接交還至連接池時(shí),若連接標(biāo)記為保持reuse=true,則將由連接管理器保持一段時(shí)間;若連接沒有標(biāo)記為保持,則直接從連接池中刪除并關(guān)閉entry。 連接保持時(shí),會(huì)更新PoolEntry的expiry到期時(shí)間,計(jì)算邏輯為: 1、如果response頭中的keep-alive字段中timeout屬性值存在且為正值:newExpiry = 連接歸還至連接池時(shí)間System.currentTimeMillis() + timeout; 2、如timeout屬性值不存在或?yàn)樨?fù)值:newExpiry = Long.MAX_VALUE(無窮) 3、最后會(huì)和PoolEntry原本的expiry到期時(shí)間比較,選出一個(gè)最小值作為新的到期時(shí)間。 2.4、instream.close()、response.close()、httpclient.close()的區(qū)別 * 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(); HttpGet httpget = new HttpGet("http://localhost/"); System.out.println("Executing request " + httpget.getRequestLine()); CloseableHttpResponse response = httpclient.execute(httpget); 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 InputStream instream = entity.getContent(); // 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 // Closing the input stream will trigger connection release
在HttpClient Manual connection release的例子中可以看到,從內(nèi)層依次調(diào)用的是instream.close()、response.close()、httpClient.close(),那么它們有什么區(qū)別呢? instream.close() 在主動(dòng)操作輸入流,或者調(diào)用EntityUtils.toString(httpResponse.getEntity())時(shí)會(huì)調(diào)用instream.read()、instream.close()等方法。instream的實(shí)現(xiàn)類為org.apache.http.conn.EofSensorInputStream。 在每次通過instream.read()讀取數(shù)據(jù)流后,都會(huì)判斷流是否讀取結(jié)束 public int read(final byte[] b, final int off, final int len) throws IOException { l = wrappedStream.read(b, off, len); } catch (final IOException ex) {
在EofSensorInputStream#checkEOF()方法中如果eof=-1,流已經(jīng)讀完,如果連接可重用,就會(huì)嘗試釋放連接,否則關(guān)閉連接。 protected void checkEOF(final int eof) throws IOException { if ((wrappedStream != null) && (eof < 0)) { boolean scws = true; // should close wrapped stream? if (eofWatcher != null) { scws = eofWatcher.eofDetected(wrappedStream);
ResponseEntityWrapper#eofDetected public boolean eofDetected(final InputStream wrapped) throws IOException { // there may be some cleanup required, such as // reading trailers after the response body: releaseConnection(); //釋放連接 或 關(guān)閉連接
ConnectionHolder#releaseConnection public void releaseConnection() { synchronized (this.managedConn) { this.manager.releaseConnection(this.managedConn, this.state, this.validDuration, this.tunit); this.managedConn.close(); log.debug("Connection discarded"); } catch (final IOException ex) { if (this.log.isDebugEnabled()) { this.log.debug(ex.getMessage(), ex); this.manager.releaseConnection( this.managedConn, null, 0, TimeUnit.MILLISECONDS);
如果沒有instream.read()讀取數(shù)據(jù),在instream.close()時(shí)會(huì)調(diào)用EofSensorInputStream#checkClose(),也會(huì)有類似上面的邏輯。 所以就如官方例子注釋的一樣,在正常操作輸入流后,會(huì)釋放連接。 response.close() 最終是調(diào)用ConnectionHolder#abortConnection() public void abortConnection() { synchronized (this.managedConn) { this.managedConn.shutdown(); log.debug("Connection discarded"); } catch (final IOException ex) { if (this.log.isDebugEnabled()) { this.log.debug(ex.getMessage(), ex); this.manager.releaseConnection( this.managedConn, null, 0, TimeUnit.MILLISECONDS);
所以,如果在調(diào)用response.close()之前,沒有讀取過輸入流,也沒有關(guān)閉輸入流,那么連接沒有被釋放,released=false,就會(huì)關(guān)閉連接。 httpClient.close() 最終調(diào)用的是InternalHttpClient#close(),會(huì)關(guān)閉整個(gè)連接管理器,并關(guān)閉連接池中所有連接。 this.connManager.shutdown(); if (this.closeables != null) { for (final Closeable closeable: this.closeables) { } catch (final IOException ex) { this.log.error(ex.getMessage(), ex);
總結(jié): 1、使用連接池時(shí),要正確釋放連接需要通過讀取輸入流 或者 instream.close()方式; 2、如果已經(jīng)釋放連接,response.close()直接返回,否則會(huì)關(guān)閉連接; 3、httpClient.close()會(huì)關(guān)閉連接管理器,并關(guān)閉其中所有連接,謹(jǐn)慎使用。 2.5、過期和空閑連接清理 在連接池保持連接的這段時(shí)間,可能出現(xiàn)兩種導(dǎo)致連接過期或失效的情況: 1、連接保持時(shí)間到期 每個(gè)連接對(duì)象PoolEntry都有expiry到期時(shí)間,在創(chuàng)建和釋放歸還連接是都會(huì)為expiry到期時(shí)間賦值,在連接池保持連接的這段時(shí)間,連接已經(jīng)到了過期時(shí)間(注意,這個(gè)過期時(shí)間是為了管理連接所設(shè)定的,并不是指的TCP連接真的不能使用了)。 對(duì)于這種情況,在每次從連接池獲取連接時(shí),都會(huì)從routeToPool的available隊(duì)列獲取Entry并檢測(cè)此時(shí)Entry是否已關(guān)閉或者已過期,若是則關(guān)閉并分別從routeToPool、httpConnPool的available隊(duì)列移除該Entry,之后再次嘗試獲取連接。代碼如下 /**AbstractConnPool#getPoolEntryBlocking()*/ //從availabe鏈表頭迭代查找符合state的entry entry = pool.getFree(state); //如果entry已關(guān)閉或已過期,關(guān)閉entry,并從routeToPool、httpConnPool的available隊(duì)列移除 if (entry.isClosed() || entry.isExpired(System.currentTimeMillis())) { this.available.remove(entry);
2、底層連接已被關(guān)閉 在連接池保持連接的時(shí)候,可能會(huì)出現(xiàn)連接已經(jīng)被服務(wù)端關(guān)閉的情況,而此時(shí)連接的客戶端并沒有阻塞著去接收服務(wù)端的數(shù)據(jù),所以客戶端不知道連接已關(guān)閉,無法關(guān)閉自身的socket。 對(duì)于這種情況,在從連接池獲取可用連接時(shí)無法知曉,在獲取到可用連接后,如果連接是打開的,會(huì)有判斷連接是否陳舊的邏輯,如下 /**MainClientExec#execute()*/ if (config.isStaleConnectionCheckEnabled()) { if (managedConn.isOpen()) { this.log.debug("Stale connection check"); if (managedConn.isStale()) { this.log.debug("Stale connection detected");
isOpen()會(huì)通過連接的狀態(tài)判斷連接是否是open狀態(tài); isStale()會(huì)通過socket輸入流嘗試讀取數(shù)據(jù),在讀取前暫時(shí)將soTimeout設(shè)置為1ms,如果讀取到的字節(jié)數(shù)小于0,即已經(jīng)讀到了輸入流的末尾,或者發(fā)生了IOException,可能連接已經(jīng)關(guān)閉,那么isStale()返回true,需要關(guān)閉連接;如果讀到的字節(jié)數(shù)大于0,或者發(fā)生了SocketTimeoutException,可能是讀超時(shí),isStale()返回false,連接還可用。 /**BHttpConnectionBase#isStale()*/ public boolean isStale() { final int bytesRead = fillInputBuffer(1); } catch (final SocketTimeoutException ex) { } catch (final IOException ex) {
如果在整個(gè)判斷過程中發(fā)現(xiàn)連接是陳舊的,就會(huì)關(guān)閉連接,那么這個(gè)從連接池獲取的連接就是不可用的,后面的代碼邏輯里會(huì)重建當(dāng)前PoolEntry的socket連接,繼續(xù)后續(xù)請(qǐng)求邏輯。 后臺(tái)監(jiān)控線程檢查連接 上述過程是在從連接池獲取連接后,檢查連接是否可用,如不可用需重新建立socket連接,建立連接的過程是比較耗時(shí)的,可能導(dǎo)致性能問題,也失去了連接池的意義,針對(duì)這種情況,HttpClient采取一個(gè)策略,通過一個(gè)后臺(tái)的監(jiān)控線程定時(shí)的去檢查連接池中連接是否還“新鮮”,如果過期了,或者空閑了一定時(shí)間則就將其從連接池里刪除掉。 ClientConnectionManager提供了 closeExpiredConnections()和closeIdleConnections()兩個(gè)方法,關(guān)閉過期或空閑了一段時(shí)間的連接,并從連接池刪除。 closeExpiredConnections() 該方法關(guān)閉超過連接保持時(shí)間的連接,并從池中移除。 closeIdleConnections(timeout,tunit) 該方法關(guān)閉空閑時(shí)間超過timeout的連接,空閑時(shí)間從交還給連接池時(shí)開始,不管是否已過期,超過空閑時(shí)間則關(guān)閉。 下面是httpClient官方給出的清理過期、空閑連接的例子 public static class IdleConnectionMonitorThread extends Thread { private final ClientConnectionManager connMgr; private volatile boolean shutdown; public IdleConnectionMonitorThread(ClientConnectionManager connMgr) { // Close expired connections connMgr.closeExpiredConnections(); // Optionally, close connections // that have been idle longer than 30 sec connMgr.closeIdleConnections(30, TimeUnit.SECONDS); } catch (InterruptedException ex) {
三、如何設(shè)置合理的參數(shù)關(guān)于設(shè)置合理的參數(shù),這個(gè)說起來真的不是一個(gè)簡(jiǎn)單的話題,需要考慮的方面也聽到,是需要一定經(jīng)驗(yàn)的,這里先簡(jiǎn)單的說一下自己的理解,歡迎各位批評(píng)指教。 這里主要涉及兩部分參數(shù):連接數(shù)相關(guān)參數(shù)、超時(shí)時(shí)間相關(guān)參數(shù) 1、連接數(shù)相關(guān)參數(shù) 根據(jù)“利爾特法則”可以得到簡(jiǎn)單的公式: 
簡(jiǎn)單地說,利特爾法則解釋了這三種變量的關(guān)系:L—系統(tǒng)里的請(qǐng)求數(shù)量、λ—請(qǐng)求到達(dá)的速率、W—每個(gè)請(qǐng)求的處理時(shí)間。例如,如果每秒10個(gè)請(qǐng)求到達(dá),處理一個(gè)請(qǐng)求需要1秒,那么系統(tǒng)在每個(gè)時(shí)刻都有10個(gè)請(qǐng)求在處理。如果處理每個(gè)請(qǐng)求的時(shí)間翻倍,那么系統(tǒng)每時(shí)刻需要處理的請(qǐng)求數(shù)也翻倍為20,因此需要20個(gè)線程。連接池的大小可以參考 L。 qps指標(biāo)可以作為“λ—請(qǐng)求到達(dá)的速率”,由于httpClient是作為http客戶端,故需要通過一些監(jiān)控手段得到服務(wù)端集群訪問量較高時(shí)的qps,如客戶端集群為4臺(tái),服務(wù)端集群為2臺(tái),監(jiān)控到每臺(tái)服務(wù)端機(jī)器的qps為100,如果每個(gè)請(qǐng)求處理時(shí)間為1秒,那么2臺(tái)服務(wù)端每個(gè)時(shí)刻總共有 100 * 2 * 1s = 200 個(gè)請(qǐng)求訪問,平均到4臺(tái)客戶端機(jī)器,每臺(tái)要負(fù)責(zé)50,即每臺(tái)客戶端的連接池大小可以設(shè)置為50。 當(dāng)然實(shí)際的情況是更復(fù)雜的,上面的請(qǐng)求平均處理時(shí)間1秒只是一種業(yè)務(wù)的,實(shí)際情況的業(yè)務(wù)情況更多,評(píng)估請(qǐng)求平均處理時(shí)間更復(fù)雜。所以在設(shè)置連接數(shù)后,最好通過比較充分性能測(cè)試驗(yàn)證是否可以滿足要求。 還有一些Linux系統(tǒng)級(jí)的配置需要考慮,如單個(gè)進(jìn)程能夠打開的最大文件描述符數(shù)量open files默認(rèn)為1024,每個(gè)與服務(wù)端建立的連接都需要占用一個(gè)文件描述符,如果open files值太小會(huì)影響建立連接。 還要注意,連接數(shù)主要包含maxTotal-連接總數(shù)、maxPerRoute-路由最大連接數(shù),尤其是maxPerRoute默認(rèn)值為2,很小,設(shè)置不好的話即使maxTotal再大也無法充分利用連接池。 2、超時(shí)時(shí)間相關(guān)參數(shù) connectTimeout -- 連接超時(shí)時(shí)間 根據(jù)網(wǎng)絡(luò)情況,內(nèi)網(wǎng)、外網(wǎng)等,可設(shè)置連接超時(shí)時(shí)間為2秒,具體根據(jù)業(yè)務(wù)調(diào)整 socketTimeout -- 讀超時(shí)時(shí)間(等待數(shù)據(jù)超時(shí)時(shí)間) 需要根據(jù)具體請(qǐng)求的業(yè)務(wù)而定,如請(qǐng)求的API接口從接到請(qǐng)求到返回?cái)?shù)據(jù)的平均處理時(shí)間為1秒,那么讀超時(shí)時(shí)間可以設(shè)置為2秒,考慮并發(fā)量較大的情況,也可以通過性能測(cè)試得到一個(gè)相對(duì)靠譜的值。 socketTimeout有默認(rèn)值,也可以針對(duì)每個(gè)請(qǐng)求單獨(dú)設(shè)置。 connectionRequestTimeout -- 從池中獲取連接超時(shí)時(shí)間 建議設(shè)置500ms即可,不要設(shè)置太大,這樣可以使連接池連接不夠時(shí)不用等待太久去獲取連接,不要讓大量請(qǐng)求堆積在獲取連接處,盡快拋出異常,發(fā)現(xiàn)問題。 參考資料: httpClient 4.3.x configuration 官方樣例 使用httpclient必須知道的參數(shù)設(shè)置及代碼寫法、存在的風(fēng)險(xiǎn) HttpClient連接池的連接保持、超時(shí)和失效機(jī)制 HttpClient連接池原理及一次連接時(shí)序圖
|