
前言
IM系統在互聯網初期即存在,其基礎技術架構在這十幾年的發(fā)展中更新迭代多次,從早期的CS、P2P架構,到現在后臺已經演變?yōu)橐粋復雜的分布式系統,涉及移動端、網絡通信、協議、安全、存儲和搜索等技術的方方面面。IM系統中最核心的部分是消息系統,消息系統中最核心的功能是消息的同步、存儲和檢索:
- 消息的同步:將消息完整、快速地從發(fā)送方傳遞到接收方,就是消息的同步。消息同步系統最重要的衡量指標就是消息傳遞的實時性、完整性以及能支撐的消息規(guī)模。從功能上來說,一般至少要支持在線和離線推送,高級的IM系統還支持『多端同步』。
- 消息的存儲:消息存儲即消息的持久化保存,傳統消息系統通常只能支持消息在接收端的本地存儲,數據基本不具備可靠性。現代消息系統能支持消息在服務端的在線存儲,功能上對應的就是『消息漫游』,消息漫游的好處是可以實現賬號在任意端登陸查看所有歷史消息。
- 消息的檢索:消息般是文本,所以支持全文檢索也是必備的能力之一。傳統消息系統通常來說也是只能支持消息的本地檢索,基于本地存儲的消息數據來構建。而現在消息系統在能支持消息的在線存儲后,也具備了消息的『在線檢索』能力。
本篇文章內容主要涉及IM系統中的消息系統架構,會介紹一種基于阿里云表格存儲 Tablestore 的 Timeline 模型構建的消息系統; Tablestore Timeline 構建的現代消息系統,能夠同時支持消息系統的眾多高級特性,包括『多端同步』、『消息漫游』和『在線檢索』。在性能和規(guī)模上,能夠做到全量消息云端存儲和索引,百萬 TPS 寫入以及毫秒級延遲的消息同步和檢索能力。
之后我們會繼續(xù)發(fā)表兩篇文章,來更詳細介紹 Tablestore Timeline 模型概念及使用:
- 模型篇:詳細介紹 Tablestore Timeline 模型的基本概念和基礎數據結構,并結合 IM 系統進行基本的建模。
- 實現篇:會基于 Tablestore Timeline 實現一個具備『多端同步』、『消息漫游』和『在線檢索』這些高級功能的簡易IM系統,并共享我們的源代碼。
傳統架構 vs 現代架構

傳統架構下,消息是先同步后存儲。對于在線的用戶,消息會直接實時同步到在線的接收方,消息同步成功后,并不會在服務端持久化。而對于離線的用戶或者消息無法實時同步成功時,消息會持久化到離線庫,當接收方重新連接后,會從離線庫拉取所有未讀消息。當離線庫中的消息成功同步到接收方后,消息會從離線庫中刪除。傳統的消息系統,服務端的主要工作是維護發(fā)送方和接收方的連接狀態(tài),并提供在線消息同步和離線消息緩存的能力,保證消息一定能夠從發(fā)送方傳遞到接收方。服務端不會對消息進行持久化,所以也無法支持消息漫游。消息的持久化存儲及索引同樣只能在接收端本地實現,數據可靠性極低。
現代架構下,消息是先存儲后同步。先存儲后同步的好處是,如果接收方確認接收到了消息,那這條消息一定是已經在云端保存了。并且消息會有兩個庫來保存,一個是消息存儲庫,用于全量保存所有會話的消息,主要用于支持消息漫游。另一個是消息同步庫,主要用于接收方的多端同步。消息從發(fā)送方發(fā)出后,經過服務端轉發(fā),服務端會先將消息保存到消息存儲庫,后保存到消息同步庫。
完成消息的持久化保存后,對于在線的接收方,會直接選擇在線推送。但在線推送并不是一個必須路徑,只是一個更優(yōu)的消息傳遞路徑。對于在線推送失敗或者離線的接收方,會有另外一個統一的消息同步方式。接收方會主動的向服務端拉取所有未同步消息,但接收方何時來同步以及會在哪些端來同步消息對服務端來說是未知的,所以要求服務端必須保存所有需要同步到接收方的消息,這是消息同步庫的主要作用。
對于新的同步設備,會有消息漫游的需求,這是消息存儲庫的主要作用,在消息存儲庫中,可以拉取任意會話的全量歷史消息。消息檢索的實現依賴于對消息存儲庫內消息的索引,通常是一個近實時(NRT,near real time)的索引構建過程,這個索引同樣是在線的。
以上就是傳統架構和現代架構的一個簡單的對比,現代架構上整個消息的同步、存儲和索引流程,并沒有變復雜太多,F代架構的實現本質上是把傳統架構內本地存儲和索引都搬到云上,最大挑戰(zhàn)是需要集中管理全量消息的存儲和索引,帶來的好處是能實現多端同步、消息漫游以及在線檢索?梢钥吹浆F代架構中最核心的就是兩個消息庫『消息同步庫』和『消息存儲庫』,以及對『消息存儲庫』的『消息索引』的實現,接下來我們逐步拆解這幾個核心的設計和實現。
基礎模型
在深入講解消息系統的設計和實現之前,需要對消息系統內的幾個基本概念和基礎模型有一個理解。網上分析的很多的不同類型的消息系統實現,實現差異上主要在消息同步和存儲的方案上,在消息的數據模型上其實有很大的共性。圍繞數據同步模型的討論主要在『讀擴散』、『寫擴散』和『混合模式』這三種方案,目前還沒有更多的選擇。而對于數據模型的抽象,還沒有一個標準的定義。
本章節(jié)會介紹下表格存儲 Tablestore 提出的 Timeline 模型,這是一個對消息系統內消息模型的一個抽象,能簡化和更好地讓開發(fā)者理解消息系統內的消息同步和存儲模型,基于此模型我們會再深入探討消息的同步和存儲的選擇和實現。
Timeline模型
Timeline是一個對消息抽象的邏輯模型,該模型會幫助我們簡化對消息同步和存儲模型的理解,而消息同步庫和存儲庫的設計和實現也是圍繞Timeline的特性和需求來展開。

如圖是 Timeline 模型的一個抽象表述,Timeline 可以簡單理解為是一個消息隊列,但這個消息隊列有如下特性:
- 每條消息對應一個順序ID:每個消息擁有一個唯一的順序ID(SequenceId),隊列消息按 SequenceId 排序。
- 新消息寫入能自動分配遞增的順序 ID,保證永遠插入隊尾:Timeline 中是根據同步位點也就是順序 ID 來同步消息,所以需要保證新寫入的消息數據的順序 ID 絕對不能比已同步的消息的順序 ID 還小,否則會導致數據漏同步,所以需要支持對新寫入的數據自動分配比當前已存儲的所有消息的順序 ID 更大的順序 ID 。
- 新消息寫入也能自定義順序 ID,滿足自定義排序需求:上面提到的自動分配順序 ID,主要是為了滿足消息同步的需求,消息同步要求消息是根據『已同步』或是『已寫入』的順序來排序。而消息的存儲,通常要求消息能根據會話順序來排序,會話順序通常由端的會話來決定,而不是服務端的同步順序來定,這是兩種順序要求。
- 支持根據順序 ID 的隨機定位:可根據 SequenceId 隨機定位到 Timeline 中的某個位置,從這個位置開始正序或逆序的讀取消息,也可支持讀取指定順序ID的某條消息。
- 支持對消息的自定義索引:消息體內數據根據業(yè)務不同會包含不同的字段, Timeline 需要支持對不同字段的自定義索引,來支持對消息內容的全文索引,或者是任意字段的靈活條件組合查詢。
消息同步可以基于 Timeline 很簡單的實現,圖中的例子中,消息發(fā)送方是A,消息接收方是B,同時B存在多個接收端,分別是B1、B2和B3。A向B發(fā)送消息,消息需要同步到B的多個端,待同步的消息通過一個 Timeline 來進行交換。A向B發(fā)送的所有消息,都會保存在這個 Timeline 中,B的每個接收端都是獨立的從這個 Timeline中拉取消息。每個接收端同步完畢后,都會在本地記錄下最新同步到的消息的SequenceId,即最新的一個位點,作為下次消息同步的起始位點。服務端不會保存各個端的同步狀態(tài),各個端均可以在任意時間從任意點開始拉取消息。
消息存儲也是基于 Timeline 實現,和消息同步唯一的區(qū)別是,消息存儲要求服務端能夠對 Timeline 內的所有數據進行持久化,并且消息采用會話順序來保存,需要自定義順序 ID。
消息檢索基于 Timeline 提供的消息索引來實現,能支持比較靈活的多字段索引,根據業(yè)務的不同可有自由度較高的定制。
消息存儲模型

如圖是基于 Timeline 的消息存儲模型,消息存儲要求每個會話都對應一個獨立的 Timeline 。如圖例子所示,A與B/C/D/E/F均發(fā)生了會話,每個會話對應一個獨立的 Timeline,每個 Timeline 內存有這個會話中的所有消息,消息根據會話順序排序,服務端會對每個 Timeline 進行持久化存儲,也就擁有了消息漫游的能力。
消息同步模型
消息同步模型會比消息存儲模型稍復雜一些,消息的同步一般有讀擴散(也叫拉模式)和寫擴散(也叫推模式)兩種不同的方式,分別對應不同的 Timeline 物理模型。

如圖是讀擴散和寫擴散兩種不同同步模式下對應的不同的 Timeline 模型,按圖中的示例,A作為消息接收者,其與B/C/D/E/F發(fā)生了會話,每個會話中的新的消息都需要同步到A的某個端,看下讀擴散和寫擴散兩種模式下消息如何做同步。
- 讀擴散:消息存儲模型中,每個會話的 Timeline 中保存了這個會話的全量消息。讀擴散的消息同步模式下,每個會話中產生的新的消息,只需要寫一次到其用于存儲的 Timeline 中,接收端從這個 Timeline 中拉取新的消息。優(yōu)點是消息只需要寫一次,相比寫擴散的模式,能夠大大降低消息寫入次數,特別是在群消息這種場景下。但其缺點也比較明顯,接收端去同步消息的邏輯會相對復雜和低效。接收端需要對每個會話都拉取一次才能獲取全部消息,讀被大大的放大,并且會產生很多無效的讀,因為并不是每個會話都會有新消息產生。
- 寫擴散:寫擴散的消息同步模式,需要有一個額外的 Timeline 來專門用于消息同步,通常是每個接收端都會擁有一個獨立的同步 Timeline(或者叫收件箱),用于存放需要向這個接收端同步的所有消息。每個會話中的消息,會產生多次寫,除了寫入用于消息存儲的會話 Timeline,還需要寫入需要同步到的接收端的同步 Timeline。在個人與個人的會話中,消息會被額外寫兩次,除了寫入這個會話的存儲 Timeline,還需要寫入參與這個會話的兩個接收者的同步 Timeline。而在群這個場景下,寫入會被更加的放大,如果這個群擁有N個參與者,那每條消息都需要額外地寫N次。寫擴散同步模式的優(yōu)點是,在接收端消息同步邏輯會非常簡單,只需要從其同步 Timeline 中讀取一次即可,大大降低了消息同步所需的讀的壓力。其缺點就是消息寫入會被放大,特別是針對群這種場景。
Timeline 模型不會對選擇讀擴散還是寫擴散做約束,而是能同時支持兩種模式,因為本質上兩種模式的邏輯數據模型并無差別,只是消息數據是用一個 Timeline 來支持多端讀還是復制到多個 Timeline 來支持多端讀的問題。
針對 IM 這種應用場景,消息系統通常會選擇寫擴散這種消息同步模式。IM 場景下,一條消息只會產生一次,但是會被讀取多次,是典型的讀多寫少的場景,消息的讀寫比例大概是10:1。若使用讀擴散同步模式,整個系統的讀寫比例會被放大到100:1。一個優(yōu)化的好的系統,必須從設計上去平衡這種讀寫壓力,避免讀或寫任意一維觸碰到天花板。所以 IM 系統這類場景下,通常會應用寫擴散這種同步模式,來平衡讀和寫,將100:1的讀寫比例平衡到30:30。當然寫擴散這種同步模式,還需要處理一些極端場景,例如萬人大群。
針對這種極端寫擴散的場景,會退化到使用讀擴散。一個簡單的IM系統,通常會在產品層面限制這種大群的存在,而對于一個高級的IM系統,會采用讀寫擴散混合的同步模式,來滿足這類產品的需求。采用混合模式,會根據數據的不同類型和不同的讀寫負載,來決定用寫擴散還是讀擴散。
典型架構設計

如圖是一個典型的消息系統架構,架構中包含幾個重要組件:
- 端:作為消息的發(fā)送和接收端,通過連接消息服務器來發(fā)送和接收消息。
- 消息服務器:一組無狀態(tài)的服務器,可水平擴展,處理消息的發(fā)送和接收請求,連接后端消息系統。
- 消息隊列:新寫入消息的緩沖隊列,消息系統的前置消息存儲,用于削峰填谷以及異步消費。
- 消息處理:一組無狀態(tài)的消費處理服務器,用于異步消費消息隊列中的消息數據,處理消息的持久化和寫擴散同步。
- 消息存儲和索引庫:持久化存儲消息,每個會話對應一個 Timeline 進行消息存儲,存儲的消息建立索引來實現消息檢索。
- 消息同步庫:寫擴散形式同步消息,每個用戶的收件箱對應一個 Timeline,同步庫內消息不需要永久保存,通常對消息設定一個生命周期。
新消息會由端發(fā)出,通常消息體中會攜帶消息ID(用于去重)、邏輯時間戳(用于排序)、消息類型(控制消息、圖片消息或者文本消息等)、消息體等內容。消息會先寫入消息隊列,作為底層存儲的一個臨時緩沖區(qū)。消息隊列中的消息會由消息處理服務器消費,可以允許亂序消費。消息處理服務器對消息先存儲后同步,先寫入發(fā)件箱 Timeline (存儲庫),后寫擴散至各個接收端的收件箱(同步庫)。消息數據寫入存儲庫后,會被近實時的構建索引,索引包括文本消息的全文索引以及多字段索引(發(fā)送方、消息類型等)。
對于在線的設備,可以由消息服務器主動推送至在線設備端。對于離線設備,登錄后會主動向服務端同步消息。每個設備會在本地保留有最新一條消息的順序ID,向服務端同步該順序ID后的所有消息。
總結
本篇文章主要介紹了現代 IM 系統中消息系統所需要具備的能力,對比了傳統架構和現代架構。為方便接下來的深入探討,介紹了表格存儲 Tablestore 推出的 Timeline 模型,以及在 IM 系統中消息存儲和消息同步模型的基本概念和策略,最后介紹了一個典型的架構設計。希望和同仁共同探討、交流。來源:阿里技術