544
技術社區[雲棲]
Httpclient核心架構設計
大概兩周前,團隊需要對httpclient進行調優,領到任務後,大致研究了源碼很快就確定了調優方向。但是對於一個重度強迫症患者,對一件事情的理解就隻有兩種程度,完全懂或者完全不懂,於是悲催的開始httpclient整體架構的探索之路,每天晚上回家抽點邊角料的時間寫,之前的理解建立在一個宏觀基礎上,對整體有個把握,寫的過程中細節不斷豐富細節,理解開始變得立體起來。
背景知識
Http簡介
通常,我們使用IE或者safari來訪問互聯網上的內容,隻需要輸入資源地址,瀏覽器便會呈現給你想要的內容。這一切的背後,都是迄今為止在計算機領域最成功的協議–http協議。
Http協議分為請求和響應,客戶端建立連接,接著發送請求,服務端接受並處理請求,再發送應答,再由客戶端接受並處理應答。瀏覽器是最最常見的一種客戶端,它將用戶的交互行為作為http請求發送,並接受服務端的應答,再將應答內容展示,一般應答都是html類型的超文本。
瀏覽器顯然不是唯一的客戶端,理論上任何遵循了http規範都可作為客戶端。在程序裏也可以通過Java api實現簡單的客戶端–使用HttpURLConnection發送http請求,並解析應答。假設應答是個html或者json,則隻需要基於雙方約定的格式進行解析就能得到聚焦的結果。
Http, tcp/ip和socket區別
Tcp/ip是傳輸層協議,而http則是建立在它之上的上層應用協議。Http主要解決的是數據規範層麵的事情,而tcp/ip主要解決的則是數據傳輸層麵的事情。如果沒有規範的應用協議,數據能從網絡裏的A節點傳到B節點,但卻無法有效識別,建立在tcp/ip上的應用協議很多,像rpc,ftp等,反過來不管應用協議有多強大最終都需要依靠傳輸層協議進行數據傳輸。
Socket則是tcp/ip的一個編程實現,在程序裏http請求(連接)最終一定需要綁定到一個具體的socket連接進行上行和下行傳輸。
整體架構
對於簡單應用,HttpURLConnection完全可以滿足。但是對於1)係統複雜度高,2)性能要求高,3)可靠性要求也高的應用,則需要一個更強大的組件。
Httpclient將對接的服務器或者集群(相同域名)稱為route,並為每個route建立若幹連接池化在連接池裏。Client通過tcp/ip協議發送請求以及接受應答,在發送請求前和接收應答後都會經由interceptor進行鏈式處理,在httpclient裏這些interceptor被稱為HttpProcessor,負責處理諸如設置報文頭,報文體,編碼格式等以及解析報文頭,報文體,解碼格式等http規範報文格式範疇內的事情。
HttpClient靜態結構
- HttpClient通過建造者構建出來,用戶可以通過建造者暴露出來的參數屬性方法來組織最終生成的產品屬性。HttpClients是個工廠類,用於生產HttpClient,同時也提供custom方法返回builder,由使用者組織client屬性。
- HttpClient主要由5個組件組成,分別是:
1. Closeable: 代表需要關閉的組件,client服務關閉時會回調注冊的所有Closeable組件依次關閉。用戶可以通過HttpClientBuilder#addCloseable添加自定義關閉組件。HttpClient內部利用Closeable關閉IdleConnectionEvictor以及HttpClientConnectionManager
2. IdleConnectionEvictor: 用來關閉閑置連接,它會啟動一個守護線程進行清理工作。用戶可以通過builder#evictIdleConnections開啟該組件,並通過builder#setmaxIdleTime設置最大空閑時間。
3. HttpClientConnectionManager管理著連接的整個生命周期。連接在連接池中創建、複用以及移除。
connection被創建出來後處於閑置狀態,由連接池管理,被lease後會校驗是否是open狀態,不是的話會進行connect,connect的過程就是將http請求(連接)綁定到socket的過程。同時連接也會因為心跳或者過期等原因被close變成stale狀態,直至被下一次get到時或者連接滿時被清理出去。
同時連接池還能對連接進行限流–全局和單route連接數。Connection manager封裝了對連接池的具體操作,比如向連接池租用和歸還連接;還提供了基於不同schema(主要是http和https)創建不同的socket連接(ssl和plain)並且將http請求(連接)綁定到socket的能力,等等。
4. HttpRoutePlanner用來創建HttpRoute。後者代表客戶端request的對端服務器,主要包含rout的host以及proxy信息。
5. ClientExecChain代表一次完整的調用執行過程,它是一個包裝類,類似於java io類,每個包裝類完成一個特定的功能,多層嵌套完成一個係統性的功能,比如處理協議範疇的例如cookie、報文解析等,又比如處理自動重試的,等等。
連接池
-
CPool裏的連接分為三種–available, leased和pending,分別對應空閑,占用和堵塞三種狀態,連接池為這三種狀態建立三個列表(List/Set)。對連接數的管理則有兩個維度,分別是全局最大數和單route最大數。全局連接和單route連接都對應三種狀態列表,CPool內部維護了route和RouteSpecificPool的映射,通過後者對單route連接進行管理,並且嚴格保證一個route隻會對應一個route pool。操作(租用,釋放,阻塞或者移除等等)連接時CPool首先會依據route信息取出route pool,對其上維護的連接進行操作,之後再對CPool上的相應連接操作。RouteSpecificPool是個friend的abstract類,也就是說它是CPool隱藏起來的實現細節,對外隻暴露CPool的行為甚至用戶都可以不理會CPool隻關心connection mananger。
-
連接池對外透出的是PoolEntryFuture,後者的get方法能夠獲取一個閑置連接,或者進入堵塞等待。
連接池的連接連同route信息一起被包含在PoolEntry裏返回給消費者,除此之外,PoolEntry還包含了連接的失效時間等等,超過失效時間會在下一次被get到時close。 -
CPool還有流控功能,get請求在沒有空閑連接但連接數沒達到閾值時通過連接池創建連接並池化放入available或者leased。leased連接數達到閾值時對請求進行堵塞–PoolEntryFuture#await,並且將PoolEntryFuture放入pending。其他請求釋放連接時會喚醒堵塞請求,被喚醒的請求獲取到連接後會被從pending列表中移除。
超過任何一個最大數閾值後CPool首先都會進行收縮,超過單route最大數,則收縮單route連接,超過全局最大數,則收縮全局連接。收縮的過程隻會關閉空閑連接,直至連接數等於閾值-1。
執行鏈
-
MainClientExec是真正執行客戶端請求的,它位於包裝類的最裏層,它通過連接管理器向CPool requestConnection,綁定http請求到socket,通過request executor發送請求,並且還能基於keep-alive策略處理連接的複用等等。
-
ProtocolExec通過一係列的HttpProcessor處理鏈對Http消息按格式編碼以及解碼。每一個processor處理一個範疇的事情,比如處理header,content以及cookie等等。
-
RetryExec,對io某些特殊情形的io異常進行重連,保證可用性。
-
RedirectExec,處理301,302,303和307的情況,即move和redirect。
-
BackoffStrategyExec對出現連接或者響應超時異常的route進行降級,縮小該route上連接數,能使得服務質量更好的route能得到更多的連接。降級的速度可以通過因子設置,默認是每次降級減少一半的連接數,即降級因子是0.5。
最後注意一點,以上的這些exec隻有MainClientExec和ProtocolExec是默認開啟的,其他的都需要通過HttpClientBuilder設置參數開啟,具體可以參考文檔或者源碼。
調優方向
了解了架構原理後,就可以著手在3個方向進行調優:
1. 連接數,通過設立全局最大連接數和單route連接數,增加吞吐能力。用戶可通過HttpClientBuilder#maxConnTotal和#maxConnPerRoute分別設置。
2. 獲取連接的超時時間,調小超時時間能夠有效提高響應速度並且降低積壓請求量,但相應的也會增加請求失敗的幾率。用戶可以通過RequestConfig的connectionRequestTimeout進行設置。
3. 建立連接和route響應的超時時間,調小能夠有效的降低bad request對連接的占用,留給質量更好的請求,有效提高係統提高吞吐能力及響應速度。否則有可能在峰值期被慢請求占滿連接池,導致係統癱瘓。兩者分別可通過RequestConfig#connectionTimeout和socketTimeout進行設置。
4. 開啟BackoffStrategyExec,對狀況差的route進行降級處理,將連接讓給其他route。
最後更新:2017-06-05 11:32:58