1. 程式人生 > >一套原創分散式即時通訊(IM)系統理論架構方案

一套原創分散式即時通訊(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 盡個人能力,不斷優化中......



文/isgiker(簡書作者)
原文連結:http://www.jianshu.com/p/c3b4fb263c65
著作權歸作者所有,轉載請聯絡作者獲得授權,並標註“簡書作者”。