魅族C++协程框架(Kiev)技术内幕
Kiev框架簡(jiǎn)介
kiev是魅族科技推送平臺(tái)目前使用的Linux-C++后臺(tái)開發(fā)框架。從2012年立項(xiàng)起,先后由多位魅族資深架構(gòu)師、資深C++工程師傾力打造,到本文寫就的時(shí)間為止,已經(jīng)在推送平臺(tái)這個(gè)千萬用戶級(jí)的大型分布式系統(tǒng)上經(jīng)歷了近5年的考驗(yàn)。如今Kiev在魅族推送平臺(tái)中,每天為上百個(gè)服務(wù)完成數(shù)百億次RPC調(diào)用。
kiev作為一套完整的開發(fā)框架,是專為大型分布式系統(tǒng)后臺(tái)打造的C++開發(fā)框架,由以下幾個(gè)組件組成:
- RPC框架(TCP/UDP)
- FastCGI框架
- redis客戶端(基于hiredis封裝)
- mysql客戶端(基于mysqlclient封裝)
- mongodb客戶端
- 配置中心客戶端(Http協(xié)議, 基于curl實(shí)現(xiàn))
- 基于zookeeper的分布式組件(服務(wù)發(fā)現(xiàn)、負(fù)載均衡)
- 日志模塊
- 狀態(tài)監(jiān)控模塊
- 核心模塊是一個(gè)開源的`CSP并發(fā)模型`協(xié)程庫(libgo)
并發(fā)模型
Kiev采用了很先進(jìn)的CSP開發(fā)模型的一個(gè)變種(golang就是這種模型),這一模型是繼承自libgo的。 選擇這種模型的主要原因是這種模型的開發(fā)效率遠(yuǎn)高于異步回調(diào)模型,同時(shí)不需要在性能上做出任何妥協(xié),在文中會(huì)對(duì)常見的幾種模型做詳細(xì)的對(duì)比。
CSP模型
CSP(Communicating Sequential Process)模型是一種目前非常流行的并發(fā)模型,golang語言所采用的并發(fā)模型就是CSP模型。 在CSP模型中,協(xié)程與協(xié)程間不直接通信,也不像Actor模型那樣直接向目標(biāo)協(xié)程投遞信息,而是通過一個(gè)Channel來交換數(shù)據(jù)。
這樣設(shè)計(jì)的好處是通過Channel這個(gè)中間層減少協(xié)程間交互的耦合性,同時(shí)又保證了靈活性,非常適合開發(fā)并發(fā)程序。
RPC框架
RPC(Remote Procedure Call)是一種遠(yuǎn)程調(diào)用協(xié)議,簡(jiǎn)單地說就是能使應(yīng)用像調(diào)用本地方法一樣的調(diào)用遠(yuǎn)程的過程或服務(wù),可以應(yīng)用在分布式服務(wù)、分布式計(jì)算、遠(yuǎn)程服務(wù)調(diào)用等許多場(chǎng)景。說起 RPC 大家并不陌生,業(yè)界有很多開源的優(yōu)秀 RPC 框架,例如 Dubbo、Thrift、gRPC、Hprose 等等。 RPC框架的出現(xiàn)是為了簡(jiǎn)化后臺(tái)內(nèi)部各服務(wù)間的網(wǎng)絡(luò)通訊,讓開發(fā)人員可以專注于業(yè)務(wù)邏輯,而不必與復(fù)雜的網(wǎng)絡(luò)通訊打交道。 在我們看來,RPC框架絕不僅僅是封裝一下網(wǎng)絡(luò)通訊就可以了的,要想應(yīng)對(duì)數(shù)以百計(jì)的不同服務(wù)、數(shù)千萬用戶、百億級(jí)PV的業(yè)務(wù)量挑戰(zhàn),RPC框架還必須在高可用、負(fù)載均衡、過載保護(hù)、通信協(xié)議向后兼容、優(yōu)雅降級(jí)、超時(shí)處理、無序啟動(dòng)幾個(gè)維度都做到足夠完善才行。
服務(wù)發(fā)現(xiàn)
Kiev使用zookeeper做服務(wù)發(fā)現(xiàn),每個(gè)kiev服務(wù)開放時(shí)會(huì)在zookeeper上注冊(cè)一個(gè)節(jié)點(diǎn),包含地址和協(xié)議信息。水平擴(kuò)展時(shí),同質(zhì)化服務(wù)會(huì)注冊(cè)到同一個(gè)路徑下,產(chǎn)生多個(gè)節(jié)點(diǎn)。 依賴的服務(wù)調(diào)用時(shí),從zookeeper上查詢當(dāng)前有哪些節(jié)點(diǎn)可以使用,依照負(fù)載均衡的策略擇一連接并調(diào)用。
負(fù)載均衡
內(nèi)置兩種負(fù)載均衡策略:robin和conhash,并且根據(jù)實(shí)際業(yè)務(wù)場(chǎng)景可以定制。
過載保護(hù)
Kiev內(nèi)置了一個(gè)過載保護(hù)隊(duì)列,分為10個(gè)優(yōu)先級(jí)。每個(gè)請(qǐng)求到達(dá)時(shí)先進(jìn)入過載保護(hù)隊(duì)列,而后由工作協(xié)程(work-coroutine)取出請(qǐng)求進(jìn)行處理。 如果工作協(xié)程的處理速度低于請(qǐng)求到達(dá)的速度,過載保護(hù)隊(duì)列就會(huì)堆積、甚至堆積滿。 當(dāng)過載保護(hù)隊(duì)列堆滿時(shí),新請(qǐng)求到達(dá)后會(huì)在隊(duì)列中刪除一個(gè)更低優(yōu)先級(jí)的請(qǐng)求,騰出一個(gè)空位,塞入新請(qǐng)求。 同時(shí),隊(duì)列中的請(qǐng)求也是有時(shí)效性的,過長(zhǎng)時(shí)間未能被處理的請(qǐng)求會(huì)被丟棄掉,以此避免處理已超時(shí)的請(qǐng)求。 這種機(jī)制保證了當(dāng)系統(tǒng)過載時(shí)盡量將有限的資源提供給關(guān)鍵業(yè)務(wù)使用。
通信協(xié)議向后兼容
由于微服務(wù)架構(gòu)經(jīng)常需要部分發(fā)布,所以選擇一個(gè)支持向后兼容的通信協(xié)議是很必要的一個(gè)特性。 Kiev選取protobuf作為通信協(xié)議。
與第三方庫協(xié)同工作
最早期的Kiev是基于異步回調(diào)模型的,但是很多第三方庫只提供了同步模型的版本,很難搭配使用。 當(dāng)前的Kiev是CSP并發(fā)模型,配合libgo提供的Hook機(jī)制,可以將同步模型的第三方庫中阻塞等待的CPU時(shí)間充分利用起來執(zhí)行其他邏輯,自動(dòng)轉(zhuǎn)化成了CSP并發(fā)模型;異步回調(diào)模型的第三方庫也可以使用CSP模型中的Channel來等待回調(diào)觸發(fā);從而完美地與第三方庫協(xié)同工作。
kiev功能組件結(jié)構(gòu)圖
Kiev發(fā)展史與技術(shù)選型
2012年,魅族的推送業(yè)務(wù)剛剛有一點(diǎn)從傳統(tǒng)架構(gòu)向微服務(wù)架構(gòu)轉(zhuǎn)型的意識(shí)萌芽,為了在拆分系統(tǒng)的同時(shí)提高開發(fā)效率,我們決定做一個(gè)C++開發(fā)框架,這就是最早期Kiev的由來.
第一個(gè)版本的Kiev使用了多線程同步模型,業(yè)務(wù)邏輯順序編寫,非常簡(jiǎn)單。 但是由于os對(duì)線程數(shù)的支持有限,隨著線程數(shù)量的增長(zhǎng),調(diào)度消耗的增長(zhǎng)是非線性的,因此不能支持過高的請(qǐng)求并發(fā)。
隨著用戶量的增長(zhǎng),我們需要支持更高的并發(fā)請(qǐng)求,由于當(dāng)年協(xié)程還不像現(xiàn)在這樣流行,所以我們決定使用異步回調(diào)模型編寫Kiev。早期的業(yè)務(wù)形態(tài)非常簡(jiǎn)單,使用異步回調(diào)模型也勉強(qiáng)可以應(yīng)付開發(fā)任務(wù)。
在其后幾年中,我們使用異步回調(diào)模型的Kiev開發(fā)了大量的服務(wù),在使用中我們慢慢發(fā)現(xiàn)邏輯碎片化的問題越來越多,更可怕的是,有些時(shí)候長(zhǎng)長(zhǎng)的回調(diào)鏈還要和有限狀態(tài)機(jī)糾纏在一起,代碼越來越難以維護(hù)。常常出現(xiàn)類似于下面這樣的代碼片段:
針對(duì)這樣的問題,我們引入了騰訊開源的協(xié)程庫libco,在協(xié)程中執(zhí)行同步的代碼邏輯;同時(shí)使用Hook技術(shù),將阻塞式IO請(qǐng)求中等待的時(shí)間片利用起來,切換cpu執(zhí)行其他協(xié)程,等到IO事件觸發(fā)再切換回來繼續(xù)執(zhí)行邏輯。類似于上述的碎片化代碼就變成了連續(xù)性的業(yè)務(wù)邏輯,也不再需要手動(dòng)維護(hù)上下文數(shù)據(jù),臨時(shí)數(shù)據(jù)直接置于棧上即可,代碼變成如下的樣子:
然而,libco僅僅提供了協(xié)程和HOOK兩個(gè)功能,協(xié)程切換需要我們自己做,為了實(shí)現(xiàn)簡(jiǎn)單,RPC框架進(jìn)化成了連接池的模式,每次發(fā)起RPC調(diào)用時(shí)從連接池中取一條連接來發(fā)送請(qǐng)求,等待回復(fù),然后釋放回連接池。 每條連接同一時(shí)刻只能跑一個(gè)請(qǐng)求,rpc協(xié)議退化成了半雙工模式。此時(shí)為保證性能,不得不在每?jī)蓚€(gè)有依賴關(guān)系的服務(wù)之間建立數(shù)以百計(jì)的TCP連接,這樣在依賴了水平擴(kuò)展為很多進(jìn)程的服務(wù)上,就會(huì)與這些進(jìn)程分別建立數(shù)百連接,TCP連接高達(dá)數(shù)千,甚至上萬,對(duì)服務(wù)器造成了很大的壓力。連接請(qǐng)求如下圖所示,其中每條連接線都代表數(shù)以百計(jì)的TCP連接。
相應(yīng)地,我們也更新了kiev中的redis、mysql、fastcgi模塊,都改為了協(xié)程模型的。
在最初的幾個(gè)月中,這種方式很好地幫我們提升了開發(fā)效率,同時(shí)也有著還算不錯(cuò)的性能(Rpc請(qǐng)求差不多有20K左右的QPS)。隨著時(shí)間的流逝,我們的用戶越來越多,請(qǐng)求量也越來越大,終于在某次新品發(fā)布后,我們的一個(gè)非關(guān)鍵性業(yè)務(wù)出現(xiàn)了故障。
出現(xiàn)故障的這個(gè)業(yè)務(wù)是一個(gè)接受手機(jī)端訂閱請(qǐng)求的業(yè)務(wù),手機(jī)端在訂閱請(qǐng)求超時(shí)后(大概30s),會(huì)重新嘗試發(fā)起請(qǐng)求。由于當(dāng)時(shí)系統(tǒng)過載,處理速度慢于請(qǐng)求速度,大量請(qǐng)求積壓在隊(duì)列中,隨著時(shí)間的推移,服務(wù)處理請(qǐng)求的響應(yīng)速度越來越慢,最終導(dǎo)致很多請(qǐng)求還沒處理完手機(jī)端就認(rèn)為超時(shí)了,重新發(fā)起了第二次請(qǐng)求,形成雪崩效應(yīng)。當(dāng)時(shí)緊急增加了一些服務(wù)器,恢復(fù)了故障,事后總結(jié)下來發(fā)現(xiàn),事件的主因還是因?yàn)槲覀儧]有做好過載保護(hù)機(jī)制。于是我們決定在Kiev中內(nèi)置過載保護(hù)功能,增加一個(gè)分為10個(gè)優(yōu)先級(jí)的過載保護(hù)隊(duì)列。每個(gè)請(qǐng)求到達(dá)時(shí)先進(jìn)入過載保護(hù)隊(duì)列,而后由工作協(xié)程(work-coroutine)取出請(qǐng)求進(jìn)行處理。當(dāng)過載保護(hù)隊(duì)列堆滿時(shí),隊(duì)列中刪除一個(gè)最低優(yōu)先級(jí)的請(qǐng)求,騰出一個(gè)空位。同時(shí),隊(duì)列中的請(qǐng)求也是有時(shí)效性的,過長(zhǎng)時(shí)間未能被處理的請(qǐng)求會(huì)被丟棄掉,以此避免處理已超時(shí)的請(qǐng)求。如下圖所示:
隨著機(jī)器越來越多,以及后續(xù)出現(xiàn)了一些超長(zhǎng)鏈路請(qǐng)求的業(yè)務(wù)形態(tài)(這里解釋一下長(zhǎng)鏈路請(qǐng)求的問題,長(zhǎng)鏈路請(qǐng)求是指一個(gè)請(qǐng)求要流經(jīng)很多服務(wù)處理,在處理流程中,前面的服務(wù)一定要等到后面的服務(wù)全部處理完成或超時(shí),才會(huì)釋放其占用的TCP連接,這樣的模式會(huì)極大地影響整個(gè)系統(tǒng)的請(qǐng)求并發(fā)數(shù)),TCP連接數(shù)方面的壓力越來越大,最終不得不考慮改為單連接上使用全雙工模式。然而當(dāng)時(shí)使用的libco功能過于簡(jiǎn)單,很難基于此開發(fā)全雙工模式的RPC框架,恰好當(dāng)時(shí)有一位同事在github上做了一個(gè)叫l(wèi)ibgo的開源項(xiàng)目,是一個(gè)和golang語言一樣的CSP并發(fā)模型的協(xié)程庫,于是我們做了一段時(shí)間的技術(shù)預(yù)研,看看能否替換掉現(xiàn)有的libco. 下面的表格是兩個(gè)項(xiàng)目在我們比較關(guān)心的一些維度上的對(duì)比:
通過調(diào)研,最終我們決定使用libgo替換掉libco。
基于CSP模型實(shí)現(xiàn)全雙工通信RPC非常容易,客戶端只需在每個(gè)request發(fā)出后保存id和channel并阻塞地等待相應(yīng)的channel,收到response時(shí)根據(jù)id找到對(duì)應(yīng)的channel并寫入數(shù)據(jù)即可。這樣只需一條TCP連接,就可以并發(fā)無數(shù)個(gè)request,分布式水平擴(kuò)展帶來的TCP連接管理方面的壓力就不再是問題了。同時(shí)由于每次RPC所需的資源更少,性能也有了很大提升,Rpc請(qǐng)求的QPS輕松提升到了100K以上。這一性能指標(biāo)目前已經(jīng)超越了絕大多數(shù)開源的RPC框架。
與流行開源框架對(duì)比
總結(jié)
以上是生活随笔為你收集整理的魅族C++协程框架(Kiev)技术内幕的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C/C++协程库libco:微信怎样漂亮
- 下一篇: ACE_Service_Handler类