乡下人产国偷v产偷v自拍,国产午夜片在线观看,婷婷成人亚洲综合国产麻豆,久久综合给合久久狠狠狠9

  • <output id="e9wm2"></output>
    <s id="e9wm2"><nobr id="e9wm2"><ins id="e9wm2"></ins></nobr></s>

    • 分享

      HttpClient 4.3連接池參數(shù)配置及源碼解讀

       liang1234_ 2019-04-08

      目前所在公司使用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ù)含義

      1. import java.io.IOException;
      2. import java.io.InputStream;
      3. import java.io.InterruptedIOException;
      4. import java.net.UnknownHostException;
      5. import java.nio.charset.CodingErrorAction;
      6. import javax.net.ssl.SSLException;
      7. import org.apache.http.Consts;
      8. import org.apache.http.HttpEntity;
      9. import org.apache.http.HttpEntityEnclosingRequest;
      10. import org.apache.http.HttpHost;
      11. import org.apache.http.HttpRequest;
      12. import org.apache.http.client.HttpRequestRetryHandler;
      13. import org.apache.http.client.config.RequestConfig;
      14. import org.apache.http.client.methods.CloseableHttpResponse;
      15. import org.apache.http.client.methods.HttpGet;
      16. import org.apache.http.client.protocol.HttpClientContext;
      17. import org.apache.http.config.ConnectionConfig;
      18. import org.apache.http.config.MessageConstraints;
      19. import org.apache.http.config.SocketConfig;
      20. import org.apache.http.conn.ConnectTimeoutException;
      21. import org.apache.http.conn.routing.HttpRoute;
      22. import org.apache.http.impl.client.CloseableHttpClient;
      23. import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
      24. import org.apache.http.impl.client.HttpClients;
      25. import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
      26. import org.apache.http.protocol.HttpContext;
      27. public class HttpClientParamTest {
      28. public static void main(String[] args) {
      29. /**
      30. * 創(chuàng)建連接管理器,并設(shè)置相關(guān)參數(shù)
      31. */
      32. //連接管理器,使用無慘構(gòu)造
      33. PoolingHttpClientConnectionManager connManager
      34. = new PoolingHttpClientConnectionManager();
      35. /**
      36. * 連接數(shù)相關(guān)設(shè)置
      37. */
      38. //最大連接數(shù)
      39. connManager.setMaxTotal(200);
      40. //默認(rèn)的每個(gè)路由的最大連接數(shù)
      41. connManager.setDefaultMaxPerRoute(100);
      42. //設(shè)置到某個(gè)路由的最大連接數(shù),會(huì)覆蓋defaultMaxPerRoute
      43. connManager.setMaxPerRoute(new HttpRoute(new HttpHost("somehost", 80)), 150);
      44. /**
      45. * socket配置(默認(rèn)配置 和 某個(gè)host的配置)
      46. */
      47. SocketConfig socketConfig = SocketConfig.custom()
      48. .setTcpNoDelay(true) //是否立即發(fā)送數(shù)據(jù),設(shè)置為true會(huì)關(guān)閉Socket緩沖,默認(rèn)為false
      49. .setSoReuseAddress(true) //是否可以在一個(gè)進(jìn)程關(guān)閉Socket后,即使它還沒有釋放端口,其它進(jìn)程還可以立即重用端口
      50. .setSoTimeout(500) //接收數(shù)據(jù)的等待超時(shí)時(shí)間,單位ms
      51. .setSoLinger(60) //關(guān)閉Socket時(shí),要么發(fā)送完所有數(shù)據(jù),要么等待60s后,就關(guān)閉連接,此時(shí)socket.close()是阻塞的
      52. .setSoKeepAlive(true) //開啟監(jiān)視TCP連接是否有效
      53. .build();
      54. connManager.setDefaultSocketConfig(socketConfig);
      55. connManager.setSocketConfig(new HttpHost("somehost", 80), socketConfig);
      56. /**
      57. * HTTP connection相關(guān)配置(默認(rèn)配置 和 某個(gè)host的配置)
      58. * 一般不修改HTTP connection相關(guān)配置,故不設(shè)置
      59. */
      60. //消息約束
      61. MessageConstraints messageConstraints = MessageConstraints.custom()
      62. .setMaxHeaderCount(200)
      63. .setMaxLineLength(2000)
      64. .build();
      65. //Http connection相關(guān)配置
      66. ConnectionConfig connectionConfig = ConnectionConfig.custom()
      67. .setMalformedInputAction(CodingErrorAction.IGNORE)
      68. .setUnmappableInputAction(CodingErrorAction.IGNORE)
      69. .setCharset(Consts.UTF_8)
      70. .setMessageConstraints(messageConstraints)
      71. .build();
      72. //一般不修改HTTP connection相關(guān)配置,故不設(shè)置
      73. //connManager.setDefaultConnectionConfig(connectionConfig);
      74. //connManager.setConnectionConfig(new HttpHost("somehost", 80), ConnectionConfig.DEFAULT);
      75. /**
      76. * request請(qǐng)求相關(guān)配置
      77. */
      78. RequestConfig defaultRequestConfig = RequestConfig.custom()
      79. .setConnectTimeout(2 * 1000) //連接超時(shí)時(shí)間
      80. .setSocketTimeout(2 * 1000) //讀超時(shí)時(shí)間(等待數(shù)據(jù)超時(shí)時(shí)間)
      81. .setConnectionRequestTimeout(500) //從池中獲取連接超時(shí)時(shí)間
      82. .setStaleConnectionCheckEnabled(true)//檢查是否為陳舊的連接,默認(rèn)為true,類似testOnBorrow
      83. .build();
      84. /**
      85. * 重試處理
      86. * 默認(rèn)是重試3次
      87. */
      88. //禁用重試(參數(shù):retryCount、requestSentRetryEnabled)
      89. HttpRequestRetryHandler requestRetryHandler = new DefaultHttpRequestRetryHandler(0, false);
      90. //自定義重試策略
      91. HttpRequestRetryHandler myRetryHandler = new HttpRequestRetryHandler() {
      92. public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
      93. //Do not retry if over max retry count
      94. if (executionCount >= 3) {
      95. return false;
      96. }
      97. //Timeout
      98. if (exception instanceof InterruptedIOException) {
      99. return false;
      100. }
      101. //Unknown host
      102. if (exception instanceof UnknownHostException) {
      103. return false;
      104. }
      105. //Connection refused
      106. if (exception instanceof ConnectTimeoutException) {
      107. return false;
      108. }
      109. //SSL handshake exception
      110. if (exception instanceof SSLException) {
      111. return false;
      112. }
      113. HttpClientContext clientContext = HttpClientContext.adapt(context);
      114. HttpRequest request = clientContext.getRequest();
      115. boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
      116. //Retry if the request is considered idempotent
      117. //如果請(qǐng)求類型不是HttpEntityEnclosingRequest,被認(rèn)為是冪等的,那么就重試
      118. //HttpEntityEnclosingRequest指的是有請(qǐng)求體的request,比HttpRequest多一個(gè)Entity屬性
      119. //而常用的GET請(qǐng)求是沒有請(qǐng)求體的,POST、PUT都是有請(qǐng)求體的
      120. //Rest一般用GET請(qǐng)求獲取數(shù)據(jù),故冪等,POST用于新增數(shù)據(jù),故不冪等
      121. if (idempotent) {
      122. return true;
      123. }
      124. return false;
      125. }
      126. };
      127. /**
      128. * 創(chuàng)建httpClient
      129. */
      130. CloseableHttpClient httpclient = HttpClients.custom()
      131. .setConnectionManager(connManager) //連接管理器
      132. .setProxy(new HttpHost("myproxy", 8080)) //設(shè)置代理
      133. .setDefaultRequestConfig(defaultRequestConfig) //默認(rèn)請(qǐng)求配置
      134. .setRetryHandler(myRetryHandler) //重試策略
      135. .build();
      136. //創(chuàng)建一個(gè)Get請(qǐng)求,并重新設(shè)置請(qǐng)求參數(shù),覆蓋默認(rèn)
      137. HttpGet httpget = new HttpGet("http://www./");
      138. RequestConfig requestConfig = RequestConfig.copy(defaultRequestConfig)
      139. .setSocketTimeout(5000)
      140. .setConnectTimeout(5000)
      141. .setConnectionRequestTimeout(5000)
      142. .setProxy(new HttpHost("myotherproxy", 8080))
      143. .build();
      144. httpget.setConfig(requestConfig);
      145. CloseableHttpResponse response = null;
      146. try {
      147. //執(zhí)行請(qǐng)求
      148. response = httpclient.execute(httpget);
      149. HttpEntity entity = response.getEntity();
      150. // If the response does not enclose an entity, there is no need
      151. // to bother about connection release
      152. if (entity != null) {
      153. InputStream instream = entity.getContent();
      154. try {
      155. instream.read();
      156. // do something useful with the response
      157. }
      158. catch (IOException ex) {
      159. // In case of an IOException the connection will be released
      160. // back to the connection manager automatically
      161. throw ex;
      162. }
      163. finally {
      164. // Closing the input stream will trigger connection release
      165. // 釋放連接回到連接池
      166. instream.close();
      167. }
      168. }
      169. }
      170. catch (Exception e) {
      171. e.printStackTrace();
      172. }
      173. finally{
      174. if(response != null){
      175. try {
      176. //關(guān)閉連接(如果已經(jīng)釋放連接回連接池,則什么也不做)
      177. response.close();
      178. } catch (IOException e) {
      179. e.printStackTrace();
      180. }
      181. }
      182. if(httpclient != null){
      183. try {
      184. //關(guān)閉連接管理器,并會(huì)關(guān)閉其管理的連接
      185. httpclient.close();
      186. } catch (IOException e) {
      187. e.printStackTrace();
      188. }
      189. }
      190. }
      191. }
      192. }
          上面的代碼參考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

      1. /**
      2. * 創(chuàng)建httpClient
      3. */
      4. CloseableHttpClient httpclient = HttpClients.custom()
      5. .setConnectionManager(connManager) //連接管理器
      6. .setDefaultRequestConfig(defaultRequestConfig) //默認(rèn)請(qǐng)求配置
      7. .setRetryHandler(myRetryHandler) //重試策略
      8. .build();

          創(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í)行鏈條為:

      1. HttpRequestExecutor //發(fā)送請(qǐng)求報(bào)文,并接收響應(yīng)信息
      2. MainClientExec(requestExec, connManager, ...) //main Executor,負(fù)責(zé)連接管理相關(guān)
      3. ProtocolExec(execChain, httpprocessor) //HTTP協(xié)議封裝
      4. RetryExec(execChain, retryHandler) //重試策略
      5. 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)圖如下:

      6f3717d34737_thumb2

      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)用保持連接的邏輯,如下:

      1. // The connection is in or can be brought to a re-usable state.
      2. // 根據(jù)response頭中的信息判斷是否保持連接
      3. if (reuseStrategy.keepAlive(response, context)) {
      4. // Set the idle duration of this connection
      5. // 根據(jù)response頭中的keep-alive中的timeout屬性,得到連接可以保持的時(shí)間(ms)
      6. final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
      7. if (this.log.isDebugEnabled()) {
      8. final String s;
      9. if (duration > 0) {
      10. s = "for " + duration + " " + TimeUnit.MILLISECONDS;
      11. } else {
      12. s = "indefinitely";
      13. }
      14. this.log.debug("Connection can be kept alive " + s);
      15. }
      16. //設(shè)置連接保持時(shí)間,最終是調(diào)用 PoolEntry#updateExpiry
      17. connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);
      18. connHolder.markReusable(); //設(shè)置連接reuse=true
      19. }
      20. else {
      21. connHolder.markNonReusable();
      22. }

      連接是否保持

      客戶端如果希望保持長連接,應(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ū)別

      1. /**
      2. * This example demonstrates the recommended way of using API to make sure
      3. * the underlying connection gets released back to the connection manager.
      4. */
      5. public class ClientConnectionRelease {
      6. public final static void main(String[] args) throws Exception {
      7. CloseableHttpClient httpclient = HttpClients.createDefault();
      8. try {
      9. HttpGet httpget = new HttpGet("http://localhost/");
      10. System.out.println("Executing request " + httpget.getRequestLine());
      11. CloseableHttpResponse response = httpclient.execute(httpget);
      12. try {
      13. System.out.println("----------------------------------------");
      14. System.out.println(response.getStatusLine());
      15. // Get hold of the response entity
      16. HttpEntity entity = response.getEntity();
      17. // If the response does not enclose an entity, there is no need
      18. // to bother about connection release
      19. if (entity != null) {
      20. InputStream instream = entity.getContent();
      21. try {
      22. instream.read();
      23. // do something useful with the response
      24. } catch (IOException ex) {
      25. // In case of an IOException the connection will be released
      26. // back to the connection manager automatically
      27. throw ex;
      28. } finally {
      29. // Closing the input stream will trigger connection release
      30. instream.close();
      31. }
      32. }
      33. } finally {
      34. response.close();
      35. }
      36. } finally {
      37. httpclient.close();
      38. }
      39. }
      40. }

      在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é)束

      1. @Override
      2. public int read(final byte[] b, final int off, final int len) throws IOException {
      3. int l = -1;
      4. if (isReadAllowed()) {
      5. try {
      6. l = wrappedStream.read(b, off, len);
      7. checkEOF(l);
      8. } catch (final IOException ex) {
      9. checkAbort();
      10. throw ex;
      11. }
      12. }
      13. return l;
      14. }

      在EofSensorInputStream#checkEOF()方法中如果eof=-1,流已經(jīng)讀完,如果連接可重用,就會(huì)嘗試釋放連接,否則關(guān)閉連接。

      1. protected void checkEOF(final int eof) throws IOException {
      2. if ((wrappedStream != null) && (eof < 0)) {
      3. try {
      4. boolean scws = true; // should close wrapped stream?
      5. if (eofWatcher != null) {
      6. scws = eofWatcher.eofDetected(wrappedStream);
      7. }
      8. if (scws) {
      9. wrappedStream.close();
      10. }
      11. } finally {
      12. wrappedStream = null;
      13. }
      14. }
      15. }

      ResponseEntityWrapper#eofDetected

      1. public boolean eofDetected(final InputStream wrapped) throws IOException {
      2. try {
      3. // there may be some cleanup required, such as
      4. // reading trailers after the response body:
      5. wrapped.close();
      6. releaseConnection(); //釋放連接 或 關(guān)閉連接
      7. } finally {
      8. cleanup();
      9. }
      10. return false;
      11. }

      ConnectionHolder#releaseConnection

      1. public void releaseConnection() {
      2. synchronized (this.managedConn) {
      3. //如果連接已經(jīng)釋放,直接返回
      4. if (this.released) {
      5. return;
      6. }
      7. this.released = true;
      8. //連接可重用,釋放回連接池
      9. if (this.reusable) {
      10. this.manager.releaseConnection(this.managedConn,
      11. this.state, this.validDuration, this.tunit);
      12. }
      13. //不可重用,關(guān)閉連接
      14. else {
      15. try {
      16. this.managedConn.close();
      17. log.debug("Connection discarded");
      18. } catch (final IOException ex) {
      19. if (this.log.isDebugEnabled()) {
      20. this.log.debug(ex.getMessage(), ex);
      21. }
      22. } finally {
      23. this.manager.releaseConnection(
      24. this.managedConn, null, 0, TimeUnit.MILLISECONDS);
      25. }
      26. }
      27. }
      28. }

       

      如果沒有instream.read()讀取數(shù)據(jù),在instream.close()時(shí)會(huì)調(diào)用EofSensorInputStream#checkClose(),也會(huì)有類似上面的邏輯。

      所以就如官方例子注釋的一樣,在正常操作輸入流后,會(huì)釋放連接。

       

      response.close()

      最終是調(diào)用ConnectionHolder#abortConnection()

      1. public void abortConnection() {
      2. synchronized (this.managedConn) {
      3. //如果連接已經(jīng)釋放,直接返回
      4. if (this.released) {
      5. return;
      6. }
      7. this.released = true;
      8. try {
      9. //關(guān)閉連接
      10. this.managedConn.shutdown();
      11. log.debug("Connection discarded");
      12. } catch (final IOException ex) {
      13. if (this.log.isDebugEnabled()) {
      14. this.log.debug(ex.getMessage(), ex);
      15. }
      16. } finally {
      17. this.manager.releaseConnection(
      18. this.managedConn, null, 0, TimeUnit.MILLISECONDS);
      19. }
      20. }
      21. }

      所以,如果在調(diào)用response.close()之前,沒有讀取過輸入流,也沒有關(guān)閉輸入流,那么連接沒有被釋放,released=false,就會(huì)關(guān)閉連接。

       

      httpClient.close()

      最終調(diào)用的是InternalHttpClient#close(),會(huì)關(guān)閉整個(gè)連接管理器,并關(guān)閉連接池中所有連接。

      1. public void close() {
      2. this.connManager.shutdown();
      3. if (this.closeables != null) {
      4. for (final Closeable closeable: this.closeables) {
      5. try {
      6. closeable.close();
      7. } catch (final IOException ex) {
      8. this.log.error(ex.getMessage(), ex);
      9. }
      10. }
      11. }
      12. }

       

      總結(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,之后再次嘗試獲取連接。代碼如下

      1. /**AbstractConnPool#getPoolEntryBlocking()*/
      2. for (;;) {
      3. //從availabe鏈表頭迭代查找符合state的entry
      4. entry = pool.getFree(state);
      5. //找不到entry,跳出
      6. if (entry == null) {
      7. break;
      8. }
      9. //如果entry已關(guān)閉或已過期,關(guān)閉entry,并從routeToPool、httpConnPool的available隊(duì)列移除
      10. if (entry.isClosed() || entry.isExpired(System.currentTimeMillis())) {
      11. entry.close();
      12. this.available.remove(entry);
      13. pool.free(entry, false);
      14. }
      15. else { //找到可用連接
      16. break;
      17. }
      18. }

      2、底層連接已被關(guān)閉

      在連接池保持連接的時(shí)候,可能會(huì)出現(xiàn)連接已經(jīng)被服務(wù)端關(guān)閉的情況,而此時(shí)連接的客戶端并沒有阻塞著去接收服務(wù)端的數(shù)據(jù),所以客戶端不知道連接已關(guān)閉,無法關(guān)閉自身的socket。

      對(duì)于這種情況,在從連接池獲取可用連接時(shí)無法知曉,在獲取到可用連接后,如果連接是打開的,會(huì)有判斷連接是否陳舊的邏輯,如下

      1. /**MainClientExec#execute()*/
      2. if (config.isStaleConnectionCheckEnabled()) {
      3. // validate connection
      4. if (managedConn.isOpen()) {
      5. this.log.debug("Stale connection check");
      6. if (managedConn.isStale()) {
      7. this.log.debug("Stale connection detected");
      8. managedConn.close();
      9. }
      10. }
      11. }

      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,連接還可用。

      1. /**BHttpConnectionBase#isStale()*/
      2. public boolean isStale() {
      3. if (!isOpen()) {
      4. return true;
      5. }
      6. try {
      7. final int bytesRead = fillInputBuffer(1);
      8. return bytesRead < 0;
      9. } catch (final SocketTimeoutException ex) {
      10. return false;
      11. } catch (final IOException ex) {
      12. return true;
      13. }
      14. }

      如果在整個(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官方給出的清理過期、空閑連接的例子

      1. public static class IdleConnectionMonitorThread extends Thread {
      2. private final ClientConnectionManager connMgr;
      3. private volatile boolean shutdown;
      4. public IdleConnectionMonitorThread(ClientConnectionManager connMgr) {
      5. super();
      6. this.connMgr = connMgr;
      7. }
      8. @Override
      9. public void run() {
      10. try {
      11. while (!shutdown) {
      12. synchronized (this) {
      13. wait(5000);
      14. // Close expired connections
      15. connMgr.closeExpiredConnections();
      16. // Optionally, close connections
      17. // that have been idle longer than 30 sec
      18. connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
      19. }
      20. }
      21. } catch (InterruptedException ex) {
      22. // terminate
      23. }
      24. }
      25. public void shutdown() {
      26. shutdown = true;
      27. synchronized (this) {
      28. notifyAll();
      29. }
      30. }
      31. }

       

      三、如何設(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)單的公式:

      bb1dddfc6ee63

      簡(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í)序圖

        本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
        轉(zhuǎn)藏 分享 獻(xiàn)花(0

        0條評(píng)論

        發(fā)表

        請(qǐng)遵守用戶 評(píng)論公約

        類似文章 更多