溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么探討RPC框架中的服務(wù)線程隔離

發(fā)布時間:2021-12-27 14:04:24 來源:億速云 閱讀:124 作者:柒染 欄目:大數(shù)據(jù)

本篇文章給大家分享的是有關(guān)怎么探討RPC框架中的服務(wù)線程隔離,小編覺得挺實用的,因此分享給大家學(xué)習(xí),希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。

微服務(wù)如今應(yīng)當(dāng)是一個優(yōu)秀的程序員必須學(xué)習(xí)的一種架構(gòu)思想,而RPC框架作為微服務(wù)的核心,不說讀一遍源碼吧,起碼它的實現(xiàn)原理還是應(yīng)該知道的。

然而目前的RPC服務(wù)框架,大多存在一個問題,就是當(dāng)服務(wù)提供端Provider應(yīng)用中,有的服務(wù)流量大,耗時長,導(dǎo)致線程池資源被這些服務(wù)占盡,從而影響同一應(yīng)用中的其他服務(wù)正常提供。為此,下面主要介紹一下我對于這方面的思考。

 

前言

在進入正文之前,可以先看一下阿里中間件島風(fēng)大佬的這篇博文(傳送門),這篇博文復(fù)現(xiàn)了Dubbo應(yīng)用中,線程池耗盡的場景。這其實在線上是十分普遍,解決方法無非是根據(jù)業(yè)務(wù)調(diào)整參數(shù),或者引入其他的限流、資源隔離框架,例如Hystrix、Sentinel等,使得資源間互不干擾。其實本身Dubbo也可以對不同的服務(wù)配置不同的業(yè)務(wù)線程池(通過配置protocol)從而實現(xiàn)服務(wù)的資源隔離,但是這種方式的弊端在于,一旦服務(wù)增多,線程數(shù)量會迅速膨脹。線程池過多不便于統(tǒng)一管理,同時過多的線程所帶來過多的上下文切換也會影響服務(wù)器性能。

在絕大多數(shù)場景下,對服務(wù)資源的隔離可以通過開源框架Sentinel來實現(xiàn),其通過配置某個服務(wù)的并發(fā)數(shù),來達到限流和線程資源隔離的目的。坦白的講,這已經(jīng)能夠滿足絕大多數(shù)需求了,但是手動取配置這些參數(shù)還是比較有難度的,大多得靠大佬們的經(jīng)驗了,而且也不夠靈活。

我在學(xué)習(xí)的時候,也突發(fā)奇想,有沒有可能不依賴外部的組件,而實現(xiàn)內(nèi)部的服務(wù)資源隔離?再更進一步,有沒有可能根據(jù)應(yīng)用內(nèi)各個服務(wù)的流量數(shù)據(jù),對每個服務(wù)資源進行動態(tài)的分配和綁定呢?

打個比方說,某個應(yīng)用里存在A、B兩個服務(wù),100個線程。白天的時候,A服務(wù)的流量大,B服務(wù)的流量很小,那么在這個時間段內(nèi),我們的應(yīng)用分配給A的資源理應(yīng)更多。但是也不能全給A拿走了,B也得喝口湯,不然又會出現(xiàn)線程耗盡的情況,所以此時我們可能根據(jù)流量數(shù)據(jù)的比對分給A服務(wù)80個線程,B服務(wù)20個線程;而到了晚上,A服務(wù)沒啥人用了,B服務(wù)流量來了,那我們就給B更多的資源,但也要保證A可用,比如說,A服務(wù)20線程,B服務(wù)80線程。

我承認我一開始只是想簡單寫個RPC框架,學(xué)習(xí)實現(xiàn)原理而已。但突然有了這樣一個想法,我就來了動力,想看看自己的想法行不行得通,下面我便介紹下我的思考,說的有不對的地方也歡迎大家指出和探討。

 

線程隔離的三個組件

借鑒了傳統(tǒng)的RPC框架的實現(xiàn)原理后,我們只需要修改或者增加三樣?xùn)|西,就可以完成上述的功能,分別為:線程池、數(shù)據(jù)監(jiān)控節(jié)點Metric和線程動態(tài)分配的Monitor。這三者之間的關(guān)系可以先看一下這張圖有個大概的印象。

怎么探討RPC框架中的服務(wù)線程隔離

 

線程池

首先需要修改的自然是線程池。以Dubbo為例,其默認的線程池為fixed線程池,io線程接收到請求后,委托Dubbo線程池完成后續(xù)的處理,通過調(diào)用ExecutorService.execute。

但是在這里,使用JDK中的線程池顯然是行不通了。線程池中的Thread也不再是單純的Thread,而需要更進一步的抽象。這里參考Netty中NioEventLoop的設(shè)計思想,將每條Thread抽象為一條Loop,其既是任務(wù)執(zhí)行的本體Thread,也是ExecutorService的抽象,而所有Loop交由LoopGroup統(tǒng)一管理,由LoopGroup決定將任務(wù)提交至哪一個線程。這里我實現(xiàn)的比較簡單,每個線程有個專屬的id,通過拿到線程的id,將任務(wù)提交到對應(yīng)的線程,原理可以參考下圖:

怎么探討RPC框架中的服務(wù)線程隔離

私以為核心在于維護服務(wù)與線程id的對應(yīng)關(guān)系,以及在請求到來時,LoopGroup會根據(jù)請求中服務(wù)的類型,選擇對應(yīng)id的線程,并交由該線程去處理請求。

 

數(shù)據(jù)監(jiān)控

數(shù)據(jù)的監(jiān)控相對來說是最好辦的。這里我參考了Sentinel的實現(xiàn),使用時間窗口法統(tǒng)計各個服務(wù)的流量數(shù)據(jù),包括pass、success、rt、reject、excetpion等。(關(guān)于Sentinel中的時間窗口,后面有時間再專門寫篇源碼分析)

而至于監(jiān)控節(jié)點的形式,根據(jù)調(diào)用鏈路的具體實現(xiàn)不同,在Dubbo中可以是一個filter,而我因為將調(diào)用鏈路抽象為一個Pipeline,所以它作為Pipeline上的一個節(jié)點,參考下圖:

怎么探討RPC框架中的服務(wù)線程隔離

這里貼上MetricContext的關(guān)鍵源碼:

//處理請求時,pass+1,同時記錄開始時間并保存在線程上下文中
@Override
protected void handle(Object obj) {
   if(obj instanceof RpcRequest){
       RpcContext rpcContext=RpcContext.getContext();
       rpcContext.setStartTime(TimeUtil.currentTimeMillis());
       paladinMetric.addPass(1);
   }
}

//響應(yīng)請求時,說明請求處理正常,則通過線程上下文拿到開始時間,
//計算出響應(yīng)時間rt后將rt寫入統(tǒng)計數(shù)據(jù),同時success+1
@Override
protected void response(Object obj) {
       RpcContext rpcContext=RpcContext.getContext();
       Long startTime=rpcContext.getStartTime();
       if(startTime!=null){
           Long rt=TimeUtil.currentTimeMillis()-startTime;
           paladinMetric.addRT(rt);
           paladinMetric.addSuccess(1);
           logger.warn(rpcContext.getRpcRequest().getClassName()
                   +":"
                   +rpcContext.getRpcRequest().getMethodName()
                   +" 's RT is "
                   +rt);
       }else{
           logger.error(rpcContext.getRpcRequest().getClassName()
                   +":"
                   +rpcContext.getRpcRequest().getMethodName()
                   +"has no start time!");
       }
}

//這里就是統(tǒng)一處理異常的方法,區(qū)分為普通異常和拒絕異常,
//如果是拒絕異常,說明線程已滿,拒絕添加任務(wù),reject+1
@Override
protected void caughtException(Object obj) {
   paladinMetric.addException(1);
   if(obj instanceof RejectedExecutionException){
       paladinMetric.addReject(1);
   }
}
 

每個Context都會繼承AbstractContext,只需要實現(xiàn)handle、response和caughtException方法即可,由AbstractContext屏蔽了底層pipeline的順序調(diào)用。

 

線程分配

最后就是如何動態(tài)的將線程分配給服務(wù)。在這里,我們需要抽象一個評價模型,去評估各個服務(wù)應(yīng)該占用多少資源(線程),可以參考下圖:

怎么探討RPC框架中的服務(wù)線程隔離


簡單來說,由于監(jiān)控節(jié)點的存在,我們很容易就拿到每個服務(wù)的流量數(shù)據(jù),然后抽象出每一個服務(wù)的評價模型,最后通過某種策略,得到線程分配的結(jié)果。

同時服務(wù)-線程的對應(yīng)關(guān)系的讀寫,顯然是一個讀多寫少的場景。可以后臺開啟一個線程,每隔一段時間(比如20s),執(zhí)行一次動態(tài)分配的策略。采用CopyOnWrite的思想,將對應(yīng)關(guān)系的引用用volatile修飾,線程重新分配完成之后,直接替換掉其引用即可,這樣對性能的影響便沒有那么大了。

這里的問題在于,如何合理的制定分配的策略。由于我實在缺乏相應(yīng)的經(jīng)驗,所以寫的比較撈,希望有大佬可以指點一二。

 

效果如何

說了這么多,那我們便來看看效果如何。代碼我都放在了github上(由于時間比較短再加上本人菜,寫得比較粗糙,請大家見諒T T),代碼樣例都在paladin-demo模塊中,這里我就直接上結(jié)果了。

先定義一下參數(shù),線程數(shù)總共20,每個服務(wù)最少能分配線程數(shù)為5,每條線程的阻塞隊列容量為4,服務(wù)端兩個服務(wù),一個阻塞時間長,另一個無阻塞。

這里先定義一個阻塞時間長的服務(wù)HelloWorld。

怎么探討RPC框架中的服務(wù)線程隔離


然后我們通過http請求觸發(fā)任務(wù),模擬大流量請求。

怎么探討RPC框架中的服務(wù)線程隔離


同時給出一個無阻塞的服務(wù)HelloPaladin,可以通過http訪問。

怎么探討RPC框架中的服務(wù)線程隔離


先后啟動服務(wù)服務(wù)提供端和消費端,開啟任務(wù)。控制臺直接炸裂。

怎么探討RPC框架中的服務(wù)線程隔離


服務(wù)瘋狂拋出拒絕異常。我們再輸入localhost:8080/helloPaladin?value=lalala,多點幾次,可以發(fā)現(xiàn)頁面很快就能返回結(jié)果,這也意味著這個服務(wù)并沒有被干擾。

最后我們來看一下,在任務(wù)啟動后,線程分配的情況如何:

22:15:06,653  INFO PaladinMonitor:81 - totalScore: 594807
22:15:06,653  INFO PaladinMonitor:91 - service: com.lcf.HelloPaladin:1.0.0_paladin, score: 1646
22:15:06,653  INFO PaladinMonitor:91 - service: com.lcf.HelloWorld:1.0.0_paladin, score: 593161
22:15:06,654  INFO PaladinMonitor:113 - Threads re-distribution result: {com.lcf.HelloPaladin:1.0.0_paladin=[1, 2, 3, 4, 5], com.lcf.HelloWorld:1.0.0_paladin=[6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]}
 

第一行輸出的是所有服務(wù)總共的分數(shù),接下來兩行分別是兩個服務(wù)所得到的分數(shù),最后一行是線程分配之后的結(jié)果。

我們穿插調(diào)用的HelloPaladin服務(wù)得到的分數(shù)遠遠低于跑任務(wù)的服務(wù)HelloWorld,但是由于設(shè)置了最小線程數(shù),所以HelloPaladin服務(wù)分到了5條線程,而HelloWorld服務(wù)占據(jù)了其余的線程。(這里由于還開啟了一個單線程服務(wù),所以沒有0號線程,至于什么是單線程服務(wù)可以看后文)

可以看到,服務(wù)間的線程資源確實隔離了,某一個服務(wù)的不可用不會影響到其他服務(wù),同時資源也會向大流量的服務(wù)傾斜。

 

更花哨的玩法

在實現(xiàn)上面的功能之后,或許還有更加花哨的玩法??紤]這樣一個場景,如果某個服務(wù)存在頻繁加鎖的場景,那么多個線程并發(fā)加鎖執(zhí)行,未必會有單個線程串行無鎖執(zhí)行來的效率高,畢竟鎖和線程切換的開銷也不容忽視。

在實現(xiàn)了服務(wù)與線程的對應(yīng)關(guān)系之后,這種串行無鎖執(zhí)行的思路就很容易實現(xiàn)了,在初始化的時候,直接分配給這個服務(wù)固定的線程id號即可,這個線程也不會參與后續(xù)的動態(tài)分配流程??梢酝ㄟ^注解參數(shù)的方式來實現(xiàn):


@RpcService(type = RpcConstans.SINGLE)
public class HelloSynWorldImpl implements HelloSynWorld
 

就是這么簡單,服務(wù)器啟動之后你就會發(fā)現(xiàn),這個服務(wù)都會使用某條固定的線程去執(zhí)行,自然也就用不著加鎖了(除非要跟其他服務(wù)同時操作共享資源,那就不適用于這種場景),不過這種串行場景我想了想,好像并不多,只有在那種純內(nèi)存的操作中可能會比較有性能優(yōu)勢(是不是很像Redis),所以也就圖一樂。

 

相比原來的線程模型有何優(yōu)劣?

話又說回來了,雖然解決了服務(wù)資源隔離和分配的問題,那么相比原來的線程模型是否就沒有劣勢了呢?

因為加入了更多的組件,考慮到監(jiān)控節(jié)點的性能損耗,增加了分配線程、選擇線程的邏輯,或許在性能上相比原來的線程模型會差一點,至于差多少,我可能也沒法定量給出解答,還需要進一步的測試。不過可以肯定的是,可以通過更多的優(yōu)化,使得兩者的性能更加接近,例如:用JcTool的無鎖隊列替換JDK中的阻塞隊列;給出合適的評價模型,使得資源分配更合理以及分配過程性能更優(yōu)等等。

當(dāng)然最關(guān)鍵的還是你業(yè)務(wù)代碼寫的咋樣,畢竟框架優(yōu)化的再好,業(yè)務(wù)代碼不大行,那點優(yōu)化效果微乎其微。

以上就是怎么探討RPC框架中的服務(wù)線程隔離,小編相信有部分知識點可能是我們?nèi)粘9ぷ鲿姷交蛴玫降?。希望你能通過這篇文章學(xué)到更多知識。更多詳情敬請關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

rpc
AI