從新手到專家:如何設計一套億級消息量的分佈式 IM 系統

本文原作者 Chank,原題 “如何設計一個億級消息量的 IM 系統”,爲了提升內容質量,即時通訊收錄時有修訂和改動。

1、寫有前面

本文將在億級消息量、分佈式 IM 系統這個技術前提下,分析和總結實現這套系統所需要掌握的知識點,內容沒有高深的技術概念,儘量做到新手老手皆能讀懂。

本文不會給出一套通用的 IM 方案,也不會評判某種架構的好壞,而是討論設計 IM 系統的常見難題跟業界的解決方案。

因爲也沒有所謂的通用 IM 架構方案,不同的解決方案都各有其優缺點,只有最滿足業務的系統纔是一個好的系統。

在人力、物力、時間資源有限的前提下,通常需要做出很多權衡,此時,一個能夠支持快速迭代、方便擴展的 IM 系統纔是最優解。

2、相關文章

與本文類似,以下兩篇也非常適合同時閱讀,有興趣可以一併學習。

1)《一套億級用戶的 IM 架構技術乾貨 (上篇):整體架構、服務拆分等

2)《一套億級用戶的 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 的專題文章:

1)《(一):微信的海量 IM 聊天消息序列號生成實踐(算法原理篇)

2)《(二):微信的海量 IM 聊天消息序列號生成實踐(容災方案篇)》

3)《(三):解密融雲 IM 產品的聊天消息 ID 生成策略

4)《(四):深度解密美團的分佈式 ID 生成算法

5)《(五):開源分佈式 ID 生成器 UidGenerator 的技術實現

6)《(六):深度解密滴滴的高性能 ID 生成器 (Tinyid)

在 IM 系統中需要唯一 Id 的地方主要是:

1)聊天會話 ID;

2)聊天消息 ID。

5.2 消息 ID

我們來看看在設計消息 ID 時需要考慮的三個問題。

5.2.1)消息 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.3Twitter

什麼?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.458 到家

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 架構技術乾貨 (上篇):整體架構、服務拆分等》

(**以上鍊接請從 52im 社區原文中點擊:**http://www.52im.net/thread-3472-1-1.html)

8、IM 需要解決的技術痛點

8.1 如何保證消息的實時性

PS:如果你還不瞭解 IM 裏的消息實時性是什麼,務必先讀這篇《零基礎 IM 開發入門 (二):什麼是 IM 系統的實時性?》;

在通信協議的選擇上,我們主要有以下幾個選擇:

1)使用 TCP Socket 通信,自己設計協議:58 到家等等;

2)使用 UDP Socket 通信:QQ 等等(見《爲什麼 QQ 用的是 UDP 協議而不是 TCP 協議?》);

3)使用 HTTP 長輪循:微信網頁版等等。

不管使用哪種方式,我們都能夠做到消息的實時通知,但影響我們消息實時性的可能會在我們處理消息的方式上。

例如:假如我們推送的時候使用 MQ 去處理並推送一個萬人羣的消息,推送一個人需要 2ms,那麼推完一萬人需要 20s,那麼後面的消息就阻塞了 20s。如果我們需要在 10ms 內推完,那麼我們推送的併發度應該是:人數:10000 / (推送總時長:10 / 單個人推送時長:2) = 2000。

因此:我們在選擇具體的實現方案的時候一定要評估好我們系統的吞吐量,系統的每一個環節都要進行評估壓測。只有把每一個環節的吞吐量評估好了,才能保證消息推送的實時性。

IM 消息實時性中羣聊消息和單聊消息的處理又有很大區別,有興趣可以深入閱讀:

1)《IM 消息送達保證機制實現 (一):保證在線實時消息的可靠投遞》:

http://www.52im.net/thread-294-1-1.html

2)《移動端 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 消息亂序的可能性還有很多,這裏就不一一展開,以下幾篇值得深入閱讀。

1)《如何保證 IM 實時消息的 “時序性” 與“一致性”?》:

http://www.52im.net/thread-714-1-1.html

2)《一個低成本確保 IM 消息時序的方法探討》:

http://www.52im.net/thread-866-1-1.html

3)一套億級用戶的 IM 架構技術乾貨 (下篇):可靠性、有序性、弱網優化等

8.3 用戶在線狀態如何做

很多 IM 系統都需要展示用戶的狀態:是否在線,是否忙碌等。

要實現用戶在線狀態的存儲,主要可以使用:

1)Redis;

2)分佈式一致性哈希來。

Redis 存儲用戶在線狀態:

看上面的圖可能會有人疑惑:爲什麼每次心跳都需要更新 Redis?

如果我使用的是 TCP 長連接那是不是就不用每次心跳都更新了?

確實:正常情況下服務器只需要在新建連接或者斷開連接的時候更新一下 Redis 就好了。但由於服務器可能會出現異常,或者服務器跟 Redis 之間的網絡會出現問題,此時基於事件的更新就會出現問題,導致用戶狀態不正確。因此,如果需要用戶在線狀態準確的話最好通過心跳來更新在線狀態。

由於 Redis 是單機存儲的,因此,爲了提高可靠性跟性能,我們可以使用 Redis Cluster 或者 Codis。

分佈式一致性哈希存儲用戶在線狀態:

使用分佈式一致性哈希需要注意在對 Status Server Cluster 進行擴容或者縮容的時候要先對用戶狀態進行遷移,不然在剛操作時會出現用戶狀態不一致的情況。同時還需要使用虛擬節點避免數據傾斜的問題。

PS:用戶狀態在客戶端的更新也是個很有挑戰性的問題,有興趣可以讀一下《IM 單聊和羣聊中的在線狀態同步應該用 “推” 還是“拉”?》:http://www.52im.net/thread-715-1-1.html。

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。

微信團隊分享的這篇《微信後臺基於時間序的海量數據冷熱分級架構設計實踐》:http://www.52im.net/thread-895-1-1.html,或許可以有些啓發。

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》:http://www.52im.net/thread-464-1-1.html。

10、參考資料

[1] 58 到家實時消息系統的架構設計及技術選型經驗總結:

http://www.52im.net/thread-300-1-1.html

[2] 一套海量在線用戶的移動端 IM 架構設計實踐分享 (含詳細圖文):

http://www.52im.net/thread-812-1-1.html

[3] 現代 IM 系統中聊天消息的同步和存儲方案探討

[4] 一套億級用戶的 IM 架構技術乾貨 (上篇):整體架構、服務拆分等
[5] 微信的海量 IM 聊天消息序列號生成實踐(容災方案篇):

http://www.52im.net/thread-1999-1-1.html

[6] 快速裂變:見證微信強大後臺架構從 0 到 1 的演進歷程(二):

http://www.52im.net/thread-170-1-1.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/3M48kkOzdfd0uCQOs5urmQ