<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          如何設(shè)計(jì)一個(gè)億級(jí)消息量的 IM 系統(tǒng)

          共 8999字,需瀏覽 18分鐘

           ·

          2021-05-20 11:28

          來(lái)源:https://xie.infoq.cn/article/19e95a78e2f5389588debfb1c

          IM核心概念

          用戶(hù) :系統(tǒng)的使用者

          消息 :是指用戶(hù)之間的溝通內(nèi)容。通常在IM系統(tǒng)中,消息會(huì)有以下幾類(lèi):文本消息、表情消息、圖片消息、視頻消息、文件消息等等

          會(huì)話(huà) :通常指兩個(gè)用戶(hù)之間因聊天而建立起的關(guān)聯(lián)

           :通常指多個(gè)用戶(hù)之間因聊天而建立起的關(guān)聯(lián)

          終端 :指用戶(hù)使用IM系統(tǒng)的機(jī)器。通常有Android端、iOS端、Web端等等

          未讀數(shù) :指用戶(hù)還沒(méi)讀的消息數(shù)量

          用戶(hù)狀態(tài) :指用戶(hù)當(dāng)前是在線(xiàn)、離線(xiàn)還是掛起等狀態(tài)

          關(guān)系鏈 :是指用戶(hù)與用戶(hù)之間的關(guān)系,通常有單向的好友關(guān)系、雙向的好友關(guān)系、關(guān)注關(guān)系等等。這里需要注意與會(huì)話(huà)的區(qū)別,用戶(hù)只有在發(fā)起聊天時(shí)才產(chǎn)生會(huì)話(huà),但關(guān)系并不需要聊天才能建立。對(duì)于關(guān)系鏈的存儲(chǔ),可以使用圖數(shù)據(jù)庫(kù)(Neo4j等等),可以很自然地表達(dá)現(xiàn)實(shí)世界中的關(guān)系,易于建模

          單聊 :一對(duì)一聊天

          群聊 :多人聊天

          客服 :在電商領(lǐng)域,通常需要對(duì)用戶(hù)提供售前咨詢(xún)、售后咨詢(xún)等服務(wù)。這時(shí),就需要引入客服來(lái)處理用戶(hù)的咨詢(xún)

          消息分流 :在電商領(lǐng)域,一個(gè)店鋪通常會(huì)有多個(gè)客服,此時(shí)決定用戶(hù)的咨詢(xún)由哪個(gè)客服來(lái)處理就是消息分流。通常消息分流會(huì)根據(jù)一系列規(guī)則來(lái)確定消息會(huì)分流給哪個(gè)客服,例如客服是否在線(xiàn)(客服不在線(xiàn)的話(huà)需要重新分流給另一個(gè)客服)、該消息是售前咨詢(xún)還是售后咨詢(xún)、當(dāng)前客服的繁忙程度等等

          信箱 :本文的信箱我們指一個(gè)Timeline、一個(gè)收發(fā)消息的隊(duì)列

          讀擴(kuò)散 vs 寫(xiě)擴(kuò)散

          讀擴(kuò)散

          我們先來(lái)看看讀擴(kuò)散。如上圖所示,A與每個(gè)聊天的人跟群都有一個(gè)信箱(有些博文會(huì)叫Timeline),A在查看聊天信息的時(shí)候需要讀取所有有新消息的信箱。這里的讀擴(kuò)散需要注意與Feeds系統(tǒng)的區(qū)別,在Feeds系統(tǒng)中,每個(gè)人都有一個(gè)寫(xiě)信箱,寫(xiě)只需要往自己的寫(xiě)信箱里寫(xiě)一次就好了,讀需要從所有關(guān)注的人的寫(xiě)信箱里讀。但I(xiàn)M系統(tǒng)里的讀擴(kuò)散通常是每?jī)蓚€(gè)相關(guān)聯(lián)的人就有一個(gè)信箱,或者每個(gè)群一個(gè)信箱。

          讀擴(kuò)散的優(yōu)點(diǎn):

          • 寫(xiě)操作(發(fā)消息)很輕量,不管是單聊還是群聊,只需要往相應(yīng)的信箱寫(xiě)一次就好了
          • 每一個(gè)信箱天然就是兩個(gè)人的聊天記錄,可以方便查看聊天記錄跟進(jìn)行聊天記錄的搜索

          讀擴(kuò)散的缺點(diǎn):

          • 讀操作(讀消息)很重


          寫(xiě)擴(kuò)散

          接下來(lái)看看寫(xiě)擴(kuò)散。

          在寫(xiě)擴(kuò)散中,每個(gè)人都只從自己的信箱里讀取消息,但寫(xiě)(發(fā)消息)的時(shí)候,對(duì)于單聊跟群聊處理如下:

          • 單聊:往自己的信箱跟對(duì)方的信箱都寫(xiě)一份消息,同時(shí),如果需要查看兩個(gè)人的聊天歷史記錄的話(huà)還需要再寫(xiě)一份(當(dāng)然,如果從個(gè)人信箱也能回溯出兩個(gè)人的所有聊天記錄,但這樣效率會(huì)很低)。
          • 群聊:需要往所有的群成員的信箱都寫(xiě)一份消息,同時(shí),如果需要查看群的聊天歷史記錄的話(huà)還需要再寫(xiě)一份。可以看出,寫(xiě)擴(kuò)散對(duì)于群聊來(lái)說(shuō)大大地放大了寫(xiě)操作。

          寫(xiě)擴(kuò)散優(yōu)點(diǎn):

          • 讀操作很輕量
          • 可以很方便地做消息的多終端同步

          寫(xiě)擴(kuò)散缺點(diǎn):

          • 寫(xiě)操作很重,尤其是對(duì)于群聊來(lái)說(shuō)


          注意,在Feeds系統(tǒng)中:

          • 寫(xiě)擴(kuò)散也叫:Push、Fan-out或者Write-fanout
          • 讀擴(kuò)散也叫:Pull、Fan-in或者Read-fanout

          唯一ID設(shè)計(jì)

          通常情況下,ID的設(shè)計(jì)主要有以下幾大類(lèi):

          • UUID
          • 基于Snowflake的ID生成方式
          • 基于申請(qǐng)DB步長(zhǎng)的生成方式
          • 基于Redis或者DB的自增ID生成方式
          • 特殊的規(guī)則生成唯一ID

          具體的實(shí)現(xiàn)方法跟優(yōu)缺點(diǎn)可以參考之前的一篇博文:分布式唯一 ID 解析

          在IM系統(tǒng)中需要唯一Id的地方主要是:

          • 會(huì)話(huà)ID
          • 消息ID

          消息ID

          我們來(lái)看看在設(shè)計(jì)消息ID時(shí)需要考慮的三個(gè)問(wèn)題。

          消息ID不遞增可以嗎

          我們先看看不遞增的話(huà)會(huì)怎樣:

          • 使用字符串,浪費(fèi)存儲(chǔ)空間,而且不能利用存儲(chǔ)引擎的特性讓相鄰的消息存儲(chǔ)在一起,降低消息的寫(xiě)入跟讀取性能
          • 使用數(shù)字,但數(shù)字隨機(jī),也不能利用存儲(chǔ)引擎的特性讓相鄰的消息存儲(chǔ)在一起,會(huì)加大隨機(jī)IO,降低性能;而且隨機(jī)的ID不好保證ID的唯一性

          因此,消息ID最好是遞增的。

          全局遞增 vs 用戶(hù)級(jí)別遞增 vs 會(huì)話(huà)級(jí)別遞增

          全局遞增:指消息ID在整個(gè)IM系統(tǒng)隨著時(shí)間的推移是遞增的。全局遞增的話(huà)一般可以使用Snowflake(當(dāng)然,Snowflake也只是worker級(jí)別的遞增)。此時(shí),如果你的系統(tǒng)是讀擴(kuò)散的話(huà)為了防止消息丟失,那每一條消息就只能帶上上一條消息的ID,前端根據(jù)上一條消息判斷是否有丟失消息,有消息丟失的話(huà)需要重新拉一次。

          用戶(hù)級(jí)別遞增:指消息ID只保證在單個(gè)用戶(hù)中是遞增的,不同用戶(hù)之間不影響并且可能重復(fù)。典型代表:微信。如果是寫(xiě)擴(kuò)散系統(tǒng)的話(huà)信箱時(shí)間線(xiàn)ID跟消息ID需要分開(kāi)設(shè)計(jì),信箱時(shí)間線(xiàn)ID用戶(hù)級(jí)別遞增,消息ID全局遞增。如果是讀擴(kuò)散系統(tǒng)的話(huà)感覺(jué)使用用戶(hù)級(jí)別遞增必要性不是很大。

          會(huì)話(huà)級(jí)別遞增:指消息ID只保證在單個(gè)會(huì)話(huà)中是遞增的,不同會(huì)話(huà)之間不影響并且可能重復(fù)。典型代表:QQ。

          連續(xù)遞增 vs 單調(diào)遞增

          連續(xù)遞增是指ID按 1,2,3...n 的方式生成;而單調(diào)遞增是指只要保證后面生成的ID比前面生成的ID大就可以了,不需要連續(xù)。

          據(jù)我所知,QQ的消息ID就是在會(huì)話(huà)級(jí)別使用的連續(xù)遞增,這樣的好處是,如果丟失了消息,當(dāng)下一條消息來(lái)的時(shí)候發(fā)現(xiàn)ID不連續(xù)就會(huì)去請(qǐng)求服務(wù)器,避免丟失消息。此時(shí),可能有人會(huì)想,我不能用定時(shí)拉的方式看有沒(méi)有消息丟失嗎?當(dāng)然不能,因?yàn)橄D只在會(huì)話(huà)級(jí)別連續(xù)遞增的話(huà)那如果一個(gè)人有上千個(gè)會(huì)話(huà),那得拉多少次啊,服務(wù)器肯定是抗不住的。

          對(duì)于讀擴(kuò)散來(lái)說(shuō),消息ID使用連續(xù)遞增就是一種不錯(cuò)的方式了。如果使用單調(diào)遞增的話(huà)當(dāng)前消息需要帶上前一條消息的ID(即聊天消息組成一個(gè)鏈表),這樣,才能判斷消息是否丟失。


          總結(jié)一下就是:

          • 寫(xiě)擴(kuò)散:信箱時(shí)間線(xiàn)ID使用用戶(hù)級(jí)別遞增,消息ID全局遞增,此時(shí)只要保證單調(diào)遞增就可以了
          • 讀擴(kuò)散:消息ID可以使用會(huì)話(huà)級(jí)別遞增并且最好是連續(xù)遞增

          會(huì)話(huà)ID

          我們來(lái)看看設(shè)計(jì)會(huì)話(huà)ID需要注意的問(wèn)題:

          其中,會(huì)話(huà)ID有種比較簡(jiǎn)單的生成方式(特殊的規(guī)則生成唯一ID):拼接 from_user_id 跟 to_user_id

          1. 如果 from_user_id 跟 to_user_id 都是32位整形數(shù)據(jù)的話(huà)可以很方便地用位運(yùn)算拼接成一個(gè)64位的會(huì)話(huà)ID,即:conversation_id = ${from_user_id} << 32 | ${to_user_id} (在拼接前需要確保值比較小的用戶(hù)ID是 from_user_id,這樣任意兩個(gè)用戶(hù)發(fā)起會(huì)話(huà)可以很方便地知道會(huì)話(huà)ID)
          2. 如果from_user_id 跟 to_user_id 都是64位整形數(shù)據(jù)的話(huà)那就只能拼接成一個(gè)字符串了,拼接成字符串的話(huà)就比較傷了,浪費(fèi)存儲(chǔ)空間性能又不好。

          前東家就是使用的上面第1種方式,第1種方式有個(gè)硬傷:隨著業(yè)務(wù)在全球的擴(kuò)展,32位的用戶(hù)ID如果不夠用需要擴(kuò)展到64位的話(huà)那就需要大刀闊斧地改了。32位整形ID看起來(lái)能夠容納21億個(gè)用戶(hù),但通常我們?yōu)榱朔乐箘e人知道真實(shí)的用戶(hù)數(shù)據(jù),使用的ID通常不是連續(xù)的,這時(shí),32位的用戶(hù)ID就完全不夠用了。因此,該設(shè)計(jì)完全依賴(lài)于用戶(hù)ID,不是一種可取的設(shè)計(jì)方式。

          因此,會(huì)話(huà)ID的設(shè)計(jì)可以使用全局遞增的方式,加一個(gè)映射表,保存from_user_idto_user_idconversation_id的關(guān)系。

          推模式 vs 拉模式 vs 推拉結(jié)合模式

          在IM系統(tǒng)中,新消息的獲取通常會(huì)有三種可能的做法:

          • 推模式:有新消息時(shí)服務(wù)器主動(dòng)推給所有端(iOS、Android、PC等)
          • 拉模式:由前端主動(dòng)發(fā)起拉取消息的請(qǐng)求,為了保證消息的實(shí)時(shí)性,一般采用推模式,拉模式一般用于獲取歷史消息
          • 推拉結(jié)合模式:有新消息時(shí)服務(wù)器會(huì)先推一個(gè)有新消息的通知給前端,前端接收到通知后就向服務(wù)器拉取消息

          推模式簡(jiǎn)化圖如下:

          如上圖所示,正常情況下,用戶(hù)發(fā)的消息經(jīng)過(guò)服務(wù)器存儲(chǔ)等操作后會(huì)推給接收方的所有端。但推是有可能會(huì)丟失的,最常見(jiàn)的情況就是用戶(hù)可能會(huì)偽在線(xiàn)(是指如果推送服務(wù)基于長(zhǎng)連接,而長(zhǎng)連接可能已經(jīng)斷開(kāi),即用戶(hù)已經(jīng)掉線(xiàn),但一般需要經(jīng)過(guò)一個(gè)心跳周期后服務(wù)器才能感知到,這時(shí)服務(wù)器會(huì)錯(cuò)誤地以為用戶(hù)還在線(xiàn);偽在線(xiàn)是本人自己想的一個(gè)概念,沒(méi)想到合適的詞來(lái)解釋?zhuān)R虼巳绻麊渭兪褂猛颇J降脑?huà),是有可能會(huì)丟失消息的。

          推拉結(jié)合模式簡(jiǎn)化圖如下:

          可以使用推拉結(jié)合模式解決推模式可能會(huì)丟消息的問(wèn)題。在用戶(hù)發(fā)新消息時(shí)服務(wù)器推送一個(gè)通知,然后前端請(qǐng)求最新消息列表,為了防止有消息丟失,可以再每隔一段時(shí)間主動(dòng)請(qǐng)求一次。可以看出,使用推拉結(jié)合模式最好是用寫(xiě)擴(kuò)散,因?yàn)閷?xiě)擴(kuò)散只需要拉一條時(shí)間線(xiàn)的個(gè)人信箱就好了,而讀擴(kuò)散有N條時(shí)間線(xiàn)(每個(gè)信箱一條),如果也定時(shí)拉取的話(huà)性能會(huì)很差。

          業(yè)界解決方案

          前面了解了IM系統(tǒng)的常見(jiàn)設(shè)計(jì)問(wèn)題,接下來(lái)我們?cè)倏纯礃I(yè)界是怎么設(shè)計(jì)IM系統(tǒng)的。研究業(yè)界的主流方案有助于我們深入理解IM系統(tǒng)的設(shè)計(jì)。以下研究都是基于網(wǎng)上已經(jīng)公開(kāi)的資料,不一定正確,大家僅作參考就好了。

          微信

          雖然微信很多基礎(chǔ)框架都是自研,但這并不妨礙我們理解微信的架構(gòu)設(shè)計(jì)。從微信公開(kāi)的《[從0到1:微信后臺(tái)系統(tǒng)的演進(jìn)之路](#》這篇文章可以看出,微信采用的主要是:寫(xiě)擴(kuò)散 + 推拉結(jié)合。由于群聊使用的也是寫(xiě)擴(kuò)散,而寫(xiě)擴(kuò)散很消耗資源,因此微信群有人數(shù)上限(目前是500)。所以這也是寫(xiě)擴(kuò)散的一個(gè)明顯缺點(diǎn),如果需要萬(wàn)人群就比較難了。

          從文中還可以看出,微信采用了多數(shù)據(jù)中心架構(gòu):


          微信每個(gè)數(shù)據(jù)中心都是自治的,每個(gè)數(shù)據(jù)中心都有全量的數(shù)據(jù),數(shù)據(jù)中心間通過(guò)自研的消息隊(duì)列來(lái)同步數(shù)據(jù)。為了保證數(shù)據(jù)的一致性,每個(gè)用戶(hù)都只屬于一個(gè)數(shù)據(jù)中心,只能在自己所屬的數(shù)據(jù)中心進(jìn)行數(shù)據(jù)讀寫(xiě),如果用戶(hù)連了其它數(shù)據(jù)中心則會(huì)自動(dòng)引導(dǎo)用戶(hù)接入所屬的數(shù)據(jù)中心。而如果需要訪(fǎng)問(wèn)其它用戶(hù)的數(shù)據(jù)那只需要訪(fǎng)問(wèn)自己所屬的數(shù)據(jù)中心就可以了。同時(shí),微信使用了三園區(qū)容災(zāi)的架構(gòu),使用Paxos來(lái)保證數(shù)據(jù)的一致性。

          從微信公開(kāi)的《萬(wàn)億級(jí)調(diào)用系統(tǒng):微信序列號(hào)生成器架構(gòu)設(shè)計(jì)及演變》這篇文章可以看出,微信的ID設(shè)計(jì)采用的是:基于申請(qǐng)DB步長(zhǎng)的生成方式 + 用戶(hù)級(jí)別遞增。如下圖所示:

          微信的序列號(hào)生成器由仲裁服務(wù)生成路由表(路由表保存了uid號(hào)段到AllocSvr的全映射),路由表會(huì)同步到AllocSvr跟Client。如果AllocSvr宕機(jī)的話(huà)會(huì)由仲裁服務(wù)重新調(diào)度uid號(hào)段到其它AllocSvr。

          釘釘

          釘釘公開(kāi)的資料不多,從《阿里釘釘技術(shù)分享:企業(yè)級(jí)IM王者——釘釘在后端架構(gòu)上的過(guò)人之處》這篇文章我們只能知道,釘釘最開(kāi)始使用的是寫(xiě)擴(kuò)散模型,為了支持萬(wàn)人群,后來(lái)貌似優(yōu)化成了讀擴(kuò)散。

          但聊到阿里的IM系統(tǒng),不得不提的是阿里自研的Tablestore。一般情況下,IM系統(tǒng)都會(huì)有一個(gè)自增ID生成系統(tǒng),但Tablestore創(chuàng)造性地引入了主鍵列自增,即把ID的生成整合到了DB層,支持了用戶(hù)級(jí)別遞增(傳統(tǒng)MySQL等DB只能支持表級(jí)自增,即全局自增)。具體可以參考:《如何優(yōu)化高并發(fā)IM系統(tǒng)架構(gòu)》

          Twitter

          什么?Twitter不是Feeds系統(tǒng)嗎?這篇文章不是討論IM的嗎?是的,Twitter是Feeds系統(tǒng),但Feeds系統(tǒng)跟IM系統(tǒng)其實(shí)有很多設(shè)計(jì)上的共性,研究下Feeds系統(tǒng)有助于我們?cè)谠O(shè)計(jì)IM系統(tǒng)時(shí)進(jìn)行參考。再說(shuō)了,研究下Feeds系統(tǒng)也沒(méi)有壞處,擴(kuò)展下技術(shù)視野嘛。

          Twitter的自增ID設(shè)計(jì)估計(jì)大家都耳熟能詳了,即大名鼎鼎的Snowflake,因此ID是全局遞增的。

          從這個(gè)視頻分享《How We Learned to Stop Worrying and Love Fan-In at Twitter》可以看出,Twitter一開(kāi)始使用的是寫(xiě)擴(kuò)散模型,F(xiàn)anout Service負(fù)責(zé)擴(kuò)散寫(xiě)到Timelines Cache(使用了Redis),Timeline Service負(fù)責(zé)讀取Timeline數(shù)據(jù),然后由API Services返回給用戶(hù)。

          但由于寫(xiě)擴(kuò)散對(duì)于大V來(lái)說(shuō)寫(xiě)的消耗太大,因此后面Twitter又使用了寫(xiě)擴(kuò)散跟讀擴(kuò)散結(jié)合的方式。如下圖所示:

          對(duì)于粉絲數(shù)不多的用戶(hù)如果發(fā)Twitter使用的還是寫(xiě)擴(kuò)散模型,由Timeline Mixer服務(wù)將用戶(hù)的Timeline、大V的寫(xiě)Timeline跟系統(tǒng)推薦等內(nèi)容整合起來(lái),最后再由API Services返回給用戶(hù)

          IM需要解決的問(wèn)題

          如何保證消息的實(shí)時(shí)性

          在通信協(xié)議的選擇上,我們主要有以下幾個(gè)選擇:

          1. 使用TCP Socket通信,自己設(shè)計(jì)協(xié)議:58到家等等
          2. 使用UDP Socket通信:QQ等等
          3. 使用HTTP長(zhǎng)輪循:微信網(wǎng)頁(yè)版等等

          不管使用哪種方式,我們都能夠做到消息的實(shí)時(shí)通知。但影響我們消息實(shí)時(shí)性的可能會(huì)在我們處理消息的方式上。例如:假如我們推送的時(shí)候使用MQ去處理并推送一個(gè)萬(wàn)人群的消息,推送一個(gè)人需要2ms,那么推完一萬(wàn)人需要20s,那么后面的消息就阻塞了20s。如果我們需要在10ms內(nèi)推完,那么我們推送的并發(fā)度應(yīng)該是:人數(shù):10000 / (推送總時(shí)長(zhǎng):10 / 單個(gè)人推送時(shí)長(zhǎng):2) = 2000

          因此,我們?cè)谶x擇具體的實(shí)現(xiàn)方案的時(shí)候一定要評(píng)估好我們系統(tǒng)的吞吐量,系統(tǒng)的每一個(gè)環(huán)節(jié)都要進(jìn)行評(píng)估壓測(cè)。只有把每一個(gè)環(huán)節(jié)的吞吐量評(píng)估好了,才能保證消息推送的實(shí)時(shí)性。

          如何保證消息時(shí)序

          以下情況下消息可能會(huì)亂序:

          • 發(fā)送消息如果使用的不是長(zhǎng)連接,而是使用HTTP的話(huà)可能會(huì)出現(xiàn)亂序。因?yàn)楹蠖艘话闶羌翰渴穑褂肏TTP的話(huà)請(qǐng)求可能會(huì)打到不同的服務(wù)器,由于網(wǎng)絡(luò)延遲或者服務(wù)器處理速度的不同,后發(fā)的消息可能會(huì)先完成,此時(shí)就產(chǎn)生了消息亂序。解決方案:
          • 前端依次對(duì)消息進(jìn)行處理,發(fā)送完一個(gè)消息再發(fā)送下一個(gè)消息。這種方式會(huì)降低用戶(hù)體驗(yàn),一般情況下不建議使用。
          • 帶上一個(gè)前端生成的順序ID,讓接收方根據(jù)該ID進(jìn)行排序。這種方式前端處理會(huì)比較麻煩一點(diǎn),而且聊天的過(guò)程中接收方的歷史消息列表中可能會(huì)在中間插入一條消息,這樣會(huì)很奇怪,而且用戶(hù)可能會(huì)漏讀消息。但這種情況可以通過(guò)在用戶(hù)切換窗口的時(shí)候再進(jìn)行重排來(lái)解決,接收方每次收到消息都先往最后面追加。
          • 通常為了優(yōu)化體驗(yàn),有的IM系統(tǒng)可能會(huì)采取異步發(fā)送確認(rèn)機(jī)制(例如:QQ)。即消息只要到達(dá)服務(wù)器,然后服務(wù)器發(fā)送到MQ就算發(fā)送成功。如果由于權(quán)限等問(wèn)題發(fā)送失敗的話(huà)后端再推一個(gè)通知下去。這種情況下MQ就要選擇合適的Sharding策略了:
          • to_user_id進(jìn)行Sharding:使用該策略如果需要做多端同步的話(huà)發(fā)送方多個(gè)端進(jìn)行同步可能會(huì)亂序,因?yàn)椴煌?duì)列的處理速度可能會(huì)不一樣。例如發(fā)送方先發(fā)送m1然后發(fā)送m2,但服務(wù)器可能會(huì)先處理完m2再處理m1,這里其它端會(huì)先收到m2然后是m1,此時(shí)其它端的會(huì)話(huà)列表就亂了。
          • conversation_id進(jìn)行Sharding:使用該策略同樣會(huì)導(dǎo)致多端同步會(huì)亂序。
          • from_user_id進(jìn)行Sharding:這種情況下使用該策略是比較好的選擇
          • 通常為了優(yōu)化性能,推送前可能會(huì)先往MQ推,這種情況下使用to_user_id才是比較好的選擇。
          用戶(hù)在線(xiàn)狀態(tài)如何做

          很多IM系統(tǒng)都需要展示用戶(hù)的狀態(tài):是否在線(xiàn),是否忙碌等。主要可以使用Redis或者分布式一致性哈希來(lái)實(shí)現(xiàn)用戶(hù)在線(xiàn)狀態(tài)的存儲(chǔ)。

          1. Redis存儲(chǔ)用戶(hù)在線(xiàn)狀態(tài)

          看上面的圖可能會(huì)有人疑惑,為什么每次心跳都需要更新Redis?如果我使用的是TCP長(zhǎng)連接那是不是就不用每次心跳都更新了?確實(shí),正常情況下服務(wù)器只需要在新建連接或者斷開(kāi)連接的時(shí)候更新一下Redis就好了。但由于服務(wù)器可能會(huì)出現(xiàn)異常,或者服務(wù)器跟Redis之間的網(wǎng)絡(luò)會(huì)出現(xiàn)問(wèn)題,此時(shí)基于事件的更新就會(huì)出現(xiàn)問(wèn)題,導(dǎo)致用戶(hù)狀態(tài)不正確。因此,如果需要用戶(hù)在線(xiàn)狀態(tài)準(zhǔn)確的話(huà)最好通過(guò)心跳來(lái)更新在線(xiàn)狀態(tài)。

          由于Redis是單機(jī)存儲(chǔ)的,因此,為了提高可靠性跟性能,我們可以使用Redis Cluster或者Codis。


          1. 分布式一致性哈希存儲(chǔ)用戶(hù)在線(xiàn)狀態(tài)

          使用分布式一致性哈希需要注意在對(duì)Status Server Cluster進(jìn)行擴(kuò)容或者縮容的時(shí)候要先對(duì)用戶(hù)狀態(tài)進(jìn)行遷移,不然在剛操作時(shí)會(huì)出現(xiàn)用戶(hù)狀態(tài)不一致的情況。同時(shí)還需要使用虛擬節(jié)點(diǎn)避免數(shù)據(jù)傾斜的問(wèn)題。

          多端同步怎么做

          讀擴(kuò)散

          前面也提到過(guò),對(duì)于讀擴(kuò)散,消息的同步主要是以推模式為主,單個(gè)會(huì)話(huà)的消息ID順序遞增,前端收到推的消息如果發(fā)現(xiàn)消息ID不連續(xù)就請(qǐng)求后端重新獲取消息。但這樣仍然可能丟失會(huì)話(huà)的最后一條消息,為了加大消息的可靠性,可以在歷史會(huì)話(huà)列表的會(huì)話(huà)里再帶上最后一條消息的ID,前端在收到新消息的時(shí)候會(huì)先拉取最新的會(huì)話(huà)列表,然后判斷會(huì)話(huà)的最后一條消息是否存在,如果不存在,消息就可能丟失了,前端需要再拉一次會(huì)話(huà)的消息列表;如果會(huì)話(huà)的最后一條消息ID跟消息列表里的最后一條消息ID一樣,前端就不再處理。這種做法的性能瓶頸會(huì)在拉取歷史會(huì)話(huà)列表那里,因?yàn)槊看涡孪⒍夹枰『蠖艘淮危绻次⑿诺牧考?jí)來(lái)看,單是消息就可能會(huì)有20萬(wàn)的QPS,如果歷史會(huì)話(huà)列表放到MySQL等傳統(tǒng)DB的話(huà)肯定抗不住。因此,最好將歷史會(huì)話(huà)列表存到開(kāi)了AOF(用RDB的話(huà)可能會(huì)丟數(shù)據(jù))的Redis集群。這里只能感慨性能跟簡(jiǎn)單性不能兼得。

          寫(xiě)擴(kuò)散

          對(duì)于寫(xiě)擴(kuò)散來(lái)說(shuō),多端同步就簡(jiǎn)單些了。前端只需要記錄最后同步的位點(diǎn),同步的時(shí)候帶上同步位點(diǎn),然后服務(wù)器就將該位點(diǎn)后面的數(shù)據(jù)全部返回給前端,前端更新同步位點(diǎn)就可以了。

          如何處理未讀數(shù)

          在IM系統(tǒng)中,未讀數(shù)的處理非常重要。未讀數(shù)一般分為會(huì)話(huà)未讀數(shù)跟總未讀數(shù),如果處理不當(dāng),會(huì)話(huà)未讀數(shù)跟總未讀數(shù)可能會(huì)不一致,嚴(yán)重降低用戶(hù)體驗(yàn)。

          讀擴(kuò)散

          對(duì)于讀擴(kuò)散來(lái)說(shuō),我們可以將會(huì)話(huà)未讀數(shù)跟總未讀數(shù)都存在后端,但后端需要保證兩個(gè)未讀數(shù)更新的原子性跟一致性,一般可以通過(guò)以下兩種方法來(lái)實(shí)現(xiàn):

          1. 使用Redis的multi事務(wù)功能,事務(wù)更新失敗可以重試。但要注意如果你使用Codis集群的話(huà)并不支持事務(wù)功能。
          2. 使用Lua嵌入腳本的方式。使用這種方式需要保證會(huì)話(huà)未讀數(shù)跟總未讀數(shù)都在同一個(gè)Redis節(jié)點(diǎn)(Codis的話(huà)可以使用Hashtag)。這種方式會(huì)導(dǎo)致實(shí)現(xiàn)邏輯分散,加大維護(hù)成本。


          寫(xiě)擴(kuò)散

          對(duì)于寫(xiě)擴(kuò)散來(lái)說(shuō),服務(wù)端通常會(huì)弱化會(huì)話(huà)的概念,即服務(wù)端不存儲(chǔ)歷史會(huì)話(huà)列表。未讀數(shù)的計(jì)算可由前端來(lái)負(fù)責(zé),標(biāo)記已讀跟標(biāo)記未讀可以只記錄一個(gè)事件到信箱里,各個(gè)端通過(guò)重放該事件的形式來(lái)處理會(huì)話(huà)未讀數(shù)。使用這種方式可能會(huì)造成各個(gè)端的未讀數(shù)不一致,至少微信就會(huì)有這個(gè)問(wèn)題。

          如果寫(xiě)擴(kuò)散也通過(guò)歷史會(huì)話(huà)列表來(lái)存儲(chǔ)未讀數(shù)的話(huà)那用戶(hù)時(shí)間線(xiàn)服務(wù)跟會(huì)話(huà)服務(wù)緊耦合,這個(gè)時(shí)候需要保證原子性跟一致性的話(huà)那就只能使用分布式事務(wù)了,會(huì)大大降低系統(tǒng)的性能。

          如何存儲(chǔ)歷史消息

          讀擴(kuò)散

          對(duì)于讀擴(kuò)散,只需要按會(huì)話(huà)ID進(jìn)行Sharding存儲(chǔ)一份就可以了。


          寫(xiě)擴(kuò)散

          對(duì)于寫(xiě)擴(kuò)散,需要存儲(chǔ)兩份:一份是以用戶(hù)為T(mén)imeline的消息列表,一份是以會(huì)話(huà)為T(mén)imeline的消息列表。以用戶(hù)為T(mén)imeline的消息列表可以用用戶(hù)ID來(lái)做Sharding,以會(huì)話(huà)為T(mén)imeline的消息列表可以用會(huì)話(huà)ID來(lái)做Sharding。

          數(shù)據(jù)冷熱分離

          對(duì)于IM來(lái)說(shuō),歷史消息的存儲(chǔ)有很強(qiáng)的時(shí)間序列特性,時(shí)間越久,消息被訪(fǎng)問(wèn)的概率也越低,價(jià)值也越低。

          如果我們需要存儲(chǔ)幾年甚至是永久的歷史消息的話(huà)(電商IM中比較常見(jiàn)),那么做歷史消息的冷熱分離就非常有必要了。數(shù)據(jù)的冷熱分離一般是HWC(Hot-Warm-Cold)架構(gòu)。對(duì)于剛發(fā)送的消息可以放到Hot存儲(chǔ)系統(tǒng)(可以用Redis)跟Warm存儲(chǔ)系統(tǒng),然后由Store Scheduler根據(jù)一定的規(guī)則定時(shí)將冷數(shù)據(jù)遷移到Cold存儲(chǔ)系統(tǒng)。獲取消息的時(shí)候需要依次訪(fǎng)問(wèn)Hot、Warm跟Cold存儲(chǔ)系統(tǒng),由Store Service整合數(shù)據(jù)返回給IM Service。

          接入層怎么做

          實(shí)現(xiàn)接入層的負(fù)載均衡主要有以下幾個(gè)方法:

          1. 硬件負(fù)載均衡:例如F5、A10等等。硬件負(fù)載均衡性能強(qiáng)大,穩(wěn)定性高,但價(jià)格非常貴,不是土豪公司不建議使用。
          2. 使用DNS實(shí)現(xiàn)負(fù)載均衡:使用DNS實(shí)現(xiàn)負(fù)載均衡比較簡(jiǎn)單,但使用DNS實(shí)現(xiàn)負(fù)載均衡如果需要切換或者擴(kuò)容那生效會(huì)很慢,而且使用DNS實(shí)現(xiàn)負(fù)載均衡支持的IP個(gè)數(shù)有限制、支持的負(fù)載均衡策略也比較簡(jiǎn)單。
          3. DNS + 4層負(fù)載均衡 + 7層負(fù)載均衡架構(gòu):例如 DNS + DPVS + Nginx 或者 DNS + LVS + Nginx。有人可能會(huì)疑惑為什么要加入4層負(fù)載均衡呢?這是因?yàn)?層負(fù)載均衡很耗CPU,并且經(jīng)常需要擴(kuò)容或者縮容,對(duì)于大型網(wǎng)站來(lái)說(shuō)可能需要很多7層負(fù)載均衡服務(wù)器,但只需要少量的4層負(fù)載均衡服務(wù)器即可。因此,該架構(gòu)對(duì)于HTTP等短連接大型應(yīng)用很有用。當(dāng)然,如果流量不大的話(huà)只使用DNS + 7層負(fù)載均衡即可。但對(duì)于長(zhǎng)連接來(lái)說(shuō),加入7層負(fù)載均衡Nginx就不大好了。因?yàn)镹ginx經(jīng)常需要改配置并且reload配置,reload的時(shí)候TCP連接會(huì)斷開(kāi),造成大量掉線(xiàn)。
          4. DNS + 4層負(fù)載均衡:4層負(fù)載均衡一般比較穩(wěn)定,很少改動(dòng),比較適合于長(zhǎng)連接。

          對(duì)于長(zhǎng)連接的接入層,如果我們需要更加靈活的負(fù)載均衡策略或者需要做灰度的話(huà),那我們可以引入一個(gè)調(diào)度服務(wù),如下圖所示:

          Access Schedule Service可以實(shí)現(xiàn)根據(jù)各種策略來(lái)分配Access Service,例如:

          • 根據(jù)灰度策略來(lái)分配
          • 根據(jù)就近原則來(lái)分配
          • 根據(jù)最少連接數(shù)來(lái)分配

          架構(gòu)心得

          最后,分享一下做大型應(yīng)用的架構(gòu)心得:

          1. 灰度!灰度!灰度!
          2. 監(jiān)控!監(jiān)控!監(jiān)控!
          3. 告警!告警!告警!
          4. 緩存!緩存!緩存!
          5. 限流!熔斷!降級(jí)!
          6. 低耦合,高內(nèi)聚!
          7. 避免單點(diǎn),擁抱無(wú)狀態(tài)!
          8. 評(píng)估!評(píng)估!評(píng)估!
          9. 壓測(cè)!壓測(cè)!壓測(cè)!


          Redis 大數(shù)據(jù)量(百億級(jí))Key存儲(chǔ)需求及解決方案


          程序員過(guò)關(guān)斬將--錯(cuò)誤的IOC和DI



          瀏覽 76
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩性爱第一页 | 一级免费黄色电影 | 天天干天天操天天干天天操天天干 | 国产强奸视频 | 青青草大香蕉伊人 |