58集團面向億級用戶IM長連接服務設計與實踐
作者 | 趙忠生
出處 | 58 架構師 公眾號
長連接服務簡介
微聊,是 58 一款聊天工具,目前已經接入 58 的大部分產品。及時準確數據傳輸,是對一款聊天工具最基本的要求。長連接服務就是在客戶端到服務端之間建立一條全雙工的數據通路,實現客戶端和服務之間邏輯收發數據,在線離線等功能。
角色
長連接服務在整個微聊系統中,位於客戶端與後台邏輯之間;
整個後台服務最重要的直接對外介面之一 (另一個是短連接請求的 nginx);
長連接服務對外和對內的均採用 tcp 連接。
系統瓶頸
長連接服務主要功能是收發數據,保持在線,使用的系統資源主要包括:CPU,內存,網卡;
當連接非常活躍的時候,大量數據接收與發送,會用到更多的 cpu 和網卡;當大量用戶在線的時候,需要維持這些連接,保持會話,需要用到大量內存;
考慮到微聊的實際場景,app 端佔有很大的比例,由於手機的網路環境相對 pc 來說,不太穩定,需要處理大量連接的新建與斷開,所以系統對 cpu 使用率比較高。
設計難點
設計單台物理機連接數 100W 的處理能力;
cpu 資源充分利用,線程的分配;
內存合理分配,數據結構選擇;
非同步化,剝離業務邏輯和網路 io 之間的相互依賴。
架構設計
架構的設計需要考慮 tcp 連接管理與應用層協議解析與處理相分離,以下是系統主要的功能模塊:
tcp 連接層 (黑色虛線框)- 連接保持,session 管理等,實現了 tcp 和 tls 層;
BlayServer - 邏輯層服務,邏輯層節點管理,協議解析等;
ClientServer - 客戶端服務,客戶端協議解析;
protocol - http,websocket,protocol 協議封裝;
tools - json,log,config,crypto。
線程管理
為了充分地使用 CPU,需要合理的進行線程規劃。整個長連接服務使用事件驅動,包括:定時器事件和 io 事件(listen fd,socket fd, pipe fd), 所以線程規劃就是合理給這些事件分配線程。
線程優化
大量連接 socket 需要平均分配到各個線程;
新的連接請求量比較大,listen 線程壓力較大,需要考慮多線程處理。
這裡主要涉及到以下 3 種 fd 的線程分配:
監聽 fd,包括監聽邏輯層的連接請求和監聽客戶端的連接請求,並且支持啟動多個監聽埠,每個埠多個線程同時工作(可配置),整體會按順序分配到線程;
連接 fd,包括邏輯層的連接和客戶端的連接,採用 fd 取余線程數,保證平均分配到所有線程;
pipe fd, 負責本線程管道中數據的讀取,每個線程一個。
這樣使得每個線程的 CPU 使用率相當。
線程間通信
客戶端 fd 和 listen fd,採用不同的規則分配到同一組線程中,當客戶端的數據需要發給邏輯層,或是邏輯層數據需要發給客戶端,就存在客戶端連接和邏輯層的連接存在同一線程或不同線程兩種情況,不同線程之間傳輸數據需要用到線程通信。解決不同線程訪問資源,傳輸數據主要考慮了下面幾種方式:
加鎖 (比如 session 讀寫鎖,從而讓 session 可以在多線程中操作) - 會導致多個線程競爭資源,阻塞線程;
共享內存,需要加鎖保證原子性,需要線程設置定時器做可讀性檢測,實現起來比較複雜;
我們採用操作系統提供的管道 -pipe 解決線程間通信的問題。
pipe 通信協議:
close session,一個線程需要關閉另一個線程上的連接,比如:收到邏輯層需要把某個用戶踢下線的命令;
send to session, 一個線程給別一個線程上的連接發送數據,比如:給某個用戶推送消息;
應用層事件,應用層事件跨線程轉發,一種通用的跨線程調用。
內存管理
在長連接服務工作過程中,會有大量會話不停創建和銷毀,在會話過程中,又會有大量長度不等的數據通信,長時間穩定的服務需要合理高效的內存使用。長連接服務中對內存的使用,包括以下幾個方面:操作系統 Tcp 協議棧內存使用,服務中 session 管理,讀寫數據緩存 (buffer) 等。
說明:
其中最主要的內存使用是 tcp 協議棧,包括 tcb 和讀寫緩存,其它內存使用體現在 session 存儲和 session 讀寫 buffer;
由於在 tcp 上層實現了更加邏輯友好的 buffer(後面會詳細講到),實際部署中可以把 tcp 協議棧中的讀寫 buffer 設置成比較小的空間,比如設置為 1k。
session 靜態內存模型
當一個客戶端在線,服務端會生成一個 session 保存該連接的狀態,一些邏輯信息,以及 socket 信息等。 大量的會話保持,需要服務端合理的管理這些 session。
考慮以下幾種存儲結構:
hash - 能夠實現比較快速存取,需要自己實現 hash 演算法,經常分配釋放內存會導致內存碎片;
固定數組 - 需要一開始分配大塊內存,不支持動態擴展,但存取快速,不會有內存碎片;
動態數據 - 動態擴展,存取快速,無內存碎片,但擴展的時候會有大塊內存分配與拷貝。
長連接服務採用數組存儲 session:
預分配 100W session 全量的空間,相當於 100Wsession 的數組(一共 418M),大小可接受,並且有容量使用監控,防止空間滿了建立連接失敗;
直接使用 session 對應的 fd 作為下標,實現存取 O(1);
由於 fd 分配策略是從小到大分配空閑的,所以可以保證數組在當前連接數以下的空間是飽和的,空間利用率比較高。
buffer
在 socket 讀寫過程中,會遇到以下幾個問題:
當向 socket 中寫數據的時候,如果當前不足以寫下要寫的所有數據,那麼會寫入部分數據,剩下的數據需要在 socket 可寫的時候繼續寫入,以保證數據的連貫性,這裡需要有一個保存並記錄需要寫入的數據的數據結構,並且需要保證寫入的數據的先後順序,先入先出;
當從 socket 中讀數據的時候,tcp 只保證數據流的順序性,並不知道應用層協議包大小,所以需要從 tcp 流中分離出一個個應用層協議包,在解決拆包和粘包問題的過程中,會遇到數據包不夠的情況下,需要等待後續數據的讀入,直到讀到的數據構成一個完整的應用層協議包,然後把就個協議整體返回給上層;
在發送和接收的過程中,需要支持大小不一樣的應用層數據包(聊天內容為一個字或是 1M 的字),所以這裡需要一個可以發送拆解大應用層包的緩存隊列。
為了滿足以上功能和要求,我們設計了 buffer:
分配與釋放採用固定存儲單元防止產生內存碎片;
動態擴展的雙向循環鏈表(隊列);
對外提供了連續數據存取介面。
tcp 拆包流程
tcp 是面向字元流的傳輸,tcp 保證了傳輸數據的順序性和可靠性,當接收到字元流的時候,如何從字元流中分離出一段段上層協議,是 tcp 拆包應該考慮的問題。
如上圖所示,邏輯層需要實現 getProtocolSize 和 receiveCallback 兩個介面,前者通過參數中傳入的數據判斷出當前應用層協議包大小,後者是返回應用層協議包的回調。 當 socket 讀事件發生時,首先從 socket 中讀取數據,寫入 buffer 中,然後,調用 buffer 的預讀介面(只是返回隊列頭部的只讀指針,並不拷貝數據),調用 getProtocolSize 介面,由邏輯層返回應用層協議包大小(只有應用層邏輯才知道自己該識別哪種協議),再根據該大小,從 buffer 隊列中讀出協議包,最後通過 receiveCallback 返回給上層處理。
長連接保活
由於 tcp 自身的斷開確認機制,如果一條 tcp 連接中間網路斷開,此時客戶端和服務端物理網路的斷開導致了客戶端和服務端都沒有辦法通知對方連接的斷開,這樣,服務端和客戶端就會存在死連接,造成假在線,佔用的資源得不到有效的回收。長連接保活主要有下面兩種方式:
tcp keepalive,通過設置 keepalive 參數,tcp 協議棧會在超過一定時間沒有數據交互的時候,發送 keepalive 探測包,如果連接幾次都沒有收到回包,則斷開連接。 優點是,tcp 協議棧提供的功能,更加穩定,並且佔用較少的帶寬;
採用應用層心跳包的方式,客戶端定時向服務端發送心跳包,服務端收到心跳包後立即進行回包,客戶端如果沒有檢測到回包,則斷開連接;服務端檢測超時還沒有收到心跳包,則斷開連接。優點主要是應用層有感知,可控,並且可以帶些業務數據,比如時間戳。
微聊長連接,考慮到多端支持(android, ios, web 等),兼容老版本實現方式,應用層協議主要使用 http,提供了 http-chunk 和 http-long-polling,兩種 http 接入方式,這樣就限制了客戶端上發數據,所以我們採用了服務端下發心跳,和 keepalive 結合的方式,實現服務端和客戶端的保活, 如下圖:
服務端通過開啟 tcp keepalive 進行保活,當一條連接超過一定時間沒有活動, 服務端會發 keepalive 包,如果連續 3 次都沒收到回包,服務端就會認為這是一個死連接,從而關閉它;
而對於客戶端,服務端會定時下發心跳包,客戶端通過監測心跳包來判別當前連接是否工作正常,如果不能正常收到心跳包,則會重新建立新的連接。
容量控制
在 tcp 連接建立到通信的流程中,為了防止一些惡意連接與攻擊,長連接服務做了容量控制,如果體現在下面幾個方面:
客戶端建立 tcp 連接到 tls 握手再到發出登錄數據返回登錄結果,這個過程是連貫的,如果客戶端停在中間的某一步而不往下進行,就會一直占著服務端資源,針對這種情況,服務端增加了定時控制,在 tcp 連接之後 30s,還沒有收到登錄請求,服務端會主動斷開連接;
在服務中統計了正在進行 tls 握手,正在進行登錄校驗的連接數量,分別設置上限,防止當同時出現大量請求的時候,對後端服務的衝擊;
增加了會話通信過程中 buffer 內存使用量的上限,防止對端不接收數據,導致服務端數據積壓;
增加 ip 統計服務,當建立連接後,會將新連接的 ip 等信息發送到 ip 統計服務,對整個服務的客戶端 ip 情況作統計監控,增加 ip 黑名單功能。
總 結
長連接服務是微聊的基礎服務,穩定性尤其重要;
在穩定性的基礎上,通過 tls 握手優化等,不斷提高建立連接的速度,更好的應對斷網,弱網等複雜的外網環境;
通過支持更多的應用層協議,提高多端多設備的接入能力;
通過監控,不斷挖掘潛在在安全威脅,同時預防常見的網路安全問題。
這篇分享主要在長連接服務的整體架構,線程,內存分配等一些普遍問題技術選型方面進行整體性的介紹,隨著業務的接入與用戶的增長,長連接服務也伴隨著新的挑戰,在穩定性,高並發,高效率,安全性方面的探索與提高永遠沒有盡頭。
最後,歡迎對分散式長連接服務,linux 內核網路協議棧感興趣的同學一起交流。
作者介紹
趙忠生
,來自 58 集團 TEG 基礎服務部,後端工程師,專註分散式高可用架構設計。本文轉載自 58 架構師公眾號。

TAG:InfoQ |