一套原創分布式即時通訊(IM)系統理論架構方案

一 典型的即時通訊架構可能是這樣的

無論是IM消息通信系統還是客戶消息系統,其本質都是一套消息發送與投遞系統,或者說是一套網絡通信系統,其本質兩個詞:存儲與轉發。

1攜程異步消息系統初期架構

上圖所示顯示了攜程家的消息系統的初期架構,圖中架構直接用mongodb作為消息隊列,然后就把系統開發出來了,圖中中可以見到一個常見IT系統的接口層。

2京東咚咚初期架構

上圖揭示了京東家的消息系統的初期架構,其特點是“為了業務的快速上線,1.0 版本的技術架構實現是非常直接且簡單粗暴的”,而且后臺系統使用.net基于Redis就把一個IM系統開發出來了。

兩家系統的初期架構說明,一套消息系統對提升自家的服務質量是多么的重要,可以認為現代的服務型的互聯網公司成長過程就是一套IM系統的進化史。

二 本次方案的整體思路

本文結合鄙人對IM系統的了解,也給出一套初具IM系統系統特點的消息系統架構模型。本文只考慮IM系統的在線消息模型,不考慮其離線消息系統[能夠存儲IM消息的系統]。

1根據個人理解,其應有的feature如下

A 整個系統中Server端提供存儲轉發能力,無論整體架構是B/S還是C/S;

B 消息發送者能夠成功發送消息給后端,且得到后端地確認;

C 接收端能夠不重不漏地接收Server端轉發來的沒有超過消息生命周期和系統承載能力的消息;

D 整個系統只考慮文本短消息[即限制其長度];

E 每條消息都有生命周期,如一天,且有長度限制如1440B【盡量不要超過一個frame的size】,只考慮在線消息的處理,無論是超時的消息還是超出系統承載能力的消息[如鍵盤狂人或者鍵盤狂機器人發出的消息]都被認為是"垃圾消息";

F 為簡單起見,不給消息很多類型,如個人對個人消息,群消息,討論組消息等,都認為是一種群[下文用channel替代之,也有人用Room這個詞]消息類型;

G 為簡單起見,這個群的建立與銷毀流程本文不述及,也即消息流程開始的時候各個消息群都已經組建完畢,且流程中沒有成員的增減;

H 賬戶申請、用戶鑒權和天朝獨有的黃反詞檢查等IM安全層等暫不考慮。

2根據以上系統特點,先給出一套稍微完備的IM系統的框架圖

3系統名詞解釋

1PC:單機型客戶端,如windows端和mac端等等;

2Web/h5:網頁客戶端;

3Android:手機移動端,取其典型Android端,當然也有ios端[但是考慮到各家開發App都是安卓客戶端最先上系統新版本,故用Android代表之];

4broker:文本消息的有線或者無線接口端,考慮到攜程采用了這個詞,我也姑且先用之,它提供了消息的接收與投遞功能;

5Relay:圖片/語音/視頻 轉發接口端,其后端可以是自家的服務也可以是第三方服務(如提供圖片存儲服務的七牛、提供云視頻解決方案的騰訊云等);

6msg chat server:消息邏輯處理端;

7Router:在線狀態服務端,存儲在線的用戶以及其登錄的broker接口機的id以及一些心跳包時間等數據;

8Counter:消息計數器,為每個text等類型的消息分配MSG id;

9Msg Queue:每個channel消息的msg id隊列,存儲每個client未接收的且未超時的且未超出隊列大小的msg id集合;

10Mysql/mongodb:消息存儲服務、用戶資料數據、以及channel成員列表服務數據庫,因為二者比較典型,所以取用了這個名字,當然你可以在其上部署一層cache服務;

11Client:客戶端層;

12Interface/If(下文簡稱If):服務接口層;

13Logic:消息邏輯處理層,[這層其實應該有系統最多的模塊];

14DB:存儲層,存儲了在線狀態、消息id以及msg id隊列和消息內容等;

15http:消息發送和接收協議,IM協議中一般理解為long polling消息處理方式,在web端多采用這種協議;

16Websocket:另一種消息發送和接收協議,在移動環境或者采用html5開發的系統多采用這種協議;

17TCP:另一種消息發送和接收協議,在環境或者采用html5開發的系統多采用這種協議;

18UDP:另一種消息發送和接收協議,某個不保證提供穩定消息傳輸服務的廠家采用的協議,也許也是用戶最多使用的協議,它的優點是無論是無線還是有線環境下都非常快,又由于http/Websocket的基礎都是tcp協議,UDP協議在環境擁塞情況下由于不提供擁塞控制等退讓算法,反而會去爭用網絡通道,所以在網絡復雜的特別是發生網絡風暴的情況下它會顯得更快^ _ ^ & ^ _ ^【呵呵噠】;

19RPC:一種遠程過程調用協議,提供分布式環境下的函數調用能力;

20Restful:一種遠程服務提供的架構風格,跟RPC比起來貌似更高級點。

三 具體消息發送流程

在介紹消息發送流程之前,先介紹一些基本概念。

1pub/sub、UIN和session

一個消息系統,從宏觀上來說,就是一個PUB/SUB系統,有消息生成者publisher[or producer],有消息中轉者broker,有消息處理者msg server,以及消息消費者subscriber[or consumer]。消息消費者可以是一個人,也可以是一群人,在pub/sub系統之中producer&consumer一起構成了一個channel,或者稱之為room,或者稱之為group。

無論是producer還是consumer,每個具體單位都要由系統分配給一個id,稱之為UIN[名詞來源于icq]。

后端的if層的broker機器可以在全球或者某個區域分布多個,UIN依據dns系統可以得到if層所有的機器列表,如果dns層由于機器壞掉或者是被攻擊時不能服務,那么客戶端應該根據記憶[無論是上次成功登陸的機器還是被廠家內置的機器列表]知道某些機器的ip&port地址,然后根據測速結果來選擇一個離其最近的broker。

UIN在于broker之間進行一段時間內有效的會話服務,稱之為一個session。這個session存活于一個長連接里,也可以橫跨幾個長連接或者短連接,即session自身依賴的網絡鏈接是不穩定的。session有效期間內,Server認為UIN在線,session有效期內客戶端要定時地給broker發送心跳包。本文認為的session可以是不穩定的,即session有效期內下發給客戶端的消息可以丟失,但是可以通過一些其他手段保證消息被投遞給客戶端。

2四 發送流程

消息的制造者[producer]一般是IM系統的最基本單元UIN[即一個自然人],既然是一個自然人,就認為其發送能力有限,不可能一秒內發出多于一條的消息,即其消息頻率最高為: 1條msg / s。高于這個頻率,都被認為是鍵盤狂人或者狂躁機器人,客戶端或者服務端應該具有拒絕給這種人提供服務或者丟棄其由于發狂而發出的消息。

基于上面這個假設,producer發出的消息請求被稱為msg req,服務器給客戶端返回的消息響應稱為msg ack。整個消息流程為:

A client以阻塞方式發出msg req,req = {producer uin, channel name, msg device id, msg time, msg content};

B broker收到消息后,以uin為hash或者通過其他hash方式把消息轉發給某個msg chat server;

C msg chat server收到消息后以key = Hash{producer uin【發送者id】 + msg device id【設備id】+ msg time【消息發送時間,精確到秒】}到本地消息緩存中查詢消息是否已經存在,如果存在則終止消息流程,給broker返回"duplicate msg"這個msg ack,否則繼續;

D msg chat server到Counter模塊以channel name為key查詢其最新的msg id,把msg id自增一后作為這條消息的id;

E msg chat server把分配好id的消息插入本地msg cache和msg DB[mysql/mongoDB]中;

F msg chat server給broker返回msg ack, ack = {producer uin, channel name, msg device id, msg time, msg id};

G broker把msg ack下發給producer;

H producer收到ack包后終止消息流程,如果在發送流程超時后仍未收到消息則轉到步驟1進行重試,并計算重試次數;

I 如果重試次數超過兩次依然失敗則提示“系統繁忙” or “網絡環境不佳,請主人稍后再嘗試發送”等,終止消息發送流程。

上面設計到了一個模塊圖中沒有的概念:msg cache,之所以沒有繪制出來,是因為msg cache的大小是可預估的,它只是用于消息去重判斷,所以只需存下去重msg key即可。假設msg chat server的服務人數是40 000人,消息發送頻率是1條/s,消息的生命周期是24 hour,消息key長度是64B,那么這個cache大小 = 64B * (24 * 3600)s * 40000 = 221 184 000 000B,這個數字可能有點恐怖,如果是真實商業環境這個數字只會更小,因為沒有人一天一夜不吃不喝不停發消息嘛。其本質是一個hashset(C++中對應的是unordered_set),物理存儲介質當然是共享內存了。

[2016/03/10日:經過思考,msg cache只需存下某個UIN在某個device上的最新的消息時間即可,msg cache的結構應為hashtable,以{UIN + device id}為key,以其最新的消息的發送時間(客戶端發送消息的時間)為value,不再考慮消息的生命周期。msg chat server每收到一條新消息就把新消息中記錄的發送時間與緩存中記錄的消息時間比較即可,如果新消息的時間小于這個msg pool記錄的時間即說明其為重復消息,大于則為新消息,并用新消息的msg time作為msg cache中對應kv的value的最新值。假設UIN為4B,device id為4B,時間為4B,則msg cache的數據的size(不計算hashtable數據結構本身占用的內存size)為12B * 40000 = 480 000B,新msg pool完全與每條消息的lifetime無關,這就大大下降了其內存占用。

那么還有一個問題,如果用戶修改了手機的本地時間怎么辦?那就換做另一個參數:本地手機時鐘累計運行時長,手機出廠后其運行累計時長只會一直增加不會減小。

這個流程牽涉到一個比較重要的模塊:Counter,這個模塊其實都可以用Redis充當,怎么做你自己想^ _ ^。這個模塊自身的實現就是一個分布式的計數器,直接使用Redis也沒什么問題,但是最好的方法是采用消息id批發器的方式,msg chat server到Counter每次批發一批id回來,然后分配給每個msg,當使用完畢的時候再接著去Counter申請一批回來,以減輕Counter的壓力,具體的設計請參考專利《即時消息的處理方法和裝置》[參考文檔9]。

上面還有一個概念未敘述到:發送端的消息郵箱{有人稱為消息盒子,或者某大廠稱之為客戶端消息db},它存儲了所有本地發送出去的消息,其中沒有服務端分配的msg id的消息都被認為是發送失敗的消息,待用戶主動嘗試發送或者網絡環境重新穩定后可以有客戶端嘗試重新發送流程。

用戶查看消息郵箱中的本地歷史消息的時候,就要依據msg id把消息排序好展現給用戶。至于用戶發送過程中看到的消息可以認為是本地消息的一個cache,每個channel最多給他展現100條,這100條消息的排序要依照每條消息的發出時間或者是消息的接收時間[這個接收到的消息時間以消息到達本機時的本地時鐘為依據]。當用戶要查看超出數目如100條消息之外的消息,客戶端要引導用戶去走歷史消息查看流程。

3消息狀態部分流程

在進行消息的發送流程中,msg chat server充當了消息的處理者,其實消息的發送流程就可以認為是一次客戶端與服務端進行簡單的“心跳邏輯”的過程,這個過程msg chat server[實際上就是下面提到的heartbeat server]還要完成如下部分消息狀態處理邏輯:

1 heartbeat server到Router中直接修改producer的狀態為在線;

2 heartbeat server要把client連接的broker的id以及其最新登錄時間更新至Router中;

至于Router具體的構造,下一章節會敘述到。

4關于長文本消息

還有一個問題,如果消息超過服務端規定的短文本消息的最大長度怎么辦?

一種方法是干脆丟棄,拒絕給客戶端發送出去,貌似用戶體驗沒那么好。

還有另一種方法,分片。用分片的方法拆成若干條短消息,每條短消息由客戶端或者服務端自己給他分配好序列號,待用戶收到的時候再拼裝起來。其本質跟tcp層處理大package時拆分若干個子packet道理一樣。

長文本如果能借用第二種方法處理,發送圖片是不是也可以這么干?其本質都是數據嘛,語音和視頻數據的處理亦不外乎如是。

四 消息處理以及消息投遞流程

上述的消息發送流程中,msg chat server把分配的msg id的消息返回給producer后,還要繼續進行消息的投遞。消息的投遞涉及到一系列的技巧,涉及到消息的訂閱者能否不重不漏地在消息還“活著”的消息,這些技巧其實也沒什么神秘之處,下面的流程會詳細地描述到。

1消息投遞流程

消息投遞,顧名思義,就是消息的下發而已,有人美其名曰消息Push流程。

如果說消息的發送 = msg req + msg ack, 那么消息的投遞就簡單多了:

A msg chat server到channel成員列表服務數據庫拉取成員列表;

B msg chat server循環到Router中查看每個成員是否在線,如果在線則獲取成員連接的broker接口機地址;

C msg chat server發送消息到broker;

D broker接收到消息后就把msg下發給客戶端;

E msg chat server循環給在線的成員發送完消息后,把msg id放入其channel在msg queue中的msg id list的末尾;

F 如果msg queue的msg id list超過長度限制,則要刪除掉鏈表的head部分的若干id,以保證list長度不超過系統規定的參數;

G 流程結束。

消息的投遞是不是顯得輕松多了,至于"被認為在線"客戶端有沒有收到msg,msg chat server壓根就不管!

這個流程牽涉到另一個比較重要的模塊:router,它其實也可以用Redis充當,利用Redis的bitmap記錄所有用戶的狀態,0標示離線,1表示在線,然后再利用hashtable存儲每個用戶登錄的broker的id和最新登錄時間。

至于msg queue模塊,其實也是一個hashtable,key為channel的name或者id,value就是一個msg id list。

聽說Redis最近要添加Bloom Filter,那就更好玩了,關鍵就看其能否應對刪除操作,如果有刪除接口,把它當做bitmap玩玩倒也無妨。

五 心跳流程

一個客戶端要維持與服務端的session有效,就須與其broker維持一個心跳流程,以被認為是處于在線狀態。那么,最基本的問題就是:心跳時長。

這個問題會讓很多移動開發者頭疼許久,最基本的要根據網絡環境來設計不同的心跳時長:譬如有線環境把頻率設置為10s,wifi環境下這個頻率設計為30s,在3G或者4G環境下設置為1.5分鐘,在2G環境下設置為4分鐘。總之其原則是:網絡環境越差勁,心跳時間間隔越長。

心跳時間間隔長那么其心跳頻率就低,其消息收發速度就慢。

進一步,無線環境下這個心跳時間長度不是固定不變的,具體時長要由服務端進行判斷,如果無線環境下假設起始心跳間隔是4分鐘,客戶端連續最近3次心跳有一次失敗,那就把時長修改為2分鐘,如果有兩次失敗就修改為1分鐘,如果連續3次超時未上報心跳,就認為客戶端離線!

(2016/03/10): 經過今日思考,覺得上面這一段的例子中參數是錯誤的,它違背了上上段敘述的原則,當出現心跳超時的情況后就說明網絡環境發生了變化,但是僅僅憑借一次超時還不足以說明網絡環境變好還是變壞。其實把心跳時長的問題轉換一個角度進行思考:當知道了前三次或者前兩次實際心跳時間間隔,怎么預測接下來的心跳時間間隔?其本質就是一個拉格朗日外插法的應用而已。我這里不多敘述,僅僅給出一種方法:如果已經知道最近兩次心跳時間間隔為iv1和iv2,則接下來的給客戶端返回的iv3 = k * ((iv1 + iv2) / 2),如果iv1 > iv2,則k = 0.8,否則k = 1.2,這兩個值也僅僅是經驗值而已,具體怎么取值需系統設計者自己權衡,但足以自適應一些復雜的網絡環境,如坐在火車上使用移動網絡的APP。

如果系統設計者覺得麻煩,就可以把上面的值修改為經驗參數值,如無線環境下假設起始心跳間隔是4分鐘,客戶端連續最近3次心跳有一次失敗,那就把時長修改為4.5分鐘,如果有兩次失敗就修改為5.5分鐘,如果連續3次超時未上報心跳,就認為客戶端離線!

解決了心跳時長問題,再來看看具體的心跳流程:

A 客戶端發送心跳包hearbeat,heartbeat = {uin, device id, network type, list{channel name:newest channel msg id},other info},即heartbeat包要上報uin所在的所有channel,以及本地歷史消息記錄中每個channel最新的消息的id;

B broker把心跳包轉給專門處理心跳邏輯的msg chat server[以下稱為heartbeat server];

C heartbeat server到Router中更新client的在線狀態以及登錄的broker的id和最新登錄時間;

D heartbeat server到Counter服務器循環查詢每個channel的最新消息id,如果客戶端上報的id與這個id不等,就發送一條msg通知msg chat server,msg = {uin, channel name, client newest msg id of channel};

E msg chat server收到這條消息后,重新啟動消息下發邏輯,到msg queue中取出所有的大于{client newest msg id of channel}的id列表;

F msg chat server依據list中的id到消息存儲服務器中依次取出每個msg[取不到也就表示這個消息因為超時而被消息存儲服務器刪除了];

G msg chat server把這些消息作為"未讀消息"下發給客戶端;

H heartbeat server根據Router存儲的客戶端的最近三次的登錄時間,調整session的心跳時間間隔,作為心跳回包的一部分參數值給客戶端下發heartbeat ack包,其他數據包括其所在的每個channel的最新消息的msg id;

I heartbeat server定時地到Router中檢查所有客戶端的最新登錄時間,如果超過其session有效時間,就把其state置為“離線”,并刪除其登錄服務id等數據;

J 客戶端收到heartbeat ack包后,修改下次心跳時間,并依據每個channel的最新的msg id與本地消息郵箱中對應的channel的最新消息id做對比,如果id不等,客戶端可以啟動拉取消息流程或者等待server端把這些消息下發過來。

上面提到的一個詞:newest channel id 或者 client newest msg id of channel,其意思就是消息接收者所在的channel的所擁有的本地消息的最新id。一般地,如果server端的Counter能夠穩定地提供服務,channel中的msg id應該是連續的,如果客戶端檢測到msg id不連續,可以把不連續處的id作為newest channel id,要求server端再把這個msg id以后的消息重發下來,這就要求client有消息去重判斷的功能。

每次收到server端下發的消息后,用戶必須更新local newest channel msg id,把消息id窗口往前推進,不要因為id不連續而一直不更新這個值,因為服務端的服務也不一定超級穩定。

上面的一段我寫的稍嫌“囋”一些,其實其思想類似于tcp的滑動窗口思想,自己做對比去理解之。

step H要求router至少要存儲client最新四次的登錄時間,然后根據這三次時間間隔以及網絡類型修改下次心跳時間間隔有效時長。我這里已經很明了的寫出了原理了,至于怎么取值可以依據上面提到的原理修改相關參數[這個得需要測試才能得出一些關鍵數據,但是這個參數值應該跟我本文提到的參數值相差無幾]。

至于step J敘述到的client是否啟用消息拉取邏輯,取決于你的服務類型。具體場景分別對待,本文不會再設計消息的pull流程。

其實結合第4章節以及本章節,用流行的術語來說,消息的下發就是微信所謂的"是參考Activesyec,SYNC協議"[參考文檔7]流程,江湖人稱推拉相結合的過程。

這個過程可以用一副流程圖做參考:

注意上圖與本文一些名詞的用法不同,它的所謂的“離線消息”,咱本文中被稱為"未讀消息"。隨著本章節的結束,IM的主要流程就描述完畢。

六 消息存儲服務

由于本文敘述的消息系統是一個在線消息模型,所以msg db中存儲的超時消息必須被刪除。首先db的大小可以根據服務人數的數目以及每條消息的時長估算出來。

其次,簡單的im系統中不考慮用戶的等級的話,可以認為所有的msg都是平等的有相同的lifetime。但是如果區分了用戶優先級,則其消息lifetime也就不等,就得有服務等級不同用戶的msg db[其實優先級越高,其消息存儲越久,企業付出了存儲成本,某種神秘的力量也就越容易獲取到其聊天數據]。

最后,啟動一個定時消息刪除模塊,它定時啟動刪除msg db中超時的msg即可。

七 其他類型消息

由于本文只是描述文本型短消息服務的相關流程,如果還要考慮圖片、聲音和視頻流服務,這些消息就會被稱為富媒體消息。最基本的富媒體消息應該有一個文本消息與之對應,文本消息中包含了這些富媒體文件的url地址或者其他方式定義的地址。消費者拉倒這樣類型的消息,就可以根據消息地址去拉取富媒體文件。

至于富媒體文件怎么存儲,個人建議可以借助目前成熟的第三方服務平臺,如借助七牛的云圖片服務[我舉個栗子而已,沒收任何費用,無做廣告的嫌疑^ _ ^]存儲服務存儲圖片,借助騰訊云的視頻服務能力處理語音和視頻消息。

富媒體消息拉取和上傳都要經過你的Relay接口,這個服務接口因為邏輯與正常的文本消息差別很大,所以建議獨立做一個接口叫做Relay模塊,以與broker作區分,也為以后更換第三方服務廠商打好基礎。

如果你廠有錢又有人,那就考慮自己做富媒體文件的存儲吧,此時在邏輯層應該有個對應的模塊叫做rich text msg server[下面簡稱為rich server],其邏輯應該為:

A 不管是語音還是視頻,client采用合適的文件格式格式化后壓縮好,然后再分片上傳到relay,每個分片要分好分片序號;

B Relay收到這些分片后把數據透傳給rich server;

C rich server先把分片數據存儲在cache中,當收到最后一個分片的時候查收缺失的分片;

D rich server如果發現了缺失分片,就把缺失分片列表告知客戶端,讓其重傳即可;

E 待所有分片都收集好,rich server就可以再次把數據拼裝好放入mongodb或者其他什么db中。

整個邏輯就完成了,是不是也很easy的^ _ ^。

八 方案總結

這套IM系統,總體有以下特點:

1 其完備的IM系統設計;

2 以Counter作為系統的心臟驅動整個系統的流程設計;

3 客戶端的消息流程方案有所涉及;

4 保證服務質量的情況下保障消息不重不漏;

5 詳細敘述了消息下發的技術流程;

6 給出了自己設計的智能心跳方案;

7 對長消息、圖片、語音和視頻等“長數據”的處理給出了自己的解決方法;

8 天生的分布式能力,保證其多IDC的部署能力;

9 盡個人能力,不斷優化中......


來源:http://www.52im.net/thread-151-1-1.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,933評論 18 139
  • 分布式開放消息系統(RocketMQ)的原理與實踐 來源:http://www.lxweimin.com/p/453...
    meng_philip123閱讀 13,067評論 6 104
  • 實時消息協議---流的分塊 版權聲明: 版權(c)2009 Adobe系統有限公司。全權所有。 摘要: 本備忘錄描...
    一個人zy閱讀 1,951評論 0 9
  • kafka的定義:是一個分布式消息系統,由LinkedIn使用Scala編寫,用作LinkedIn的活動流(Act...
    時待吾閱讀 5,361評論 1 15
  • 一兒女又一次聚在一起,談論母親的去留。牙齒差不多都脫落干凈的母親,坐在沙發上,她抿緊嘴唇,像極了那張既是...
    凝固的火閱讀 113評論 0 0