jetty http client 實現分析
阿新 • • 發佈:2019-01-02
背景
談到http client,可能大多數想到就是apache的那個http client 或者jdk自帶的urlconnection,也許有人會考慮使用netty
無論如何,jetty的高效能實現總歸是讓人感到好奇,接下來我們一探究竟
樣例
我們結合樣例程式碼具體分析
- 初始化
httpClient = new HttpClient(); httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL); httpClient.setMaxConnectionsPerAddress(10); httpClient.setThreadPool(new QueuedThreadPool(20)); // max 20 threads httpClient.setTimeout(5000); // 5 seconds timeout; if no server reply, the request expire httpClient.start();
- 執行
ContentExchange exchange = new ContentExchange(true) { @Override protected void onResponseComplete() throws IOException { if (getResponseStatus() == 200) { String content = getResponseContent(); System.out.println(content); } } @Override protected void onExpire() { System.out.println("time out"); } }; exchange.setMethod("GET"); exchange.setURL("http://127.0.0.1:8080/simple?id=x"); httpClient.send(exchange);
程式碼分為兩段
- 初始化:設定httpclient
- 執行:例項化ContentExchange,定義callback,本例定義了兩個常用的callback:onResponseComplete 和onExpire,更多的callbac可參考官方文件
- APP在呼叫httpClient.send(exchange);後不會象往常一樣等待返回而是立即返回, 如果有結果或者超時會通過上面的callback通知到APP
httpclient的原理及實現
1 )httpclient的模型
- SelectConnector: 作為一個connection管理器,封裝了selector和connection
- HttpDestination:一個host的抽象一個HttpClient會連線到多個HttpDestination
- HttpExchange:一次http請求的封裝,一個HttpDestination會有多個HttpExchange以及多個AsyncHttpConnection
- AsyncHttpConnection:HttpClient對某個HttpDestination的一個網路連線,底層包含一個對應的socket, 可複用來完成多次請求, 如果空閒太久會被廢棄
- SelectChannelEndPoint:socket的封裝,AsyncHttpConnection和SelectChannelEndPoint一一對應, 但AsyncHttpConnection承載了更多的東西
- HttpGenerator:生成http request,在jetty server中負責生成http response
- HttpParser: 解析http response, 在jetty server中負責解析http request
- ThreadPool: 執行緒池,httpclient需要使用執行緒池配合完成無阻塞IO,這個會在後面的httpclient整體架構分析中詳述
- Timeout:一個已時間排序的連結串列結構,連結串列中儲存需要過期執行的task,這個會在後面流程分析詳述
2)httpclient的整體架構
http client 分為3組執行緒配合完成
- selector執行緒組:數目可設定,預設為1,從_change佇列中獲取socket註冊並掃描作業系統級別的網路事件, 通常是socket可讀, 可寫的資訊,一旦發現有socket可讀寫,會將相關socket任務丟入_jobs佇列供worker執行緒執行
- worker執行緒組:數目根據併發的情況決定,從_jobs佇列獲取任務,如果任務阻塞會丟入_changes佇列非同步等待通知再幹活
- tick執行緒:數目1個,專門用於監控超時的請求以及空閒太久的連線
- 所有的執行緒都來自執行緒池,所以執行緒池最小為3,否則無法work
3) 典型的場景分析
模擬一次請求
3.1 )httpclient初始化
- 1-2設定兩個超時連結串列,一個是超時請求連結串列,一個是超時連線連結串列
- 3 啟動httpbuffer
- 4 啟動執行緒池
- 5 啟動SelectConnector,此時會啟動selector執行緒任務
- 6 啟動tick執行緒任務
3.2)jetty http client runtime
3.2.1)httpClient.send(exchange)到底幹了什麼
- 1-2 正如樣例程式碼所示,APP設定HttpExchange,然後httpclient的send方法
- 2.1-2.2 httpclient根據http exchange獲取對應http destination,並呼叫其send方法
- 2.2.1 將次請求加入請求超時連結串列
- 2.2.2 - 2.2.3 獲取空閒連線,如果沒有,則產生一個新的連線,並呼叫select進行註冊,否則直接使用該連線,並將此連線丟入 _jobs佇列讓worker執行緒完成請求
- 此時客戶端就這樣無阻塞的完成了
- 1-3 selector執行緒從_change佇列獲取到新的socket, 開始例項化SelectChannelEndPoint
- 4 通知http desination連線完成,於是http detination將次連線丟入連線超時連結串列
- 5-6 將此連線/請求丟入_jobs佇列供worker執行緒使用
- 其實在selector執行緒內部還有一個該死的任務來處理空閒太久的socket,這個其實和tick執行緒有些重複了,我想這主要是因為jetty http client複用jetty server中select的結果
3.2.3)worker執行緒又如何參與這個場景
- worker執行緒從佇列中獲取任務
- 1.1 通過此連線傳送請求,請求內容http generator產生
- 1.2 一發完請求立即通過http parser讀取響應,如果伺服器夠快,通常會讀到響應
- 1.3 如果伺服器不能及時響應,那麼呼叫SelectChannelEndPoint的updateKey。向select更新此時感興趣讀, 並等待select非同步通知
- 此時worker執行緒並不會阻塞等待服務返回,而是返回到執行緒池中去完成別的請求任務
- 輪詢兩個連結串列_timeoutQ、_idleTimeoutQ,沒啥事休眠200ms
- 請求超時連結串列_timeoutQ
- 1 從連結串列中刪除自己
- 2 執行連結串列取出的task,一個http exchang中匿名內部類例項
- 2.1 執行APP 定義的callback: onExpire函式
- 2.2 http desination專門維護一個exchange list來跟蹤進行中的請求,此時呼叫其exchangeExpired, 刪除list中該請求(可能此時list並沒有該請求)
- 2.3 關閉連線
- 連線超時連結串列_idleTimeoutQ
- 1從連結串列中刪除自己
- 2 關閉連線
- http desination 維護了兩個list:_connections和 _idle,前者跟蹤該host的所有連線, 後者跟蹤該host的所有空閒連線,此時也會從這兩個list刪除連線
小結
從jetty http client應該能感知到一個高效能的客戶端的某種設計模式
- worker 執行緒非同步幹活,使得app執行緒無阻塞,app執行緒通常在web 應用中也是一種服務執行緒,所以無阻塞特別重要, 想想在jetty server中使用jetty client的場景
- select 執行緒通知網路ready事件,使得worker執行緒無阻塞,如果沒有select執行緒,worker執行緒也失去了意義, 對於app執行緒來說無非是壓力堆積到了worker執行緒這邊,worker執行緒遲早是瓶頸
- tick執行緒,一種解決超時問題的設計
但這種模式未必適合那種效能很好且穩定的cache server,比如redis,memcache之類,如果後端處理夠快, 少量執行緒甚至單執行緒+佇列都能work,但無論如何比起常規的連線池模式強了不少