魅族 C++ 微服務框架技術內幕揭秘

JerrodHamle 8年前發布 | 18K 次閱讀 微服務 移動開發 C/C++

kiev,是魅族科技推送平臺目前使用的C++后臺開發框架。2012年,魅族的推送業務剛剛有一點從傳統架構向微服務架構轉型的意識萌芽,為了在拆分系統的同時提高開發效率,決定做一個C++開發框架,這就是最早期Kiev的由來。在不斷的演變中,框架也經過了多次調整升級,在此一一進行講述和揭秘。框架是為架構在做服務,所以整篇內容會在架構演進和框架演進兩條線之間交錯展開。

第一版:沒有開發框架

首個版本的架構非常簡單粗暴,首先開一個WEB接口,接入PUSH,再開一個TCP長連接的接口,讓手機連上。這么做的目的就是為了能快速上線。不過快是快了,問題也很嚴重。這個版本沒有開發框架,完全從socket寫起,不僅難寫,而且不能水平擴展,承載能力也非常有限。

第二版:框架首次出現

隨著魅族用戶量級的快速提升,很快迭代了第二個版本。第二版首次出現了開發框架,命名為“Kiev”。這個版本對手機連接的部分進行了拆分,拆出接入層和路由層,業務層支持水平擴展,這樣重構以后抗住了百萬級的用戶量。不過同樣存在不少問題,因為還是在用普通的HASH算法在做均衡負載,擴容非常不平滑,容易影響用戶體驗。而且隨著用戶量的增長,日志量變的非常多,甚至都要把磁盤刷爆。此外,由于使用的文本協議很臃腫,當某一天中午12點推送高峰期的時候,整個公司的機房帶寬都被吃完,其他業務受到了不同程度的干擾。

這個版本的框架如下,左上角是Kiev協議,左下角是使用到的一些開源的第三方庫 ,包括谷歌開源的Protobuf、用于加解密的Openssl、 用于支持HTTP的Curl、優化內存分配的Tcmalloc等。右上角是Kiev框架的功能組件,包括提供HTTP接口的FastCGI、一些常用的算法和數據結構、日志模塊、編碼常用的定時器以及一個自研的單鏈接能達到10W+QPS的Redis Client。

第三版:增加限速、業務流程優化、日志切割和壓縮

考慮到前面說到的帶寬撐爆問題,第三版增加了限速模塊。此外還做了一個業務流程上的優化,使用redis存儲離線消息,用戶上線時再推送出去。負載均衡上,改用一致性HASH算法,這樣做的好處是每次擴容受到影響的只有遷移的那一部分用戶,另一部分用戶則不會受任何影響,擴容變得平滑了很多。針對日志刷爆磁盤的問題,做了一個每天定時切割和壓縮日志的腳本。

看看這個版本在框架上做的一些修改,圖中深色部分為新增的東西:

第四版:全面重構

為了徹底解決第二版的一些問題,花了半年多的時間對框架進行全面重構。重構主要針對以下幾點:

一是將限速、接入層、路由層、邏輯層等都做成了無狀態服務,這樣的話在整個擴容的過程中可以做到完全平滑;

二是對協議進行優化,將原本臃腫的文本協議改為二進制協議,協議頭從700字節降到6個字節,大幅度降低了流量;

三是流程上的優化,這個還是趨于流量的考量。大家都知道移動互聯網有個很顯著的特點,就是手機網絡特別不穩定,可能這一秒在線,下一秒走進電梯就失去信號,這個時候如果直接進行消息推送的話,既浪費機房帶寬,又沒效果,而且還可能會出現重復推送的問題。所以針對這種情況,魅族的做法是每次先推一個很小的只有幾個字節的消息過去,如果手機端的網絡穩定,它會回復一個同樣很小的消息,這時候再真正進行消息推送,這樣可以有效利用帶寬資源。而且給每一條消息打上唯一的序號,當手機端每次收到消息時,會將序號儲存起來,下次拉取消息的時候再帶上來,比如某用戶已收到1、2、3的消息,拉取的時候把3帶上來,服務端就知道1、2、3都已經推過了,直接推送4之后的消息即可,避免消息重復。

這個版本的框架改進比較小,在上個版本的基礎上引入MongoDBClient,對序號進行索引。

業務越做越大,發現新問題1

隨著業務越做越大,業務流程也變得越來越復雜。舉個栗子,魅族有一個業務流程中,請求過來時,會先和Redis來回交互幾次,然后才訪問MongoDB,最后還要和Redis交互幾次才能返回結果。

這種時候如果按早期的異步模式去寫代碼,會很難看。可以看到整個業務流程被切割的支離破碎,寫代碼的和看代碼的人都會覺得這種方式很不舒服,也容易出錯。

針對這種復雜的問題,魅族引入了“協程”,用仿造Golang的方式自己做了一套協程框架Libgo。重構后的代碼變成如下圖左側的方式,整個業務流程是順序編寫的,不僅沒有損失運行的效率,同時還提高了開發的效率。

Libgo的簡介和開源地址如下:

  • 提供CSP模型的協程功能
  • Hook阻塞的系統調用,IO等待時自動切換協程
  • 無縫集成使用同步網絡模型的第三方庫 (mysqlclient/CURL)
  • 完善的功能體系:Channel / 協程鎖 / 定時器 / 線程池等等

業務越做越大,發現新問題2

在這個時期,在運營過程中有遇到一個問題,每天早上9點鐘,手機端會向服務端發一個小小的訂閱請求,這個請求一旦超時會再來一遍,不斷重試。當某天用戶量增長到1300萬左右的時候,服務器雪崩了!

雪崩的原因是因為過載產生的,通過分析發現過載是在流程中的兩個服務器間產生的。服務器A出現了大量的請求超時的log,服務器B出現接收隊列已滿的log,此時會將新請求進行丟棄。此時發現,在服務器B的接收隊列中積壓了大量請求,而這些請求又都是已經超時的請求,手機端已經在重試第二次,所以當服務器拿起之前這些請求來處理,也是在做無用功,正因為服務器一直在做無用功,手機端就會一直重試,因此在外部看來整個服務是處于不可用狀態,也就形成了雪崩效應。

當時的緊急處理方式是先對接收隊列的容量進行縮小,提供有損服務。所謂的有損服務就是當服務器收到1000個請求但只能處理200個請求時,就會直接丟棄剩下的800個請求,而不是讓他們排隊等待,這樣就能避免大量超時請求的問題。

那緊急處理后,要怎么樣根治這個問題呢?首先對這個過載問題產生的過程進行分析,發現是在接收隊列堵塞,所以對接收點進行改造,從原來的單隊列變為多隊列,按優先級進行劃分。核心級業務會賦予最高級的優先處理隊列,當高優先級的請求處理完后才會處理低優先級的請求。這樣做的就能保證核心業務不會因為過載問題而受到影響。

還有一點是使用固定數量的工作協程處理請求,這樣做的好處是可以控制整個系統的并發量,防止請求積壓過多,拖慢系統響應速度。

業務越做越大,發現新問題3

在最早的時候,這一塊是沒有灰度發布機制的,所有發布都是直接發全網,一直到機器量漲到上百臺時依然是用這種方式,如果沒問題當然皆大歡喜,有問題則所有一起死。這種方式肯定是無法長遠進行,需要灰度和分組。但由于服務是基于TCP長連接的,在業內目前沒有成熟的解決方案,所以只能自己摸索。

當時的第一個想法是進行分組,分為組1和組2,所有的請求過來前都加上中間層。這樣做的好處是可以分流用戶,當某一組出現故障時,不會影響到全部,也可以導到另外一組去,而且在發布的時候也可以只發其中一組。

那中間層這一塊要怎么做呢?在參考了很多業界的成熟方案,但大多是基于HTTP協議的,很少有基于TCP長連接的方案,最終決定做一個反向代理。它的靈感是來源于Nginx反向代理,Nginx反向代理大家知道是針對HTTP協議,而這個是要針對框架的Kiev協議,恰好魅族在使用ProtoBuf在做協議解析,具有動態解析的功能,因此基于這樣一個功能做了Kiev反向代理的組件。這個組件在啟動時會向后端查詢提供哪些服務、每個服務有哪些接口、每個接口要什么樣的請求、回復什么樣的數據等等。將這些請求存儲在反向代理組件中,組成一張路由表。接收到前端的請求時,對請求的數據進行動態解析,在路由表中找到可以處理的后端服務并轉發過去。

第五版:針對問題,解決問題

有了上述這些規則后,第五版也就是目前使用的版本部署如下圖。對邏輯層進行了分組,分流用戶。在實際使用過程中精準調控用戶分流規則,慢慢進行遷移,一旦發現有問題,立即往回倒。此外,還精簡了存儲層,把性價比不高的MongoDB砍掉,降低了70%的存儲成本。

很多項目特別是互聯網項目,在剛剛上線的時候都有個美好的開始,美好之處在于最初所有服務的協議版本號都是一樣的。就比如說A服務、B服務、C服務剛開始的時候全都是1.0,完全不用去考慮兼容性問題。當有一天,你需要升級了,要把這三個服務都變成2.0的時候,如果想平滑的去升級就只能一個一個來。而在這個升級的過程中,會出現低版本調用高版本,也會出現高版本調用低版本的情況,特別蛋疼,這就要求選擇的通訊協議支持雙向兼容,這也是魅族使用Protobuf的原因。

最終,完整的框架生態如下。虛線框內為后續將加入的服務。

魅族消息推送服務的現狀

該服務在過去的4年多來一直只是默默的為魅族的100多個項目提供,前段時間,正式向社區所有的開發者開放了這種推送能力,接入的交流群:QQ488591713。目前有3000萬的長連接用戶,為100多個項目提供服務。集群中有20多個微服務和數百個服務進程,有100多臺服務器,每天的推送量在2億左右。

(文章內容由開源中國整理自2016年9月10日的【OSC源創會】珠海站,轉載請注明出處。)

 

來自:https://my.oschina.net/osccreate/blog/752986

 

 本文由用戶 JerrodHamle 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!