開源OpenIM:高性能、可伸縮、易擴展的即時通訊架構


網上有很多關於IM的教程和技術博文,有億級用戶的IM架構,有各種淺談原創自研IM架構,也有微信技術團隊分享的技術文章,有些開發者想根據這些資料自研IM。理想很豐滿,現實很骨感,最后做出來的產品很難達到商用標准。事實上,很多架構沒有經過海量用戶的考驗,當然我們也不會評判某種架構的好壞,如果開發者企圖根據網上教程做出一個商用的IM,可能有點過於樂觀了。本文主要從我個人角度深度剖析100%開源的OpenIM架構。當然,世界上沒有最完美的架構,只有最合適的架構,也沒有所謂的通用方案,不同的解決方案都有其優缺點,只有最滿足業務的系統才是一個好的系統。而且,在有限的人力、物力,綜合考慮時間成本,通常需要做出很多權衡。我們OpenIM的設計初衷,充分考慮了中小企業的需求,輕量級部署,同時也支持集群擴展,能支持幾萬用戶,也能輕松擴展到上億用戶,是一個可信賴的開源項目。

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系統術語以及本文檔專有名詞解釋

conversationId:會話Id,會話是指用戶和用戶之間,以及用戶和群之間,進行通訊后產生的關聯。

userId:用戶Id:注冊使用IM的用戶Id,從消息的發送和接收來看有兩個身份:發送者和接收者

sendId:消息發送者Id

receiverId :消息接收者Id

msg:消息是指用戶之間的溝通內容,一般指用戶主動產生的。同時也包括用戶看不見的各種指令和通知,包括但不限於進群退群通知、好友添加通知等

inbox:用戶收件箱,給某人發送消息,實際上是往接收者“信箱”寫入消息,這個信箱就是收件箱

seq:用戶收件箱中消息序列號,分為local seq,和server seq,前者表示app本地消息seq,后者表示服務端消息seq,seq是連續且遞增的。

conn:登錄用戶的連接信息,用於消息推送;

MQ:消息隊列,一般用來解決應用解耦,異步消息,流量削峰等問題,實現高性能,高可用,可伸縮和最終一致性架構,本文采用kafka組件。

OpenIM的誕生

隨着移動互聯網的蓬勃發展, IM 作為一種通訊能力,已經成為互聯網上的基礎設施,也是許多 APP 不可或缺的功能。如何讓每一個應用都具備IM功能,同時考慮企業的接入成本、服務器資源以及最重要的數據安全性和私密性。本人從微信離職后,創辦了開源OpenIM,是全球首家100%開源、免費項目,並提供IMSDK,覆蓋所有主流開發平台,iOS、Android、Flutter、react native、Windows、Linux、Unity、web、小程序等。

開源IM現狀

github 上 IM 開源項目不少,但開發者卻難以使用,主要有幾點原因(1)個人項目居多,但近幾年都無人維護,遇到問題無人解決,企業商業化產品不敢冒險使用(2)大部分項目不是 IM 技術專業團隊完成的,技術實力和技術架構存疑,也沒有經過大項目和海量用戶檢驗;(3)只開源服務端或者客戶端,只開源某一端,需要開發者實現另外一端,研發成本同樣不小,另外,開源項目大部分都是以聊天app形式開源,開發者如何把 IM 集成到自身 app 中,同樣存在大量的修改和適配成本。(4)部分項目打着開源的旗號,社區版免費,但核心功能缺失,商業版收費。

雲服務商的弊端

IM 雲服務商提供 IM SDK 和 API ,讓開發者簡單集成 IM 功能,當然這里也存在明顯的問題(1)成本問題:企業每年額外支付上萬乃至數十萬的雲服務費用,從長期來看是個不小的成本;(2)數據隱私問題:企業的用戶數據、聊天記錄等核心數據托管在 IM 雲服務商,如何保證客戶的數據隱私和安全性;(3)需求定制問題:IM 需求多樣化,IM 功能只能由 IM 雲服務商通過 SDK 的形式提供給大家使用,開發受限,所有功能都需要封裝成接口;(4)捆綁問題:一旦使用 IM 雲服務,形成捆綁關系,遷移成本高,受制於人。

自研的尷尬

IM 是一個看起來門檻很低的項目,網上有很多所謂的 IM 開發教程,甚至很多畢業設計也是做一個 IM 系統。由於這個誤區的存在,很多企業盲目樂觀組建 3-5 人的 IM 團隊,歷時一年半載,最后只完成了一個 demo 版本。由於架構設計不合理,demo 版本存在消息丟失、系統異常等 bug,無法達到商用的要求。IM系統除了面臨互聯網業務系統本身的挑戰,還存在上文分析的可靠性、時序性、擴展性等問題,所以,自研IM,對於中小企業來說,可能是最糟糕的選擇。

OpenIM的整體架構

OpenIM分為兩大塊

(一)Open-IM-SDK-Core 采用golang實現客戶端邏輯,主要負責本地db存儲及更新;斷網重連及管理;消息及各種通知回調。本地消息、會話等數據存儲,通過通知機制完成本地數據實時同步,同時兼顧客戶端緩存的作用,有效緩解了服務端壓力。另外,golang跨平台的特性,使得各移動平台都能無縫調用,開發者只需根據產品需求編寫UI界面,通過回調機制和SDK完成數據交互和通知。

(二)Open-IM-Server 由接入層、邏輯層和存儲層組成,好處在於各層能夠依據業務特點專注於自己的事情,提高系統復用性,降低業務間的耦合。

(1)接入層:消息通過 websocket 協議接入,其他業務通過 http/https 協議提供REST API實現。消息是高頻及核心功能,通過雙協議路由,體現了輕重分離的設計思想。

(2)邏輯層:通過 rpc 實現無狀態邏輯服務,易於平行擴展,模塊通過 MQ 解耦。

(3)存儲層:redis 存儲 token 和 seq;mongodb 存儲離線消息,並定時刪除 14 天內(可自行配置)數據;mysql 存儲全量歷史消息以及用戶相關資料。數據分層存儲,充分利用不同存儲組件的特性。

(4)Etcd:服務注冊和發現、以及分布式配置中心。

消息網關msg_gateway

消息接入層,采用websocket協議接入,import gorilla具體實現,服務模塊無狀態,柔性伸縮,運維簡單。通過MQ讓業務模塊之間解耦,消息寫入MQ即表示發送成功。

(1)負責用戶連接管理,保持長連接,存儲uid->conn映射關系;

(2)負責消息接收落地,成功寫入MQ后給客戶端返回成功;

(3)負責把消息推送給在線狀態的接收者;

下圖是客戶端發送消息流程

消息轉發msg_transfer

消息處理rpc,作為消費者從MQ中消費(讀取)消息,遞增接收者收件箱seq,關聯seq和msg,並存儲到mongodb。全量歷史消息無收件箱概念,消息作為流水記錄落地mysql即可,兩者通過協程獨立處理,雙方互不影響。msg作為無狀態服務節點,如果消息量增加,可以啟動冗余節點服務,加快消息處理流程。

(1)負責消費MQ中的消息,作為消費者,實時感知新信息達到,並觸發回調邏輯;

(2)生成msgId作為全局消息Id;

(3)讀取receiver userId,並通過redis的incr操作遞增服務端對應的seq;

(4)關聯seq和msgid,並存入以receiver userid為key的mongodb中,作為離線消息,一般在14天后會刪除;

(5)同時,把消息作為歷史記錄存入mysql中,作為消息備份,或其他用途。

(4)和(5)是兩個獨立的協程並行執行的,mysql寫入快慢不會影響mongodb的寫入,這樣既完成了冷熱數據分離,也充分利用了機器資源。

下圖是消息處理入庫流程

 

消息推送push

msg_transfer完成存儲消息到后,向push發起消息推送任務,msg_gateway查詢本地userId->conn表,如果用戶在線則推送給接收者,對於msg_gateway的推送架構設計,做成了“半狀態”服務,即在節點本地存儲了用戶連接信息,作為局部信息,沒有通過redis全局共享。push推送消息時,向所有msg_gateway發送推送請求,帶來一定的“驚群效應”,由於msg_gateway節點不多,所以影響有限,帶來的好處則是在不影響性能的前提下,msg_gateway設計和實現簡單,運維也更簡單。

(1)msg_transfer把消息寫入mongodb后,發送push消息推送請求;

(2)push提供rpc推送服務,通過etcd找到所有注冊的msg_gateway,並發送推送請求;

(3)msg_gateway從本節點內存中查詢userId->conn,如果找到conn,則向客戶端推送消息;

(4)如果消息接收者不在線,msg_gateway無法推送消息,但客戶端網絡重連時會及時同步歷史消息,進行消息補齊;

下圖是消息實時推送流程:

 

### 消息同步及對齊seq

由於網絡的波動以及負責的網絡環境,導致消息推送存在不確定性。OpenIM采用local seq和server seq消息對齊,同時結合拉取和推送的方式,簡單高效地解決了消息的可靠性問題。這里分兩種場景進行表述:

(1)客戶端接收推送消息時,比如客戶端收到推送消息的seq為100,如果local seq為99,因為seq遞增且連續,所以消息正常顯示即可。如果local seq大於100,說明重復推送了消息,拋棄此消息即可。如果local seq小於99,說明中間有歷史消息丟失,拉取(local seq+1, 100)的消息,進行補齊即可;

(2)用戶在登錄、或者斷網重連時,客戶端會從服務端拉取最大seq(max seq),讀取客戶端本地seq(local seq),如果local seq 小於 max seq,說明存在歷史消息未同步的情況,調用接口同步自身收件箱[local seq+1, max seq]的數據完成消息對齊。

下圖是消息同步流程圖

本文主要簡單闡述了OpenIM的架構以及消息流程,讓開發者對其有初步認識,在接下來的文章中,我們會詳細講解OpenIM服務端消息架構,OpenIM客戶端架構,同時會詳細分析OpenIM如何簡單高效解決消息的可靠性、實時性、一致性和擴展性問題。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM