本文原作者Chank,原題“如何設計一個億級消息量的 IM 系統”,為了提升內容質量,本次有修訂和改動。
1、寫有前面
本文將在億級消息量、分布式IM系統這個技術前提下,分析和總結實現這套系統所需要掌握的知識點,內容沒有高深的技術概念,盡量做到新手老手皆能讀懂。
本文不會給出一套通用的IM方案,也不會評判某種架構的好壞,而是討論設計IM系統的常見難題跟業界的解決方案。
因為也沒有所謂的通用IM架構方案,不同的解決方案都各有其優缺點,只有最滿足業務的系統才是一個好的系統。
在人力、物力、時間資源有限的前提下,通常需要做出很多權衡,此時,一個能夠支持快速迭代、方便擴展的IM系統才是最優解。
學習交流:
- 即時通訊/推送技術開發交流5群:215477170 [推薦]
- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》
- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK
(本文同步發布于:http://www.52im.net/thread-3472-1-1.html)
2、相關文章
與本文類似,以下兩篇也非常適合同時閱讀,有興趣可以一并學習。
《一套億級用戶的IM架構技術干貨(上篇):整體架構、服務拆分等》
《一套億級用戶的IM架構技術干貨(下篇):可靠性、有序性、弱網優化等》
3、IM常見術語
0)用戶:系統的使用者。
1)消息:是指用戶之間的溝通內容(通常在IM系統中,消息會有以下幾類:文本消息、表情消息、圖片消息、視頻消息、文件消息等等)。
2)會話:通常指兩個用戶之間因聊天而建立起的關聯。
3)群:通常指多個用戶之間因聊天而建立起的關聯。
4)終端:指用戶使用IM系統的機器(通常有Android端、iOS端、Web端等等)。
5)未讀數:指用戶還沒讀的消息數量。
6)用戶狀態:指用戶當前是在線、離線還是掛起等狀態。
7)關系鏈:是指用戶與用戶之間的關系,通常有單向的好友關系、雙向的好友關系、關注關系等等(這里需要注意與會話的區別:用戶只有在發起聊天時才產生會話,但關系并不需要聊天才能建立。對于關系鏈的存儲,可以使用圖數據庫(Neo4j等等),可以很自然地表達現實世界中的關系,易于建模)。
8)單聊:一對一聊天。
9)群聊:多人聊天。
10)客服:在電商領域,通常需要對用戶提供售前咨詢、售后咨詢等服務(這時,就需要引入客服來處理用戶的咨詢)。
11)消息分流:在電商領域,一個店鋪通常會有多個客服,此時決定用戶的咨詢由哪個客服來處理就是消息分流(通常消息分流會根據一系列規則來確定消息會分流給哪個客服,例如客服是否在線(客服不在線的話需要重新分流給另一個客服)、該消息是售前咨詢還是售后咨詢、當前客服的繁忙程度等等)。
12)信箱:本文的信箱我們指一個Timeline、一個收發消息的隊列。
4、讀擴散 vs 寫擴散
IM系統里經常會涉及到讀擴散和寫擴散這兩個技術概念,我們來看看。
4.1 讀擴散
如上圖所示:A與每個聊天的人跟群都有一個信箱(有些博文會叫Timeline,見《現代IM系統中聊天消息的同步和存儲方案探討》),A在查看聊天信息的時候需要讀取所有有新消息的信箱。
需要注意與Feeds系統的區別:在Feeds系統中,每個人都有一個寫信箱,寫只需要往自己的寫信箱里寫一次就好了,讀需要從所有關注的人的寫信箱里讀。但IM系統里的讀擴散通常是每兩個相關聯的人就有一個信箱,或者每個群一個信箱。
讀擴散的優點:
- 1)寫操作(發消息)很輕量,不管是單聊還是群聊,只需要往相應的信箱寫一次就好了;
- 2)每一個信箱天然就是兩個人的聊天記錄,可以方便查看聊天記錄跟進行聊天記錄的搜索。
讀擴散的缺點:讀操作(讀消息)很重,在復雜業務下,一條讀擴散消息源需要復雜的邏輯才能擴散成目標消息。
4.2 寫擴散
接下來看看寫擴散。
如上圖所示:在寫擴散中,每個人都只從自己的信箱里讀取消息。
但寫(發消息)的時候,對于單聊跟群聊處理如下:
- 1)單聊:往自己的信箱跟對方的信箱都寫一份消息,同時,如果需要查看兩個人的聊天歷史記錄的話還需要再寫一份(當然,如果從個人信箱也能回溯出兩個人的所有聊天記錄,但這樣效率會很低);
- 2)群聊:需要往所有的群成員的信箱都寫一份消息,同時,如果需要查看群的聊天歷史記錄的話還需要再寫一份。可以看出,寫擴散對于群聊來說大大地放大了寫操作。
PS:實際上群聊中消息擴散是IM開發中的技術痛點,有興趣建議詳細閱讀:《有關IM群聊技術實現的文章匯總》。
寫擴散優點:
- 1)讀操作很輕量;
- 2)可以很方便地做消息的多終端同步。
寫擴散缺點:寫操作很重,尤其是對于群聊來說(因為如果群成員很多的話,1條消息源要擴散寫成“成員數-1”條目標消息,這是很恐怖的)。
在Feeds系統中:
- 1)寫擴散也叫:Push、Fan-out或者Write-fanout;
- 2)讀擴散也叫:Pull、Fan-in或者Read-fanout。
5、唯一ID的技術方案
5.1 基礎知識
通常情況下,ID設計主要有以下幾大類:
- 1)UUID;
- 2)基于Snowflake算法的ID生成方式;
- 3)基于申請DB步長的生成方式;
- 4)基于Redis或者DB的自增ID生成方式;
- 5)特殊的規則生成唯一ID。
- ... ...
具體的實現方法跟優缺點可以參考以下IM消息ID的專題文章:
《IM消息ID技術專題(一):微信的海量IM聊天消息序列號生成實踐(算法原理篇)》
《IM消息ID技術專題(二):微信的海量IM聊天消息序列號生成實踐(容災方案篇)》
《IM消息ID技術專題(三):解密融云IM產品的聊天消息ID生成策略》
《IM消息ID技術專題(四):深度解密美團的分布式ID生成算法》
《IM消息ID技術專題(五):開源分布式ID生成器UidGenerator的技術實現》
《IM消息ID技術專題(六):深度解密滴滴的高性能ID生成器(Tinyid)》
在IM系統中需要唯一Id的地方主要是:
5.2 消息ID
我們來看看在設計消息ID時需要考慮的三個問題。
5.2.1)消息ID不遞增可以嗎?
我們先看看不遞增的話會怎樣:
- 1)使用字符串,浪費存儲空間,而且不能利用存儲引擎的特性讓相鄰的消息存儲在一起,降低消息的寫入跟讀取性能;
- 2)使用數字,但數字隨機,也不能利用存儲引擎的特性讓相鄰的消息存儲在一起,會加大隨機IO,降低性能;而且隨機的ID不好保證ID的唯一性。
因此,消息ID最好是遞增的。
5.2.3)全局遞增 vs 用戶級別遞增 vs 會話級別遞增:
全局遞增:指消息ID在整個IM系統隨著時間的推移是遞增的。全局遞增的話一般可以使用Snowflake(當然,Snowflake也只是worker級別的遞增)。此時,如果你的系統是讀擴散的話為了防止消息丟失,那每一條消息就只能帶上上一條消息的ID,前端根據上一條消息判斷是否有丟失消息,有消息丟失的話需要重新拉一次。
用戶級別遞增:指消息ID只保證在單個用戶中是遞增的,不同用戶之間不影響并且可能重復。典型代表:微信(見《微信的海量IM聊天消息序列號生成實踐(算法原理篇)》)。如果是寫擴散系統的話信箱時間線ID跟消息ID需要分開設計,信箱時間線ID用戶級別遞增,消息ID全局遞增。如果是讀擴散系統的話感覺使用用戶級別遞增必要性不是很大。
會話級別遞增:指消息ID只保證在單個會話中是遞增的,不同會話之間不影響并且可能重復。典型代表:QQ。
5.2.3)連續遞增 vs 單調遞增:
連續遞增是指ID按 1,2,3...n 的方式生成;而單調遞增是指只要保證后面生成的ID比前面生成的ID大就可以了,不需要連續。
據我所知:QQ的消息ID就是在會話級別使用的連續遞增,這樣的好處是,如果丟失了消息,當下一條消息來的時候發現ID不連續就會去請求服務器,避免丟失消息。
此時,可能有人會想,我不能用定時拉的方式看有沒有消息丟失嗎?當然不能,因為消息ID只在會話級別連續遞增的話那如果一個人有上千個會話,那得拉多少次啊,服務器肯定是抗不住的。
對于讀擴散來說,消息ID使用連續遞增就是一種不錯的方式了。如果使用單調遞增的話當前消息需要帶上前一條消息的ID(即聊天消息組成一個鏈表),這樣,才能判斷消息是否丟失。
5.2.4)小結一下:
寫擴散:信箱時間線ID使用用戶級別遞增,消息ID全局遞增,此時只要保證單調遞增就可以了。
讀擴散:消息ID可以使用會話級別遞增并且最好是連續遞增。
5.3 會話ID
我們來看看設計會話ID需要注意的問題。
其中,會話ID有種比較簡單的生成方式——特殊的規則生成唯一ID:即拼接 from_user_id 跟 to_user_id。
拼接邏輯可以像下面這樣:
- 1)如果 from_user_id 跟 to_user_id 都是32位整形數據的話可以很方便地用位運算拼接成一個64位的會話ID,即: conversation_id = ${from_user_id} << 32 | ${to_user_id} (在拼接前需要確保值比較小的用戶ID是 from_user_id,這樣任意兩個用戶發起會話可以很方便地知道會話ID);
- 2)如果from_user_id 跟 to_user_id 都是64位整形數據的話那就只能拼接成一個字符串了,拼接成字符串的話就比較傷了,浪費存儲空間性能又不好。
前東家就是使用的上面第1種方式,第1種方式有個硬傷:隨著業務在全球的擴展,32位的用戶ID如果不夠用需要擴展到64位的話那就需要大刀闊斧地改了。32位整形ID看起來能夠容納21億個用戶,但通常我們為了防止別人知道真實的用戶數據,使用的ID通常不是連續的,這時32位的用戶ID就完全不夠用了。該設計完全依賴于用戶ID,不是一種可取的設計方式。
因此:會話ID的設計可以使用全局遞增的方式,加一個映射表:保存from_user_id、to_user_id跟conversation_id的關系。
6、新息的“推模式 vs 拉模式 vs 推拉結合模式”
在IM系統中,新消息的獲取通常會有三種可能的做法:
- 1)推模式:有新消息時服務器主動推給所有端(iOS、Android、PC等);
- 2)拉模式:由前端主動發起拉取消息的請求,為了保證消息的實時性,一般采用推模式,拉模式一般用于獲取歷史消息;
- 3)推拉結合模式:有新消息時服務器會先推一個有新消息的通知給前端,前端接收到通知后就向服務器拉取消息。
推模式簡化圖如下:
如上圖所示:正常情況下,用戶發的消息經過服務器存儲等操作后會推給接收方的所有端。
但推是有可能會丟失的:最常見的情況就是用戶可能會偽在線(是指如果推送服務基于長連接,而長連接可能已經斷開,即用戶已經掉線,但一般需要經過一個心跳周期后服務器才能感知到,這時服務器會錯誤地以為用戶還在線;偽在線是本人自己想的一個概念,沒想到合適的詞來解釋)。因此如果單純使用推模式的話,是有可能會丟失消息的。
PS:為什么會出現作者所述“偽在線”這個問題,可以讀一下《為什么說基于TCP的移動端IM仍然需要心跳保活?》。
推拉結合模式簡化圖如下:
可以使用推拉結合模式解決推模式可能會丟消息的問題:即在用戶發新消息時服務器推送一個通知,然后前端請求最新消息列表,為了防止有消息丟失,可以再每隔一段時間主動請求一次。可以看出,使用推拉結合模式最好是用寫擴散,因為寫擴散只需要拉一條時間線的個人信箱就好了,而讀擴散有N條時間線(每個信箱一條),如果也定時拉取的話性能會很差。
7、業界的IM解決方案參考
前面了解了IM系統的常見設計問題,接下來我們再看看業界是怎么設計IM系統的。
研究業界的主流方案有助于我們深入理解IM系統的設計。以下研究都是基于網上已經公開的資料,不一定正確,大家僅作參考就好了。
7.1 微信
雖然微信很多基礎框架都是自研,但這并不妨礙我們理解微信的架構設計。
從微信公開的《快速裂變:見證微信強大后臺架構從0到1的演進歷程(二)》這篇文章可以看出,微信采用的主要是:寫擴散 + 推拉結合。由于群聊使用的也是寫擴散,而寫擴散很消耗資源,因此微信群有人數上限(目前是500)。所以這也是寫擴散的一個明顯缺點,如果需要萬人群就比較難了。
從文中還可以看出,微信采用了多數據中心架構:
▲ 圖片引用自《快速裂變:見證微信強大后臺架構從0到1的演進歷程(二)》
微信每個數據中心都是自治的,每個數據中心都有全量的數據,數據中心間通過自研的消息隊列來同步數據。
為了保證數據的一致性,每個用戶都只屬于一個數據中心,只能在自己所屬的數據中心進行數據讀寫,如果用戶連了其它數據中心則會自動引導用戶接入所屬的數據中心。而如果需要訪問其它用戶的數據那只需要訪問自己所屬的數據中心就可以了。
同時,微信使用了三園區容災的架構,使用Paxos來保證數據的一致性。
從微信公開的《微信的海量IM聊天消息序列號生成實踐(容災方案篇)》這篇文章可以看出,微信的ID設計采用的是:基于申請DB步長的生成方式 + 用戶級別遞增。
如下圖所示:
▲ 圖片引用自《微信的海量IM聊天消息序列號生成實踐(容災方案篇)》
微信的序列號生成器由仲裁服務生成路由表(路由表保存了uid號段到AllocSvr的全映射),路由表會同步到AllocSvr跟Client。如果AllocSvr宕機的話會由仲裁服務重新調度uid號段到其它AllocSvr。
PS:微信團隊分享了大量的技術資料,有興趣可以看看《QQ、微信技術分享 - 匯總》。
7.2 釘釘
釘釘公開的資料不多,從《阿里釘釘技術分享:企業級IM王者——釘釘在后端架構上的過人之處》這篇文章我們只能知道,釘釘最開始使用的是寫擴散模型,為了支持萬人群,后來貌似優化成了讀擴散。
但聊到阿里的IM系統,不得不提的是阿里自研的Tablestore:一般情況下,IM系統都會有一個自增ID生成系統,但Tablestore創造性地引入了主鍵列自增,即把ID的生成整合到了DB層,支持了用戶級別遞增(傳統MySQL等DB只能支持表級自增,即全局自增),具體可以參考:《如何優化高并發IM系統架構》。
PS:釘釘團隊公開的技術很少,這是另一篇:《釘釘——基于IM技術的新一代企業OA平臺的技術挑戰(視頻+PPT)》,有興趣可以研究研究。
7.3 Twitter
什么?Twitter不是Feeds系統嗎?這篇文章不是討論IM的嗎?
是的,Twitter是Feeds系統,但Feeds系統跟IM系統其實有很多設計上的共性,研究下Feeds系統有助于我們在設計IM系統時進行參考。再說了,研究下Feeds系統也沒有壞處,擴展下技術視野嘛。
Twitter的自增ID設計估計大家都耳熟能詳了,即大名鼎鼎的 Snowflake,因此ID是全局遞增的。
從這個視頻分享《How We Learned to Stop Worrying and Love Fan-In at Twitter》可以看出,Twitter一開始使用的是寫擴散模型,Fanout Service負責擴散寫到Timelines Cache(使用了Redis),Timeline Service負責讀取Timeline數據,然后由API Services返回給用戶。
但由于寫擴散對于大V來說寫的消耗太大,因此后面Twitter又使用了寫擴散跟讀擴散結合的方式。
如下圖所示:
對于粉絲數不多的用戶如果發Twitter使用的還是寫擴散模型,由Timeline Mixer服務將用戶的Timeline、大V的寫Timeline跟系統推薦等內容整合起來,最后再由API Services返回給用戶。
7.4 58到家
58到家實現了一個通用的實時消息平臺:
▲ 圖片引用自《58到家實時消息系統的架構設計及技術選型經驗總結》
可以看出:msg-server保存了應用跟MQ主題之間的對應關系,msg-server根據這個配置將消息推到不同的MQ隊列,具體的應用來消費就可以了。因此,新增一個應用只需要修改配置就可以了。
58到家為了保證消息投遞的可靠性,還引入了確認機制:消息平臺收到消息先落地數據庫,接收方收到后應用層ACK再刪除。使用確認機制最好是只能單點登錄,如果多端能夠同時登錄的話那就比較麻煩了,因為需要所有端都確認收到消息后才能刪除。
PS:58到家平臺部負責人任桃術還分享過《58到家實時消息系統的協議設計等技術實踐分享》一文,有興趣可以一并閱讀。
看到這里,估計大家已經明白了,設計一個IM系統很有挑戰性。我們還是繼續來看設計一個IM系統需要考慮的問題吧。
7.5 其它業界方案
即時通訊網也收錄了大量其它的業界IM或類IM系統的設計方案,限于篇幅原因這里就不一一列出,有興趣可以選擇性地閱讀,一下是文章匯總。
《一套海量在線用戶的移動端IM架構設計實踐分享(含詳細圖文)》
《一套原創分布式即時通訊(IM)系統理論架構方案》
《從零到卓越:京東客服即時通訊系統的技術架構演進歷程》
《蘑菇街即時通訊/IM服務器開發之架構選擇》
《現代IM系統中聊天消息的同步和存儲方案探討》
《WhatsApp技術實踐分享:32人工程團隊創造的技術神話》
《微信朋友圈千億訪問量背后的技術挑戰和實踐總結》
《以微博類應用場景為例,總結海量社交系統的架構設計步驟》
《子彈短信光鮮的背后:網易云信首席架構師分享億級IM平臺的技術實踐》
《一套高可用、易伸縮、高并發的IM群聊、單聊架構方案設計實踐》
《從游擊隊到正規軍(一):馬蜂窩旅游網的IM系統架構演進之路》
《從游擊隊到正規軍(三):基于Go的馬蜂窩旅游網分布式IM系統技術實踐》
《瓜子IM智能客服系統的數據架構設計(整理自現場演講,有配套PPT)》
《阿里技術分享:電商IM消息平臺,在群聊、直播場景下的技術實踐》
《一套億級用戶的IM架構技術干貨(上篇):整體架構、服務拆分等》
8、IM需要解決的技術痛點
8.1 如何保證消息的實時性
PS:如果你還不了解IM里的消息實時性是什么,務必先讀這篇《零基礎IM開發入門(二):什么是IM系統的實時性?》;
在通信協議的選擇上,我們主要有以下幾個選擇:
不管使用哪種方式,我們都能夠做到消息的實時通知,但影響我們消息實時性的可能會在我們處理消息的方式上。
例如:假如我們推送的時候使用MQ去處理并推送一個萬人群的消息,推送一個人需要2ms,那么推完一萬人需要20s,那么后面的消息就阻塞了20s。如果我們需要在10ms內推完,那么我們推送的并發度應該是:人數:10000 / (推送總時長:10 / 單個人推送時長:2) = 2000。
因此:我們在選擇具體的實現方案的時候一定要評估好我們系統的吞吐量,系統的每一個環節都要進行評估壓測。只有把每一個環節的吞吐量評估好了,才能保證消息推送的實時性。
IM消息實時性中群聊消息和單聊消息的處理又有很大區別,有興趣可以深入閱讀:
《IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞》
《移動端IM中大規模群消息的推送如何保證效率、實時性?》
8.2 如何保證消息時序
在IM的技術實現中,以下情況下消息可能會亂序(提示:如果你還不了解什么是IM的消息時序,務必先閱讀《零基礎IM開發入門(四):什么是IM系統的消息時序一致性?》)。
8.2.1)發送消息如果使用的不是長連接,而是使用HTTP的話可能會出現亂序:
因為后端一般是集群部署,使用HTTP的話請求可能會打到不同的服務器,由于網絡延遲或者服務器處理速度的不同,后發的消息可能會先完成,此時就產生了消息亂序。
解決方案:
- 1)前端依次對消息進行處理,發送完一個消息再發送下一個消息。這種方式會降低用戶體驗,一般情況下不建議使用;
- 2)帶上一個前端生成的順序ID,讓接收方根據該ID進行排序。這種方式前端處理會比較麻煩一點,而且聊天的過程中接收方的歷史消息列表中可能會在中間插入一條消息,這樣會很奇怪,而且用戶可能會漏讀消息。但這種情況可以通過在用戶切換窗口的時候再進行重排來解決,接收方每次收到消息都先往最后面追加。
8.2.2)通常為了優化體驗,有的IM系統可能會采取異步發送確認機制(例如:QQ):
即消息只要到達服務器,然后服務器發送到MQ就算發送成功。如果由于權限等問題發送失敗的話后端再推一個通知下去。
這種情況下MQ就要選擇合適的Sharding策略了:
- 1)按to_user_id進行Sharding:使用該策略如果需要做多端同步的話發送方多個端進行同步可能會亂序,因為不同隊列的處理速度可能會不一樣。例如發送方先發送m1然后發送m2,但服務器可能會先處理完m2再處理m1,這里其它端會先收到m2然后是m1,此時其它端的會話列表就亂了;
- 2)按conversation_id進行Sharding:使用該策略同樣會導致多端同步會亂序;
- 3)按from_user_id進行Sharding:這種情況下使用該策略是比較好的選擇。
通常為了優化性能,推送前可能會先往MQ推,這種情況下使用to_user_id才是比較好的選擇。
PS:實際上,導致IM消息亂序的可能性還有很多,這里就不一一展開,以下幾篇值得深入閱讀。
《如何保證IM實時消息的“時序性”與“一致性”?》
《一個低成本確保IM消息時序的方法探討》
《一套億級用戶的IM架構技術干貨(下篇):可靠性、有序性、弱網優化等》
8.3 用戶在線狀態如何做
很多IM系統都需要展示用戶的狀態:是否在線,是否忙碌等。
要實現用戶在線狀態的存儲,主要可以使用:
Redis存儲用戶在線狀態:
看上面的圖可能會有人疑惑:為什么每次心跳都需要更新Redis?
如果我使用的是TCP長連接那是不是就不用每次心跳都更新了?
確實:正常情況下服務器只需要在新建連接或者斷開連接的時候更新一下Redis就好了。但由于服務器可能會出現異常,或者服務器跟Redis之間的網絡會出現問題,此時基于事件的更新就會出現問題,導致用戶狀態不正確。因此,如果需要用戶在線狀態準確的話最好通過心跳來更新在線狀態。
由于Redis是單機存儲的,因此,為了提高可靠性跟性能,我們可以使用Redis Cluster或者Codis。
分布式一致性哈希存儲用戶在線狀態:
使用分布式一致性哈希需要注意在對Status Server Cluster進行擴容或者縮容的時候要先對用戶狀態進行遷移,不然在剛操作時會出現用戶狀態不一致的情況。同時還需要使用虛擬節點避免數據傾斜的問題。
PS:用戶狀態在客戶端的更新也是個很有挑戰性的問題,有興趣可以讀一下《IM單聊和群聊中的在線狀態同步應該用“推”還是“拉”?》。
8.4 多端同步怎么做
8.4.1)讀擴散:
前面也提到過:對于讀擴散,消息的同步主要是以推模式為主,單個會話的消息ID順序遞增,前端收到推的消息如果發現消息ID不連續就請求后端重新獲取消息。
但這樣仍然可能丟失會話的最后一條消息。
為了加大消息的可靠性:可以在歷史會話列表的會話里再帶上最后一條消息的ID,前端在收到新消息的時候會先拉取最新的會話列表,然后判斷會話的最后一條消息是否存在,如果不存在,消息就可能丟失了,前端需要再拉一次會話的消息列表;如果會話的最后一條消息ID跟消息列表里的最后一條消息ID一樣,前端就不再處理。
這種做法的性能瓶頸會在拉取歷史會話列表那里,因為每次新消息都需要拉取后端一次,如果按微信的量級來看,單是消息就可能會有20萬的QPS,如果歷史會話列表放到MySQL等傳統DB的話肯定抗不住。
因此,最好將歷史會話列表存到開了AOF(用RDB的話可能會丟數據)的Redis集群。這里只能感慨性能跟簡單性不能兼得。
8.4.2)寫擴散:
對于寫擴散來說,多端同步就簡單些了。前端只需要記錄最后同步的位點,同步的時候帶上同步位點,然后服務器就將該位點后面的數據全部返回給前端,前端更新同步位點就可以了。
PS:多端同步這也是IM里比較坑爹的技術痛點,有興趣請移步《淺談移動端IM的多點登錄和消息漫游原理》。
8.5 如何處理未讀數
在IM系統中,未讀數的處理非常重要。
未讀數一般分為會話未讀數跟總未讀數,如果處理不當,會話未讀數跟總未讀數可能會不一致,嚴重降低用戶體驗。
8.5.1)讀擴散:
對于讀擴散來說,我們可以將會話未讀數跟總未讀數都存在后端,但后端需要保證兩個未讀數更新的原子性跟一致性。
一般可以通過以下兩種方法來實現:
- 1)使用Redis的multi事務功能,事務更新失敗可以重試。但要注意如果你使用Codis集群的話并不支持事務功能;
- 2)使用Lua嵌入腳本的方式。使用這種方式需要保證會話未讀數跟總未讀數都在同一個Redis節點(Codis的話可以使用Hashtag)。這種方式會導致實現邏輯分散,加大維護成本。
8.5.2)寫擴散:
對于寫擴散來說,服務端通常會弱化會話的概念,即服務端不存儲歷史會話列表。
未讀數的計算可由前端來負責,標記已讀跟標記未讀可以只記錄一個事件到信箱里,各個端通過重放該事件的形式來處理會話未讀數。
使用這種方式可能會造成各個端的未讀數不一致,至少微信就會有這個問題(Jack Jiang 注:實際上QQ也同樣有這個問題,在分布式和多端IM中這確實是個很頭痛的問題,大家都不會例外,哈哈 ~~)。
如果寫擴散也通過歷史會話列表來存儲未讀數的話那用戶時間線服務跟會話服務緊耦合,這個時候需要保證原子性跟一致性的話那就只能使用分布式事務了,會大大降低系統的性能。
8.6 如何存儲歷史消息
讀擴散:對于讀擴散,只需要按會話ID進行Sharding存儲一份就可以了。
寫擴散:對于寫擴散,需要存儲兩份,一份是以用戶為Timeline的消息列表,一份是以會話為Timeline的消息列表。以用戶為Timeline的消息列表可以用用戶ID來做Sharding,以會話為Timeline的消息列表可以用會話ID來做Sharding。
PS:如果你對Timeline這個概念不熟悉,請讀這篇《現代IM系統中聊天消息的同步和存儲方案探討》。
8.7 數據冷熱分離
對于IM來說,歷史消息的存儲有很強的時間序列特性,時間越久,消息被訪問的概率也越低,價值也越低。
如果我們需要存儲幾年甚至是永久的歷史消息的話(電商IM中比較常見),那么做歷史消息的冷熱分離就非常有必要了。
數據的冷熱分離一般是HWC(Hot-Warm-Cold)架構。
對于剛發送的消息可以放到Hot存儲系統(可以用Redis)跟Warm存儲系統,然后由Store Scheduler根據一定的規則定時將冷數據遷移到Cold存儲系統。
獲取消息的時候需要依次訪問Hot、Warm跟Cold存儲系統,由Store Service整合數據返回給IM Service。
微信團隊分享的這篇《微信后臺基于時間序的海量數據冷熱分級架構設計實踐》,或許可以有些啟發。
8.8 接入層怎么做
對于分布于IM來說,接入層是必須要考慮的的。
實現接入層的負載均衡主要有以下幾個方法:
- 1)硬件負載均衡:例如F5、A10等等。硬件負載均衡性能強大,穩定性高,但價格非常貴,不是土豪公司不建議使用;
- 2)使用DNS實現負載均衡:使用DNS實現負載均衡比較簡單,但使用DNS實現負載均衡如果需要切換或者擴容那生效會很慢,而且使用DNS實現負載均衡支持的IP個數有限制、支持的負載均衡策略也比較簡單;
- 3)DNS + 4層負載均衡 + 7層負載均衡架構:例如 DNS + DPVS + Nginx 或者 DNS + LVS + Nginx;
- 4)DNS + 4層負載均衡:4層負載均衡一般比較穩定,很少改動,比較適合于長連接。
對于第 3)點:有人可能會疑惑為什么要加入4層負載均衡呢?
這是因為7層負載均衡很耗CPU,并且經常需要擴容或者縮容,對于大型網站來說可能需要很多7層負載均衡服務器,但只需要少量的4層負載均衡服務器即可。因此,該架構對于HTTP等短連接大型應用很有用。
當然,如果流量不大的話只使用DNS + 7層負載均衡即可。但對于長連接來說,加入7層負載均衡Nginx就不大好了。因為Nginx經常需要改配置并且reload配置,reload的時候TCP連接會斷開,造成大量掉線。
對于長連接的接入層,如果我們需要更加靈活的負載均衡策略或者需要做灰度的話,那我們可以引入一個調度服務。
如下圖所示:
Access Schedule Service可以實現根據各種策略來分配Access Service。
例如:
- 1)根據灰度策略來分配;
- 2)根據就近原則來分配;
- 3)根據最少連接數來分配。
9、寫在最后
看完上面的內容你應該能深刻體會到,要實現一個穩定可靠的大用戶量分布式IM系統難度是相當大的,所謂路漫漫其修遠兮。。。
在不斷追求體驗更好、性能更高、負載更多、成本更低的動力下,IM架構優化這條路是沒有盡頭的,所以為了延緩程序員發量較少的焦慮,大家代碼一定要悠著點擼,頭涼是很難受滴 ~~
PS:本篇主要是從IM架構設計這個角度來講的,對于IM初學者來說是不容易看的明白,建議初學者從這篇開始:《新手入門一篇就夠:從零開發移動端IM》。
10、參考資料
[1] 58到家實時消息系統的架構設計及技術選型經驗總結
[2] 一套海量在線用戶的移動端IM架構設計實踐分享(含詳細圖文)
[3] 現代IM系統中聊天消息的同步和存儲方案探討
[4] 一套億級用戶的IM架構技術干貨(上篇):整體架構、服務拆分等
[5] IM消息ID技術專題(二):微信的海量IM聊天消息序列號生成實踐(容災方案篇)
[6] 快速裂變:見證微信強大后臺架構從0到1的演進歷程(二)
本文已同步發布于“即時通訊技術圈”公眾號。

▲ 本文在公眾號上的鏈接是:點此進入。同步發布鏈接是:http://www.52im.net/thread-3472-1-1.html