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

分享嘉賓簡介:魅族科技平臺事業部於洋子,專一於高併發高性能服務端架構設計與開發,參與過flyme通信、推送平臺、實時大數據統計等項目。前端

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

初版:沒有開發框架git

首個版本的架構很是簡單粗暴,首先開一個WEB接口,接入PUSH,再開一個TCP長鏈接的接口,讓手機連上。這麼作的目的就是爲了能快速上線。不過快是快了,問題也很嚴重。這個版本沒有開發框架,徹底從socket寫起,不只難寫,並且不能水平擴展,承載能力也很是有限。github

第二版:框架首次出現redis

隨着魅族用戶量級的快速提高,很快迭代了第二個版本。第二版首次出現了開發框架,命名爲「Kiev」。這個版本對手機鏈接的部分進行了拆分,拆出接入層和路由層,業務層支持水平擴展,這樣重構之後抗住了百萬級的用戶量。不過一樣存在很多問題,由於仍是在用普通的HASH算法在作均衡負載,擴容很是不平滑,容易影響用戶體驗。並且隨着用戶量的增加,日誌量變的很是多,甚至都要把磁盤刷爆。此外,因爲使用的文本協議很臃腫,當某一天中午12點推送高峯期的時候,整個公司的機房帶寬都被吃完,其餘業務受到了不一樣程度的干擾。算法

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

第三版:增長限速、業務流程優化、日誌切割和壓縮後端

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

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

第四版:全面重構

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

一是將限速、接入層、路由層、邏輯層等都作成了無狀態服務,這樣的話在整個擴容的過程當中能夠作到徹底平滑;

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

三是流程上的優化,這個仍是趨於流量的考量。你們都知道移動互聯網有個很顯著的特色,就是手機網絡特別不穩定,可能這一秒在線,下一秒走進電梯就失去信號,這個時候若是直接進行消息推送的話,既浪費機房帶寬,又沒效果,並且還可能會出現重複推送的問題。因此針對這種狀況,魅族的作法是每次先推一個很小的只有幾個字節的消息過去,若是手機端的網絡穩定,它會回覆一個一樣很小的消息,這時候再真正進行消息推送,這樣能夠有效利用帶寬資源。並且給每一條消息打上惟一的序號,當手機端每次收到消息時,會將序號儲存起來,下次拉取消息的時候再帶上來,好比某用戶已收到一、二、3的消息,拉取的時候把3帶上來,服務端就知道一、二、3都已經推過了,直接推送4以後的消息便可,避免消息重複。

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

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

隨着業務越作越大,業務流程也變得愈來愈複雜。舉個栗子,魅族有一個業務流程中,請求過來時,會先和Redis來回交互幾回,而後才訪問MongoDB,最後還要和Redis交互幾回才能返回結果。

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

針對這種複雜的問題,魅族引入了「協程」,用仿造Golang的方式本身作了一套協程框架Libgo。重構後的代碼變成以下圖左側的方式,整個業務流程是順序編寫的,不只沒有損失運行的效率,同時還提升了開發的效率。

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

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

開源地址:https://github.com/yyzybb537/libgo

業務越作越大,發現新問題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源創會】珠海站,轉載請註明出處。)

相關文章
相關標籤/搜索