(一)IM系統技術挑戰
可靠性
IM消息系統的可靠性,通常就是指消息投遞的可靠性,即我們經常聽到的“消息必達”,通常用消息的不丢失和不重複兩個技術名額來表示。確定消息被發送後,能被接收者收到。由于網絡環境的複雜性,以及使用者線上的不确定性,消息的可靠性(不丢失、不重複)無疑是IM系統的核心名額,也是IM系統實作中的難點之一。總體來說,IM系統的消息“可靠性”,通常就是指聊天消息投遞的可靠性(準确的說,這個“消息”是廣義的,因為還存使用者看不見的各種指令和通知,包括但不限于進群退群通知、好友添加通知等,為了友善描述,統稱“消息”)。
從消息發送者和接收者使用者行為來講,消息“可靠性”應該分為以下幾種情況:
(1)發送失敗,對于這種情況IM系統必須要感覺到,明确回報發送方。如果此消息沒有發送成功,發送方可以選擇重試或者稍後再試。
(2)發送成功,如果接收方處在“線上”狀态,應該立即收到此消息。如果接收方處在“離線”狀态不能收到消息,一旦上線則立刻收到消息。
(3)消息不能重複,用數學術語表示:“有且僅有這條消息”,如果重複了,可能表達的意思就變了。
總之,一個商用 IM系統,必須包含消息“可靠性”邏輯,才能談基本可用,這是IM系統最基本也是最核心的邏輯。
有序性(一緻性)
IM系統中,特别需要考慮消息時序問題,如果後發送的消息先顯示,可能嚴重擾亂聊天消息所要表達的意義,會造成聊天語義不連貫,引起誤會。消息的時序性,也稱為消息收發一緻性,主要目标是:保證聊天消息的絕對時序。IM系統中消息時序的一緻性問題看似簡單,實則是非常有難度的技術熱點話題之一。為什麼會出現時序問題 1、分布式系統的出現導緻時序不一緻。IM系統子產品衆多,接入層、消息邏輯層等、每層都分布式叢集化,這些應用分布在不同的機器上,如何保證時序是個難點。2、網絡傳輸延遲導緻時序不一緻。不同使用者發送的消息到達伺服器的延時差異較大,給消息時序性帶來挑戰。
消息時序是分布式系統架構設計中非常難的問題,一個分布式的IM系統必須要解決這個問題,如何高效、低成本解決這個問題,是我們OpenIM要考慮的方向。
實時性
實時性,即消息實時到達接收方,如果使用者線上,則實時可達,如果使用者不線上,則登入時可達。由于網絡波動,以及移動端作業系統對應用前背景切換的管理,如何實作使用者連接配接管理、消息實時推送,推送失敗的處理方式,用戶端重連機制,消息如何補齊等,都是需要IM系統考慮,同時要結合移動端的特點,兼顧耗電量,網絡,性能等。由于TCP開發略微複雜,早期的基于HTTP短輪詢、長輪詢的低效的技術方案,也無法達到實時性的要求。
擴充性
一般來說網際網路系統的擴充性包含多個含義,我們側重講解關于IM消息的擴充性。IM業務特性多,功能豐富,從聊天類型來看,分為:單聊、群聊,聊天室等;從消息類型來看,分為:文本、圖檔、視訊、地理位置、自定義消息等;從消息功能來看,分為:撤回、線上狀态、對方正在輸入、閱後即焚等;從通知角度來看,分為:進群、退群、添加好友、驗證好友等各種通知。如何有效支撐、擴充功能,高效實作,是考驗IM擴充性的一個方面,也是對系統架構設計能力的考驗。為了更好地提高資料通道對業務支撐的擴充性,我們首創了“一切皆消息”的消息模型,即通訊雙方産生的所有消息、通知,服務端以消息統一處理,扮演了消息通道的角色,用戶端針對不同消息類型做不同的UI展示,完美解決了擴充性問題。
IM系統在網際網路初期即存在,其基礎技術架構在這十幾年的發展中更新疊代多次,從早期的CS、P2P架構,到現在背景已經演變為一個複雜的分布式系統,涉及移動端、網絡通信、協定、安全、存儲和搜尋等技術的方方面面。IM系統中最核心的部分是消息系統,消息系統中最核心的功能是消息的同步、存儲:
- 消息的同步 :将消息完整的、快速的從發送方傳遞到接收方,就是消息的同步。消息同步系統最重要的衡量名額就是消息傳遞的實時性、完整性以及能支撐的消息規模。從功能上來說,一般至少要支援線上和離線推送,進階的IM系統還支援『多端同步』。
- 消息的存儲 :消息存儲即消息的持久化儲存,傳統消息系統通常隻能支援消息在接收端的本地存儲,資料基本不具備可靠性。現代消息系統能支援消息在服務端的線上存儲,功能上對應的就是『消息漫遊』,消息漫遊的好處是可以實作賬号在任意端登陸檢視所有曆史消息。
本篇文章内容主要涉及IM系統中的消息系統架構,會介紹一種基于阿裡雲表格存儲Tablestore的Timeline模型建構的消息系統。基于Tablestore Timeline建構的現代消息系統,能夠同時支援消息系統的衆多進階特性,包括『多端同步』、『消息漫遊』和『線上檢索』。在性能和規模上,能夠做到全量消息雲端存儲和索引,百萬TPS寫入以及毫秒級延遲的消息同步和檢索能力。
表格存儲(Tablestore) 是 NoSQL 多模型資料庫,提供海量結構化資料存儲以及快速的查詢和分析服務。表格存儲的分布式存儲和強大的索引引擎能夠提供 PB 級存儲、千萬 TPS 以及毫秒級延遲的服務能力。
Timeline是 基于“基于時間軸”的技術模型。
(二)傳統架構 vs 現代架構
傳統架構 下,消息是先同步後存儲。對于線上的使用者,消息會直接實時同步到線上的接收方,消息同步成功後,并不會在服務端持久化。而對于離線的使用者或者消息無法實時同步成功時,消息會持久化到離線庫,當接收方重新連接配接後,會從離線庫拉取所有未讀消息。當離線庫中的消息成功同步到接收方後,消息會從離線庫中删除。傳統的消息系統,服務端的主要工作是維護發送方和接收方的連接配接狀态,并提供線上消息同步和離線消息緩存的能力,保證消息一定能夠從發送方傳遞到接收方。服務端不會對消息進行持久化,是以也無法支援消息漫遊。消息的持久化存儲及索引同樣隻能在接收端本地實作,資料可靠性極低。
現代架構 下,消息是先存儲後同步。先存儲後同步的好處是,如果接收方确認接收到了消息,那這條消息一定是已經在雲端儲存了。并且消息會有兩個庫來儲存,一個是消息存儲庫,用于全量儲存所有會話的消息,主要用于支援消息漫遊。另一個是消息同步庫,主要用于接收方的多端同步。消息從發送方發出後,經過服務端轉發,服務端會先将消息儲存到消息存儲庫,後儲存到消息同步庫。完成消息的持久化儲存後,對于線上的接收方,會直接選擇線上推送。但線上推送并不是一個必須路徑,隻是一個更優的消息傳遞路徑。對于線上推送失敗或者離線的接收方,會有另外一個統一的消息同步方式。接收方會主動的向服務端拉取所有未同步消息,但接收方何時來同步以及會在哪些端來同步消息對服務端來說是未知的,是以要求服務端必須儲存所有需要同步到接收方的消息,這是消息同步庫的主要作用。對于新的同步裝置,會有消息漫遊的需求,這是消息存儲庫的主要作用,在消息存儲庫中,可以拉取任意會話的全量曆史消息。消息檢索的實作依賴于對消息存儲庫内消息的索引,通常是一個近實時(NRT,near real time)的索引建構過程,這個索引同樣是線上的。
以上就是傳統架構和現代架構的一個簡單的對比,現代架構上整個消息的同步、存儲和索引流程,并沒有變複雜太多。現代架構的實作本質上是把傳統架構内本地存儲和索引都搬到雲上,最大挑戰是需要集中管理全量消息的存儲和索引,帶來的好處是能實作多端同步 、消息漫遊 以及線上檢索 。可以看到現代架構中最核心的就是兩個消息庫『消息同步庫』和『消息存儲庫』,以及對『消息存儲庫』的『消息索引』的實作,接下來我們逐漸拆解這幾個核心的設計和實作。
(三)基礎模型
在深入講解消息系統的設計和實作之前,需要對消息系統内的幾個基本概念和基礎模型有一個了解。網上分析的很多的不同類型的消息系統實作,實作差異上主要在消息同步和存儲的方案上,在消息的資料模型上其實有很大的共性。圍繞資料同步模型的讨論主要在『讀擴散』、『寫擴散』和『混合模式』這三種方案,目前還沒有更多的選擇。而對于資料模型的抽象,還沒有一個标準的定義。
本章節會介紹下表格存儲Tablestore提出的Timeline模型,這是一個對消息系統内消息模型的一個抽象,能簡化和更好的讓開發者了解消息系統内的消息同步和存儲模型,基于此模型我們會再深入探讨消息的同步和存儲的選擇和實作。
Timeline模型
Timeline是一個對消息抽象的邏輯模型,該模型會幫助我們簡化對消息同步和存儲模型的了解,而消息同步庫和存儲庫的設計和實作也是圍繞Timeline的特性和需求來展開。
如圖是Timeline模型的一個抽象表述,Timeline可以簡單了解為是一個消息隊列,但這個消息隊列有如下特性:
- 每條消息對應一個順序ID :每個消息擁有一個唯一的順序ID(SequenceId),隊列消息按SequenceId排序。
- 新消息寫入能自動配置設定遞增的順序ID,保證永遠插入隊尾 :Timeline中是根據同步位點也就是順序ID來同步消息,是以需要保證新寫入的消息資料的順序ID絕對不能比已同步的消息的順序ID還小,否則會導緻資料漏同步,是以需要支援對新寫入的資料自動配置設定比目前已存儲的所有消息的順序ID更大的順序ID。
- 新消息寫入也能自定義順序ID,滿足自定義排序需求 :上面提到的自動配置設定順序ID,主要是為了滿足消息同步的需求,消息同步要求消息是根據『已同步』或是『已寫入』的順序來排序。而消息的存儲,通常要求消息能根據會話順序來排序,會話順序通常由端的會話來決定,而不是服務端的同步順序來定,這是兩種順序要求。
- 支援根據順序ID的随機定位 :可根據SequenceId随機定位到Timeline中的某個位置,從這個位置開始正序或逆序的讀取消息,也可支援讀取指定順序ID的某條消息。
- 支援對消息的自定義索引 :消息體内資料根據業務不同會包含不同的字段,Timeline需要支援對不同字段的自定義索引,來支援對消息内容的全文索引,或者是任意字段的靈活條件組合查詢。
消息同步 可以基于Timeline很簡單的實作,圖中的例子中,消息發送方是A,消息接收方是B,同時B存在多個接收端,分别是B1、B2和B3。A向B發送消息,消息需要同步到B的多個端,待同步的消息通過一個Timeline來進行交換。A向B發送的所有消息,都會儲存在這個Timeline中,B的每個接收端都是獨立的從這個Timeline中拉取消息。每個接收端同步完畢後,都會在本地記錄下最新同步到的消息的SequenceId,即最新的一個位點,作為下次消息同步的起始位點。服務端不會儲存各個端的同步狀态,各個端均可以在任意時間從任意點開始拉取消息。
消息存儲 也是基于Timeline實作,和消息同步唯一的差別是,消息存儲要求服務端能夠對Timeline内的所有資料進行持久化,并且消息采用會話順序來儲存,需要自定義順序ID。
消息檢索 基于Timeline提供的消息索引來實作,能支援比較靈活的多字段索引,根據業務的不同可有自由度較高的定制。
消息存儲模型
如圖是基于Timeline的消息存儲模型,消息存儲要求每個會話都對應一個獨立的Timeline。如圖例子所示,A與B/C/D/E/F均發生了會話,每個會話對應一個獨立的Timeline,每個Timeline記憶體有這個會話中的所有消息,消息根據會話順序排序,服務端會對每個Timeline進行持久化存儲,也就擁有了消息漫遊的能力。
消息同步模型
消息同步模型會比消息存儲模型稍複雜一些,消息的同步一般有讀擴散(也叫拉模式)和寫擴散(也叫推模式)兩種不同的方式,分别對應不同的Timeline實體模型
如圖是讀擴散和寫擴散兩種不同同步模式下對應的不同的Timeline模型,按圖中的示例,A作為消息接收者,其與B/C/D/E/F發生了會話,每個會話中的新的消息都需要同步到A的某個端,看下讀擴散和寫擴散兩種模式下消息如何做同步。
- 讀擴散 :消息存儲模型中,每個會話的Timeline中儲存了這個會話的全量消息。讀擴散的消息同步模式下,每個會話中産生的新的消息,隻需要寫一次到其用于存儲的Timeline中,接收端從這個Timeline中拉取新的消息。優點是消息隻需要寫一次,相比寫擴散的模式,能夠大大降低消息寫入次數,特别是在群消息這種場景下。但其缺點也比較明顯,接收端去同步消息的邏輯會相對複雜和低效。接收端需要對每個會話都拉取一次才能擷取全部消息,讀被大大的放大,并且會産生很多無效的讀,因為并不是每個會話都會有新消息産生。
-
寫擴散 :寫擴散的消息同步模式,需要有一個額外的Timeline來專門用于消息同步,通常是每個接收端都會擁有一個獨立的同步Timeline(或者叫收件箱),用于存放需要向這個接收端同步的所有消息。每個會話中的消息,會産生多次寫,除了寫入用于消息存儲的會話Timeline,還需要寫入需要同步到的接收端的同步Timeline。在個人與個人的會話中,消息會被額外寫兩次,除了寫入這個會話的存儲Timeline,還需要寫入參與這個會話的兩個接收者的同步Timeline。而在群這個場景下,寫入會被更加的放大,如果這個群擁有N個參與者,那每條消息都需要額外的寫N次。寫擴散同步模式的優點是,在接收端消息同步邏輯會非常簡單,隻需要從其同步Timeline中讀取一次即可,大大降低了消息同步所需的讀的壓力。其缺點就是消息寫入會被放大,特别是針對群這種場景。
Timeline模型不會對選擇讀擴散還是寫擴散做限制,而是能同時支援兩種模式,因為本質上兩種模式的邏輯資料模型并無差别,隻是消息資料是用一個Timeline來支援多端讀還是複制到多個Timeline來支援多端讀的問題。
針對IM這種應用場景,消息系統通常會選擇寫擴散這種消息同步模式。IM場景下,一條消息隻會産生一次,但是會被讀取多次,是典型的讀多寫少的場景,消息的讀寫比例大概是10:1。若使用讀擴散同步模式,整個系統的讀寫比例會被放大到100:1。一個優化的好的系統,必須從設計上去平衡這種讀寫壓力,避免讀或寫任意一維觸碰到天花闆。是以IM系統這類場景下,通常會應用寫擴散這種同步模式,來平衡讀和寫,将100:1的讀寫比例平衡到30:30。當然寫擴散這種同步模式,還需要處理一些極端場景,例如萬人大群。針對這種極端寫擴散的場景,會退化到使用讀擴散。一個簡單的IM系統,通常會在産品層面限制這種大群的存在,而對于一個進階的IM系統,會采用讀寫擴散混合的同步模式,來滿足這類産品的需求。采用混合模式,會根據資料的不同類型和不同的讀寫負載,來決定用寫擴散還是讀擴散。
(四)典型架構設計
如圖是一個典型的消息系統架構,架構中包含幾個重要元件:
- 端 :作為消息的發送和接收端,通過連接配接消息伺服器來發送和接收消息。
- 消息伺服器 :一組無狀态的伺服器,可水準擴充,處理消息的發送和接收請求,連接配接後端消息系統。
- 消息隊列 :新寫入消息的緩沖隊列,消息系統的前置消息存儲,用于削峰填谷以及異步消費。
- 消息處理 :一組無狀态的消費處理伺服器,用于異步消費消息隊列中的消息資料,處理消息的持久化和寫擴散同步。
- 消息存儲和索引庫 :持久化存儲消息,每個會話對應一個Timeline進行消息存儲,存儲的消息建立索引來實作消息檢索。
-
消息同步庫 :寫擴散形式同步消息,每個使用者的收件箱對應一個Timeline,同步庫内消息不需要永久儲存,通常對消息設定一個生命周期。
新消息會由端發出,通常消息體中會攜帶消息ID(用于去重)、邏輯時間戳(用于排序)、消息類型(控制消息、圖檔消息或者文本消息等)、消息體等内容。消息會先寫入消息隊列,作為底層存儲的一個臨時緩沖區。消息隊列中的消息會由消息處理伺服器消費,可以允許亂序消費。消息處理伺服器對消息先存儲後同步,先寫入發件箱Timeline(存儲庫),後寫擴散至各個接收端的收件箱(同步庫)。消息資料寫入存儲庫後,會被近實時的建構索引,索引包括文本消息的全文索引以及多字段索引(發送方、消息類型等)。
對于線上的裝置,可以由消息伺服器主動推送至線上裝置端。對于離線裝置,登入後會主動向服務端同步消息。每個裝置會在本地保留有最新一條消息的順序ID,向服務端同步該順序ID後的所有消息。