微信Mars策略分析【轉】
轉自 https://blog.dreamtobe.cn/mars/
前幾年在微信工作時,參加一些內部會議當時也有做了相關簡要整理: Android網路,前端時間也有總結了微信的心跳機制,今天主要是對已開源的Mars進行窺探。如果對目前行業中網際網路現狀感興趣也可以留意下這裡
Mars是微信已經開源的小資料傳輸解決方案與一些客戶端開發常用工具集專案: https://github.com/Tencent/mars
具體部分可以參考官方圖片:
- Commn: 網路元件
- STN: 信令網路(小資料傳輸),Mars的主要部分
- SDT:
- CDN: 資料網路(大資料傳輸)(由於耦合騰訊CDN服務,因此沒有開源)
- XLOG: 日誌元件
- 終端質量平臺
I. STN模組
- 要求: 高可用、高效能、低負載、容災性
- 架構: 穩定、簡化
STN特徵
1. 對比
相比與IOS的AFNetworking、Android的Retrofit、OkHttp而言
- STN跨平臺
- STN優化是基於Socket層(大多數其他的庫都是HTTP層),因此能達到更深入的優化
- STN更適用於小資料傳輸,信令網路
2. 特點
- 包含兩種通道: 長連線(效能要求高的請求),短連線(普通一來一回的請求),對於上層業務而言只需要關注業務
- 更適合做移動網際網路元件: 前後臺、活躍態、休眠、省電、省流量。DNS防劫持,負載,容災
- 更多: 資料監控(網路情況),引數配置
STN連線策略
建立連線
1. 連線超時情景
- 連不通: 無論如何都差不多
- 被劫持/伺服器故障: 希望更快的返回,以便於更換IP埠快速查詢可用資源
- 弱網路,基站繁忙、連線訊號弱,丟包率高: 希望超時更長些,延時高,丟包率高,等長一點,重試多些(由於換IP/埠無用)
2. 連線超時考量點
可用性,網路敏感性,使用者體驗(< 1min)
3. 連線超時間隔策略
- Linux與Android: 指數遞增(0,2,4,8,16)
- IOS: 優化後的指數遞增(1,1,1,1,1,2,4,8,16)
- STN: 策略權衡相對適用於微信(可能有些應用需要更高的敏感性): 10s
STN連線超時間隔10s的原因:
- 佔用重試頻率比較高的間隔
- 10s之後,Android需要經過5s才能發起下一次重試(需要相對長無效的等待時間)
4. STN連線效率策略
- 目標: 快速找到有效可用的IP/埠資源
常見策略:
- 序列連線: 資源佔用少、無伺服器負載問題、超時選擇困難、最慢可用選擇
- 併發連線: 網路資源競爭(複雜度、效能消耗)、伺服器負責(成倍增加)、最快可用(提高效率)
STN策略:
5. 建立連線其他優化
維持連線
1. 優化探究
- 鏈路層: 需要在不可靠的物理裝置的基礎上,實現節點與節點間可靠的資訊傳輸,一般使用混合自動重傳請求(
HARQ(Hybrid Automatic Repeat reQuest) = FEC(前饋式錯誤修正) + ARQ(自動重傳請求)
),其能夠使得前一個失敗的嘗試中存下有用資訊供之後的解碼使用,這個需要手機與RNC都支援 - 傳輸層(TCP層): 需要基於不可靠的鏈路做端與端之間每個TCP資料包的可靠傳輸,是通過超時和重傳做到的,在傳送資料時設定一個定時器,定時器溢位時還沒有收到ACK,則重傳該資料
應用層:
- 更應該為使用者體驗考慮(儘可能提高成功率)
- 保障弱網路下的可用性
- 具有網路敏感性,快速的發現新鏈路
2. 讀寫超時間隔策略
TCP確認失敗的時間: Android系統16min, IOS系統1min~3.5min
- UNIX的指數退避: [1,3,6,12,24,48,64,64…]
- Android: (OPPO手機資料,前部分更積極) [0.25,0.5,1,2,4,8,16,32,64,64,64…]
- IOS: (前部分更積極,後部分也很積極) [1,1,1,2,4.5,9,13.5,26,26…]
P.S. Unix中的
指數退避
的間隔是取決於 RTT,而RTT本身由於受網路路由、流量等影響,有極為複雜的測量演算法(平滑演算法、Karn演算法、Jacbson演算法))。
STN讀寫超時(多級超時方案)(應用層讀寫超設計):
- 目標: 高效能(使用者體驗以及儘可能的提高成功率)、可用性(弱網場景)、敏感性(網路敏感性,快速發現新鏈路)
- 做法: 將原有連線斷掉,重新選擇IP與埠
- 作用: 減少無效的等待時間(因為重傳間隔越來越大,斷連重連,使TCP層保持積極重連間隔),增加重試次數;切換鏈路,在較大波動/嚴重擁塞,通過更換連線(IP與埠)獲得更好的效能
STN讀寫超時具體多級超時方案:
- 總讀寫超時: 請求發出去到完整的伺服器回包收完為止 =
發包大小/最低網速(主觀 評估值)
+伺服器約定最大耗時(主觀)
+最大回包大小(由於無法事先獲知回包大小(微信最大回包128KB))/最低網速(主觀 評估值)
+併發數 * 常量
由於 總讀寫超時 太主觀,並且是一個差網路下、完整的完成單次信令互動的時間估值,因此其值通常都會顯得過長,特別在網路波動或擁塞時,無法敏感的發現問題並重試,遂根據步驟進行拆分:
- 首包超時: 當傳送資料大於MSS時,資料會被分段傳輸,分段到達接收端後重新組合,因此這裡將首個數據分段到達超時定義為首包超時 =
發包大小/最低網速(主觀 評估值)
+伺服器約定最大耗時(主觀)
+併發數 * 常量
- 包包超時: 兩個資料分段之間的超時時間,這個時候因為服務端已經處理完成,不需要再計算等待耗時、請求傳輸耗時、伺服器處理耗時 =
發包大小/網速(客觀 準確值)
+併發數 * 常量
- 動態超時: 分析網路狀態,當趨於穩定的時候就減少首包超時,此時如果網路波動時我們預期它能夠快速恢復,所以儘快超時然後進行重試,從而改善使用者體驗
心跳策略
其中的有考慮到如何讓手機更省電,因此有與Android的alarm對齊喚醒的處理(可以參見已經開源的mars的smart_heartbeat.cc
)
II. XLog
高效能跨平臺日誌模組
所解決的問題
效能、可靠性、安全
- 避免頻繁GC: 由於Native的實現,有效避免了頻繁寫檔案造成的頻繁的GC
- 避免頻繁落盤的IO與加密: 達到一定數量日誌了再壓縮,壓縮完再加密(由於短語式加密,如果先加密後壓縮率會被嚴重影響)
- 避免丟日誌: 將日誌寫入mmap中,避免程式被系統殺死不會有事件通知
- 避免IO的耗時: 通過寫mmap來達到寫檔案,其效能與直接操作記憶體相當
- 避免日誌洩露: 在寫入mmap之前就已經進行逐行壓縮,並且寫入前進行了加密
- 避免CPU短暫飆高: 採用多條日誌流式壓縮(日誌行數累計到一定大小作為一個壓縮單元進行壓縮),壓縮演算法效能較高,由於每個單元的日誌並不多,可以把壓縮時間分散在整個分散週期內,CPU曲線更平滑
mmap
1. 引入mmap的原因
常規寫檔案所帶來的問題:
寫檔案,系統是不會直接把資料寫入磁碟,而是先把資料寫入到系統快取(dirty page),再根據策略將dirty page寫入磁碟
dirty page寫入磁碟策略:
- 定時寫回(相關變數在:
/proc/sys/vm/dirty_writeback_centisecs
、/proc/sys/vm/dirty_expire_centisecs
) - dirty page的大小超過一定比例(呼叫write時檢測,比例變數儲存在
/proc/sys/vm/dirty_background_ratio
、/proc/sys/vm/dirty_ratio
) - 記憶體不足: 當記憶體不足時,就會block住當前執行緒
- 日誌的場景: 日誌的場景與普通的寫檔案場景不同,是斷斷續續,但是不斷有日誌
內容從程式呼叫,到寫入磁碟過程: 使用者空間記憶體
-> 核心空間快取
-> 磁碟
,因此涉及使用者空間與核心空間頻繁切換。應用層不可控,出現瓶頸。
綜合頻繁寫檔案會帶來的瓶頸以及寫入記憶體快取帶來的丟日誌的問題(還有共享記憶體在Android 4.0以後便不再有許可權使用),因此更好的解決方案mmap。
2. 什麼是mmap?
邏輯記憶體對磁碟檔案進行對映(不再有拷貝)
mmap帶來的益處顯而易見,操作記憶體
= 操作檔案
, 從而避免了 使用者空間與核心空間頻繁的切換,也正因為如此,我們只需要將日誌寫入mmap,就不會應用當前程序(/虛擬機器)被殺導致日誌丟失的問題。
3. XLog中將mmap回寫檔案的策略
- 記憶體不足
- 程序 crash
- 呼叫 msync 或者 munmap
- 不設定 MAP_NOSYNC 情況下 30s-60s(僅限FreeBSD)
壓縮演算法
為什麼只使用LZ77中的短語式壓縮
因為進一步的壓縮(如自定義字典/huffman表)無法帶來顯著的效果,並且只要提供控制好壓縮單元的長度,僅僅進行 短語式壓縮 就能帶來83.7%
的壓縮率。
壓縮策略
- 沒被壓縮的字元依然是ascci編碼(整數表示),因此最終的壓縮結果都是一堆整數
- 滑動歷史快取視窗: 一般是32kb,因此只需要一定大小的壓縮單元就可以達到很好的壓縮率,並非 大小越大的單元壓縮率越大
- 採用分單元流式壓縮,而非整個app生命週期內一起壓縮,雖然耗時慢了許多但是由於耗時極小因此可以忽略,並且帶來了很多好處:1. 個別的壓縮錯誤影響面縮小到對應的單元; 2. 壓縮時間分散在各個單元壓縮時,因此CPU曲線更平滑
加密策略
考慮採用DH(Diffie-Hellman)。
- DH中有Ax, Ay, Bx, By。
- 其中通過DH演算法:
DH(Ax, By) = DH(Ay, Bx)
- 後端存固定的Bx與By,客戶端儲存固定的Bx
- 客戶端生成Ax, Ay
- 客戶端將DH(Ay, Bx)作為key,將Ax作為keybuffer
程式碼架構分層
XLog的程式碼主要分為三層
1. 收集層
該層主要與平臺語言相關,主要是結構化日誌
- format等處理在各類邏輯之後再處理,如有些日誌級別我們不做儲存,那麼在這之前就不進行format
- format處理需要考慮有些內容中本身就包含了
%
,如100%
,導致format引數不足報錯等問題,XLog中自己做了一層路由做這層處理
2. 介面層
這一層主要是C語言定義的對收集層暴露的介面
3. 輸出層
將收集過來的日誌進行處理,如落盤到檔案、輸出到控制檯、輸出到logcat等
也是對介面層的具體實現。
- 壓縮與策略
- 加密與策略
- 落盤與策略