【騰訊Bugly乾貨分享】微信終端跨平臺組件 Mars 系列 - 咱們如約而至

導語

昨天上午,微信在廣州舉辦了微信公開課Pro。因而,精神哥這兩天的朋友圈被小龍的「八不作」刷屏了。小夥伴們可能不知道,下午,微信公開課專門開設了技術分論壇。在分論壇中,微信開源了跨平臺的網絡組件Mars。git

以前Bugly也爲你們介紹過Mars的一些相關模塊的內容:
微信終端跨平臺組件 mars 系列(一) - 高性能日誌模塊xlog
微信終端跨平臺組件 mars 系列(二) - 信令傳輸超時設計github

今天,微信的小夥伴和你們一塊兒聊聊Mars的設計原則以及發展史。算法

背景

2012 年中,微信支持包括 Android、iOS、Symbian 等三個平臺。但在各個平臺上,微信客戶端沒有任何統一的基礎模塊。2012 年的微信正處於高速發展時期,各平臺的迭代速度不1、使用的編程語言各異,後臺架構也處在不斷探索的過程當中。多種因素使得各個平臺基礎模塊的實現出現了差別,致使出現屢次須要服務器作兼容的善後工做。網絡做爲微信的基礎,重要性不言而喻。任何網絡實現的 bug 均可能致使重大事故。例如微信的容災實現,若是由於版本的實現差別,致使某些版本上沒法進行容災恢復,將會嚴重的影響用戶體驗,甚至形成用戶的流失。咱們急需一套統一的網絡基礎庫,爲微信的高速發展保駕護航。編程

剛好,這個時候塞班漸入日暮,微信對塞班的支持也逐漸減弱。老大從塞班組抽調人力,組成一個三人小 team 的初始團隊,開始着手作通用的基礎組件。這個基礎組件最初就定位爲:跨平臺、跨業務的基礎組件。如今看,這個組件除了解決了已有問題,還給微信的高速發展帶來了不少優點,例如:安全

  • 基礎組件方便了開展專項的網絡基礎研究與優化。
  • 基礎組件爲多平臺的快速實現提供了有力的支持。

通過四年多的發展,跨平臺的基礎組件已經包含了網絡組件、日誌組件在內的多個組件。回頭看,這是一條開荒路。服務器

設計原則

在基礎模塊的開發中,設計尤其重要。在設計上,微信基礎組件以跨平臺、跨業務爲前提,聽從高可用,高性能,負載均衡的設計原則。微信

可用是一個即時通信類 App 的立身之本。高可用又體如今多個層面上:網絡的可用性、 App 的可用性、系統的可用性等。網絡

  • 網絡的可用性
    移動互聯網有着丟包率高、帶寬受限、延遲波動、第三方影響等特色,使得網絡的可用性,尤爲是弱網絡下的可用性變得尤其關鍵。Mars 的 STN 組件做爲基於 socket 層的網絡解決方案,在不少細節設計上會充分考慮弱網絡下的可用性。多線程

  • App 的可用性
    App 的可用性包含穩定性、運行性能等多個方面。文章高性能日誌模塊 xlog 描述了 xlog 在不影響 App 運行性能的前提下進行的大量設計思考。架構

  • 系統的可用性
    除了考慮正常的使用場景,APP的設計還須要從整個系統的角度進行設計思考。例如在容災設計上,Mars 不只使用了服務器容災方案,也設計了客戶端的本地容災。當部分服務器出災時,目前微信能夠作到,15min 內把95%以上的用戶轉移到可用服務器上。

保障高可用並不表明能夠犧牲性能,對於一個用戶使用最頻繁的應用,反而更要對使用的資源精打細算。例如在 Mars 信令傳輸超時設計中,多級超時的設計充分的考慮了可用性與高性能之間的平衡取捨。

若是說高可用高性能只是客戶端自己的考慮的話,負載均衡就須要結合服務器端來考慮了,作一個客戶端網絡永遠不能只把眼光放在客戶端上。任何有關網絡訪問的決策都要考慮給服務器所帶來的額外壓力是多大。爲了選用質量較好的 IP,曾經寫了完整的客戶端測速代碼,後來刪掉,其中一個緣由是由於不想給服務器帶來額外的負擔。Mars 的代碼中,選擇 IP 時用了大量的隨機函數也是爲了規避大量的用戶同時訪問同一臺服務器而作的。

在這四年,我學到最多的就是簡單和平衡。 把方案作的儘量簡單,這樣纔不容易出錯。設計方案時大多數時候都不可能知足全部想達到的條件,這個時候就須要去平衡各個因素。在組件中一個很好的例子就是長鏈接的鏈接頻率(具體實現見longlink_connect_monitor.cc),這個鏈接頻率就是綜合耗電量,流量,網絡高可用,用戶行爲等因素進行綜合考慮的。

Mars 的發展歷程

階段一:讓微信跑起來

跨平臺基礎組件的需求起源於微信,首要目標固然是先承載起微信業務。爲了避免侷限於微信,知足跨平臺、跨業務的設計目標,在設計上,網絡組件定位爲客戶端與服務端之間的無狀態網絡信令通道,即交互方式主要包含一來一回、主動push兩種方式。這使得基礎組件無需考慮請求間的關聯性、時序性,核心接口獲得了極大的簡化。同時,簡潔的交互也使得業務邏輯的耦合極少。目前基礎組件與業務的交互只包括:編解碼、auth狀態查詢兩部分。核心接口以下:(具體見stn_logic.h)。

void StartTask(...);
int OnTaskEnd(...);
void OnPush(...);
bool Req2Buf(...);
int Buf2Resp(...);
bool MakeSureAuthed();

在線程模型的選擇上,最先使用的是多線程模型。當須要異步作一個工做,就起一個線程。多線程勢必少不了鎖。但當灰度幾回以後發現,想要規避死鎖的四個必要條件並無想象中的那麼容易。用戶使用場景複雜,客戶端的時序、狀態的影響因素多,例如網絡切換事件、先後臺事件、定時器事件、網絡事件、任務事件等,致使了很多的死鎖現象和對象析構時序錯亂致使的內存非法訪問問題。

這時,咱們開始思考,多線程確實有它的優勢:能夠併發甚至並行提升運行速度。可是對於網絡模塊來講,性能瓶頸主要是在網絡耗時上,並不在於本地程序執行速度上。那爲什麼不把大部分程序執行改爲串行的,這樣就不會存在多線程臨界區的問題,無鎖天然就不會死鎖。

所以,咱們目前使用了消息隊列的方案(具體實現見 comm/messagequeue 目錄),把絕大多數非阻塞操做放到消息隊列裏執行。而且規定,基礎組件與調用方之間的交互必須1. 儘快完成,不進行任何阻塞操做;2. 單向調用,避免造成環狀的複雜時序。消息隊列的引入很好的改善了死鎖問題,但消息隊列的線程模型中,咱們仍是不能避免存在須要阻塞的調用,例如網絡操做。在將來的嘗試中,咱們計劃引入協程的方式,將線程模型儘量的簡化。

在其它技術選型上,有時甚至須要細節到API 的使用,好比考慮平臺兼容性問題,捨棄了一些函數的線程安全版本,使用了 asctime、localtime、rand 等非線程安全的版本。

階段二:修煉內功

在屢次的灰度驗證、數據比對下,微信各平臺的網絡邏輯順利的過渡到了統一基礎組件。爲了有效的驗證組件的效果,咱們開發了 smc 的統計監控組件,開始關注網絡的各項指標,進行網絡基礎研究與優化,尤爲是關注移動網絡的特徵。

  • 基礎網絡優化。
    常規的網絡能力,例如 DNS 防劫持、動態 IP 下發、就近接入、容災恢復等,在這一階段獲得逐步的建設與完善。除此以外,Mars 的網絡模塊是基於 socket 層的網絡解決方案,在缺失大而全的 HTTP 能力的同時,卻能夠將優化作到更細緻,細緻到鏈接策略、鏈接超時、多級讀寫超時、收發策略等每一個網絡過程當中。例如,當遇到弱網絡下連通率較低,或者某些連通率很差的的服務器影響使用時,咱們使用了複合鏈接(代碼見complexconnect.inl)和 IP 排序(代碼見simple_ipport_sort.cc)的方案很好的應對這兩個問題。

  • 平臺特性優化。雖然 Mars 是跨平臺的基礎組件,但在不少設計上是須要結合各平臺的特性的。例如爲了儘可能減小頻繁的喚醒手機,引入了智能心跳,而且在智能心跳中考慮了 Android 的 alarm 對齊特性(具體實現見smart_heartbeat.cc)。再如在網絡切換時,爲了平滑切換的過程,使用了 iOS 中網絡的特性,在 iOS 中作了延遲處理等。

  • 移動特性優化。微信的使用場景大部分是在手機端進行使用,在組件的設計過程當中,咱們也會研究移動設備的特性,並進行結合優化。例如,結合移動設備的無線電資源控制器(RRC)的狀態切換,對一些性能要求特別特別敏感的請求,進行提早激活的優化處理等。

階段三:「抓妖記」

基礎組件全量上線微信後,以微信的用戶量,固然也會遇到各類各樣的「妖」。例如,寫網絡程序躲不開運營商。印象比較深入的某地的用戶反饋鏈接 WiFi 時,微信不可用,後來 tcpdump 發現,當包的大小超過必定大小後就發不出去。解決方案:在 WiFi 網絡下強制把 MSS 改成1400(代碼見 unix_socket.cc)。

作移動客戶端更避不開手機廠商。一次遇到了一個百思不得其解的 crash,堆棧以下:

#00  pc 0x43e50  /system/lib/libc.so (???)
#01  pc 0x3143  /system/vendor/lib/libvendorconn.so (handleDpmIpcReq+154)
#02  pc 0x2f6d  /system/vendor/lib/libvendorconn.so (send_ipc_req+276)
#03  pc 0x30ff  /system/vendor/lib/libcneconn.so (connect+438)

看堆棧結合程序 xlog 分析,非阻塞 socket 卡在了 connect 函數裏超過了6 min, 被咱們自帶的 anr 檢測(代碼見anr.cc)發現而後自殺。最後實在一籌莫展,聯繫廠商一塊兒排查,最終查明緣由:爲了省電,當手機鎖屏時連的不是 WiFi 且又沒有下行網絡數據時,芯片 gate 會關閉,block 住全部網絡請求,直到有下行數據或者超過 20min 纔會放開。當手機有網絡即便是手機網絡的狀況下,很難沒有下行數據,因此基本不會觸發組件自帶的 anr 檢測,但當手機沒鏈接任何網絡時,就很容易觸發。解決方案:廠商修改代碼邏輯,當沒有任何網絡時不 block 網絡請求。

運營商和手機廠商對咱們來講已是一個黑盒,但其實也遇到過更黑的黑盒。當手機長時間不重啓,有極小几率不能繼續使用微信,重啓手機會恢復。但由於一直找不到一個願意配合咱們又知足條件的用戶,致使這個問題很長一段時間內都沒有任何進展,最終偶然一個機會,在一臺測試機器上重現了該問題,tcpdump 發如今三步握手階段,服務器帶回的客戶端帶過去的 tsval 字段被篡改,致使三步握手直接失敗,並且這個篡改發生在離開服務器以後到達客戶端以前。


這個問題是微信網絡模塊中排查時間最長也是花費精力最多的一個問題,不只由於很長一段時間內無案例可分析,也由於在重現後,聯繫了大量的同事和外部有關人的幫忙,想排查出罪魁禍首。但由於中間涉及的環節和運營商相關部門過多,沒法繼續排查下去,最終也沒找到根本緣由。 解決辦法:服務器更改 net.ipv4.tcp_timestamps = 0。

這段時間是痛並快樂着,見識到了各類極差的網絡,才切膚感覺到移動網絡環境的惡劣程度,但看着咱們的網絡性能數據在穩步提高又有種知足感。截止到今天,已經不多有真正的網絡問題須要跟進了。這也是咱們能有時間開始把這些代碼開源出去的很大的一個緣由。

Mars 介紹

講述了一大堆 Mars 的發展歷程,終於來到主角的介紹了。大概一年前,咱們開始有想法把基礎組件開源出去,當時你們都在糾結叫什麼名字好呢?此時恰逢《火星救援》正在熱映,一位同事說乾脆叫 Mars 吧,因而就定下來叫了 Mars。看了看代碼,發現想要開源出去可能仍是須要作一些其餘工做的。

代碼重構

首先,代碼風格方面,由於最初咱們使用文件名、函數名、變量名的規則是內部定義的規則,爲了能讓其餘人讀起來更舒心,咱們決定把代碼風格改成谷歌風格,好比:變量名一概小寫, 單詞之間用下劃線鏈接;左大括號不換行等等。可是爲了更好的區分訪問空間,咱們又在谷歌代碼風格進行了一些變通,好比:私有函數所有是"__"開頭;函數參數所有以"_"開頭 等等。

其次,雖然最初的設計一直是秉承着業務性無關的設計,但在實際開發過程當中仍然不免帶上了微信的業務性相關代碼,比較典型的就是 newdns 。爲了 Mars 之後的維護以及保證開源出去代碼的同源,在開源出去以前必須把這些業務性有關的代碼抽離出來,抽離後的結構以下:

  • mars-open 也就是要開源出去的代碼,獨立 git repo。
  • mars-private 是可能開源出去的代碼,依賴 mars-open。
  • mars-wechat 是微信業務性相關的代碼,依賴 mars-open 和 mars-private。

最後,爲了接口更易用,對調用接口以及回調接口的參數也進行了反覆思考與修改。

編譯優化

在 Mars以前,是直接給 Android 提供動態庫(.so),由於代碼邏輯都已經固定,不須要有可定製的部分。給 Apple 系平臺提供靜態庫(.a),由於對外暴露的函數幾乎不會改變,直接把相應的頭文件放到相應的項目裏就行。但對外開源就徹底不同了:日誌的加密算法可能別人須要本身實現;長連或者短連的包頭有人須要本身定製;對外接口的頭文件咱們可能會修改……

爲了讓使用者可定製代碼,對於編譯 Android 平臺咱們提供了兩種選擇:1. 動態庫。有些可能須要定製的代碼都提供了默認實現。2. 先編譯靜態庫,再編譯動態庫。編譯出來靜態庫後,實現本身須要定製的代碼後,執行 ndk-build 後便可編譯出來動態庫。 對於 Apple 系平臺,把頭文件所有收攏爲 Mars 維護,直接編譯出 Framework。

爲了能讓開發者快速的入門,咱們提供了 Android、iOS、OS X 平臺的 demo,其餘平臺的編譯和 demo 會在不久就加上支持。

成型的 Mars 結構圖以下:

業界對比

咱們作的一直都不是知足全部需求的組件,只是作了一個更適合咱們使用的組件,這裏也列了下和同類型的開源代碼的對比。

  Mars AFNtworking Retrofit OkHttp
跨平臺 yes no no no
實現語言 C++ Objective-C Java Java
具體實現 基於 socket 基於 HTTP 基於 HTTP 基於 HTTP
支持完整的 HTTP no yes yes yes
支持長連 yes no no no
日誌 yes no no no
DNS 擴展 yes no no no
結合移動 App作設計 yes no no no
能夠看出:
  1. Mars 中包括一個完整的高性能的日誌組件 xlog;
  2. Mars 中 STN 是一個跨平臺的 socket 層解決方案,並不支持完整的 HTTP 協議;
  3. Mars 中 STN 模塊是更加貼合「移動互聯網」、「移動平臺」特性的網絡解決方案,尤爲針對弱網絡、平臺特性等有不少的相關優化策略。

總的來講,Mars 是一個結合移動 App 所設計的基於 socket 層的解決方案,在網絡調優方面有更好的可控性,對於 HTTP 完整協議的支持,已經考慮後續版本會加入。

總結

常常有朋友和我說:發現網絡信號差的時候或者其餘應用不能用的時候,微信仍然能發出去消息。不知不覺咱們好像什麼都沒作,回頭看,原來咱們已經作了這麼多。我想,並非任何一行代碼均可以經歷日活躍5億用戶的考驗,感謝微信給咱們提供了這麼一個平臺。如今咱們想把這些代碼和大家分享,運營方式上 Mars 所開源出去的代碼會和微信所用的代碼保持同源,全部開源出去的代碼也首先會在微信上驗證經過後再公開。開源並非結束,只是開始。咱們後續仍然會繼續探索在移動互聯網下的網絡優化。Talk is cheap, show you our code.


關注 Mars , 來 Github 給咱們 star 吧

https://github.com/Tencent/mars


更多精彩內容歡迎關注騰訊 Bugly的微信公衆帳號:

騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!

相關文章
相關標籤/搜索