汽車之家 IM 即時通訊平臺技術分享
1. 前言
早期之家在 C 端產品的即時通信功能是直接使用第三方商業軟件服務(SaaS),功能擴展性上存在很大制約,某些定製化業務需求很難實現,考慮到後續業務發展需要、數據安全、內容實時審覈及性能、自主可控高可用架構等因素,決定自主開發 IM 即時通信平臺並逐步迭代替換。
2. 網絡通信框架及協議
2.1
網絡通信框架
網絡通信系統通常可以選擇原生 NIO 庫或者第三方網絡框架進行開發,原生 NIO 的類庫 API 比較底層基礎,缺少對常用操作的封裝,例如粘包、解碼、重連等,開發工作量和維護成本會比較高,需要關注很多底層的東西。我們選擇了目前非常熱門的網絡框架 Netty 進行開發,Netty 功能強大開發門檻較低,預置了多種主流協議編解碼功能,成熟、穩定,且修復了大量已經發現的 JDK NIO BUG。
****► Netty 具備如下優點:
-
入門簡單,文檔齊全,無其他依賴,只依賴 JDK 就夠了;
-
使用簡單,預置了多種編解碼功能,支持多種主流協議,對大量常用操作進行了封裝,減少開發週期;
-
高性能,高吞吐,低延遲,資源消耗少;
-
靈活的線程模型,支持阻塞和非阻塞的 I/O 模型;
-
代碼質量高,目前主流版本基本沒有 Bug;
-
社區活躍,版本迭代週期短,發現的 BUG 可以被及時修復,同時,更多的新功能會加入;
7. 經歷了大規模的商業應用考驗,穩定性得到驗證。
2.2
通信協議
TCP 是個 “流” 協議,就是沒有界限的一串數據,數據傳輸時可能會存多包或半包傳輸的情況,原因如下:
-
應用程序寫入字節大小於套接口發送緩衝區大小;
-
進行 MSS 大小的 TCP 分段;
-
以太網幀的 payload 大於 MTU 進行 IP 分片
► 解決策略
-
消息定長,例如每個報文大小固定 200 字節,如果不夠,空位補空格。
-
用回車符進行分割,例如 FTP 協議。
-
約定特殊字符作爲消息結束標記。
-
將消息分爲消息頭和消息體,消息頭中包含表示消息總長度(或消息體長度)
-
更復雜的應用層協議。
按約定的方式編解碼保證數據完整性,就是通信協議。將消息分爲消息頭和消息體是最常見的協議設計方式(定長消息頭,可變長度消息體),如下圖:
► 消息頭
固定長度字節來標記消息類型,及消息體字節長度。
►消息體
可以使用文本、XML、JSON、Protobuf 等擴展性好的數據格式,儘量考慮以下幾點:
-
精簡消息體大小,不要有冗餘數據,減少網絡帶寬佔用(尤其是大量用戶發消息羣聊場景下),提高傳輸效率
-
數據安全性(例如加密傳輸)
-
編碼效率及可擴展性
► 除了自己設計實現通信協議,還可以直接使用現成的設計好的公開協議,如目前比較流行的 websocket 協議,相對於私有協議有以下優點:
-
瀏覽器直接支持,方便 web 端接入
-
降低接入成本,websocket 是公開協議,接入時不需要在協議規範上耗費太多的溝通時間。
-
很多框架包括 Netty 都自帶實現了 websocket 協議解碼器。
3. 架構設計
客戶端:用戶收發消息終端
接入層:爲客戶端連接、收發消息、關係建立提供入口
數據層:負責各類業務邏輯數據及消息數據緩存或持久化存儲,及消息指令的分發渠道。
3.1
消息設置
聊天場景常見的消息類型通常有:文本、圖片、表情、語音,視頻。我們把消息分爲消息類型,及消息內容兩部分,客戶端通過識別消息類型去解析消息內容並展示,像圖片視頻等消息,並不需要通過消息即時傳遞其內容,而是先將圖片或視頻上傳到文件服務器,消息中只需要把 uri 帶過去,並且帶上 base64 編碼的縮略圖即可,如下示例:
msgType:msg_image
msgContent: {user: { id:1,name:xxx,portrait:xxxx}, content: "縮略小圖 base64 編碼",url:"大圖地址", extra: "" }
消息下發流程如下圖:
如圖,整體思路是將上行消息通過 webapi 寫入消息隊列,socket 服務器通過消息隊列或離線消息庫進行增量消息分發同步。
socket 服務承載連接着所有在線用戶,發生部署上線將會導致所有在線用戶斷開重連,瞬間大量增加其它服務器的連接壓力,且對部分用戶行爲可能會有短暫的影響,上行消息通過 webAPI 實現,統一了消息入口,方便各類渠道消息的接入,並且使得 socket 服務的職責變得簡單單一,僅用於保持連接和推送消息,上行消息業務邏輯頻繁變動時僅需要重新部署 webAPI,對用戶基本無感知。
還有一些建議是,對消息進行合併,壓縮能極大提升消息推送吞吐能力,減少帶寬佔用,經測試十條以上消息壓縮率能達到 80% 左右;細分消息隊列,按不同維度拆分消息隊列可以分散壓力防止某類消息高峯期對其它業務消息造成延遲推送,保證其它消息的時效性,比如可以按場景用戶消息,系統指令等各自使用獨立的消息隊列。
3.2
消息擴散
消息擴散分發是 im 設計的重點,尤其是一對多的場景,如羣聊。簡單的說就是每條消息需要擴散給羣裏所有人收到,通常有兩種方式,讀擴散,寫擴散。什麼是讀擴散?什麼是寫擴散?
► 讀擴散:
描述:每條消息只存一份,羣組所有成員讀取這一份數據。
優點:節省存儲空間,無寫入壓力。
缺點:
-
獲取離線增量消息邏輯複雜,需要根據用戶所有會話關係去遍歷獲取,且並未知道會話是否有增量消息,會造成大量無效空讀,效率極慢。
-
針對消息做單個用戶個性化操作時設計會變的麻煩,比如其中一個用戶刪除消息,已讀回執等操作時,不能去影響其它用戶的視角。
► 寫擴散:
描述:每個用戶有獨立的消息列表,每條消息給所有消息關聯人同步一份副本。
優點:讀取邏輯簡單,效率極快,通過用戶自身消息列表一次性獲取所有離線增量消息即可,用戶個性化操作實現簡單,不會影響其它用戶的視角。
缺點:存儲空間增加,寫入壓力較大。
兩種方案如圖所示:
對比兩種消息擴散方案優缺點都比較明確,同時也都面臨無法接受的極端情況,比如讀擴散,如果某用戶擁有巨量會話,那他每次上線時讀取消息就是災難,再如寫擴散,如遇到萬人羣,每條消息都會產生一萬分副本,作爲設計者必須根據實際情況從邏輯上去結合兩種方案來平衡讀寫壓力。
消息寫擴散按需延遲擴散,我們通過登陸時間把用戶分類活躍和非活躍,只對活躍用戶進行即時寫擴散,非活躍用戶在上線時做一次補償同步操作,這樣能有效分散消息寫入壓力,還能對減少殭屍賬號進行無意義消息副本同步。
3.3
IMSDK 架構構圖
IMSDK 架構按模塊分工可以分爲中間件層、核心層、協議層。
►中間件層
負責請求連接時需要的 Token,以及對 Token 的緩存、Token 過期更新等邏輯。當 App 啓動之後平臺層會設置用戶信息到中間件,中間件根據設置的用戶信息判斷本地是否緩存該用戶的 Token,如果有則直接用該 Token 進行連接;如果沒有則會請求接口獲取 Token。獲取到之後使用獲取到的 Token 進行連接,連接成功之後將該 Token 本地緩存,方便下次使用。如果連接過程中服務器端返回 Token 過期的錯誤,客戶端會刪除掉本地緩存的 Token,並重新請求 Token 進行再次連接。
►核心層
包含連接模塊、監聽模塊、API 封裝模塊、日誌模塊、本地數據庫模塊。連接模塊負責 Socket 的連接、保活、斷線重連、斷開、收發消息等操作,連接模塊每隔 50 秒發送一次 ping 來保活,連接模塊重連機制是 2 秒、4 秒、8 秒、16 秒等 2 的 n 次方逐漸增加,當重試次數到 10 次後會認爲當前網絡有問題,不再重試,等待監聽網絡變化或者前後臺切換之後再次重試;監聽模塊當收到會話變更、消息變更、連接狀態變更時將變更內容通知到所有註冊監聽的業務層,業務層根據收到的通知做出相應處理。
API 封裝模塊提供一些 SDK 基礎功能的 API 到業務層,例如發送消息、撤回消息、刪除消息、獲取會話未讀數、會話草稿等等,業務層調用提供的 API 完成相關功能。日誌模塊提供日誌採集的 API,將每一條日誌順序的記錄到日誌文件中。日誌模塊還負責日誌文件的創建、刪除、保存與上傳工作。本地數據庫模塊負責數據庫相關的工作,當上層設置用戶信息時,數據庫模塊會打開相應的數據庫,如果沒有則會新建。數據庫模塊負責會話表和消息表的創建,負責會話和消息的增、刪、改、查。數據庫模塊執行操作時會增加一些必要的邏輯到其中,例如,在插入消息時需要更新會話的未讀數、會話的最後一條消息;在刪除消息時需要更新會話的未讀數、會話的最後一條消息;在消息已讀功能中需要修改會話的未讀數等等。
►協議層
主要負責跟服務端通訊內容的編解碼工作,包括消息的編解碼、會話的編解碼、命令的編解碼等工作,協議層將收到的數據進行解析,區分出諸如收到新消息、刪除消息、撤回消息、增加會話、刪除會話、會話更新等行爲,同時將收到的數據解碼成消息或者會話 Model 傳遞到上層,通知到業務層。
3.4
連接流程圖
App 需要同 App Server 進行數據交互,獲取 IM 連接需要的 Token 數據,並且 App Server 負責維護業務數據,如用戶數據、會話數據、好友關係等;
App 通過 Token 數據與 IM Server 進行連接,建立數據通道實現消息的實時接收與推送功能;
IM Server 維護 App 的連接狀態,在接收實時消息時判斷用戶是否在線,將消息轉發給目標設備或保存爲離線消息;
►會話及氣泡:
在面對特殊用戶會話較多時每次從服務拉取會話信息時將面臨較大壓力,我們採用本地和服務端結合方案實現,本地緩存一份會話,接收消息對會話氣泡進行疊加,本地會話設置發生變更如免打擾,隱藏會話,已讀消息等,上報給服務端,服務端在發生好友關係或加入羣組等操作時產生會話,或會話信息變更時也會對本地進行同步,服務器和本地之間會話同步包括連接時同步、實時同步、主動同步。
連接時同步: 用戶每次連接時會向服務器傳遞會話唯一標識,服務端通過用戶傳遞的標識進行邏輯判斷並向用戶推送會話數據,包括全部會話和增量會話,SDK 接收到會話數據後進行本地的新增、修改、刪除操作,並通知給 UI 層,本地會話 “標識” 由服務端每次同步會話時提供。
實時同步: 本地會話修改會通知給服務端,服務端接收到會話信息變更時,會即時通過 IM 推送給 SDK,其中包括新增、修改、刪除會話的操作,SDK 接收到會話數據後進行本地的新增、修改、刪除操作,並通知給 UI 層。
主動同步: 客戶端接收到不存在的會話消息時,會主動向服務器獲取此會話信息進行本地保存並通知 UI 層。
會話設計圖:
會話部分字段:
單聊: 兩個用戶之間進行一對一的聊天,聊天消息可以持久化保存至本地進行查看。
羣組: 兩個或兩個以上的用戶在同一個會話中進行聊天,發送的消息會被羣組所有成員接收並可以持久化保存至本地進行查看。
公衆號: 企業或官方通過系統賬號向單個或多個賬號推送消息,消息可以持久化保存至本地進行查看。
聊天室: 一個或多個用戶在同一個會話中進行聊天,發送的消息會被推送至當前聊天室中所有用戶,用戶接收端消息不會保存本地。
本地記錄每次會話同步時間,連接時服務端根據本地上報最後更新時間對比,增量同步變動過的會話記錄,保證本地會話與服務器端保持一致。之家的會話列表體現在個人主頁的消息部分,如下:
4. 服務器優化
我們系統的設計要求是單機百萬連接支持、下行消息 QPS 一百萬。由於一個連接會佔用一個文件描述符,首先就要對系統的文件描述符上限做調整,讓服務器能夠支持百萬級連接的建立;
/etc/security/limits.conf
-
soft nporc 1500000
-
hard nporc 1500000
-
soft nofile 1500000
-
hard nofile 1500000
/etc/sysctl.conf
fs.nr_open = 3000000
fs.file-max = 3000000
我們使用 Nginx 作爲七層負載,並且開啓了 TLS 保障數據的傳輸安全。當 Nginx 層作爲客戶端在與後端應用服務器建立連接的時候,會遇到本地端口瓶頸,可以依據 TCP 四元組規則,增加後端應用服務器的監聽端口,來實現本地端口的複用,從而突破本地端口資源的限制;
在實際壓測過車中,客戶端接收消息的毛刺問題比較嚴重,經排查發現 nginx 服務器有丟包,並且 CPU 的使用非常不均衡,特別是軟中斷,只集中在少數 CPU 上。爲解決以上問題前,先了解下網卡收包流程以及相關的一些概念。
網卡收到數據幀後,將數據幀以 DMA 的方式拷貝到內存的 Ring Buffer 中,該步驟不需要 CPU 的參與。當拷貝完成後,網卡便會觸發一個網卡硬件中斷,CPU 必須立即響應硬件中斷,CPU 根據中斷類型,在中斷註冊表中查找對應的中斷處理程序,然後調用網卡註冊的中斷處理程序 (網卡驅動),此後網卡驅動程序觸發一個軟中斷,自此硬件中斷返回,硬中斷不會做過多事情,它只負責通知驅動有數據到達,具體的操作由軟中斷過程來處理。
DMA 是 Direct Memory Access,直接存儲器訪問。在 DMA 出現之前,CPU 與外設之間的數據傳送方式有程序傳送方式、中斷傳送方式。CPU 是通過系統總線與其他部件連接並進行數據傳輸,DMA 就是指外部設備不通過 CPU 而直接與系統內存交換數據的接口技術。
Ring Buffer 網卡環形緩衝區,如果該緩衝區被佔滿,新到來的數據包將會被丟棄,從而導致丟包。把該緩衝區由原來的 512 調整到 2048 後,丟包問題得以解決,方法如下:
查看當前 Buffer
ethtool -g em1
Ring parameters for em1:
Pre-set maximums:
RX: 4096
RX Mini: 0
RX Jumbo: 0
TX: 4096
Current hardware settings:
RX: 4096
RX Mini: 0
RX Jumbo: 0
TX: 4096
ethtool -G em1 rx 2048
ethtool -G em1 tx 2048
修改
ethtool -G em1 rx 4096
ethtool -G em1 tx 4096
4.1
設置網卡隊列
對於 CPU 軟中斷不均衡,與網卡的設置與 CPU 的親和力綁定有直接關係,借用網絡一張圖先了解下 RSS 多隊列網卡,如下:
當網卡收到報文時,通過 Hash 包頭的 SIP、SPort、DIP、DPort 四元組信息,將數據投遞到相應的網卡隊列,同時會觸發該隊列綁定的中斷,通知 CPU 進一步處理;由此可知 CPU 使用不均衡,無非就是兩個原因,隊列收到的數據包不均衡或者 CPU 與網卡隊列綁定不合理導致;
# 設定網卡隊列
ethtool -L em1 combined 16
4.2
網卡中斷號
Interrupt Request,簡稱 IRQ,中斷就是由硬件或軟件所發送的中斷請求信號。系統上的每個硬件設備都會被分配一個 IRQ 號,通過這個唯一的 IRQ 號就能區是來自哪個硬件了。開啓了多隊列的網卡,每個隊列都會有唯一的中斷號。
# 查看網卡隊列中斷號
cat /proc/interrupts |grep em1 |awk '{print $1 $NF}'
4.3
CPU 親和力綁定
把每個網卡隊列與 CPU 一一綁定,均衡 CPU 的使用
# CPU 綁定
echo /proc/irq/107/smp_affinity_list 0
echo /proc/irq/108/smp_affinity_list 1
中斷號爲 107 的隊列綁定 0 號 CPU,中斷號爲 108 的隊列綁定 1 號 CPU,依此類推,每個網卡隊列都需要綁定到不同的 CPU 核上,這樣我們就完成了網卡隊列的設置以及與 CPU 的綁定。
這裏還有個問題是,CPU 不緊要處理網路數據,還要處理 Nginx 應用,這個時候 CPU 資源的分配要根據具體的壓測情況進行調整;
4.4
Intel Flow Director
上面提到投遞到網卡隊列的數據包是均衡的,如何能做到均衡呢?有個辦法是使用 Intel 以太網 FD(Flow Director) 技術,自定義數據包投遞規則,應用監聽多個端口,不同目標端口的數據包按制定的規則投遞到相應的網卡隊列,從而達到均衡數據包的目的,進而均衡 CPU 的使用;
數據投遞規則 根據目的端口把數據投遞到不同隊列
ethtool --features em1 ntuple on
ethtool --config-ntuple em1 flow-type tcp4 dst-port 9500 action 0 loc 1
ethtool --config-ntuple em1 flow-type tcp4 dst-port 9501 action 1 loc 2
至此網卡丟包以及 CPU 使用率不均衡的問題得到解決,基本就是解決了客戶端接收數據毛刺的問題;
4.5
其他優化
對部分需要更高時時性及穩定的用戶,如客服、商家號等,可以增加專用服務器來應對,專用服務器通常接入量較小,連接和消息推送都會更穩定快速。
增加備用域名(不同 cdn),在連接失敗重試過程中使用可有效減少外部網絡波動帶來的影響,使系統更加穩定可靠。我們之前有一次線上故障,使用的第三方的即時通訊服務,
當時該服務商的一個關鍵域名被誤封,導致整個即時通訊服務不可用,由於處理過程複雜,故障持續了半天的時間才得以恢復。基於此,我們自研的時候,把這部分設計了進去。
爲提高連接效率及服務器連接壓力 SDK 增加了 token 緩存機制,IM 連接成功後,會將 token 進行本地緩存,當設備再次觸發連接時,會優先檢查本地是否存在 token,如存在立刻使用緩存數據進行連接,若不存在或過期會向服務器獲取新 token 數據進行連接,連接成功後將新 Token 緩存本地。
SDK 具備自動重連機制,在整個應用全局只需要調用一次連接即可,連接異常斷開後會啓動重連機制進行多次重連,在這之後如果仍沒有連接成功,還會在當檢測到設備網絡狀態變化時再次進行重連。
心跳保活,爲了保持客戶端和服務端的實時雙向通信,需要確保客戶端和服務端之間的 TCP 通道保持連接不斷開,IM 連接成功後,SDK 每間隔 50 秒會向服務器發送一個 ping 包,服務器接收到 ping 包後立即響應一個 pong 包,如果服務在 120 秒內檢查沒有收到過 ping 包會立即斷開此連接,釋放資源。SDK 在檢測斷開後會自動觸發重連機制。
發送方 -> 接收方:ping
接收放 -> 發送方:pong
5. 總結
本文內容介紹了之家 IM 即時通信平臺部分設計策略,藉此機會總結設計方案及技術實踐,與大家一起學習提升。目前該項目在之家已經落地兩年有餘,接入十幾條業務線,包含單聊、羣聊、聊天室、公衆號、通用信令服務等場景,日均服務三端用戶在千萬級,爲之家三端產品提供全雙工消息總線基礎設施支撐;目前一些個性化產品需求以及相關運營平臺仍在不斷完善中,如果你對此很有興趣,歡迎加入我們。
作者簡介
林道輝
■ C 端及中臺產研中心 - 看選技術團隊
■ 2012 年加入汽車之家,目前主要負責參與熱聊業務及之家 im 通信平臺架構及研發工作。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/kMGEpE_piFbeWTm9YxWVRA