【轉】【spring】HttpClient 4.3連接池參數配置及源碼解讀

轉自:http://www.cnblogs.com/trust-freedom/p/6349502.html

目前所在公司使用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連接池、請求參數含義

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

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異常拋出來,導致等待讀取數據的,或者等待從池中獲取連接的越積越多,像滾雪球一樣,導致相關業務都開始變得緩慢,而如果配置合理的超時時間就可以及時拋出異常,發現問題。

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

 

二、執行原理及源碼解讀

1、創建HttpClient,執行request

1

2

3

4

5

6

7

8

/**

 * 創建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 …)並返回。

 

1

CloseableHttpResponse httpResponse = httpClient.execute(httpUriRequest);

    HttpClient使用了責任鏈模式,所有Executor都實現了ClientExecChain接口的execute()方法,每個Executor都持有下一個要執行的Executor的引用,這樣就會形成一個Executor的執行鏈條,請求在這個鏈條上傳遞。按照上面的方式構造的httpClient形成的執行鏈條爲:

1

2

3

4

5

HttpRequestExecutor                              //發送請求報文,並接收響應信息

MainClientExec(requestExec, connManager, ...)    //main Executor,負責連接管理相關

ProtocolExec(execChain, httpprocessor)           //HTTP協議封裝

RetryExec(execChain, retryHandler)               //重試策略

RedirectExec(execChain, routePlanner, redirectStrategy)   //重定向

    請求執行是按照從下到上的順序(即每個下面的Executor都持有上面一個Executor的引用),每一個執行器都會負責請求過程中的一部分工作,最終返回response。

 

2、連接池管理

2.1、連接池結構

連接池結構圖如下:

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

// 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()的區別

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

/**

 * 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()讀取數據流後,都會判斷流是否讀取結束

1

2

3

4

5

6

7

8

9

10

11

12

13

14

@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,流已經讀完,如果連接可重用,就會嘗試釋放連接,否則關閉連接。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

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

1

2

3

4

5

6

7

8

9

10

11

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

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()

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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(),會關閉整個連接管理器,並關閉連接池中所有連接。

1

2

3

4

5

6

7

8

9

10

11

12

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

/**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。

對於這種情況,在從連接池獲取可用連接時無法知曉,在獲取到可用連接後,如果連接是打開的,會有判斷連接是否陳舊的邏輯,如下

1

2

3

4

5

6

7

8

9

10

11

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

/**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官方給出的清理過期、空閒連接的例子

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

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、連接數相關參數

根據“利爾特法則”可以得到簡單的公式:

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再大也無法充分利用連接池。

2、超時時間相關參數

connectTimeout  --  連接超時時間

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

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

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

socketTimeout有默認值,也可以針對每個請求單獨設置。

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

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

 

參考資料:

httpClient 4.3.x configuration 官方樣例

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

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章