實時的響應老是讓人興奮的,就如你在微信裏看到對方正在輸入,如你在王者峽谷裏一呼百應,如大家在直播彈幕裏不約而同的 666,它們的背後都離不開長鏈接技術的加持。
每一個互聯網公司裏幾乎都有一套長鏈接系統,它們被應用在消息提醒、即時通信、推送、直播彈幕、遊戲、共享定位、股票行情等等場景。而當公司發展到必定規模,業務場景變得更復雜後,更有多是多個業務都須要同時使用長鏈接系統。
業務間分開設計長鏈接會致使研發和維護成本陡增、浪費基礎設施、增長客戶端耗電、沒法複用已有經驗等等問題。共享長鏈接系統又須要協調好不一樣系統間的認證、鑑權、數據隔離、協議拓展、消息送達保證等等需求,迭代過程當中協議須要向前兼容,同時由於不一樣業務的長鏈接匯聚到一個系統致使容量管理的難度也會增大。
通過了一年多的開發和演進,通過咱們服務面向內和外的數個 App、接入十幾個需求和形態萬千的長鏈接業務、數百萬設備同時在線、突發大規模消息發送等等場景的錘鍊,咱們提煉出一個長鏈接系統網關的通用解決方案,解決了多業務共用長鏈接時遇到的種種問題。
知乎長鏈接網關致力於業務數據解耦、消息高效分發、解決容量問題,同時提供必定程度的消息可靠性保證。
支撐多業務的長鏈接網關其實是同時對接多客戶端和多業務後端的,是多對多的關係,他們之間只使用一條長鏈接通信。
這種多對多的系統在設計時要避免強耦合。業務方邏輯也是會動態調整的,若是將業務的協議和邏輯與網關實現耦合會致使全部的業務都會互相牽連,協議升級和維護都會異常困難。
因此咱們嘗試使用經典的發佈訂閱模型來解耦長鏈接網關跟客戶端與業務後端,它們之間只須要約定 Topic 便可自由互相發佈訂閱消息。傳輸的消息是純二進制數據,網關也無需關心業務方的具體協議規範和序列化方式。
咱們使用發佈訂閱解耦了網關與業務方的實現,咱們仍然須要控制客戶端對 Topic 的發佈訂閱的權限,避免有意或無心的數據污染或越權訪問。
假如講師正在知乎 Live 的 165218 頻道開講,當客戶端進入房間嘗試訂閱 165218 頻道的 Topic 時就須要知乎 Live 的後端判斷當前用戶是否已經付費。這種狀況下的權限其實是很靈活的,當用戶付費之後就能訂閱,不然就不能訂閱。權限的狀態只有知乎 Live 業務後端知曉,網關沒法獨立做出判斷。
因此咱們在 ACL 規則中設計了基於回調的鑑權機制,能夠配置 Live 相關 Topic 的訂閱和發佈動做都經過 HTTP 回調給 Live 的後端服務判斷。
同時根據咱們對內部業務的觀察,大部分場景下業務須要的只是一個當前用戶的私有 Topic 用來接收服務端下發的通知或消息,這種狀況下若是讓業務都設計回調接口來判斷權限會很繁瑣。
因此咱們在 ACL 規則中設計了 Topic 模板變量來下降業務方的接入成本,咱們給業務方配置容許訂閱的 Topic 中包含鏈接的用戶名變量標識,表示只容許用戶訂閱或發送消息到本身的 Topic。
此時網關能夠在不跟業務方通訊的狀況下,獨立快速判斷客戶端是否有權限訂閱或往 Topic 發送消息。
網關做爲消息傳輸的樞紐,同時對接業務後端和客戶端,在轉發消息時須要保證消息在傳輸過程的可靠性。
TCP 只能保證了傳輸過程當中的順序和可靠性,但遇到 TCP 狀態異常、客戶端接收邏輯異常或發生了 Crash 等等狀況時,傳輸中的消息就會發生丟失。
爲了保證下發或上行的消息被對端正常處理,咱們實現了回執和重傳的功能。重要業務的消息在客戶端收到並正確處理後須要發送回執,而網關內暫時保存客戶端未收取的消息,網關會判斷客戶端的接收狀況並嘗試再次發送,直到正確收到了客戶端的消息回執。
而面對服務端業務的大流量場景,服務端發給網關的每條消息都發送回執的方式效率較低,咱們也提供了基於消息隊列的接收和發送方式,後面介紹發佈訂閱實現時再詳細闡述。
在設計通信協議時咱們參考了 MQTT 規範,拓展了認證和鑑權設計,完成了業務消息的隔離與解耦,保證了必定程度的傳輸可靠性。同時保持了與 MQTT 協議必定程度上兼容,這樣便於咱們直接使用 MQTT 的各端客戶端實現,下降業務方接入成本。
爲了保證可靠性,咱們沒有考慮像傳統長鏈接系統那樣將內部數據存儲、計算、消息路由等等組件所有集中到一個大的分佈式系統中維護,這樣增大系統實現和維護的複雜度。咱們嘗試將這幾部分的組件獨立出來,將存儲、消息路由交給專業的系統完成,讓每一個組件的功能儘可能單一且清晰。
同時咱們也須要快速地水平擴展能力。互聯網場景下各類營銷活動均可能致使鏈接數陡增,同時發佈訂閱模型系統中下發消息數會隨着 Topic 的訂閱者的個數線性增加,此時網關暫存的客戶端未接收消息的存儲壓力也倍增。將各個組件拆開後減小了進程內部狀態,咱們就能夠將服務部署到容器中,利用容器來完成快速並且幾乎無限制的水平擴展。
一、接入層使用 OpenResty 實現,負責鏈接負載均衡和會話保持
二、長鏈接 Broker,部署在容器中,負責協議解析、認證與鑑權、會話、發佈訂閱等邏輯
四、Kafka 消息隊列,分發消息給 Broker 或業務方
其中 Kafka 和 Redis 都是業界普遍使用的基礎組件,它們在知乎都已平臺化和容器化,它們也都能完成分鐘級快速擴容。
OpenResty 是業界使用很是普遍的支持 Lua 的 Nginx 拓展方案,靈活性、穩定性和性能都很是優異,咱們在接入層的方案選型上也考慮使用OpenResty。
接入層是最靠近用戶的一側,在這一層須要完成兩件事:
一、負載均衡,保證各長鏈接 Broker 實例上鍊接數相對均衡
二、會話保持,單個客戶端每次鏈接到同一個 Broker,用來提供消息傳輸可靠性保證
負載均衡其實有不少算法都能完成,不論是隨機仍是各類 Hash 算法都能比較好地實現,麻煩一些的是會話保持。
常見的四層負載均衡策略是根據鏈接來源 IP 進行一致性 Hash,在節點數不變的狀況下這樣能保證每次都 Hash 到同一個 Broker 中,甚至在節點數稍微改變時也能大機率找到以前鏈接的節點。
以前咱們也使用過來源 IP Hash 的策略,主要有兩個缺點:
一、分佈不夠均勻,部分來源 IP 是大型局域網 NAT 出口,上面的鏈接數多,致使 Broker 上鍊接數不均衡
二、不能準確標識客戶端,當移動客戶端掉線切換網絡就可能沒法鏈接回剛纔的 Broker 了
因此咱們考慮七層的負載均衡,根據客戶端的惟一標識來進行一致性 Hash,這樣隨機性更好,同時也能保證在網絡切換後也能正確路由。常規的方法是須要完整解析通信協議,而後按協議的包進行轉發,這樣實現的成本很高,並且增長了協議解析出錯的風險。
最後咱們選擇利用 Nginx 的 preread 機制實現七層負載均衡,對後面長鏈接 Broker 的實現的侵入性小,並且接入層的資源開銷也小。
Nginx 在接受鏈接時能夠指定預讀取鏈接的數據到 preread buffer 中,咱們經過解析 preread buffer 中的客戶端發送的第一個報文提取客戶端標識,再使用這個客戶端標識進行一致性 Hash 就拿到了固定的 Broker。
咱們引入了業界普遍使用的消息隊列 Kafka 來做爲內部消息傳輸的樞紐。前面提到了一些這麼使用的緣由:
其中利用消息隊列削峯好理解,下面咱們看一下怎麼利用 Kafka 與業務方更好地完成對接。
長鏈接 Broker 會根據路由配置將消息發佈到 Kafka Topic,同時也會根據訂閱配置去消費 Kafka 將消息下發給訂閱客戶端。路由規則和訂閱規則是分別配置的,那麼可能會出現四種狀況:
一、消息路由到 Kafka Topic,但不消費,適合數據上報的場景。
二、消息路由到 Kafka Topic,也被消費,普通的即時通信場景。
三、直接從 Kafka Topic 消費並下發,用於純下發消息的場景。
四、消息路由到一個 Topic,而後從另外一個 Topic 消費,用於消息須要過濾或者預處理的場景。
這套路由策略的設計靈活性很是高,能夠解決幾乎全部的場景的消息路由需求。同時由於發佈訂閱基於 Kafka,能夠保證在處理大規模數據時的消息可靠性。
當長鏈接 Broker 從 Kafka Topic 中消費出消息後會查找本地的訂閱關係,而後將消息分發到客戶端會話。
咱們最開始直接使用 HashMap 存儲客戶端的訂閱關係。當客戶端訂閱一個 Topic 時咱們就將客戶端的會話對象放入以 Topic 爲 Key 的訂閱 Map 中,當反查消息的訂閱關係時直接用 Topic 從 Map 上取值就行。
由於這個訂閱關係是共享對象,當訂閱和取消訂閱發生時就會有鏈接嘗試操做這個共享對象。爲了不併發寫咱們給 HashMap 加了鎖,但這個全局鎖的衝突很是嚴重,嚴重影響性能。
本地同時建立數百個 HashMap,當須要在某個 Key 上存取數據前經過 Hash 和取模找到其中一個 HashMap 而後進行操做,這樣將全局鎖分散到了數百個 HashMap 中,大大下降了操做衝突,也提高了總體的性能。
當消息被分發給會話 Session 對象後,由 Session 來控制消息的下發。
Session 會判斷消息是不是重要 Topic 消息, 是的話將消息標記 QoS 等級爲 1,同時將消息存儲到 Redis 的未接收消息隊列,並將消息下發給客戶端。等到客戶端對消息的 ACK 後,再將未確認隊列中的消息刪除。
有一些業界方案是在內存中維護了一個列表,在擴容或縮容時這部分數據無法跟着遷移。也有部分業界方案是在長鏈接集羣中維護了一個分佈式內存存儲,這樣實現起來複雜度也會變高。
咱們將未確認消息隊列放到了外部持久化存儲中,保證了單個 Broker 宕機後,客戶端從新上線鏈接到其餘 Broker 也能恢復 Session 數據,減小了擴容和縮容的負擔。
在發送消息時,每條 QoS 1 的消息須要被通過傳輸、客戶端處理、回傳 ACK 才能確認下發完成,路徑耗時較長。若是消息量較大,每條消息都等待這麼長的確認才能下發下一條,下發通道帶寬不能被充分利用。
爲了保證發送的效率,咱們參考 TCP 的滑動窗口設計了並行發送的機制。咱們設置必定的閾值爲發送的滑動窗口,表示通道上能夠同時有這麼多條消息正在傳輸和被等待確認。
咱們應用層設計的滑動窗口跟 TCP 的滑動窗口實際上還有些差別。
TCP 的滑動窗口內的 IP 報文沒法保證順序到達,而咱們的通信是基於 TCP 的因此咱們的滑動窗口內的業務消息是順序的,只有在鏈接狀態異常、客戶端邏輯異常等狀況下才可能致使部分窗口內的消息亂序。
由於 TCP 協議保證了消息的接收順序,因此正常的發送過程當中不須要針對單條消息進行重試,只有在客戶端從新鏈接後纔對窗口內的未確認消息從新發送。消息的接收端同時會保留窗口大小的緩衝區用來消息去重,保證業務方接收到的消息不會重複。
咱們基於 TCP 構建的滑動窗口保證了消息的順序性同時也極大提高傳輸的吞吐量。
基礎架構組負責知乎的流量入口和內部基礎設施建設,對外咱們奮鬥在直面海量流量的的第一戰線,對內咱們爲全部的業務提供堅如磐石的基礎設施,用戶的每一次訪問、每個請求、內網的每一次調用都與咱們的系統息息相關。
文章來源:https://zhuanlan.zhihu.com/p/66807833
不知你們讀完此文是否想了解更多的知乎案例?7月6-7日,第43屆MPD工做坊北京站咱們邀請到了知乎測試架構師王守宇,爲咱們進行3小時的深度分享:
課程概要:QA 團隊的主要目標是保障產品的交付質量,在工做過程當中咱們會碰到多個共性的問題:提測質量差、人工測試多、線上故障多。本次 Topic ,知乎 QA 團隊給出了一條新的解決問題之道,經過建設全員重視質量、全員參與質量保障的質量文化來得到高質量的產品。
除知乎外,阿里、百度、騰訊、新浪、去哪兒、滴滴、網易、VIPKID等企業的一線專家及技術大牛們,將經過演講、實操、分組討論、組別之間的PK等形式,以案例說話,還原真實工做場景,交付真實的工做技能。