乾貨|現代IM系統中訊息推送和儲存架構的實現
摘要:前言 IM全稱是『Instant Messaging』,中文名是即時通訊。在這個高度資訊化的移動網際網路時代,生活中IM類產品已經成為必備品,比較有名的如釘釘、微信、QQ等以IM為核心功能的產品。當然目前微信已經成長為一個生態型產品,但其核心功能還是IM。
前言
IM全稱是『Instant Messaging』,中文名是即時通訊。在這個高度資訊化的移動網際網路時代,生活中IM類產品已經成為必備品,比較有名的如釘釘、微信、QQ等以IM為核心功能的產品。當然目前微信已經成長為一個生態型產品,但其核心功能還是IM。還有一些非以IM系統為核心的應用,最典型的如一些線上遊戲、社交應用,IM也是其重要的功能模組。可以說,帶有社交屬性的應用,IM功能一定是必不可少的。
IM系統在網際網路初期即存在,其基礎技術架構在這十幾年的發展中更新迭代多次,從早期的CS、P2P架構,到現在後臺已經演變為一個複雜的分散式系統,涉及移動端、網路、安全和儲存等技術的方方面面。其支撐的規模也從早期的少量日活,到現在微信這個巨頭最新公佈的達到9億的日活的體量。
IM系統中最核心的部分是訊息系統,訊息系統中最核心的功能是訊息的同步和儲存:
訊息的同步:將訊息完整的、快速的從傳送方傳遞到接收方,就是訊息的同步。訊息同步系統最重要的衡量指標就是訊息傳遞的實時性、完整性以及能支撐的訊息規模。從功能上來說,一般至少要支援線上和離線推送,高階的IM系統還支援『多端同步』。
訊息的儲存:訊息儲存即訊息的持久化儲存,這裡不是指訊息在客戶端本地的儲存,而是指雲端的儲存,功能上對應的就是『訊息漫遊』。『訊息漫遊』的好處是可以實現賬號在任意端登陸檢視所有歷史訊息,這也是高階IM系統特有的功能之一。
本篇文章內容主要涉及IM系統中的訊息系統架構,會介紹一種基於TableStore構建的訊息同步以及儲存系統的架構實現,能夠支援訊息系統中的高階特性『多端同步』以及『訊息漫遊』。在效能和規模上,能夠做到全量訊息雲端儲存,百萬TPS以及毫秒級延遲的訊息同步能力。
架構設計
本章主要會介紹基於TableStore的現代IM訊息系統的架構設計,在詳細介紹架構設計之前,會先介紹一種Timeline邏輯模型,來抽象和簡化對IM訊息同步和儲存模型的理解。理解了Timeline模型後,會介紹如何基於此模型對訊息的同步以及儲存進行建模。基於Timeline模型,在實現訊息同步和儲存時還會有各方面的技術權衡,例如如何對訊息同步常見的讀擴散和寫擴散兩種模型進行對比和選擇,以及針對Timeline模型的特徵如何來選擇底層資料庫。
傳統架構 vs 現代架構
上圖是訊息系統傳統架構與現代架構的簡單對比。
傳統架構下,訊息是先同步後儲存。對於線上的使用者,訊息會直接實時同步到線上的接收方,訊息同步成功後,並不會進行持久化。而對於離線的使用者或者訊息無法實時同步成功時,訊息會持久化到離線庫,當接收方重新連線後,會從離線庫拉取所有未讀訊息。當離線庫中的訊息成功同步到接收方後,訊息會從離線庫中刪除。傳統的訊息系統,服務端的主要工作是維護髮送方和接收方的連線狀態,並提供線上訊息同步和離線訊息快取的能力,保證訊息一定能夠從傳送方傳遞到接收方。服務端不會對訊息進行持久化,所以也無法支援訊息漫遊。
現代架構下,訊息是先儲存後同步。先儲存後同步的好處是,如果接收方確認接收到了訊息,那這條訊息一定是已經在雲端儲存了。並且訊息會有兩個庫來儲存,一個是訊息儲存庫,用於全量儲存所有會話的訊息,主要用於支援訊息漫遊。另一個是訊息同步庫,主要用於接收方的多端同步。訊息從傳送方發出後,經過服務端轉發,服務端會先將訊息儲存到訊息儲存庫,後儲存到訊息同步庫。完成訊息的持久化儲存後,對於線上的接收方,會直接選擇線上推送。但線上推送並不是一個必須路徑,只是一個更優的訊息傳遞路徑。對於線上推送失敗或者離線的接收方,會有另外一個統一的訊息同步方式。接收方會主動的向服務端拉取所有未同步訊息,但接收方何時來同步以及會在哪些端來同步訊息對服務端來說是未知的,所以要求服務端必須儲存所有需要同步到接收方的訊息,這是訊息同步庫的主要作用。對於新的同步裝置,會有訊息漫遊的需求,這是訊息儲存庫的主要作用,在訊息儲存庫中,可以拉取任意會話的全量歷史訊息。
以上是傳統架構和現代架構的一個簡單的對比,現代架構上整個訊息的同步和儲存流程,並沒有變複雜太多,但是其能實現多端同步以及訊息漫遊。現代架構中最核心的就是兩個訊息庫『訊息同步庫』和『訊息儲存庫』,是訊息同步和儲存最核心的基礎。而本篇文章接下來的部分,都是圍繞這兩個庫的設計和實現來展開。
Timeline模型
在分析『訊息同步庫』和『訊息儲存庫』的設計和實現之前,在本章會先介紹一個邏輯模型-Timeline。Timeline模型會幫助我們簡化對訊息同步和儲存模型的理解,而訊息庫的設計和實現也是圍繞Timeline的特性和需求來展開。
如圖是Timeline模型的一個抽象表述,Timeline可以簡單理解為是一個訊息佇列,但這個訊息佇列有如下特性:
每個訊息擁有一個順序ID(SeqId),在佇列後面的訊息的SeqId一定比前面的訊息的SeqId大,也就是保證SeqId一定是增長的,但是不要求嚴格遞增。
新的訊息永遠在尾部新增,保證新的訊息的SeqId永遠比已經存在佇列中的訊息都大。
可根據SeqId隨機定位到具體的某條訊息進行讀取,也可以任意讀取某個給定範圍內的所有訊息。
有了這些特性後,訊息的同步可以拿Timeline來很簡單的實現。圖中的例子中,訊息傳送方是A,訊息接收方是B,同時B存在多個接收端,分別是B1、B2和B3。A向B傳送訊息,訊息需要同步到B的多個端,待同步的訊息通過一個Timeline來進行交換。A向B傳送的所有訊息,都會儲存在這個Timeline中,B的每個接收端都是獨立的從這個Timeline中拉取訊息。每個接收端同步完畢後,都會在本地記錄下最新同步到的訊息的SeqId,即最新的一個位點,作為下次訊息同步的起始位點。服務端不會儲存各個端的同步狀態,各個端均可以在任意時間從任意點開始拉取訊息。
訊息漫遊也是基於Timeline,和訊息同步唯一的區別是,訊息漫遊要求服務端能夠對Timeline內的所有資料進行持久化。
基於Timeline,從邏輯模型上能夠很簡單的理解在服務端如何去實現訊息同步和儲存,並支援多端同步和訊息漫遊這些高階功能。落地到實現的難點主要在如何將邏輯模型對映到物理模型,Timeline的實現對資料庫會有哪些要求?我們應該選擇何種資料庫去實現?這些是接下來會討論到的問題。
訊息儲存模型
如圖是基於Timeline的訊息儲存模型,訊息儲存要求每個會話都對應一個獨立的Timeline。如圖例子所示,A與B/C/D/E/F均發生了會話,每個會話對應一個獨立的Timeline,每個Timeline記憶體有這個會話中的所有訊息,服務端會對每個Timeline進行持久化。服務端能夠對所有會話Timeline中的全量訊息進行持久化,也就擁有了訊息漫遊的能力。
訊息同步模型
訊息同步模型會比訊息儲存模型稍複雜一些,訊息的同步一般有讀擴散和寫擴散兩種不同的方式,分別對應不同的Timeline物理模型。
如圖是讀擴散和寫擴散兩種不同同步模式下對應的不同的Timeline模型,按圖中的示例,A作為訊息接收者,其與B/C/D/E/F發生了會話,每個會話中的新的訊息都需要同步到A的某個端,看下讀擴散和寫擴散兩種模式下訊息如何做同步。
讀擴散:訊息儲存模型中,每個會話的Timeline中儲存了這個會話的全量訊息。讀擴散的訊息同步模式下,每個會話中產生的新的訊息,只需要寫一次到其用於儲存的Timeline中,接收端從這個Timeline中拉取新的訊息。優點是訊息只需要寫一次,相比寫擴散的模式,能夠大大降低訊息寫入次數,特別是在群訊息這種場景下。但其缺點也比較明顯,接收端去同步訊息的邏輯會相對複雜和低效。接收端需要對每個會話都拉取一次才能獲取全部訊息,讀被大大的放大,並且會產生很多無效的讀,因為並不是每個會話都會有新訊息產生。
寫擴散:寫擴散的訊息同步模式,需要有一個額外的Timeline來專門用於訊息同步,通常是每個接收端都會擁有一個獨立的同步Timeline,用於存放需要向這個接收端同步的所有訊息。每個會話中的訊息,會產生多次寫,除了寫入用於訊息儲存的會話Timeline,還需要寫入需要同步到的接收端的同步Timeline。在個人與個人的會話中,訊息會被額外寫兩次,除了寫入這個會話的儲存Timeline,還需要寫入參與這個會話的兩個接收者的同步Timeline。而在群這個場景下,寫入會被更加的放大,如果這個群擁有N個參與者,那每條訊息都需要額外的寫N次。寫擴散同步模式的優點是,在接收端訊息同步邏輯會非常簡單,只需要從其同步Timeline中讀取一次即可,大大降低了訊息同步所需的讀的壓力。其缺點就是訊息寫入會被放大,特別是針對群這種場景。
在IM這種應用場景下,通常會選擇寫擴散這種訊息同步模式。IM場景下,一條訊息只會產生一次,但是會被讀取多次,是典型的讀多寫少的場景,訊息的讀寫比例大概是10:1。若使用讀擴散同步模式,整個系統的讀寫比例會被放大到100:1。一個優化的好的系統,必須從設計上去平衡這種讀寫壓力,避免讀或寫任意一維觸碰到天花板。所以IM系統這類場景下,通常會應用寫擴散這種同步模式,來平衡讀和寫,將100:1的讀寫比例平衡到30:30。當然寫擴散這種同步模式,還需要處理一些極端場景,例如萬人大群。針對這種極端寫擴散的場景,會退化到使用讀擴散。一個簡單的IM系統,通常會在產品層面限制這種大群的存在,而對於一個高階的IM系統,會採用讀寫擴散混合的同步模式,來滿足這類產品的需求。
訊息庫設計
基於Timeline模型,以及Timeline模型在訊息儲存和訊息同步的應用,我們看下訊息同步庫和訊息儲存庫的設計。
如圖是基於Timeline的訊息庫設計。
訊息同步庫:訊息同步庫用於儲存所有用於訊息同步的Timeline,每個Timeline對應一個接收端,主要用作寫擴散模式的訊息同步。這個庫不需要永久保留所有需要同步的訊息,因為訊息在同步到所有端後其生命週期就可以結束,就可以被回收。但是如前面所介紹的,一個實現簡單的多端同步訊息系統,在服務端不會儲存有所有端的同步狀態,而是依賴端自己主動來做同步。所以服務端不知道訊息何時可以回收,通常的做法是為這個庫裡的訊息設定一個固定的生命週期,例如一週或者一個月,生命週期結束可被淘汰。
訊息儲存庫:訊息儲存庫用於儲存所有會話的Timeline,每個Timeline包含了一個會話中的所有訊息。這個庫主要用於訊息漫遊時拉取某個會話的所有歷史訊息,也用於讀擴散模式的訊息同步。
訊息同步庫和訊息儲存庫,對資料庫有不同的要求,如何對資料庫做選型,在下面會討論。
資料庫選型
訊息系統最核心的兩個庫是訊息同步庫和訊息儲存庫,兩個庫對資料庫有不同的要求:
總結下來,對資料庫的要求有如下幾點:
1. 表結構設計能夠滿足Timeline模型的功能要求:不要求關係模型,能夠實現佇列模型,並能夠支援生成自增的SeqId。
2. 能夠支援高併發寫和範圍讀,規模在十萬級TPS。
3. 能夠儲存海量資料,百TB級。
4. 能夠為資料定義生命週期。
阿里雲表格儲存(TableStore)是基於LSM儲存引擎的分散式NoSQL資料庫,支援百萬TPS高併發讀寫,PB級資料儲存,資料支援TTL,能夠很好的滿足以上需求,並且支援自增列,能夠非常完美的設計和實現Timeline的物理模型。
架構實現
本章會以一段非常精簡的程式碼,來展示如何基於TableStore實現Timeline模型,並基於Timeline模型進行訊息儲存和推送。
這篇文章中給出的程式碼,主要目的是為了演示如何能夠實現一個精簡Timeline的最基本功能。馬上我們會推出一個完整的Timeline Library,來將基於Timeline進行訊息儲存和推送的程式碼的開發變得無比簡單。
所有示例程式碼基於如下SDK版本:
表結構設計
以上是建立Timeline表的示例程式碼,總共需要建立兩張表,一張表作為訊息同步庫,名稱為『PushTable』,另一張表作為訊息儲存庫,名稱為『StoreTable』。
推送和儲存實現
以上是模擬一個群內訊息同步和儲存的示例程式碼。群名稱為『TableStore(釘釘號:11789671)』,群內成員有『A, B, C, D, E』。群內新的訊息,需要先儲存到群的儲存Timeline(Timeline ID為群名稱),之後需要以寫擴散的模式推送到群內每個成員的同步Timeline(以群成員名稱作為Timeline ID)。
以上是拉取群內歷史訊息以及某個群成員進行訊息同步的示例程式碼,主要邏輯在syncMessages函式內。示例程式碼中,拉取訊息都是從seq_id為0開始,0為TableStore自增列中最小值,所以代表了從最小的一個位點開始拉取訊息,即拉取全量訊息。
後記
這篇文章主要介紹了現代IM系統中訊息推送和儲存架構的實現,基於邏輯的Timeline模型,我們可以很清晰明瞭的理解整個訊息推送和儲存的架構。基於TableStore,可以非常簡單的實現Timeline模型,其中自增列功能,完美的匹配了Timeline模型中所需要的最關鍵的SeqId自增。
TableStore(表格儲存)是阿里雲自主研發的專業級分散式NoSQL資料庫,是基於共享儲存的高效能、低成本、易擴充套件、全託管的半結構化資料儲存平臺,支撐網際網路和物聯網資料的高效計算與分析。IM系統的訊息推送和儲存場景,是TableStore在社交領域的重要應用之一。
基於Timeline的訊息儲存和推送模型,將不光應用在IM訊息系統中,還可應用在例如Feeds流、實時訊息同步、直播彈幕等場景。在Feeds流場景下,我們也有了比較深入的研究
看完本文有收穫?請轉發分享給更多人
歡迎關注“暢聊架構”,我們分享最有價值的網際網路技術乾貨文章,助力您成為有思想的全棧架構師,我們只聊網際網路、只聊架構!打造最有價值的架構師圈子和社群。
長按下方的二維碼可以快速關注我們