這篇文章主旨在於講明白:
- 什么是http的keepalive
- keepalive在客戶端和服務端都有什么不同表現,影響如何(Httpclient/4與tomcat/8為例)
- keepalive是通過什么方式實現鏈接復用的
先上一張圖,回顧下基本知識,這張圖包含了一個完整的TCP鏈接的生命周期。這張圖內容很豐富,每個節點都要看仔細咯,不能忽略。
現在一般默認都是用HTTP/1.1 進行網絡訪問,Http/1.1是支持長連接的,一個連接可以被多次使用,發起不同的http請求,這個長連接就是基於Http Header參數Connection: keep-alive進行控制,隨便查看一個http請求的報文,都可以發現類型以下的請求頭:
表明上面是一個長連接。
∞
創建一個HttpClient的代碼一般如下格式:定義ConnectionManager、RequestConfig 用來初始化httpClient,變量timeToLive可以認為是keepalive超時時間。
HttpClientConnectionManager connectionManager = connectionManagerFactory
.newConnectionManager(false, maxTotalConnections,
maxConnectionsPerHost, timeToLive, ttlUnit, registryBuilder);
RequestConfig defaultRequestConfig = RequestConfig.custom()
.setConnectTimeout(connectTimeout)
.setRedirectsEnabled(followRedirects).build();
CloseableHttpClient httpClient = httpClientFactory.createBuilder().
setDefaultRequestConfig(defaultRequestConfig)
.setConnectionManager(connectionManager).build();
其中的ConnectionManager最終被初始化為一個PoolingHttpClientConnectionManager實例,也就是說,創建一個http的連接池來管理連接;連接池的特性就是連接用完后直接放回池中(這個ConnectionManager還有些其他的處理,稍后再分析),下次直接從池中取出鏈接使用。
服務端keepalive配置代碼如下:
@Configuration public class ServerConfig { @Bean public EmbeddedServletContainerFactory getEmbeddedServletContainerFactory() { TomcatEmbeddedServletContainerFactory containerFactory = new TomcatEmbeddedServletContainerFactory(); containerFactory .addConnectorCustomizers( (TomcatConnectorCustomizer) connector -> ((AbstractProtocol) connector.getProtocolHandler()).setKeepAliveTimeout(2000)); return containerFactory; } }
下圖是一個正常情況下,完整的一次http交互,3次握手建立連接,4次握手關閉鏈接
服務端keepalive有效期為2秒,客戶端keepalive有效期為10秒,與上圖時間吻合。關閉鏈接有服務端主動發起,但客戶端沒有立即響應。
可以通過命令查看已建立的鏈接及數量
netstat -an | grep 8090 | wc -l
下圖是一個鏈接復用的例子,同一個連接(50188 <-> 8090)處理了多次請求,提高了IO的利用率。
∞
- 如果客戶端的 keepalive有效期(5秒)比服務端(10秒)的短,一個http請求的過程有什么變化,看下圖
這次是客戶端主動發起的關閉鏈接請求(可以通過兩種方式進行關閉,后續進行說明),服務端及時響應。
- 如果一次請求過程中,服務端響應時間超過了keepalive的有效期,鏈接並不會提前中斷,如下圖,keepalive超時時間為10秒,而服務端返回結果用了13秒,連接是在正常返回后中斷的。
- 如果客戶端keepalive的有效期(100秒)遠遠超過了服務端的keepalive的有效期(10秒鍾),會出現什么樣的結果,這也是本篇最關注的問題,同樣通過抓包查看連接的交互過程。
下圖是我通過手動點擊發送請求對應的網絡抓包,可以看到交互順利,還有連接復用,貌似一切正常。
但如果將上面的配置放到生產環境或者進行一個簡單壓力測試,將會時不時地發生failed to respond這個異常,但具有一定的偶然性。
異常拋出點如下
拋出這個異常的條件為讀取到了連接結束的標記 i=-1。為何會出現這個情況,和httpclient維護連接的方式有關,簡單描述如下:
httpclient默認使用PoolingHttpClientConnectionManager這個類管理連接,主要用到的是
歸還連接的方法
public void releaseConnection(final HttpClientConnection managedConn, final Object state, final long keepalive, final TimeUnit tunit)
獲取連接的方法
public ConnectionRequest requestConnection( final HttpRoute route, final Object state)
- 連接歸還
歸還連接的過程相對簡單,主要是根據參數keepalive更新連接過期時間並放入連接池.
keepalive這個參數是從服務端返回的HttpHeader中獲取的,如請求頭中 Keep-Alive: timeout=5, max=100,表示5毫秒后超時,還能請求100次。
參考下面的代碼:
事實上,tomcat返回的頭部中沒有關於keepalive的參數,所以httpclient任務此鏈接永久有效,會打印如下日志:
Connection [id: 18][route: {}->http://localhost:8090] can be kept alive indefinitely
- 獲取連接
獲取鏈接的過程稍微復雜一下,跟蹤代碼,最終會進入org.apache.http.pool.AbstractConnPool#getPoolEntryBlocking方法,如圖
getPoolEntryBlocking方法比較長,重點關注上圖中的這幾行就可以,其中的entry就可以認為是一個連接,取出連接的過程做了一些判斷,盡可能的保障了連接的可用性(之所以是盡可能,是因為存在某些巧合,導致雖然檢測通過了,但是連接依舊是不可用的)
檢測包括兩部分:
- 是否過期,isExpired方法,根據創建時間和keepalive超時時間和當前時間進行比較
- 是否可用,根據參數validateAfterInactivity(默認值2000)每超過一定時間,判斷連接是否可用,也就是存在一個時間窗口,在這個范圍內是不檢測的,這就給異常埋下了伏筆。
第二項檢測會出現異常的道理很簡單,如果這個時間窗口內服務端關閉了鏈接,客戶端是不知曉的,或者超出了本時間窗口且檢查通過,但發送請求前服務端主動關閉了鏈接,客戶端也不知曉。
拋出這種異常情況,客戶端就可以重復使用連接池中的對象,發送請求,達到連接復用的效果。
還有一種清除過期連接的方式,配合使用效果更好,啟動定時任務,定期清理過期連接:
this.connectionManagerTimer.schedule(new TimerTask() { @Override public void run() { connectionManager.closeExpiredConnections(); } }, 1000, 10000);
還可以在出事化httpclient時,配置好HttpRequestRetryHandler,DefaultHttpRequestRetryHandler.INSTANCE似乎不能處理NoHttpResponseException,最好自定義,這樣在發生NoHttpResponseException異常時可以進行重試,一般也能解決問題。
this.httpClient = httpClientFactory.createBuilder().setRetryHandler(DefaultHttpRequestRetryHandler.INSTANCE). setDefaultRequestConfig(defaultRequestConfig) .setConnectionManager(connectionManager).build();
當然這些都是補救措施,最好的方式應該是協調好客戶端與服務端keepalive配置,客戶端keepalive時間不要超過服務端keepalive時間。
經過這些改造,在進行測試,就沒有再發現相關錯誤。
注:
其實,一個事情驗證它有比較容易,如何驗證沒有呢?理論證明加實驗,很難窮舉所有情況,但是能確保絕大多數情況下正常。
參考:
https://blog.csdn.net/liyantianmin/article/details/82505634
https://zzc1684.iteye.com/blog/2189254