百萬(wàn)級(jí)群聊的設(shè)計(jì)實(shí)踐
當(dāng)前位置:點(diǎn)晴教程→知識(shí)管理交流
→『 技術(shù)文檔交流 』
本文介紹了服務(wù)端在搭建 Web 版的百萬(wàn)人級(jí)別的群聊系統(tǒng)時(shí),遇到的技術(shù)挑戰(zhàn)和解決思路,內(nèi)容包括:通信方案選型、消息存儲(chǔ)、消息有序性、消息可靠性、未讀數(shù)統(tǒng)計(jì)。
一、引言現(xiàn)在IM群聊產(chǎn)品多種多樣,有國(guó)民級(jí)的微信、QQ,企業(yè)級(jí)的釘釘、飛書(shū),還有許多公司內(nèi)部的IM工具,這些都是以客戶(hù)端為主要載體,而且群聊人數(shù)通常都是有限制,微信正常群人數(shù)上限是500,QQ2000人,收費(fèi)能達(dá)到3000人,這里固然有產(chǎn)品考量,但技術(shù)成本、資源成本也是很大的因素。而筆者業(yè)務(wù)場(chǎng)景上需要一個(gè)迭代更新快、輕量級(jí)(不依賴(lài)客戶(hù)端)、單群百萬(wàn)群成員的純H5的IM產(chǎn)品,本文將回顧實(shí)現(xiàn)一個(gè)百萬(wàn)人量級(jí)的群聊,服務(wù)器側(cè)需要考慮的設(shè)計(jì)要點(diǎn),希望可以給到讀者一些啟發(fā)。
二、背景介紹不同的群聊產(chǎn)品,采用的技術(shù)方案是不同的,為了理解接下來(lái)的技術(shù)選型,需要先了解下這群聊產(chǎn)品的特性。
三、通信技術(shù)即時(shí)通信常見(jiàn)的通信技術(shù)有短輪詢(xún)、長(zhǎng)輪詢(xún)、Server-Sent Events(SSE)、Websocket。短輪詢(xún)和長(zhǎng)輪詢(xún)適用于實(shí)時(shí)性要求不高的場(chǎng)景,比如論壇的消息提醒。SSE 適用于服務(wù)器向客戶(hù)端單向推送的場(chǎng)景,如實(shí)時(shí)新聞、股票行情。Websocket 適用于實(shí)時(shí)雙向通信的場(chǎng)景,實(shí)時(shí)性好,且服務(wù)端、前端都有比較成熟的三方包,如 socket.io,所以這塊在方案選擇中是比較 easy 的,前后端使用 Websocket 來(lái)實(shí)現(xiàn)實(shí)時(shí)通信。
四、消息存儲(chǔ)群聊消息的保存方式,主流有2種方式:讀擴(kuò)散、寫(xiě)擴(kuò)散。圖1展示了它們的區(qū)別,區(qū)別就在于消息是寫(xiě)一次還是寫(xiě)N次,以及如何讀取。
圖1
讀擴(kuò)散就是所有群成員共用一個(gè)群信箱,當(dāng)一個(gè)群產(chǎn)生一條消息時(shí),只需要寫(xiě)入這個(gè)群的信箱即可,所有群成員從這一個(gè)信箱里讀取群消息。 優(yōu)點(diǎn)是寫(xiě)入邏輯簡(jiǎn)單,存儲(chǔ)成本低,寫(xiě)入效率高。缺點(diǎn)是讀取邏輯相對(duì)復(fù)雜,要通過(guò)消息表與其他業(yè)務(wù)表數(shù)據(jù)聚合;消息定制化處理復(fù)雜,需要額外的業(yè)務(wù)表;可能還有IO熱點(diǎn)問(wèn)題。
舉個(gè)例子: 很常見(jiàn)的場(chǎng)景,展示用戶(hù)對(duì)消息的已讀未讀狀態(tài),這個(gè)時(shí)候公共群信箱就無(wú)法滿(mǎn)足要求,必須增加消息已讀未讀表來(lái)記錄相關(guān)狀態(tài)。還有用戶(hù)對(duì)某條消息的刪除狀態(tài),用戶(hù)可以選擇刪除一條消息,但是其他人仍然可以看到它,此時(shí)也不適合在公共群信箱里拓展,也需要用到另一張關(guān)系表,總而言之針對(duì)消息做用戶(hù)特定功能時(shí)就會(huì)比寫(xiě)擴(kuò)散復(fù)雜。 寫(xiě)擴(kuò)散就是每個(gè)群成員擁有獨(dú)立的信箱,每產(chǎn)生一條消息,需要寫(xiě)入所有群成員信箱,群成員各自從自己的信箱內(nèi)讀取群消息。優(yōu)點(diǎn)是讀取邏輯簡(jiǎn)單,適合消息定制化處理,不存在IO熱點(diǎn)問(wèn)題。缺點(diǎn)是寫(xiě)入效率低,且隨著群成員數(shù)增加,效率降低;存儲(chǔ)成本大。
所以當(dāng)單群成員在萬(wàn)級(jí)以上時(shí),用寫(xiě)擴(kuò)散就明顯不太合適了,寫(xiě)入效率太低,而且可能存在很多無(wú)效寫(xiě)入,不活躍的群成員也必須得有信箱,存儲(chǔ)成本是非常大的,因此采用讀擴(kuò)散是比較合適的。
據(jù)了解,微信是采用寫(xiě)擴(kuò)散模式,微信群設(shè)定是500人上限,寫(xiě)擴(kuò)散的缺點(diǎn)影響就比較小。
五、架構(gòu)設(shè)計(jì)5.1 整體架構(gòu)先來(lái)看看群聊的架構(gòu)設(shè)計(jì)圖,如圖2所示:
圖2
從用戶(hù)登錄到發(fā)送消息,再到群用戶(hù)收到這條消息的系統(tǒng)流程如圖3所示:
圖3
5.2 路由策略用戶(hù)應(yīng)該連接到哪一臺(tái)連接服務(wù)呢?這個(gè)過(guò)程重點(diǎn)考慮如下2個(gè)問(wèn)題:
保證均衡有如下幾個(gè)算法:
5.3 重連機(jī)制當(dāng)應(yīng)用在擴(kuò)縮容或重啟升級(jí)時(shí),在該節(jié)點(diǎn)上的客戶(hù)端怎么處理?由于設(shè)計(jì)有心跳機(jī)制,當(dāng)心跳不通或監(jiān)聽(tīng)連接斷開(kāi)時(shí),就認(rèn)為該節(jié)點(diǎn)有問(wèn)題了,就嘗試重新連接;如果客戶(hù)端正在發(fā)送消息,那么就需要將消息臨時(shí)保存住,等待重新連接上后再次發(fā)送。
5.4 線(xiàn)程策略將連接服務(wù)里的IO線(xiàn)程與業(yè)務(wù)線(xiàn)程隔離,提升整體性能,原因如下:
5.5 有狀態(tài)鏈接在這樣的場(chǎng)景中不像 HTTP 那樣是無(wú)狀態(tài)的,需要明確知道各個(gè)客戶(hù)端和連接的關(guān)系。比如需要向客戶(hù)端廣播群消息時(shí),首先得知道客戶(hù)端的連接會(huì)話(huà)保存在哪個(gè)連接服務(wù)節(jié)點(diǎn)上,自然這里需要引入第三方中間件來(lái)存儲(chǔ)這個(gè)關(guān)系。通過(guò)由連接服務(wù)主動(dòng)上報(bào)給群組服務(wù)來(lái)實(shí)現(xiàn),上報(bào)時(shí)機(jī)是客戶(hù)端接入和斷開(kāi)連接服務(wù)以及周期性的定時(shí)任務(wù)。
5.6 群組路由設(shè)想這樣一個(gè)場(chǎng)景:需要給群所有成員推送一條消息怎么做?通過(guò)群編號(hào)去前面的路由 Redis 獲取對(duì)應(yīng)群的連接服務(wù)組,再通過(guò) HTTP 方式調(diào)用連接服務(wù),通過(guò)連接服務(wù)上的長(zhǎng)連接會(huì)話(huà)進(jìn)行真正的消息下發(fā)。
5.7 消息流轉(zhuǎn)連接服務(wù)直接接收用戶(hù)的上行消息,考慮到消息量可能非常大,在連接服務(wù)里做業(yè)務(wù)顯然不合適,這里完全可以選擇 Kafka 來(lái)解耦,將所有的上行消息直接丟到 Kafka 就不管了,消息由群組服務(wù)來(lái)處理。
六、消息順序?6.1 亂序現(xiàn)象 為什么要講消息順序,來(lái)看一個(gè)場(chǎng)景。假設(shè)群里有用戶(hù)A、用戶(hù)B、用戶(hù)C、用戶(hù)D,下面以 ABCD 代替,假設(shè)A發(fā)送了3條消息,順序分別是 msg1、msg2、msg3,但B、C、D看到的消息順序不一致,如圖4所示: 圖4
這時(shí)B、C、D肯定會(huì)覺(jué)得A在胡言亂語(yǔ)了,這樣的產(chǎn)品用戶(hù)必定是不喜歡的,因此必須要保證所有接收方看到的消息展示順序是一致的。
6.2 原因分析所以先了解下消息發(fā)送的宏觀過(guò)程:
在上面的過(guò)程中,都可能產(chǎn)生順序問(wèn)題,簡(jiǎn)要分析幾點(diǎn)原因:
6.3 解決方案6.3.1 單用戶(hù)保持有序通過(guò)上面的分析可以知道,其實(shí)無(wú)法保證或是無(wú)法衡量不同用戶(hù)之間的消息順序,那么只需保證同一個(gè)用戶(hù)的消息是有序的,保證上下文語(yǔ)義,所以可以得出一個(gè)比較樸素的實(shí)現(xiàn)方式:以服務(wù)端數(shù)據(jù)庫(kù)的唯一自增ID為標(biāo)尺來(lái)衡量消息的時(shí)序,然后讓同一個(gè)用戶(hù)的消息處理串行化。那么就可以通過(guò)以下幾個(gè)技術(shù)手段配合來(lái)解決:
6.3.2 推拉結(jié)合到這里基本解決了同一個(gè)用戶(hù)的消息可以按照他自己發(fā)出的順序入庫(kù)的問(wèn)題,即解決了消息發(fā)送流程里第一、二步。
第三、四步存在的問(wèn)題是這樣的: A發(fā)送了 msg1、msg2、msg3,B發(fā)送了 msg4、msg5、msg6,最終服務(wù)端的入庫(kù)順序是msg1、msg2、msg4、msg3、msg5、msg6,那除了A和B其他人的消息順序需要按照入庫(kù)順序來(lái)展示,而這里的問(wèn)題是服務(wù)端考量推送吞吐量,在推送環(huán)節(jié)是并發(fā)的,即可能 msg4 比 msg1 先推送到用戶(hù)端上,如果按照推送順序追加來(lái)展示,那么就與預(yù)期不符了,每個(gè)人看到的消息順序都可能不一致,如果用戶(hù)端按照消息的id大小進(jìn)行比較插入的話(huà),用戶(hù)體驗(yàn)將會(huì)比較奇怪,突然會(huì)在2個(gè)消息中間出現(xiàn)一條消息。所以這里采用推拉結(jié)合方式來(lái)解決這個(gè)問(wèn)題,具體步驟如下:
圖5
圖6
舉例,圖5表示服務(wù)端的消息順序,圖6表示用戶(hù)端拉取消息時(shí)本地消息隊(duì)列和提醒隊(duì)列的變化邏輯。
通過(guò)推拉結(jié)合的方式可以保證所有用戶(hù)收到的消息展示順序一致。細(xì)心的讀者可能會(huì)有疑問(wèn),如果聊天信息流里有自己發(fā)送的消息,那么可能與其他的人看到的不一致,這是因?yàn)樽约旱南⒄故静灰蕾?lài)?yán)?,需要即時(shí)展示,給用戶(hù)立刻發(fā)送成功的體驗(yàn),同時(shí)其他人也可能也在發(fā)送,最終可能比他先入庫(kù),為了不出現(xiàn)信息流中間插入消息的用戶(hù)體驗(yàn),只能將他人的新消息追加在自己的消息后面。所以如果作為發(fā)送者,消息順序可能不一致,但是作為純接收者,大家的消息順序都是一樣的。
七、消息可靠性在IM系統(tǒng)中,消息的可靠性同樣非常重要,它主要體現(xiàn)在:
7.1 消息不丟失設(shè)計(jì)
7.2 消息不重復(fù)設(shè)計(jì)
八、未讀數(shù)統(tǒng)計(jì)為了提醒用戶(hù)有新消息,需要給用戶(hù)展示新消息提醒標(biāo)識(shí),產(chǎn)品設(shè)計(jì)上一般有小紅點(diǎn)、具體的數(shù)值2種方式。具體數(shù)值比小紅點(diǎn)要復(fù)雜,這里分析下具體數(shù)值的處理方式,還需要分為初始打開(kāi)群和已打開(kāi)群2個(gè)場(chǎng)景。
已打開(kāi)群:可以完全依賴(lài)用戶(hù)端本地統(tǒng)計(jì),用戶(hù)端獲取到新消息后,就將未讀數(shù)累計(jì)加1,等點(diǎn)進(jìn)去查看后,清空未讀數(shù)統(tǒng)計(jì),這個(gè)比較簡(jiǎn)單。
初始打開(kāi)群:由于用戶(hù)端采用H5開(kāi)發(fā),用戶(hù)端沒(méi)有緩存,沒(méi)有能力緩存最近的已讀消息游標(biāo),因此這里完全需要服務(wù)端來(lái)統(tǒng)計(jì),在打開(kāi)群時(shí)下發(fā)最新的聊天信息流和未讀數(shù),下面具體講下這個(gè)場(chǎng)景下該怎么設(shè)計(jì)。 既然由服務(wù)端統(tǒng)計(jì)未讀數(shù),那么少不了要保存用戶(hù)在某個(gè)群里已經(jīng)讀到哪個(gè)消息,類(lèi)似一個(gè)游標(biāo),用戶(hù)已讀消息,游標(biāo)往前走。用戶(hù)已讀消息存儲(chǔ)表設(shè)計(jì)如圖7所示:
圖7
游標(biāo)offset采用定時(shí)更新策略,連接服務(wù)會(huì)記錄用戶(hù)最近一次拉取到的消息ID,定時(shí)異步上報(bào)批量用戶(hù)到群組服務(wù)更新 offset。 該表第一行表示用戶(hù)1在 id=89 的群里,最新的已讀消息是id=1022消息,那么可以通過(guò)下面的SQL來(lái)統(tǒng)計(jì)他在這個(gè)群里的未讀數(shù):select count(1) from msg_info where groupId = 89 and id > 1022。但是事情并沒(méi)這么簡(jiǎn)單,一個(gè)用戶(hù)有很多群,每個(gè)群都要展示未讀數(shù),因此要求未讀數(shù)統(tǒng)計(jì)的程序效率要高,不然用戶(hù)體驗(yàn)就很差,很明顯這個(gè) SQL 的耗時(shí)波動(dòng)很大,取決于 offset 的位置,如果很靠后,SQL 執(zhí)行時(shí)間會(huì)非常長(zhǎng)。筆者通過(guò)2個(gè)策略來(lái)優(yōu)化這個(gè)場(chǎng)景:
圖8
如上圖8所示,每個(gè)群都會(huì)構(gòu)建一個(gè)長(zhǎng)度為100,score 和 member 都是消息ID,可以通過(guò) zrevrank 命令得到某個(gè) offset 的排名值,該值可以換算成未讀數(shù)。比如:用戶(hù)1在群89的未讀消息數(shù),'zrevrank 89 1022' = 2,也就是有2條未讀數(shù)。用戶(hù)2在群89的未讀數(shù),'zrevrank 89 890' = nil,那么未讀數(shù)就是99+。同時(shí)消息新增、刪除都需要同步維護(hù)該數(shù)據(jù)結(jié)構(gòu),失效或不存在時(shí)從 MySQL 初始化。
九、超大群策略前面提到,設(shè)計(jì)目標(biāo)是在同一個(gè)群里能支撐百萬(wàn)人,從架構(gòu)上可以看到,連接服務(wù)處于流量最前端,所以它的承載力直接決定了同時(shí)在線(xiàn)用戶(hù)的上限。 影響它的因素有:
9.1 消息風(fēng)暴當(dāng)同時(shí)在線(xiàn)用戶(hù)數(shù)非常多,例如百萬(wàn)時(shí),會(huì)面臨如下幾個(gè)問(wèn)題:
9.2 消息壓縮如果某一個(gè)時(shí)刻,推送消息的數(shù)量比較大,且群同時(shí)在線(xiàn)人數(shù)比較多的時(shí)候,連接服務(wù)層的機(jī)房出口帶寬就會(huì)成為消息推送的瓶頸。 做個(gè)計(jì)算,百萬(wàn)人在線(xiàn),需要5臺(tái)連接服務(wù),一條消息1KB,一般情況下,5臺(tái)連接服務(wù)集群都是部署在同一個(gè)機(jī)房,那么這個(gè)機(jī)房的帶寬就是1000000*1KB=1GB,如果多幾個(gè)超大群,那么對(duì)機(jī)房的帶寬要求就更高,所以如何有效的控制每一個(gè)消息的大小、壓縮每一個(gè)消息的大小,是需要思考的問(wèn)題。 經(jīng)過(guò)測(cè)試,使用 protobuf 數(shù)據(jù)交換格式,平均每一個(gè)消息可以節(jié)省43%的字節(jié)大小,可以大大節(jié)省機(jī)房出口帶寬。
9.3 塊消息超大群里,消息推送的頻率很高,每一條消息推送都需要進(jìn)行一次IO系統(tǒng)調(diào)用,顯然會(huì)影響服務(wù)器性能,可以采用將多個(gè)消息進(jìn)行合并推送。 主要思路:以群為維度,累計(jì)一段時(shí)間內(nèi)的消息,如果達(dá)到閾值,就立刻合并推送,否則就以勻速的時(shí)間間隔將在這個(gè)時(shí)間段內(nèi)新增的消息進(jìn)行推送。 時(shí)間間隔是1秒,閾值是10,如果500毫秒內(nèi)新增了10條消息,就合并推送這10條消息,時(shí)間周期重置;如果1秒內(nèi)只新增了8條消息,那么1秒后合并推送這8條消息。這樣做的好處如下:
十、總結(jié)在本文中,筆者介紹了從零開(kāi)始搭建一個(gè)生產(chǎn)級(jí)百萬(wàn)級(jí)群聊的一些關(guān)鍵要點(diǎn)和實(shí)踐經(jīng)驗(yàn),包括通信方案選型、消息存儲(chǔ)、消息順序、消息可靠性、高并發(fā)等方面,但仍有許多技術(shù)設(shè)計(jì)未涉及,比如冷熱群、高低消息通道會(huì)放在未來(lái)的規(guī)劃里。IM開(kāi)發(fā)業(yè)界沒(méi)有統(tǒng)一的標(biāo)準(zhǔn),不同的產(chǎn)品有適合自己的技術(shù)方案,希望本文能夠帶給讀者更好地理解和應(yīng)用這些技術(shù)實(shí)踐,為構(gòu)建高性能、高可靠性的群聊系統(tǒng)提供一定的參考。 轉(zhuǎn)自https://www.cnblogs.com/vivotech/p/18740478 該文章在 2025/3/5 11:18:40 編輯過(guò) |
關(guān)鍵字查詢(xún)
相關(guān)文章
正在查詢(xún)... |