1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        知乎千萬級高性能長連接網(wǎng)關(guān)是如何搭建的

        共 5040字,需瀏覽 11分鐘

         ·

        2021-03-26 13:05


        編輯:業(yè)余草

        來源:https://www.xttblog.com/?p=4875


        實(shí)時(shí)的響應(yīng)總是讓人興奮的,就如你在微信里看到對方正在輸入,如你在王者峽谷里一呼百應(yīng),如你們在直播彈幕里不約而同的 666,它們的背后都離不開長連接技術(shù)的加持。


        每個(gè)互聯(lián)網(wǎng)公司里幾乎都有一套長連接系統(tǒng),它們被應(yīng)用在消息提醒、即時(shí)通訊、推送、直播彈幕、游戲、共享定位、股票行情等等場景。而當(dāng)公司發(fā)展到一定規(guī)模,業(yè)務(wù)場景變得更復(fù)雜后,更有可能是多個(gè)業(yè)務(wù)都需要同時(shí)使用長連接系統(tǒng)。


        業(yè)務(wù)間分開設(shè)計(jì)長連接會導(dǎo)致研發(fā)和維護(hù)成本陡增、浪費(fèi)基礎(chǔ)設(shè)施、增加客戶端耗電、無法復(fù)用已有經(jīng)驗(yàn)等等問題。共享長連接系統(tǒng)又需要協(xié)調(diào)好不同系統(tǒng)間的認(rèn)證、鑒權(quán)、數(shù)據(jù)隔離、協(xié)議拓展、消息送達(dá)保證等等需求,迭代過程中協(xié)議需要向前兼容,同時(shí)因?yàn)椴煌瑯I(yè)務(wù)的長連接匯聚到一個(gè)系統(tǒng)導(dǎo)致容量管理的難度也會增大。


        經(jīng)過了一年多的開發(fā)和演進(jìn),經(jīng)過我們服務(wù)面向內(nèi)和外的數(shù)個(gè) App、接入十幾個(gè)需求和形態(tài)各異的長連接業(yè)務(wù)、數(shù)百萬設(shè)備同時(shí)在線、突發(fā)大規(guī)模消息發(fā)送等等場景的錘煉,我們提煉出一個(gè)長連接系統(tǒng)網(wǎng)關(guān)的通用解決方案,解決了多業(yè)務(wù)共用長連接時(shí)遇到的種種問題。


        知乎長連接網(wǎng)關(guān)致力于業(yè)務(wù)數(shù)據(jù)解耦、消息高效分發(fā)、解決容量問題,同時(shí)提供一定程度的消息可靠性保證。



        我們怎么設(shè)計(jì)通訊協(xié)議?

        業(yè)務(wù)解耦


        支撐多業(yè)務(wù)的長連接網(wǎng)關(guān)實(shí)際上是同時(shí)對接多客戶端和多業(yè)務(wù)后端的,是多對多的關(guān)系,他們之間只使用一條長連接通訊。

        這種多對多的系統(tǒng)在設(shè)計(jì)時(shí)要避免強(qiáng)耦合。業(yè)務(wù)方邏輯也是會動態(tài)調(diào)整的,如果將業(yè)務(wù)的協(xié)議和邏輯與網(wǎng)關(guān)實(shí)現(xiàn)耦合會導(dǎo)致所有的業(yè)務(wù)都會互相牽連,協(xié)議升級和維護(hù)都會異常困難。


        所以我們嘗試使用經(jīng)典的發(fā)布訂閱模型來解耦長連接網(wǎng)關(guān)跟客戶端與業(yè)務(wù)后端,它們之間只需要約定 Topic 即可自由互相發(fā)布訂閱消息。傳輸?shù)南⑹羌兌M(jìn)制數(shù)據(jù),網(wǎng)關(guān)也無需關(guān)心業(yè)務(wù)方的具體協(xié)議規(guī)范和序列化方式。

        權(quán)限控制


        我們使用發(fā)布訂閱解耦了網(wǎng)關(guān)與業(yè)務(wù)方的實(shí)現(xiàn),我們?nèi)匀恍枰刂瓶蛻舳藢?Topic 的發(fā)布訂閱的權(quán)限,避免有意或無意的數(shù)據(jù)污染或越權(quán)訪問。


        假如講師正在知乎 Live 的 165218 頻道開講,當(dāng)客戶端進(jìn)入房間嘗試訂閱 165218 頻道的 Topic 時(shí)就需要知乎 Live 的后端判斷當(dāng)前用戶是否已經(jīng)付費(fèi)。這種情況下的權(quán)限實(shí)際上是很靈活的,當(dāng)用戶付費(fèi)以后就能訂閱,否則就不能訂閱。權(quán)限的狀態(tài)只有知乎 Live 業(yè)務(wù)后端知曉,網(wǎng)關(guān)無法獨(dú)立作出判斷。


        所以我們在 ACL 規(guī)則中設(shè)計(jì)了基于回調(diào)的鑒權(quán)機(jī)制,可以配置 Live 相關(guān) Topic 的訂閱和發(fā)布動作都通過 HTTP 回調(diào)給 Live 的后端服務(wù)判斷。

        同時(shí)根據(jù)我們對內(nèi)部業(yè)務(wù)的觀察,大部分場景下業(yè)務(wù)需要的只是一個(gè)當(dāng)前用戶的私有 Topic 用來接收服務(wù)端下發(fā)的通知或消息,這種情況下如果讓業(yè)務(wù)都設(shè)計(jì)回調(diào)接口來判斷權(quán)限會很繁瑣。


        所以我們在 ACL 規(guī)則中設(shè)計(jì)了 Topic 模板變量來降低業(yè)務(wù)方的接入成本,我們給業(yè)務(wù)方配置允許訂閱的 Topic 中包含連接的用戶名變量標(biāo)識,表示只允許用戶訂閱或發(fā)送消息到自己的 Topic。



        此時(shí)網(wǎng)關(guān)可以在不跟業(yè)務(wù)方通信的情況下,獨(dú)立快速判斷客戶端是否有權(quán)限訂閱或往 Topic 發(fā)送消息。


        消息可靠性保證


        網(wǎng)關(guān)作為消息傳輸?shù)臉屑~,同時(shí)對接業(yè)務(wù)后端和客戶端,在轉(zhuǎn)發(fā)消息時(shí)需要保證消息在傳輸過程的可靠性。


        TCP 只能保證了傳輸過程中的順序和可靠性,但遇到 TCP 狀態(tài)異常、客戶端接收邏輯異?;虬l(fā)生了 Crash 等等情況時(shí),傳輸中的消息就會發(fā)生丟失。


        為了保證下發(fā)或上行的消息被對端正常處理,我們實(shí)現(xiàn)了回執(zhí)和重傳的功能。重要業(yè)務(wù)的消息在客戶端收到并正確處理后需要發(fā)送回執(zhí),而網(wǎng)關(guān)內(nèi)暫時(shí)保存客戶端未收取的消息,網(wǎng)關(guān)會判斷客戶端的接收情況并嘗試再次發(fā)送,直到正確收到了客戶端的消息回執(zhí)。

        而面對服務(wù)端業(yè)務(wù)的大流量場景,服務(wù)端發(fā)給網(wǎng)關(guān)的每條消息都發(fā)送回執(zhí)的方式效率較低,我們也提供了基于消息隊(duì)列的接收和發(fā)送方式,后面介紹發(fā)布訂閱實(shí)現(xiàn)時(shí)再詳細(xì)闡述。


        在設(shè)計(jì)通訊協(xié)議時(shí)我們參考了 MQTT 規(guī)范,拓展了認(rèn)證和鑒權(quán)設(shè)計(jì),完成了業(yè)務(wù)消息的隔離與解耦,保證了一定程度的傳輸可靠性。同時(shí)保持了與 MQTT 協(xié)議一定程度上兼容,這樣便于我們直接使用 MQTT 的各端客戶端實(shí)現(xiàn),降低業(yè)務(wù)方接入成本。


        我們怎么設(shè)計(jì)系統(tǒng)架構(gòu)?

        在設(shè)計(jì)項(xiàng)目整體架構(gòu)時(shí),我們優(yōu)先考慮的是:


        • 可靠性

        • 水平擴(kuò)展能力

        • 依賴組件成熟度


        簡單才值得信賴。


        為了保證可靠性,我們沒有考慮像傳統(tǒng)長連接系統(tǒng)那樣將內(nèi)部數(shù)據(jù)存儲、計(jì)算、消息路由等等組件全部集中到一個(gè)大的分布式系統(tǒng)中維護(hù),這樣增大系統(tǒng)實(shí)現(xiàn)和維護(hù)的復(fù)雜度。我們嘗試將這幾部分的組件獨(dú)立出來,將存儲、消息路由交給專業(yè)的系統(tǒng)完成,讓每個(gè)組件的功能盡量單一且清晰。


        同時(shí)我們也需要快速地水平擴(kuò)展能力?;ヂ?lián)網(wǎng)場景下各種營銷活動都可能導(dǎo)致連接數(shù)陡增,同時(shí)發(fā)布訂閱模型系統(tǒng)中下發(fā)消息數(shù)會隨著 Topic 的訂閱者的個(gè)數(shù)線性增長,此時(shí)網(wǎng)關(guān)暫存的客戶端未接收消息的存儲壓力也倍增。將各個(gè)組件拆開后減少了進(jìn)程內(nèi)部狀態(tài),我們就可以將服務(wù)部署到容器中,利用容器來完成快速而且?guī)缀鯚o限制的水平擴(kuò)展。


        最終設(shè)計(jì)的系統(tǒng)架構(gòu)如下圖:



        系統(tǒng)主要由四個(gè)主要組件組成:


        1. 接入層使用 OpenResty 實(shí)現(xiàn),負(fù)責(zé)連接負(fù)載均衡和會話保持

        2. 長連接 Broker,部署在容器中,負(fù)責(zé)協(xié)議解析、認(rèn)證與鑒權(quán)、會話、發(fā)布訂閱等邏輯

        3. Redis 存儲,持久化會話數(shù)據(jù)

        4. Kafka 消息隊(duì)列,分發(fā)消息給 Broker 或業(yè)務(wù)方


        其中 Kafka 和 Redis 都是業(yè)界廣泛使用的基礎(chǔ)組件,它們在知乎都已平臺化和容器化,它們也都能完成分鐘級快速擴(kuò)容。



        我們?nèi)绾螛?gòu)建長連接網(wǎng)關(guān)?


        接入層


        OpenResty 是業(yè)界使用非常廣泛的支持 Lua 的 Nginx 拓展方案,靈活性、穩(wěn)定性和性能都非常優(yōu)異,我們在接入層的方案選型上也考慮使用 OpenResty。


        接入層是最靠近用戶的一側(cè),在這一層需要完成兩件事:


        1. 負(fù)載均衡,保證各長連接 Broker 實(shí)例上連接數(shù)相對均衡

        2. 會話保持,單個(gè)客戶端每次連接到同一個(gè) Broker,用來提供消息傳輸可靠性保證


        負(fù)載均衡其實(shí)有很多算法都能完成,不管是隨機(jī)還是各種 Hash 算法都能比較好地實(shí)現(xiàn),麻煩一些的是會話保持。


        常見的四層負(fù)載均衡策略是根據(jù)連接來源 IP 進(jìn)行一致性 Hash,在節(jié)點(diǎn)數(shù)不變的情況下這樣能保證每次都 Hash 到同一個(gè) Broker 中,甚至在節(jié)點(diǎn)數(shù)稍微改變時(shí)也能大概率找到之前連接的節(jié)點(diǎn)。


        之前我們也使用過來源 IP Hash 的策略,主要有兩個(gè)缺點(diǎn):


        1. 分布不夠均勻,部分來源 IP 是大型局域網(wǎng) NAT 出口,上面的連接數(shù)多,導(dǎo)致 Broker 上連接數(shù)不均衡

        2. 不能準(zhǔn)確標(biāo)識客戶端,當(dāng)移動客戶端掉線切換網(wǎng)絡(luò)就可能無法連接回剛才的 Broker 了


        所以我們考慮七層的負(fù)載均衡,根據(jù)客戶端的唯一標(biāo)識來進(jìn)行一致性 Hash,這樣隨機(jī)性更好,同時(shí)也能保證在網(wǎng)絡(luò)切換后也能正確路由。常規(guī)的方法是需要完整解析通訊協(xié)議,然后按協(xié)議的包進(jìn)行轉(zhuǎn)發(fā),這樣實(shí)現(xiàn)的成本很高,而且增加了協(xié)議解析出錯的風(fēng)險(xiǎn)。


        最后我們選擇利用 Nginx 的 preread 機(jī)制實(shí)現(xiàn)七層負(fù)載均衡,對后面長連接 Broker 的實(shí)現(xiàn)的侵入性小,而且接入層的資源開銷也小。


        Nginx 在接受連接時(shí)可以指定預(yù)讀取連接的數(shù)據(jù)到 preread buffer 中,我們通過解析 preread buffer 中的客戶端發(fā)送的第一個(gè)報(bào)文提取客戶端標(biāo)識,再使用這個(gè)客戶端標(biāo)識進(jìn)行一致性 Hash 就拿到了固定的 Broker。


        發(fā)布與訂閱


        我們引入了業(yè)界廣泛使用的消息隊(duì)列 Kafka 來作為內(nèi)部消息傳輸?shù)臉屑~。


        前面提到了一些這么使用的原因:


        • 減少長連接 Broker 內(nèi)部狀態(tài),讓 Broker 可以無壓力擴(kuò)容

        • 知乎內(nèi)部已平臺化,支持水平擴(kuò)展


        還有一些原因是:


        • 使用消息隊(duì)列削峰,避免突發(fā)性的上行或下行消息壓垮系統(tǒng)

        • 業(yè)務(wù)系統(tǒng)中大量使用 Kafka 傳輸數(shù)據(jù),降低與業(yè)務(wù)方對接成本


        其中利用消息隊(duì)列削峰好理解,下面我們看一下怎么利用 Kafka 與業(yè)務(wù)方更好地完成對接。


        發(fā)布


        長連接 Broker 會根據(jù)路由配置將消息發(fā)布到 Kafka Topic,同時(shí)也會根據(jù)訂閱配置去消費(fèi) Kafka 將消息下發(fā)給訂閱客戶端。


        路由規(guī)則和訂閱規(guī)則是分別配置的,那么可能會出現(xiàn)四種情況:


        1. 消息路由到 Kafka Topic,但不消費(fèi),適合數(shù)據(jù)上報(bào)的場景。

        2. 消息路由到 Kafka Topic,也被消費(fèi),普通的即時(shí)通訊場景。

        3. 直接從 Kafka Topic 消費(fèi)并下發(fā),用于純下發(fā)消息的場景。

        4. 消息路由到一個(gè) Topic,然后從另一個(gè) Topic 消費(fèi),用于消息需要過濾或者預(yù)處理的場景。

        這套路由策略的設(shè)計(jì)靈活性非常高,可以解決幾乎所有的場景的消息路由需求。同時(shí)因?yàn)榘l(fā)布訂閱基于 Kafka,可以保證在處理大規(guī)模數(shù)據(jù)時(shí)的消息可靠性。


        訂閱


        當(dāng)長連接 Broker 從 Kafka Topic 中消費(fèi)出消息后會查找本地的訂閱關(guān)系,然后將消息分發(fā)到客戶端會話。


        我們最開始直接使用 HashMap 存儲客戶端的訂閱關(guān)系。當(dāng)客戶端訂閱一個(gè) Topic 時(shí)我們就將客戶端的會話對象放入以 Topic 為 Key 的訂閱 Map 中,當(dāng)反查消息的訂閱關(guān)系時(shí)直接用 Topic 從 Map 上取值就行。


        因?yàn)檫@個(gè)訂閱關(guān)系是共享對象,當(dāng)訂閱和取消訂閱發(fā)生時(shí)就會有連接嘗試操作這個(gè)共享對象。為了避免并發(fā)寫我們給 HashMap 加了鎖,但這個(gè)全局鎖的沖突非常嚴(yán)重,嚴(yán)重影響性能。


        最終我們通過分片細(xì)化了鎖的粒度,分散了鎖的沖突。


        本地同時(shí)創(chuàng)建數(shù)百個(gè) HashMap,當(dāng)需要在某個(gè) Key 上存取數(shù)據(jù)前通過 Hash 和取模找到其中一個(gè) HashMap 然后進(jìn)行操作,這樣將全局鎖分散到了數(shù)百個(gè) HashMap 中,大大降低了操作沖突,也提升了整體的性能。


        會話


        持久化


        當(dāng)消息被分發(fā)給會話 Session 對象后,由 Session 來控制消息的下發(fā)。


        Session 會判斷消息是否是重要 Topic 消息, 是的話將消息標(biāo)記 QoS 等級為 1,同時(shí)將消息存儲到 Redis 的未接收消息隊(duì)列,并將消息下發(fā)給客戶端。等到客戶端對消息的 ACK 后,再將未確認(rèn)隊(duì)列中的消息刪除。


        有一些業(yè)界方案是在內(nèi)存中維護(hù)了一個(gè)列表,在擴(kuò)容或縮容時(shí)這部分?jǐn)?shù)據(jù)沒法跟著遷移。也有部分業(yè)界方案是在長連接集群中維護(hù)了一個(gè)分布式內(nèi)存存儲,這樣實(shí)現(xiàn)起來復(fù)雜度也會變高。


        我們將未確認(rèn)消息隊(duì)列放到了外部持久化存儲中,保證了單個(gè) Broker 宕機(jī)后,客戶端重新上線連接到其他 Broker 也能恢復(fù) Session 數(shù)據(jù),減少了擴(kuò)容和縮容的負(fù)擔(dān)。


        滑動窗口


        在發(fā)送消息時(shí),每條 QoS 1 的消息需要被經(jīng)過傳輸、客戶端處理、回傳 ACK 才能確認(rèn)下發(fā)完成,路徑耗時(shí)較長。如果消息量較大,每條消息都等待這么長的確認(rèn)才能下發(fā)下一條,下發(fā)通道帶寬不能被充分利用。


        為了保證發(fā)送的效率,我們參考 TCP 的滑動窗口設(shè)計(jì)了并行發(fā)送的機(jī)制。我們設(shè)置一定的閾值為發(fā)送的滑動窗口,表示通道上可以同時(shí)有這么多條消息正在傳輸和被等待確認(rèn)。



        我們應(yīng)用層設(shè)計(jì)的滑動窗口跟 TCP 的滑動窗口實(shí)際上還有些差異。


        TCP 的滑動窗口內(nèi)的 IP 報(bào)文無法保證順序到達(dá),而我們的通訊是基于 TCP 的所以我們的滑動窗口內(nèi)的業(yè)務(wù)消息是順序的,只有在連接狀態(tài)異常、客戶端邏輯異常等情況下才可能導(dǎo)致部分窗口內(nèi)的消息亂序。


        因?yàn)?TCP 協(xié)議保證了消息的接收順序,所以正常的發(fā)送過程中不需要針對單條消息進(jìn)行重試,只有在客戶端重新連接后才對窗口內(nèi)的未確認(rèn)消息重新發(fā)送。消息的接收端同時(shí)會保留窗口大小的緩沖區(qū)用來消息去重,保證業(yè)務(wù)方接收到的消息不會重復(fù)。


        我們基于 TCP 構(gòu)建的滑動窗口保證了消息的順序性同時(shí)也極大提升傳輸?shù)耐掏铝俊?/p>


        寫在最后


        基礎(chǔ)架構(gòu)組負(fù)責(zé)知乎的流量入口和內(nèi)部基礎(chǔ)設(shè)施建設(shè),對外我們奮斗在直面海量流量的的第一戰(zhàn)線,對內(nèi)我們?yōu)樗械臉I(yè)務(wù)提供堅(jiān)如磐石的基礎(chǔ)設(shè)施,用戶的每一次訪問、每一個(gè)請求、內(nèi)網(wǎng)的每一次調(diào)用都與我們的系統(tǒng)息息相關(guān)。

        瀏覽 56
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            紧缚捆绑绳高潮呜呜图片 | 久久久久99精品成人片欧美片 | 欧美操逼一级精 | 成年人在线免费观看毛片 | 久久久91精品国产一区陈可心 | 欧美日韩性无码专区啪一啪 | 国产91欧美 | 白石真琴无码 | 孕妇孕交一区二区三区 | 五月亚洲 |