從零開始搭建高可用IM系統
此文根據【QCON高可用架構群】分享內容,由群內【編輯組】志願整理,轉發請註明出處。
沈劍,目前任58同城技術委員會主席,高階架構師,優秀講師。負責過百度hi,58幫幫等im系統的架構設計。
一、什麼是IM
1、IM概述
IM 是“instant messaging”的簡稱,翻譯成即時通訊。說到即時通訊,我們可能最先想到的是一款叫 ICQ的聊天軟體 ,後來者還有微信/skype/msn/momo等 。IM包含即時和通訊是兩個關鍵詞。
-
即時
英文為“instant”或“real time”,表示“立刻”,就是響應非常快,速度的快慢由人的預期決定,而不是絕對時間。IM訊息的發出和送達,達到秒級、毫秒級即可認為是快的。 -
通訊
通訊就是雙方按照既定協議交換資訊。所謂“協議”,是雙方約定達成一致的一個某種約定。
所以“即時通訊”,從字面上看,就是快速,按照一定協議交換資訊。 下圖是一個ICQ的截圖:
2、IM系統特性
IM系統相對其他系統而言有自己的幾個特點:
-
實時性
以Web IM為例,Web站點通常是以請求/響應互動的 ,當Web IM一端接收訊息時,最直觀的做法就是輪詢請求收取訊息,實時性和輪詢間隔有關。其實Web IM也可以做到完全實時,後續會講到如何實現。 -
推送性
IM不是請求/響應的方式,而是主動推送訊息,所以這個“推”是IM系統與其他系統有區別的地方,比如電商或搜尋,都是一請求、一響應的方式。
以股市變化曲線為例,客戶端不主動傳送請求,曲線會隨時間自動變化 :
-
訊息可達性
訊息可達性即訊息的可靠投遞 ,有一個著名的定理:SMC定理,Single-Message Communication,Published in : Communications, IEEE Transactions on (Volume:24 , Issue: 2 ) ,很短的一個論文,文章的結論是,任何端到端的訊息傳遞協議 ,訊息既不丟失,同時也不重複是不可能的。後續會分享,系統層面的重複,可以換取業務層面的不丟失和不重複。
-
狀態一致性
大部分人可能瞭解XMPP(Extensible Messaging and Presence Protocol )協議 ,即可擴充套件訊息和Presence協議 ,這個Presence直接翻譯就是“出席“,何為出席? 大家都到群內簽到了,叫出席 ,沒來聽講座叫缺席 ,在IM中出席缺席表示狀態 。登入線上叫出席,沒登入叫缺席 。
幾乎所有的IM請求都和狀態相關(比如:線上轉發,不線上存離線) ,所以狀態的一致性尤為重要 。
舉個例子:
假設使用者A有100個好友,使用者A加入了10個群,每個群有100個人。使用者A在登入的一瞬間,狀態由“不線上”變為了“線上”,他的狀態的變更需要通知要通知1100個人,包括100個好友和1000個群友,這個擴散係數是非常龐大,
如何做這1100個人的狀態推送? 常見的做法和Feed,微博類似 ,你的100個好友(實時性要求很高),採用推的模式 ,你的1000個群友採用拉的模式(實時性要求不高) 。當然,這裡還只是說狀態很很複雜,還沒涉及到狀態一致性問題。
二、協議設計
網路協議是由三個要素組成,語義、語法、時序。語義表示要做什麼,語法表示要怎麼做,時序表示做的順序。 本文主要講解重點語法的設計。
IM的協議分為3層,如下圖:
1、應用層
常用的IM應用層協議有3種 :
1.1、文字協議
文字協議是“貼近人類書面語言”的協議,典型例子是MSN ,另外HTTP協議也是文字協議,如下圖所示:
文字協議有幾個特點:
-
可讀性好/便於除錯
-
擴充套件性也好(key:value)
-
解析效率一般(一行一行讀入,按照冒號分割,解析key和value),
-
對二進位制的支援不好 ,比如語音/視訊
1.2、 二進位制協議
二進位制協議最典型的是IP協議,如下圖所示:
二進位制協議一般定長包頭和可擴充套件變長包體 ,每個欄位固定了含義 ,例如IP協議的前4個bit表示協議版本號 (Version)。IM中,QQ使用的時二進位制協議。
二進位制協議有幾個特點:
-
可讀性差/難於除錯
-
擴充套件性不好 ,如果要擴充套件欄位,舊版協議就不相容了,所以一般設計時會有一個Version欄位
-
解析效率超高(幾乎沒有解析代價)
-
對二進位制的支援不好 ,比如語音/視訊
1.3、流式XML
Xmpp協議就是使用流式XML,像Gtalk,校內通都是基於Xmpp的。
舉一個羅密歐給朱麗葉發訊息的例子:
< message to='[email protected]' from='[email protected]' type='chat' xml:lang='en' >
Wherefore art thou, Romeo?
Xmpp用 [email protected] 來標示一個賬號,稱為JID 。JID由兩個部分組成 ,前半部分是該域中的賬號體系 ,後半部分是域標識(編者注:JID還包含一個Resource部分)。Xmpp協議可以實現跨域的互通。例如Gtalk和校內通使用者聊天。只要服務端實現了s2s服務(server to server) ,不過現在的IM基本沒有互通需求 ,所以 這個服務基本沒有人實現。
Xmpp協議有幾個特點:
-
它是準標準協議,可以跨域互通
-
XML的優點,可讀性好,擴充套件性好
-
解析代價超高 (dom解析)
-
有效資料傳輸率超低(大量的標籤 )
我個人強烈不建議使用Xmpp,特別是無線端IM,這個是google上個世紀的產物了,如果要用,一定要自己做壓縮 ,減少網路流量。
實際例子
下面來看一個IM協議的實際例子 ,一般常見的做法是:定長二進位制包頭,可擴充套件變長包體可以使用用文字、XML等。 百度Hi 的IM包體用的是XML ,58同城的IM包體用的是protobuffer,包頭負責傳輸和解析效率,包體與業務無關負責保證擴充套件性。
這是一個簡單例子,我們來看一下包頭和包體 。
定長包頭(16個位元組)
-
前4個位元組是Version
-
接下來的4個位元組是個“魔法數字(magic_num)“,用來保證資料錯位或丟包問題,常見的做法是,包頭放幾個約定好的特殊字元,包尾放幾個約定好的特殊字元 約定好,發給你的協議,某幾個位元組位置,是0x 01020304 。才是正常報文
-
接下來是command(命令號),用來區分是keepalive報文、業務報文、金鑰交換報文等
-
len(包體長度),告知服務端要接收多長的包體
變長包體
可選擇 Xml、protobuffer、mcpack等 ,某些公司使用protobuffer,作者也強烈推薦,主要有幾個原因:
-
現成的解析庫種類多,可以生成C++、Java、php等程式碼
-
自帶壓縮功能
-
在工業界已廣泛應用
-
Google製造
下面貼一個protobuffer寫的使用者登入協議的示例:
2、安全層
IM協議,訊息的保密性非常重要 ,誰都不希望自己聊天內容被看到
1、HTTPS
稍微複雜,代價有點高 。
2、自行加解密
金鑰管理方式有多種
1、固定金鑰
服務端和客戶端約定好一個金鑰,同時約定好一個加密演算法(eg:AES ),每次客戶端IM在傳送前,就用約定好的演算法,以及約定好的金鑰加密 再傳輸 ,服務端收到報文後,用約定好的演算法,約定好的金鑰再解密 。這種方式,金鑰和演算法 對程式設計師都是透明的 。有些公司cookie就是這麼使用的。
2、一人一金鑰
簡單說來就是每個人的金鑰是固定的,但是每個人之間又不同,其實就是在固定金鑰的演算法中包含使用者的某一特殊屬性,比如使用者uid、手機號等。
據傳,QQ是這麼使用這種方式(未核實)
3、動態金鑰
大家瞭解ssl的過程麼,動態金鑰 一Session一金鑰的安全性更高,每次傳輸互動前協商金鑰, 客戶端第一個報文:服務端,請告訴我這次通話的金鑰 ,伺服器就隨機生成一個,返回給客戶端 然後這次會話用這個金鑰來通訊 ,這樣可以簡單的做到動態金鑰,但是一旦攻擊者擷取報文,就能知道你的動態金鑰。
SSL的用法,是2次生成非對稱加密金鑰,1次生成對稱加密金鑰 ,具體詳情這裡不展開,有興趣的可以深入研究一下。
3、傳輸層
傳輸層:TCP/UDP
現在的IM傳輸層基本都是使用TCP。有了epoll/kqueue等技術後 ,單機多連線就不是瓶頸了,單機幾十萬連結沒什麼問題。58同城現在線上單機連線好像是10w?(可能單機效能測試可以到百萬,線上一般跑到幾十萬)
關於QQ是使用UDP問題(作者觀點)
10多年前,一臺伺服器支撐不了1W個TCP連線 ,騰訊的同時線上量高,沒辦法,只有用UDP了 ,但UDP又不可靠,騰訊使用UDP實現了TCP的超時/重傳/確認等機制
三、WEB聊天室
1、需求
下面是一個Web聊天室的截圖
Web聊天室需求主要有一下幾點:
1)使用者可以設定自己的名字
2)進入聊天室後,可以看到所有其他人的名字
3)可以看到所有人的聊天
4)可以發言給所有人
上面是Web聊天室的一些簡單業務(像群吧?) ,設定名字/拉取其他人的資訊 都是簡單的請求響應式的需求這裡不展開,主要重點講怎麼看歷史聊天訊息,怎麼發訊息給別人 。
2、系統架構
Web站點三層架構大家都很熟悉,大致是這個樣子
LAMP是個典型解決方案 ,更典型的是這個樣子
-
資料層
對於聊天室的簡單需求,一個存使用者資訊,一個存訊息兩個表就可以了,下面是一個簡化了的示例:
user( name vchar(16) unique );
message( time timestamp, name vchar(16), msg vchar(140) );
有人進入,就往user裡插入 ,有人退出,就從user裡刪除 ,有人發訊息,就往message裡插入 ,有人進入,往message里拉取,就能查詢歷史資訊。
3、技術核心點
Web聊天室的核心店在於如何把傳送的訊息通知User表裡的所有人?訊息實時性,主要有三種方式,websocket、flashsocket、http輪詢,本文只講http輪詢。
1、輪詢
什麼是輪詢?
舉個例子,在火車上想上洗手間,擠到洗手間旁,卻發現洗手間有人,於是你只能回座位繼續等。過了N分鐘,又朝洗手間的方向擠過去,卻發現洗手間還是有人,又只能回坐等。這麼一而再,再而三的每隔N分鐘去洗手間檢視洗手間是否有蹲位,這就是輪詢。程式程式碼:while(1){sleep 500ms; get msg;}
大部分人最容易想到的解決方案就是輪詢(poll) 。十幾年前,四通利方/碧海銀沙就是用的輪詢,輪詢拉取訊息,每隔幾秒往message表裡,拉取最新的聊天室訊息 ,這樣做能簡單實現一個聊天室。
但輪詢問題也顯而易見 ,每隔N分鐘,輪詢呼叫 “獲取訊息”介面,有可能出現訊息的延時,某一時刻剛剛拉取完訊息,突然又產生了一條新訊息,這條訊息就必須等到N分鐘之後,再次發起“獲取訊息”輪詢時,才有機會獲取到。 可以降低時間間隔來降低延時,但絕大部分的輪詢呼叫,都沒有訊息返回,造成服務端極大的資源浪費
2、訊息連線
Web聊天室通過“http訊息連線”來保證訊息的絕對實時性,何謂訊息連線?
使用者和伺服器建立一條http連線,專門用來傳遞Notify 。例如,手機上,web聊天室裡,有一個B使用者 專門有一條http訊息連線,用來投遞Notify(訊息),如下圖 。
訊息連線如何保證 web聊天室訊息的實時性呢?它具有以下幾個特性:
1、沒有訊息到達的時候,這個http訊息連線將被夯住,不返回,由於http是短連線,這個http訊息連線最多被夯住90秒,就會被斷開(這是瀏覽器或者webserver的行為)。
2、如果http訊息連線被斷開,立馬再發起一個http訊息連線
如上圖,http訊息連線90s超時了,伺服器返回空了,web瀏覽器會立馬再次發起一個新的訊息連線 。目的是,保證一個使用者一直有一條訊息連線連著,可以接受訊息
3、每次收到訊息時,這個訊息連線就能及時將訊息帶回瀏覽器頁面,並且在返回後,會立馬再發起一個http訊息連線
如上圖,某人傳送了一條聊天室訊息 ,這個訊息要投遞給user裡的所有人,B是其中之一,此時會有一條訊息連線在,訊息連線就直接將訊息帶回 ,帶會訊息後,立馬再發起訊息連線 。
4、訊息池
上面三大特性保證了:
a)任何時間都有訊息連線在
b)訊息能在第一時間通過訊息連線返回
但這裡有個小概率事件,正在返回訊息的時候(可以認為此時沒有訊息連線),瞬間又到達了一條訊息怎麼辦,此時服務端要有一個類似於“訊息池”的東西,將這個訊息暫存起來 。訊息連線到達後,從訊息池中將訊息取回,再通過訊息連線返回。
看上圖中的步驟1-7
1)訊息要投遞給聊天室中的所有人,B是其中之一
2)此時沒有訊息連線,msg放入訊息池
3)訊息連線來晚了
4)從訊息池中獲取訊息
5)獲取到訊息了
6)返回訊息給web瀏覽器裡的B
7)新的訊息連線發起
結論:
Web聊天室通過http長輪詢可以保證訊息的實時性。這種實時性的保證不是通過增加輪詢頻率來保證的,而是通過夯住http訊息連線來保證的,在大部分時間沒有實時訊息的情況下,這個http訊息連線對於webserver的請求壓力是90秒1次,能夠大大節省了web伺服器資源。
這個訊息連線的思想,是一個觀察者模式 ,所有的聊天室使用者是observer,聊天室是subject,observer訂閱subject ,subject儲存有所有observer的集合,當有訊息發出,即subject發生改變時,通過http訊息連線反向通知subject
這裡重點討論了 web聊天室 訊息的實時性,聊天訊息的可靠投遞,暫時也先不展開了
即時,是相對時間而不是絕對時間 ,m的即時,訊息投遞在幾百毫秒,1秒內投遞完成,一般都可以接受(站點應用這個時間就接受不了解),這個時間對未來我講im服務的網路模型,影響很大 。 im的所有業務邏輯,都是在這幾百毫秒內完成的 。
4、IM典型業務場景
IM業務邏輯有一定的複雜度,舉例IM中一個典型業務場景,使用者A將使用者B加入到分組G中,如下圖:
這裡包含多少業務邏輯呢
第一步,判斷A是否是正常im使用者
第二步,判斷B是否是正常im使用者
第三步,判斷分組G是否存在
第四步,是否A和B已經是好友
第五步,使用者A是否加黑了B
第六步,使用者B是否加黑了A
第七步/使用者A的加好友頻率是否過快
第八步,加好友的驗證文字是否合法。“我是xx,請加我為好友”就是這個文字的驗證。
第九步,檢查B的加好友策略 ,是“允許所有”還是“需要驗證”還是“禁止所有”
都9步了,真正的加好友步驟卻還沒開始,這裡的每一個步驟,都不是一個簡單的本地cpu計算能完成的,都需要訪問資料庫或者後端服務 ,所以im的業務邏輯是很複雜的,做過的人懂。
上面有一個步驟,是檢查加好友的驗證短語的合法性 。IM系統中,所有能被別人看到的話,都要經過antispam驗證 ,antispam一般有敏感詞(政治,黃色)過濾,訊息頻率過濾,廣告過濾 ,每一個步驟,都有很複雜的詞庫過濾,或者分析過濾(分析一句話是不是廣告,非常複雜)
四、Q&A
Q1: xmpp中,在跨域通訊網路沒有保證的情況下,xmpp如何保證跨域訊息的可靠送達的。
A1:im訊息的可達性,是通過超時重傳確認保證的(未來會再次重點講細節),跨域只是提供了一種不同域的通訊機制
Q2:應用層協議設計中,為啥 version 會放在 magic_num 之前呢? 為啥magic_num 不是第一個呢? 是考慮不同版本的 magic_num 不一樣?
A2:是的
Q3:我沒懂magic_num的作用,這個是常量嗎?另外,為啥不直接採用http,而要自定義協議?
A3:web上可以選擇http作為應用層協議,直接tcp長連線搞的話協議得自己來
Q4:聊天室怎麼保證訊息的可靠性?
A4: 可靠性以後講,或者可以看下剛才群內有同學發的那篇文章 webim如何保證訊息的可靠投遞
Q5:安全層加密, 是對應用層的:定長二進位制包頭 + 可擴充套件變長包體 都做加密?
A5:包體和業務相關,要加密
Q6: 現在通訊行業的IM(RCS等業務)基本都用SIP(單獨或結合MSRP)來做,協議安全用TCP/TLS,WebRTC也用SIP,請問@58沈劍 不用這些公共協議而用私有協議的主要出發點除了效能,還有什麼?
A6:自己做更可控,結合自己業務做優化。另,im行業準標準協議是xmpp。
Q7:訊息推送的話,用長連線保持的話,現在也比較通用了吧?
A7:能用tcp就用tcp,有些場景,例如web,選擇http訊息連線是被逼的
Q8:實時推送,http輪詢和websocket如何選擇呢?
A8:websocket有相容性問題,很多瀏覽器不支援,據我瞭解,大規模高線上的im,好像web端還木有用websocket搞的
Q9:手機端的聊天室如何處理使用者頻繁斷線的問題?如何在斷線後獲取伺服器端聊天曆史並與本地聊天曆史合併?
A9:web聊天室,訊息連線不上,訊息就放在訊息池裡,上來再給他,一定時間上不來,就丟掉。訊息id可以去重。
Q10:能不能介紹一下注冊/IM/Subscribe/Presence等伺服器結合起來的部署架構?
A10:架構後續課程分享
Q11:websocket有什麼弊端?目前單機最高業務承載多少?
A11:同A8回答。
Q12:移動端im的注意事項
A12:後續專門講移動優化
Q13:聊天室達到10萬級別的時候,在效率推送上是怎麼考慮的?
A13:聊天室和群一樣,訊息擴散係數很影響效能,一般群不能到這個級別,這個級別的群對伺服器影響很大(微信群上限是多少?qq群上限是多少?)
Q14:我想問下,剛說的訊息池是全域性的還是針對每個使用者單獨的?
A14:訊息池本質是個map,key是uid
Q15:為什麼不用BOSH(Bidirectional-streams Over Synchronous HTTP)來描述剛才web HTTP機制?有什麼細微區別嗎?
A15:我猜bosh的本質也是訊息連線,bosh/comet還是什麼,叫法不一樣,實現方式應該是類似的
Q16:另外你說的移動IM不建議XMPP協議,是單指XML格式本身的缺陷,有效資料太少,資料解析慢,僅僅是這個原因嗎?解析這方面有做過具體的效能測試嗎?另外如果是有效資料太少,是否可以稍微擴充套件就可以大幅度減少無效資料問題。還是有其它方面原因,比如XMPP協議本身互動協商次數太多的問題?
A16:xmpp解析慢,有效傳輸率低(無線端的表現就是耗流量),你們猜用xmpp發一個登入包多大?
Q17:websocket效能會比long-polling好多少呢?
A17:tcp長連線 和 http短連線 的差異
Q18:聊天訊息中含html、css、js相關程式碼是如何處理的?
A18:這是一個富文字訊息的好問題,伺服器不管訊息的內容,只進行投遞,富文字訊息的內容,是客戶端需要理解的
Q19:im怎麼保證時序性?當伺服器接收msgpack時處理不過來,客戶端就會阻塞傳送訊息。但沒阻塞接收的訊息。這樣傳送與接著就不同步了
A19:一言難盡,簡單說,單對單的訊息,用uid+msgid進行時序保證;群訊息,用gid+msgid進行時序保證。