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