本文不會給出一套通用的 IM 方案酸役,也不會評判某種架構(gòu)的好壞袜茧,而是討論設(shè)計 IM 系統(tǒng)的常見難題跟業(yè)界的解決方案柜与。因為也沒有所謂的通用方案呼胚,不同的解決方案都有其優(yōu)缺點端壳,只有最滿足業(yè)務(wù)的系統(tǒng)才是一個好的系統(tǒng)羽嫡。而且谨湘,在有限的人力稠曼、物力跟時間資源下,通常需要做出很多權(quán)衡磷脯,此時蛾找,一個能夠快速迭代、方便擴(kuò)展的系統(tǒng)才是一個好的系統(tǒng)赵誓。
用戶:系統(tǒng)的使用者
消息:是指用戶之間的溝通內(nèi)容打毛。通常在 IM 系統(tǒng)中,消息會有以下幾類:文本消息俩功、表情消息幻枉、圖片消息、視頻消息诡蜓、文件消息等等
會話:通常指兩個用戶之間因聊天而建立起的關(guān)聯(lián)
群:通常指多個用戶之間因聊天而建立起的關(guān)聯(lián)
終端:指用戶使用 IM 系統(tǒng)的機(jī)器熬甫。通常有 Android 端、iOS 端蔓罚、Web 端等等
未讀數(shù):指用戶還沒讀的消息數(shù)量
用戶狀態(tài):指用戶當(dāng)前是在線椿肩、離線還是掛起等狀態(tài)
關(guān)系鏈:是指用戶與用戶之間的關(guān)系,通常有單向的好友關(guān)系脚粟、雙向的好友關(guān)系覆旱、關(guān)注關(guān)系等等。這里需要注意與會話的區(qū)別核无,用戶只有在發(fā)起聊天時才產(chǎn)生會話扣唱,但關(guān)系并不需要聊天才能建立。對于關(guān)系鏈的存儲团南,可以使用圖數(shù)據(jù)庫(Neo4j 等等)噪沙,可以很自然地表達(dá)現(xiàn)實世界中的關(guān)系,易于建模
單聊:一對一聊天
群聊:多人聊天
客服:在電商領(lǐng)域吐根,通常需要對用戶提供售前咨詢正歼、售后咨詢等服務(wù)。這時拷橘,就需要引入客服來處理用戶的咨詢
消息分流:在電商領(lǐng)域局义,一個店鋪通常會有多個客服喜爷,此時決定用戶的咨詢由哪個客服來處理就是消息分流。通常消息分流會根據(jù)一系列規(guī)則來確定消息會分流給哪個客服萄唇,例如客服是否在線(客服不在線的話需要重新分流給另一個客服)檩帐、該消息是售前咨詢還是售后咨詢、當(dāng)前客服的繁忙程度等等
信箱:本文的信箱我們指一個 Timeline另萤、一個收發(fā)消息的隊列
讀擴(kuò)散
我們先來看看讀擴(kuò)散湃密。如上圖所示,A 與每個聊天的人跟群都有一個信箱(有些博文會叫 Timeline)四敞,A 在查看聊天信息的時候需要讀取所有有新消息的信箱泛源。這里的讀擴(kuò)散需要注意與 Feeds 系統(tǒng)的區(qū)別,在 Feeds 系統(tǒng)中忿危,每個人都有一個寫信箱达箍,寫只需要往自己的寫信箱里寫一次就好了,讀需要從所有關(guān)注的人的寫信箱里讀癌蚁。但 IM 系統(tǒng)里的讀擴(kuò)散通常是每兩個相關(guān)聯(lián)的人就有一個信箱幻梯,或者每個群一個信箱。
讀擴(kuò)散的優(yōu)點:
寫操作(發(fā)消息)很輕量努释,不管是單聊還是群聊,只需要往相應(yīng)的信箱寫一次就好了
每一個信箱天然就是兩個人的聊天記錄咬摇,可以方便查看聊天記錄跟進(jìn)行聊天記錄的搜索
讀擴(kuò)散的缺點:
- 讀操作(讀消息)很重
寫擴(kuò)散
接下來看看寫擴(kuò)散伐蒂。
在寫擴(kuò)散中,每個人都只從自己的信箱里讀取消息肛鹏,但寫(發(fā)消息)的時候逸邦,對于單聊跟群聊處理如下:
單聊:往自己的信箱跟對方的信箱都寫一份消息,同時在扰,如果需要查看兩個人的聊天歷史記錄的話還需要再寫一份(當(dāng)然缕减,如果從個人信箱也能回溯出兩個人的所有聊天記錄,但這樣效率會很低)芒珠。
群聊:需要往所有的群成員的信箱都寫一份消息桥狡,同時,如果需要查看群的聊天歷史記錄的話還需要再寫一份皱卓」ィ可以看出,寫擴(kuò)散對于群聊來說大大地放大了寫操作娜汁。
寫擴(kuò)散優(yōu)點:
讀操作很輕量
可以很方便地做消息的多終端同步
寫擴(kuò)散缺點:
- 寫操作很重嫂易,尤其是對于群聊來說
注意,在 Feeds 系統(tǒng)中:
寫擴(kuò)散也叫:Push掐禁、Fan-out 或者 Write-fanout
讀擴(kuò)散也叫:Pull怜械、Fan-in 或者 Read-fanout
通常情況下颅和,ID 的設(shè)計主要有以下幾大類:
UUID
基于 Snowflake 的 ID 生成方式
基于申請 DB 步長的生成方式
基于 Redis 或者 DB 的自增 ID 生成方式
特殊的規(guī)則生成唯一 ID
具體的實現(xiàn)方法跟優(yōu)缺點可以參考之前的一篇博文:分布式唯一 ID 解析
在 IM 系統(tǒng)中需要唯一 Id 的地方主要是:
會話 ID
消息 ID
消息 ID
我們來看看在設(shè)計消息 ID 時需要考慮的三個問題。
消息 ID 不遞增可以嗎
我們先看看不遞增的話會怎樣:
使用字符串缕允,浪費(fèi)存儲空間峡扩,而且不能利用存儲引擎的特性讓相鄰的消息存儲在一起,降低消息的寫入跟讀取性能
使用數(shù)字灼芭,但數(shù)字隨機(jī)有额,也不能利用存儲引擎的特性讓相鄰的消息存儲在一起,會加大隨機(jī) IO彼绷,降低性能巍佑;而且隨機(jī)的 ID 不好保證 ID 的唯一性
因此,消息 ID 最好是遞增的寄悯。
全局遞增 vs 用戶級別遞增 vs 會話級別遞增
全局遞增:指消息 ID 在整個 IM 系統(tǒng)隨著時間的推移是遞增的萤衰。全局遞增的話一般可以使用 Snowflake(當(dāng)然,Snowflake 也只是 worker 級別的遞增)猜旬。此時脆栋,如果你的系統(tǒng)是讀擴(kuò)散的話為了防止消息丟失,那每一條消息就只能帶上上一條消息的 ID洒擦,前端根據(jù)上一條消息判斷是否有丟失消息椿争,有消息丟失的話需要重新拉一次。
用戶級別遞增:指消息 ID 只保證在單個用戶中是遞增的熟嫩,不同用戶之間不影響并且可能重復(fù)秦踪。典型代表:微信。如果是寫擴(kuò)散系統(tǒng)的話信箱時間線 ID 跟消息 ID 需要分開設(shè)計掸茅,信箱時間線 ID 用戶級別遞增椅邓,消息 ID 全局遞增。如果是讀擴(kuò)散系統(tǒng)的話感覺使用用戶級別遞增必要性不是很大昧狮。
會話級別遞增:指消息 ID 只保證在單個會話中是遞增的景馁,不同會話之間不影響并且可能重復(fù)。典型代表:QQ逗鸣。
連續(xù)遞增 vs 單調(diào)遞增
連續(xù)遞增是指 ID 按 1,2,3...n 的方式生成合住;而單調(diào)遞增是指只要保證后面生成的 ID 比前面生成的 ID 大就可以了,不需要連續(xù)慕购。
據(jù)我所知聊疲,QQ 的消息 ID 就是在會話級別使用的連續(xù)遞增,這樣的好處是沪悲,如果丟失了消息影所,當(dāng)下一條消息來的時候發(fā)現(xiàn) ID 不連續(xù)就會去請求服務(wù)器西壮,避免丟失消息。此時移斩,可能有人會想,我不能用定時拉的方式看有沒有消息丟失嗎?當(dāng)然不能,因為消息 ID 只在會話級別連續(xù)遞增的話那如果一個人有上千個會話,那得拉多少次啊爱致,服務(wù)器肯定是抗不住的。
對于讀擴(kuò)散來說寒随,消息 ID 使用連續(xù)遞增就是一種不錯的方式了糠悯。如果使用單調(diào)遞增的話當(dāng)前消息需要帶上前一條消息的 ID(即聊天消息組成一個鏈表),這樣妻往,才能判斷消息是否丟失互艾。
總結(jié)一下就是:
寫擴(kuò)散:信箱時間線 ID 使用用戶級別遞增,消息 ID 全局遞增讯泣,此時只要保證單調(diào)遞增就可以了
讀擴(kuò)散:消息 ID 可以使用會話級別遞增并且最好是連續(xù)遞增
會話 ID
我們來看看設(shè)計會話 ID 需要注意的問題:
其中纫普,會話 ID 有種比較簡單的生成方式(特殊的規(guī)則生成唯一 ID):拼接 from_user_id
跟 to_user_id
:
如果
from_user_id
跟to_user_id
都是 32 位整形數(shù)據(jù)的話可以很方便地用位運(yùn)算拼接成一個 64 位的會話 ID,即:conversation_id = ${from_user_id} << 32 | ${to_user_id}
(在拼接前需要確保值比較小的用戶 ID 是from_user_id
好渠,這樣任意兩個用戶發(fā)起會話可以很方便地知道會話 ID)如果
from_user_id
跟to_user_id
都是 64 位整形數(shù)據(jù)的話那就只能拼接成一個字符串了昨稼,拼接成字符串的話就比較傷了,浪費(fèi)存儲空間性能又不好拳锚。
前東家就是使用的上面第 1 種方式假栓,第 1 種方式有個硬傷:隨著業(yè)務(wù)在全球的擴(kuò)展,32 位的用戶 ID 如果不夠用需要擴(kuò)展到 64 位的話那就需要大刀闊斧地改了霍掺。32 位整形 ID 看起來能夠容納 21 億個用戶但指,但通常我們?yōu)榱朔乐箘e人知道真實的用戶數(shù)據(jù),使用的 ID 通常不是連續(xù)的抗楔,這時,32 位的用戶 ID 就完全不夠用了拦坠。因此连躏,該設(shè)計完全依賴于用戶 ID,不是一種可取的設(shè)計方式贞滨。
因此入热,會話 ID 的設(shè)計可以使用全局遞增的方式,加一個映射表晓铆,保存from_user_id
勺良、to_user_id
跟conversation_id
的關(guān)系。
在 IM 系統(tǒng)中骄噪,新消息的獲取通常會有三種可能的做法:
推模式:有新消息時服務(wù)器主動推給所有端(iOS尚困、Android、PC 等)
拉模式:由前端主動發(fā)起拉取消息的請求链蕊,為了保證消息的實時性事甜,一般采用推模式谬泌,拉模式一般用于獲取歷史消息
推拉結(jié)合模式:有新消息時服務(wù)器會先推一個有新消息的通知給前端,前端接收到通知后就向服務(wù)器拉取消息
推模式簡化圖如下:
如上圖所示逻谦,正常情況下掌实,用戶發(fā)的消息經(jīng)過服務(wù)器存儲等操作后會推給接收方的所有端。但推是有可能會丟失的邦马,最常見的情況就是用戶可能會偽在線(是指如果推送服務(wù)基于長連接贱鼻,而長連接可能已經(jīng)斷開,即用戶已經(jīng)掉線滋将,但一般需要經(jīng)過一個心跳周期后服務(wù)器才能感知到邻悬,這時服務(wù)器會錯誤地以為用戶還在線;偽在線是本人自己想的一個概念耕渴,沒想到合適的詞來解釋)拘悦。因此如果單純使用推模式的話,是有可能會丟失消息的橱脸。
推拉結(jié)合模式簡化圖如下:
可以使用推拉結(jié)合模式解決推模式可能會丟消息的問題础米。在用戶發(fā)新消息時服務(wù)器推送一個通知,然后前端請求最新消息列表添诉,為了防止有消息丟失屁桑,可以再每隔一段時間主動請求一次±父埃可以看出蘑斧,使用推拉結(jié)合模式最好是用寫擴(kuò)散,因為寫擴(kuò)散只需要拉一條時間線的個人信箱就好了须眷,而讀擴(kuò)散有 N 條時間線(每個信箱一條)竖瘾,如果也定時拉取的話性能會很差。
前面了解了 IM 系統(tǒng)的常見設(shè)計問題花颗,接下來我們再看看業(yè)界是怎么設(shè)計 IM 系統(tǒng)的捕传。研究業(yè)界的主流方案有助于我們深入理解 IM 系統(tǒng)的設(shè)計。以下研究都是基于網(wǎng)上已經(jīng)公開的資料扩劝,不一定正確庸论,大家僅作參考就好了。
微信
雖然微信很多基礎(chǔ)框架都是自研棒呛,但這并不妨礙我們理解微信的架構(gòu)設(shè)計聂示。從微信公開的《從0到1:微信后臺系統(tǒng)的演進(jìn)之路》這篇文章可以看出,微信采用的主要是:寫擴(kuò)散 + 推拉結(jié)合簇秒。由于群聊使用的也是寫擴(kuò)散鱼喉,而寫擴(kuò)散很消耗資源,因此微信群有人數(shù)上限(目前是 500)。所以這也是寫擴(kuò)散的一個明顯缺點蒲凶,如果需要萬人群就比較難了气筋。
從文中還可以看出,微信采用了多數(shù)據(jù)中心架構(gòu):
微信每個數(shù)據(jù)中心都是自治的旋圆,每個數(shù)據(jù)中心都有全量的數(shù)據(jù)宠默,數(shù)據(jù)中心間通過自研的消息隊列來同步數(shù)據(jù)。為了保證數(shù)據(jù)的一致性灵巧,每個用戶都只屬于一個數(shù)據(jù)中心搀矫,只能在自己所屬的數(shù)據(jù)中心進(jìn)行數(shù)據(jù)讀寫,如果用戶連了其它數(shù)據(jù)中心則會自動引導(dǎo)用戶接入所屬的數(shù)據(jù)中心刻肄。而如果需要訪問其它用戶的數(shù)據(jù)那只需要訪問自己所屬的數(shù)據(jù)中心就可以了瓤球。同時,微信使用了三園區(qū)容災(zāi)的架構(gòu)敏弃,使用 Paxos 來保證數(shù)據(jù)的一致性卦羡。
從微信公開的《萬億級調(diào)用系統(tǒng):微信序列號生成器架構(gòu)設(shè)計及演變》這篇文章可以看出,微信的 ID 設(shè)計采用的是:基于申請 DB 步長的生成方式 + 用戶級別遞增麦到。如下圖所示:
微信的序列號生成器由仲裁服務(wù)生成路由表(路由表保存了 uid 號段到 AllocSvr 的全映射)绿饵,路由表會同步到 AllocSvr 跟 Client。如果 AllocSvr 宕機(jī)的話會由仲裁服務(wù)重新調(diào)度 uid 號段到其它 AllocSvr瓶颠。
釘釘
釘釘公開的資料不多拟赊,從《阿里釘釘技術(shù)分享:企業(yè)級IM王者——釘釘在后端架構(gòu)上的過人之處》這篇文章我們只能知道,釘釘最開始使用的是寫擴(kuò)散模型粹淋,為了支持萬人群吸祟,后來貌似優(yōu)化成了讀擴(kuò)散。
但聊到阿里的 IM 系統(tǒng)桃移,不得不提的是阿里自研的 Tablestore屋匕。一般情況下,IM 系統(tǒng)都會有一個自增 ID 生成系統(tǒng)借杰,但 Tablestore 創(chuàng)造性地引入了主鍵列自增炒瘟,即把 ID 的生成整合到了 DB 層,支持了用戶級別遞增(傳統(tǒng) MySQL 等 DB 只能支持表級自增第步,即全局自增)。具體可以參考:《如何優(yōu)化高并發(fā)IM系統(tǒng)架構(gòu)》
什么缘琅?Twitter 不是 Feeds 系統(tǒng)嗎粘都?這篇文章不是討論 IM 的嗎?是的刷袍,Twitter 是 Feeds 系統(tǒng)翩隧,但 Feeds 系統(tǒng)跟 IM 系統(tǒng)其實有很多設(shè)計上的共性,研究下 Feeds 系統(tǒng)有助于我們在設(shè)計 IM 系統(tǒng)時進(jìn)行參考呻纹。再說了堆生,研究下 Feeds 系統(tǒng)也沒有壞處专缠,擴(kuò)展下技術(shù)視野嘛。
Twitter 的自增 ID 設(shè)計估計大家都耳熟能詳了淑仆,即大名鼎鼎的Snowflake涝婉,因此 ID 是全局遞增的。
從這個視頻分享《How We Learned to Stop Worrying and Love Fan-In at Twitter》可以看出蔗怠,Twitter 一開始使用的是寫擴(kuò)散模型墩弯,F(xiàn)anout Service 負(fù)責(zé)擴(kuò)散寫到 Timelines Cache(使用了 Redis),Timeline Service 負(fù)責(zé)讀取 Timeline 數(shù)據(jù)寞射,然后由 API Services 返回給用戶渔工。
但由于寫擴(kuò)散對于大 V 來說寫的消耗太大,因此后面 Twitter 又使用了寫擴(kuò)散跟讀擴(kuò)散結(jié)合的方式桥温。如下圖所示:
對于粉絲數(shù)不多的用戶如果發(fā) Twitter 使用的還是寫擴(kuò)散模型引矩,由 Timeline Mixer 服務(wù)將用戶的 Timeline、大 V 的寫 Timeline 跟系統(tǒng)推薦等內(nèi)容整合起來侵浸,最后再由 API Services 返回給用戶旺韭。
58 到家
58 到家實現(xiàn)了一個通用的實時消息平臺:
可以看出,msg-server 保存了應(yīng)用跟 MQ 主題之間的對應(yīng)關(guān)系通惫,msg-server 根據(jù)這個配置將消息推到不同的 MQ 隊列茂翔,具體的應(yīng)用來消費(fèi)就可以了。因此履腋,新增一個應(yīng)用只需要修改配置就可以了珊燎。
58 到家為了保證消息投遞的可靠性,還引入了確認(rèn)機(jī)制:消息平臺收到消息先落地數(shù)據(jù)庫遵湖,接收方收到后應(yīng)用層 ACK 再刪除悔政。使用確認(rèn)機(jī)制最好是只能單點登錄,如果多端能夠同時登錄的話那就比較麻煩了延旧,因為需要所有端都確認(rèn)收到消息后才能刪除谋国。
看到這里,估計大家已經(jīng)明白了迁沫,設(shè)計一個 IM 系統(tǒng)很有挑戰(zhàn)性芦瘾。我們還是繼續(xù)來看設(shè)計一個 IM 系統(tǒng)需要考慮的問題吧。
如何保證消息的實時性
在通信協(xié)議的選擇上集畅,我們主要有以下幾個選擇:
使用 TCP Socket 通信近弟,自己設(shè)計協(xié)議:58 到家等等
使用 UDP Socket 通信:QQ 等等
使用 HTTP 長輪循:微信網(wǎng)頁版等等
不管使用哪種方式,我們都能夠做到消息的實時通知挺智。但影響我們消息實時性的可能會在我們處理消息的方式上祷愉。例如:假如我們推送的時候使用 MQ 去處理并推送一個萬人群的消息,推送一個人需要 2ms,那么推完一萬人需要 20s二鳄,那么后面的消息就阻塞了 20s赴涵。如果我們需要在 10ms 內(nèi)推完,那么我們推送的并發(fā)度應(yīng)該是:人數(shù):10000 / (推送總時長:10 / 單個人推送時長:2) = 2000
因此订讼,我們在選擇具體的實現(xiàn)方案的時候一定要評估好我們系統(tǒng)的吞吐量髓窜,系統(tǒng)的每一個環(huán)節(jié)都要進(jìn)行評估壓測。只有把每一個環(huán)節(jié)的吞吐量評估好了躯嫉,才能保證消息推送的實時性纱烘。
如何保證消息時序
以下情況下消息可能會亂序:
發(fā)送消息如果使用的不是長連接,而是使用 HTTP 的話可能會出現(xiàn)亂序祈餐。因為后端一般是集群部署擂啥,使用 HTTP 的話請求可能會打到不同的服務(wù)器,由于網(wǎng)絡(luò)延遲或者服務(wù)器處理速度的不同帆阳,后發(fā)的消息可能會先完成哺壶,此時就產(chǎn)生了消息亂序。解決方案:
前端依次對消息進(jìn)行處理蜒谤,發(fā)送完一個消息再發(fā)送下一個消息山宾。這種方式會降低用戶體驗,一般情況下不建議使用鳍徽。
帶上一個前端生成的順序 ID资锰,讓接收方根據(jù)該 ID 進(jìn)行排序。這種方式前端處理會比較麻煩一點阶祭,而且聊天的過程中接收方的歷史消息列表中可能會在中間插入一條消息绷杜,這樣會很奇怪,而且用戶可能會漏讀消息濒募。但這種情況可以通過在用戶切換窗口的時候再進(jìn)行重排來解決鞭盟,接收方每次收到消息都先往最后面追加。
通常為了優(yōu)化體驗瑰剃,有的 IM 系統(tǒng)可能會采取異步發(fā)送確認(rèn)機(jī)制(例如:QQ)齿诉。即消息只要到達(dá)服務(wù)器,然后服務(wù)器發(fā)送到 MQ 就算發(fā)送成功晌姚。如果由于權(quán)限等問題發(fā)送失敗的話后端再推一個通知下去粤剧。這種情況下 MQ 就要選擇合適的 Sharding 策略了:
按
to_user_id
進(jìn)行 Sharding:使用該策略如果需要做多端同步的話發(fā)送方多個端進(jìn)行同步可能會亂序,因為不同隊列的處理速度可能會不一樣挥唠。例如發(fā)送方先發(fā)送 m1 然后發(fā)送 m2俊扳,但服務(wù)器可能會先處理完 m2 再處理 m1,這里其它端會先收到 m2 然后是 m1猛遍,此時其它端的會話列表就亂了。按
conversation_id
進(jìn)行 Sharding:使用該策略同樣會導(dǎo)致多端同步會亂序。按
from_user_id
進(jìn)行 Sharding:這種情況下使用該策略是比較好的選擇通常為了優(yōu)化性能懊烤,推送前可能會先往 MQ 推梯醒,這種情況下使用
to_user_id
才是比較好的選擇。
用戶在線狀態(tài)如何做
很多 IM 系統(tǒng)都需要展示用戶的狀態(tài):是否在線腌紧,是否忙碌等茸习。主要可以使用 Redis 或者分布式一致性哈希來實現(xiàn)用戶在線狀態(tài)的存儲。
- Redis 存儲用戶在線狀態(tài)
看上面的圖可能會有人疑惑壁肋,為什么每次心跳都需要更新 Redis号胚?如果我使用的是 TCP 長連接那是不是就不用每次心跳都更新了?確實浸遗,正常情況下服務(wù)器只需要在新建連接或者斷開連接的時候更新一下 Redis 就好了猫胁。但由于服務(wù)器可能會出現(xiàn)異常,或者服務(wù)器跟 Redis 之間的網(wǎng)絡(luò)會出現(xiàn)問題跛锌,此時基于事件的更新就會出現(xiàn)問題弃秆,導(dǎo)致用戶狀態(tài)不正確。因此髓帽,如果需要用戶在線狀態(tài)準(zhǔn)確的話最好通過心跳來更新在線狀態(tài)菠赚。
由于 Redis 是單機(jī)存儲的,因此郑藏,為了提高可靠性跟性能衡查,我們可以使用 Redis Cluster 或者 Codis。
- 分布式一致性哈希存儲用戶在線狀態(tài)
使用分布式一致性哈希需要注意在對 Status Server Cluster 進(jìn)行擴(kuò)容或者縮容的時候要先對用戶狀態(tài)進(jìn)行遷移必盖,不然在剛操作時會出現(xiàn)用戶狀態(tài)不一致的情況拌牲。同時還需要使用虛擬節(jié)點避免數(shù)據(jù)傾斜的問題。
多端同步怎么做
讀擴(kuò)散
前面也提到過筑悴,對于讀擴(kuò)散们拙,消息的同步主要是以推模式為主,單個會話的消息 ID 順序遞增阁吝,前端收到推的消息如果發(fā)現(xiàn)消息 ID 不連續(xù)就請求后端重新獲取消息砚婆。但這樣仍然可能丟失會話的最后一條消息,為了加大消息的可靠性突勇,可以在歷史會話列表的會話里再帶上最后一條消息的 ID装盯,前端在收到新消息的時候會先拉取最新的會話列表,然后判斷會話的最后一條消息是否存在甲馋,如果不存在埂奈,消息就可能丟失了,前端需要再拉一次會話的消息列表定躏;如果會話的最后一條消息 ID 跟消息列表里的最后一條消息 ID 一樣账磺,前端就不再處理芹敌。這種做法的性能瓶頸會在拉取歷史會話列表那里,因為每次新消息都需要拉取后端一次垮抗,如果按微信的量級來看氏捞,單是消息就可能會有 20 萬的 QPS,如果歷史會話列表放到 MySQL 等傳統(tǒng) DB 的話肯定抗不住冒版。因此液茎,最好將歷史會話列表存到開了 AOF(用 RDB 的話可能會丟數(shù)據(jù))的 Redis 集群。這里只能感慨性能跟簡單性不能兼得辞嗡。
寫擴(kuò)散
對于寫擴(kuò)散來說捆等,多端同步就簡單些了。前端只需要記錄最后同步的位點续室,同步的時候帶上同步位點栋烤,然后服務(wù)器就將該位點后面的數(shù)據(jù)全部返回給前端,前端更新同步位點就可以了猎贴。
如何處理未讀數(shù)
在 IM 系統(tǒng)中班缎,未讀數(shù)的處理非常重要。未讀數(shù)一般分為會話未讀數(shù)跟總未讀數(shù)她渴,如果處理不當(dāng)达址,會話未讀數(shù)跟總未讀數(shù)可能會不一致,嚴(yán)重降低用戶體驗趁耗。
讀擴(kuò)散
對于讀擴(kuò)散來說沉唠,我們可以將會話未讀數(shù)跟總未讀數(shù)都存在后端,但后端需要保證兩個未讀數(shù)更新的原子性跟一致性苛败,一般可以通過以下兩種方法來實現(xiàn):
使用 Redis 的 multi 事務(wù)功能满葛,事務(wù)更新失敗可以重試。但要注意如果你使用 Codis 集群的話并不支持事務(wù)功能罢屈。
使用 Lua 嵌入腳本的方式嘀韧。使用這種方式需要保證會話未讀數(shù)跟總未讀數(shù)都在同一個 Redis 節(jié)點(Codis 的話可以使用 Hashtag)。這種方式會導(dǎo)致實現(xiàn)邏輯分散缠捌,加大維護(hù)成本锄贷。
寫擴(kuò)散
對于寫擴(kuò)散來說,服務(wù)端通常會弱化會話的概念曼月,即服務(wù)端不存儲歷史會話列表谊却。未讀數(shù)的計算可由前端來負(fù)責(zé),標(biāo)記已讀跟標(biāo)記未讀可以只記錄一個事件到信箱里哑芹,各個端通過重放該事件的形式來處理會話未讀數(shù)炎辨。使用這種方式可能會造成各個端的未讀數(shù)不一致,至少微信就會有這個問題聪姿。
如果寫擴(kuò)散也通過歷史會話列表來存儲未讀數(shù)的話那用戶時間線服務(wù)跟會話服務(wù)緊耦合碴萧,這個時候需要保證原子性跟一致性的話那就只能使用分布式事務(wù)了乙嘀,會大大降低系統(tǒng)的性能。
如何存儲歷史消息
讀擴(kuò)散
對于讀擴(kuò)散破喻,只需要按會話 ID 進(jìn)行 Sharding 存儲一份就可以了乒躺。
寫擴(kuò)散
對于寫擴(kuò)散,需要存儲兩份:一份是以用戶為 Timeline 的消息列表低缩,一份是以會話為 Timeline 的消息列表。以用戶為 Timeline 的消息列表可以用用戶 ID 來做 Sharding曹货,以會話為 Timeline 的消息列表可以用會話 ID 來做 Sharding咆繁。
數(shù)據(jù)冷熱分離
對于 IM 來說,歷史消息的存儲有很強(qiáng)的時間序列特性顶籽,時間越久玩般,消息被訪問的概率也越低,價值也越低礼饱。
如果我們需要存儲幾年甚至是永久的歷史消息的話(電商 IM 中比較常見)坏为,那么做歷史消息的冷熱分離就非常有必要了。數(shù)據(jù)的冷熱分離一般是 HWC(Hot-Warm-Cold)架構(gòu)镊绪。對于剛發(fā)送的消息可以放到 Hot 存儲系統(tǒng)(可以用 Redis)跟 Warm 存儲系統(tǒng)匀伏,然后由 Store Scheduler 根據(jù)一定的規(guī)則定時將冷數(shù)據(jù)遷移到 Cold 存儲系統(tǒng)。獲取消息的時候需要依次訪問 Hot蝴韭、Warm 跟 Cold 存儲系統(tǒng)够颠,由 Store Service 整合數(shù)據(jù)返回給 IM Service。
接入層怎么做
實現(xiàn)接入層的負(fù)載均衡主要有以下幾個方法:
硬件負(fù)載均衡:例如 F5榄鉴、A10 等等履磨。硬件負(fù)載均衡性能強(qiáng)大,穩(wěn)定性高庆尘,但價格非常貴剃诅,不是土豪公司不建議使用。
使用 DNS 實現(xiàn)負(fù)載均衡:使用 DNS 實現(xiàn)負(fù)載均衡比較簡單驶忌,但使用 DNS 實現(xiàn)負(fù)載均衡如果需要切換或者擴(kuò)容那生效會很慢矛辕,而且使用 DNS 實現(xiàn)負(fù)載均衡支持的 IP 個數(shù)有限制、支持的負(fù)載均衡策略也比較簡單位岔。
DNS + 4 層負(fù)載均衡 + 7 層負(fù)載均衡架構(gòu):例如 DNS + DPVS + Nginx 或者 DNS + LVS + Nginx如筛。有人可能會疑惑為什么要加入 4 層負(fù)載均衡呢?這是因為 7 層負(fù)載均衡很耗 CPU抒抬,并且經(jīng)常需要擴(kuò)容或者縮容杨刨,對于大型網(wǎng)站來說可能需要很多 7 層負(fù)載均衡服務(wù)器,但只需要少量的 4 層負(fù)載均衡服務(wù)器即可擦剑。因此妖胀,該架構(gòu)對于 HTTP 等短連接大型應(yīng)用很有用芥颈。當(dāng)然,如果流量不大的話只使用 DNS + 7 層負(fù)載均衡即可赚抡。但對于長連接來說爬坑,加入 7 層負(fù)載均衡 Nginx 就不大好了。因為 Nginx 經(jīng)常需要改配置并且 reload 配置涂臣,reload 的時候 TCP 連接會斷開盾计,造成大量掉線。
DNS + 4 層負(fù)載均衡:4 層負(fù)載均衡一般比較穩(wěn)定赁遗,很少改動署辉,比較適合于長連接。
對于長連接的接入層岩四,如果我們需要更加靈活的負(fù)載均衡策略或者需要做灰度的話哭尝,那我們可以引入一個調(diào)度服務(wù),如下圖所示:
Access Schedule Service 可以實現(xiàn)根據(jù)各種策略來分配 Access Service剖煌,例如:
根據(jù)灰度策略來分配
根據(jù)就近原則來分配
根據(jù)最少連接數(shù)來分配
最后材鹦,分享一下做大型應(yīng)用的架構(gòu)心得:
灰度!灰度耕姊!灰度桶唐!
監(jiān)控!監(jiān)控箩做!監(jiān)控莽红!
告警!告警邦邦!告警安吁!
緩存!緩存燃辖!緩存鬼店!
限流!熔斷黔龟!降級妇智!
低耦合,高內(nèi)聚氏身!
避免單點巍棱,擁抱無狀態(tài)!
評估蛋欣!評估航徙!評估!
壓測陷虎!壓測到踏!壓測杠袱!
How We Developed DingTalk: Implementing the Message System Architecture
Feed Stream System Design: General Principles
融云首度披露高并發(fā)系統(tǒng)架構(gòu)設(shè)計四大要點
一個海量在線用戶即時通訊系統(tǒng)(IM)的完整設(shè)計Plus
即時通訊網(wǎng)(IM開發(fā)者社區(qū))— 技術(shù)精選
萬億級調(diào)用系統(tǒng):微信序列號生成器架構(gòu)設(shè)計及演變
微信PaxosStore:深入淺出Paxos算法協(xié)議
微信后臺基于時間序的海量數(shù)據(jù)冷熱分級架構(gòu)設(shè)計實踐
釘釘企業(yè)級 IM 存儲架構(gòu)創(chuàng)新之道
現(xiàn)代IM系統(tǒng)中聊天消息的同步和存儲方案探討
阿里釘釘技術(shù)分享:企業(yè)級IM王者——釘釘在后端架構(gòu)上的過人之處
如何優(yōu)化高并發(fā)IM系統(tǒng)架構(gòu)
現(xiàn)代IM系統(tǒng)中的消息系統(tǒng)—架構(gòu)
現(xiàn)代IM系統(tǒng)中的消息系統(tǒng)—模型
現(xiàn)代IM系統(tǒng)中的消息系統(tǒng)—實現(xiàn)
How We Learned to Stop Worrying and Love Fan-In at Twitter