基于 Netty 如何实现高性能的 HTTP Client 的连接池
使用netty作為http的客戶端,pool又該如何進行設計。本文將會進行詳細的描述。
1. 復用類型的選型
1.1 channel 復用
多個請求可以共用一個channel
模型如下:
模型特點:
-
1:callback隊列為回調隊列。 不同的callback通過一個全局的id進行標識。發送的時候會把該id發到服務端,服務端在回復的時候必須把該id再返回到客戶端。
-
2:獲取連接只需要隨機獲取一個channel即可,將callback添加到隊列里面。
-
3: 獲取連接時消除了鎖的競爭,性能高效。
-
4:結構簡單。
示例:
-
osp(唯品會的SOA框架) client pool實現(thrift協議)
-
spray 的 akka client pool
約束:
需要服務端配合支持channel復用。需要有一個全局唯一的id用于識別請求。 通常id先發給服務端,服務端還要把id會給客戶端。
1.2 channel 獨享
每個請求獨立使用一個channel。
模型如下:
模型特點:
-
1:同一個channel同時只給一個request使用。
-
2:連接池的設計較為復雜。
示例:
-
1:數據庫連接池[druid,c3p0,dbcp,hikaricp,caelus(唯品會內部連接池)]
-
2:netty的http pool ; apache的httpclient pool, httpasyncclient pool ; nginx的pool。
1.3 選擇
由于http1.1協議原生不支持channel復用(http2是支持的),如果需要支持,則需要在header里面加入一個唯一id,所有的應用服務器均需要進行改動。為了和nginx的連接池保持一致,確定使用channel的獨享方式。
?
2. 組件選型
| common-pool | 功能完整 | 不支持異步連接 |
| rxnetty pool | 功能完整,支持netty | 使用的為rxjava機制 |
| netty pool | netty原生實現 | 功能較為簡單 |
選擇netty pool作為連接池的實現。4.0.33版本有該功能,可能老版本沒有pool的功能。
?
3. pool的設計
3.1 模型
模型通過ip,port路由到對應的pool,每個pool之間完全獨立。
3.2 主要功能點
功能點3.3 獲取連接
-
1:通過控制最大連接數,來避免無限的創建連接。
-
2:當超過最大連接數時,則需要等待。由于整個流程是全異步的,需要將當前信息進行任務封裝注冊回調。
-
3:需要設置等待連接的個數及超時時間,避免把內存給撐爆。
-
4:需要對獲取的連接進行有效性檢查。一般只需校驗channel.isactive()即可。如果檢驗失敗,需要重新獲取有效連接。
3.4 資源池
-
1:使用無鎖的ConcurrentLinkedDeque 雙向隊列來存放所有idle的連接(jdk1.7才有該類)。
-
2:該隊列通過cas的操作來避免同步。 由于拿到連接后業務執行的速度較慢,所以這里的cas沖突應該很小。
3.5 歸還連接
歸還連接歸還連接主要包含兩部分:正常release和異常的forceClose
-
1:在netty中,如果收到FIN(服務端發送的正常close請求),則會通知到netty的channelInactive接口,需要在該接口處進行forceClose.
-
2:收到RST(服務端非正常的關閉),則會通知到exceptionCaught接口,需要在該接口處進行foreclose。關于RST的問題可參考:http://blog.csdn.net/hetaohappy/article/details/51851880
-
3:在收到正常數據后(channel的channelRead接口),需要判斷header里面是否有Connection:close,如果有,則進行forceClose,否則進行release
-
4:如果空閑超時,則關閉連接,來避免連接一直被無效的占用。只需要增加IdleStateHandler ,判斷連接空閑超時,則fire一個event事件。只需要注冊對該事件的監聽,進行foreclose即可。
-
5:占有超時:連接在規定的時間內未還,則進行forceClose。
6:發送請求時,發現channel已經被close掉或者其他io異常,則進行forceClose。
7:forceclose接口里面,需要通過一個狀態位來控制是否操作 acquiredChannelCount(已獲取連接數)。由于調用forceclose,連接可能在資源池中,如果操作該字段,會導致該字段統計不準確。
3.6 超時控制
獲取連接timeout
在規定的時間內沒有獲取到連接,則拋異常。
-
1:一般實現是通過ReentrantLock來設置lock的超時時間或者直接通過unsafe.park設置超時時間。該種機制會對當前線程進行block。
-
2:由于netty是純異步機制,如果進行block,會嚴重影響性能。所以這里是將當前信息進行task封裝,然后schedule一個定時任務。如果在設定時間內該task沒有被消費,則會拋出timeout的異常。
建立連接timeout
-
1:在BIO中,通過設置socket的connect(SocketAddress endpoint, int timeout) 時間即可。Tips:該值不要和setSoTimeout(int timeout)混淆,sotimeout是設置調用read的超時時間。
-
2:在NIO中,需要業務自己控制連接的超時時間。 一般是通過schedule一個定時任務來控制超時時間。(在netty中即使用的該機制)
連接空閑timeout
-
1: 通過設置一個handler(IdleStateHandler ),在新建連接的時候schedule一個任務(時間為空閑超時時間),在調用read或者write方法的時候,進行時間的更新。如果任務運行的時候發現空閑超時,則進行event的觸發。
-
2:業務handler捕獲該event,進行連接空閑超時的處理。
連接被占有timeout
避免連接泄露
-
1:在獲取連接的時候 schedule一個任務(時間為連接被占用的timeout),在歸還的時候會cancel該任務。如果該任務被運行,說明在規定的時間沒有歸還,則進行timeout的處理。
3.7 性能優化
-
1:資源池無鎖化:ConcurrentLinkedDeque (前面已有介紹)
-
2:acquiredChannelCount(已獲取連接數)的無鎖化(該字段用來控制是否達到了最大連接數,正常情況為獲取連接后加1,歸還連接后減1)。
連接池均會通過acquiredChannelCount來控制當前已經獲取的連接個數。該參數會面臨著多線程的競爭,需要進行同步或者cas的設計。如何設計讓acquiredChannelCount完全不用考慮多線程競爭?
看能不能從akka的設計中找點思路: akka消除競爭的方式就是讓一個actor同一時刻只能在一個線程中運行,這樣actor里面所有的全局參數就不需要考慮多線程競爭,一個actor里面所有的任務都是串行執行的,完全消除競爭。
那么能不能串行操作acquiredChannelCount呢? 答案是可以的,并且在netty中實現非常簡單,只需要實現如下代碼即可:
if?(executor.inEventLoop())?{acquiredChannelCount++;}?else{executor.execute(newOneTimeTask()?{@Overridepublic?void?run()?{acquiredChannelCount++;}} }其中executor 就是一個固定的線程。判定當前執行的線程是否是executor這個線程,如果是則直接執行。如果不是,則放到executor線程的隊列里達到串行操作的目的(類似于actor的mailbox) (netty的設計及抽象能力確實非常高)
3.8 配置參數
-
http_pool_aquire_timeout?:獲取連接超時時間:默認為5000ms
-
http_pool_maxConnections:連接大小:默認為1000
-
http_connection_timeout?:建立連接的超時時間:默認為5000ms
-
http_pool_idle_timeout:連接空閑多久關閉:默認為:30分鐘
-
http_pool_maxPending:連接池不夠用,最大允許有多少個pendingRequest:默認為無限大
-
http_pool_maxHolding:拿連接的使用時間。在規定時間未還,則強制close掉。默認為5000ms。
?
4. 面臨的問題
-
1:所有的操作都是純異步,導致callback嵌套的特別深(netty通過promise機制,來方便callback的使用),如果控制不好,很容易出問題。
-
2:連接被require后,一定要保證歸還,由于異步特性,很容易在某些異常下將連接漏還(筆者遇到在高并發下由于代碼bug導致漏還的情況)
-
3:如何避免在拿到連接后,同時web服務器(http的keepalive機制)將該連接給close掉了,導致執行的失敗。有兩種解決方案可以參考。
-
捕獲執行失敗的異常,如果是特定的異常,則forceClose當前的連接,重新拿一個連接進行訪問。如果超過重試次數,則拋出異常。
-
如何確定該線程定時的時間。后端web服務器對連接的超時時間可能不一致,該定時時間一定要小于web服務器的連接超時時間。
-
心跳執行的接口問題。需要所有的web服務器均需要實現固定的接口進行心跳檢測,可行性比較差。
-
3.1:可以參考common-pool的設計思想,在后端開啟一個線程定時對所有連接進行心跳檢測。問題:
-
如何確定該線程定時的時間。后端web服務器對連接的超時時間可能不一致,該定時時間一定要小于web服務器的連接超時時間。
-
心跳執行的接口問題。需要所有的web服務器均需要實現固定的接口進行心跳檢測,可行性比較差。
-
-
3.2:重試機制:
-
捕獲執行失敗的異常,如果是特定的異常,則forceClose當前的連接,重新拿一個連接進行訪問。如果超過重試次數,則拋出異常。
-
-
總結
以上是生活随笔為你收集整理的基于 Netty 如何实现高性能的 HTTP Client 的连接池的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 你从未听说过的最重要的数据库,人类登月计
- 下一篇: 当 HTTP 连接池遇上 KeepAli