GO千萬級消息推送服務

GO千萬級消息推送服務

來自專欄猿論22 人贊了文章

公司此前有一個簡單的文章訂閱業務,但是採用的是定時拉取的模式,周期比較長,時效性不佳。

於是考慮做一個長連接服務,主動把新產生的文章推送下去。

因為是web場景,所以優先考慮成熟的websocket協議,很多編程語言都有成熟的服務端開發框架。

技術核心難點

系統調用的瓶頸

假設有100萬人在線,那麼1篇文章會導致100萬次推送,10篇文章就是1000萬次推送。

根據經驗值,linux系統在處理TCP網路系統調用的時候,大概每秒只能處理100萬左右個包。

這麼看的話,推送1篇文章就已經到達了單機的處理能力極限,這是第一個難點。

鎖瓶頸

我們在推送時,需要遍歷所有的在線連接,通常這些連接被放在一個集合里。

遍歷100萬個連接去發送消息,肯定需要花費一個可觀的時間,而在推送期間客戶端仍舊在不停的上線與下線,所以這個集合是需要上鎖做並發保護的。

可見,遍歷期間上鎖的時間會非常長,而且只能有一個線程順序遍歷集合,這個耗時是無法接受的。

CPU瓶頸

一般客戶端與服務端之間基於JSON協議通訊,給每個客戶端推送消息前需要對消息做json encode編碼。

當在線連接比較少(比如1萬)而推送消息比較頻繁(每秒10萬條)的情況下,我們可以計算得到每秒要json encode編碼的次數是:10000 * 100000 = 10^9次。

即便我們提前對10萬條消息做json encode後,再向1萬個連接做分發,那麼每秒也需要10萬次的編碼。

JSON編碼是一個純CPU計算行為,非常耗費CPU,我們仍舊面臨不小的優化壓力。

解決技術難點

系統調用瓶頸

仍舊假設100萬人在線,那麼單機極限就是每秒推送1篇文章,這會帶來每秒100萬次的網路系統調用。

如果我們想推送100篇文章,仍舊使用單機處理,優化的思路是什麼呢?

很簡單,我們把100篇文章作為一條消息推送,那麼仍舊是每秒100萬次系統調用。

無論是10篇,50篇,80篇,我們都合併成1條消息推送,那麼100萬人在線的推送頻次就是恆定的每秒100萬次,不隨著文章數量的變化而變化。

當然,合併消息不可能無限大,當超過一定的閾值之後,TCP/IP層會進行大包拆分,此時底層實際包頻就會超過每秒100萬次,再次到達系統調用的極限。

鎖瓶頸

在做海量服務架構設計的時候,一個很有用的思路就是:大拆小。

既然100萬連接放在一個集合里導致鎖粒度太大,那麼我們就可以把連接通過哈希的方式散列到多個集合中,每個集合有自己的鎖。

當我們推送的時候,可以通過多個線程,分別負責若干個集合的推送任務。

因為推送的集合不同,所以線程之間沒有鎖競爭關係。而對於同一個集合併發推送多條不同的消息,我們可以把互斥鎖換成讀寫鎖,從而支持多線程並發遍歷同一個集合發送不同的消息。

其實操作系統管理CPU也是分時的,就像我們的推送任務被拆分成若干小集合一樣,每個集合只需要佔用一點點的時間片快速完成,而多個集合則儘可能的利用多核的優勢實現真並行。

CPU瓶頸

其實當我們通過消息合併的方式減少網路系統調用的時候,我們已經完成了對sys cpu的優化,操作系統用來處理網路系統調用的CPU時間大幅減少。

但是user CPU需要我們繼續做優化,我們如果在每個連接級別做json encode,那麼1篇文章就會帶來100萬次encode,是完全無法接受的性能。

因為業務上消息推送分2類,一種是按客戶端關注的主題做推送,一種是推送給所有客戶端。

基於上述特點,我們可以把消息合併動作提前到消息入口層,即把近一段時間所有要推往某個主題、推給所有在線的消息做消息合併成batch,每個batch可能包含100條消息。當1個batch塞滿後或者超時後,經過對其進行一次json encode編碼後,即可直接向目標客戶端做遍歷分發。

經過消息合併前置,編碼的CPU消耗不再與在線的連接數有關,也不再直接與要推送的消息條數有關,而是與打包後的batch個數有關,具有量級上的銳減效果。

架構考量

集群化gateway

經過上述的設計後,我們可以用GO來實現一個高並發的websocket長連接網關(gateway)。

gateway可以橫向部署構成集群,前端採用LVS/HA/DNS負載均衡。

當我們採用gateway集群化部署之後,當我們想要推送一條消息的時候,需要將消息分發給所有的gateway進程。

邏輯服務logic

因此,我實現了一個Logic服務,它本身是無狀態的,負責2個核心功能:

1,為業務提供了HTTP介面提交推送消息,因為作為推送系統的推送頻次不會太高。而且業務方在推送前會有很多業務邏輯判定,最終通過HTTP完成推送,相信是一個比較易於接入的方式。

2,負責將推送消息向各gateway進程做分發,在這裡採用了HTTP/2作為RPC協議(GRPC就是HTTP/2)保障了單連接的高並發能力,同時保障了不同gateway之間的故障隔離,互不影響。

認證服務

目前尚未引入websocket連接的登錄認證,今後存在向特定用戶推送的需求時,需要實現認證服務。

認證服務獨立於gateway與logic,可以稱作Passport。

客戶端首先基於公司帳號體系向passport完成登錄,得到一個自驗證的Login token(例如JWT),然後再發起gateway連接。

gateway驗證token後完成uid的識別,整個過程不需要與其他業務系統額外交互,當然也可以增加額外的調用服務驗證。

那麼當logic希望向特定uid推送消息的時候,當前架構下仍舊必須將消息分發給所有gateway,由gateway找到uid對應的連接。但是這無疑造成了浪費,因為uid可能只連在某一個gateway上,對其他gateway毫無意義。

session會話層

未來可以考慮增加會話層,記錄uid與gateway之間的連接關係,這樣logic經過session層反查找到uid所在的gateway,完成定向推送即可。

會話層可以做一層單獨的服務,採用純內存的方式保存uid與gateway的關係。

因為gateway宕機等原因,可能導致我們無法及時剔除掉線的會話,所以gateway與session之間應該定時傳輸健康客戶端的心跳信息。

當然,也可以簡單粗暴的將會話層用redis集群取代,僅僅提供單一的uid->gateway的反查能力。

我的慕課網課程

我在慕課網錄製了《GO實現千萬級WebSocket消息推送服務》 ,是一門免費課,大家可以花1小時快速了解一下相關技術。

源代碼

項目代碼我開源到了github,代碼量非常少,所以感興趣的話不如讀一下源碼嘍。

go-push

作者:小魚兒老師

鏈接:imooc.com/article/detai

來源:慕課網

本文原創發佈於慕課網 ,轉載請註明出處,謝謝合作


推薦閱讀:

一道淘汰85%面試者的百度開發者面試題

慕課網:Git算不算程序員的必備技能?

30行Javascript代碼實現圖片懶載入

Github上發布一天Star數破4K的項目了解一下

作為程序員的你,常用的工具軟體有哪些?

為什麼部分程序員下班後只關顯示器不關電腦?

推薦10個Java方向最熱門的開源項目

簡簡單單的移動web開發基礎知識講解

有哪些好笑的關於程序員的笑話?

學習編程太枯燥?12款助你學編程的免費遊戲

有哪些程序員特有的習慣?

【小說+乾貨】Python的十大神器

推薦閱讀:

TAG:移動應用 | iOS | Android |