本文做者潘唐磊,騰訊WXG(微信事業羣)開發工程師,畢業於中山大學。內容有修訂。html
本文總結了企業微信的IM消息系統架構設計,闡述了企業業務給IM架構設計帶來的技術難點和挑戰,以及技術方案的對比與分析。同時總結了IM後臺開發的一些經常使用手段,適用於IM消息系統。前端
* 推薦閱讀: 企業微信團隊分享的另外一篇《企業微信客戶端中組織架構數據的同步更新方案優化實戰》也值得一讀。後端
如下是本文內容中涉及到的技術名詞縮寫,具體意義以下:api
企業微信做爲一款辦公協同的產品,聊天消息收發是最基礎的功能。消息系統的穩定性、可靠性、安全性尤爲重要。安全
消息系統的構建與設計的過程當中,面臨着較多的難點。並且針對toB場景的消息系統,須要支持更爲複雜的業務場景。微信
針對toB場景的特有業務有:markdown
如上所示,總體架構分層以下。網絡
1)接入層: 統一入口,接收客戶端的請求,根據類型轉發到對應的CGI層。客戶端能夠經過長連或者短連鏈接wwproxy。活躍的客戶端,優先用長鏈接發起請求,若是長連失敗,則選用短連重試。session
2)CGI層: http服務,接收wwproxy的數據包,校驗用戶的session狀態,並用後臺派發的祕鑰去解包,如解密失敗則拒絕請求。解密成功,則把明文包體轉發到後端邏輯層對應的svr。架構
3)邏輯層: 大量的微服務和異步處理服務,使用自研的hikit rpc框架,svr之間使用tcp短連進行通訊。進行數據整合和邏輯處理。和外部系統的通訊,經過http協議,包括微信互通、手機廠商的推送平臺等。
4)存儲層: 消息存儲是採用的是基於levelDB模型開發msgkv。SeqSvr是序列號生成器,保證派發的seq單調遞增不回退,用於消息的收發協議。
企業微信的消息收發模型採用了推拉方式,這種方式可靠性高,設計簡單。
如下是消息推拉的時序圖:
PS: 如上圖所示,發送方請求後臺,把消息寫入到接收方的存儲,而後push通知接收方。接受方收到push,主動上來後臺收消息。
不重、不丟、及時觸達,這三個是消息系統的核心指標:
IM中消息分發的典型方式,通常有兩種:
即: 每條消息只存一份,羣聊成員都讀取同一份數據。
優勢: 節省存儲容量。
缺點:
即: 每條消息存多份,每一個羣聊成員在本身的存儲都有一份。
優勢:
同一條消息,在每一個人的視角會有不一樣的表現。例如:回執消息,發送方能看到已讀未讀列表,接受方只能看到是否已讀的狀態。雲端刪除某條羣消息,在本身的消息列表消失,其餘人仍是可見。
缺點: 存儲容量的增長。
企業微信採用了擴散寫的方式,消息收發簡單穩定。存儲容量的增長,能夠經過冷熱分離的方案解決,冷數據存到廉價的SATA盤,擴散讀體驗稍差,協議設計也相對複雜些。
下圖是擴散寫的協議設計:
如上圖所示:
企業微信做爲一款to B場景的聊天im工具,用於工做場景的溝通,有着較爲明顯的高峯效應(以下圖所示)。
正如上圖所示: 工做時間上午9:0012:00、下午14:0018:00,是聊天的高峯,消息量劇增。工做日和節假日也會造成明顯的對比。
高峯期系統壓力大,偶發的網絡波動或者機器過載,都有可能致使大量的系統失敗。im系統對及時性要求比較高,沒辦法進行削峯處理。那麼引入一些柔性的策略,保證系統的穩定性和可用性很是有必要。
具體的作法就是啓動過載保護策略:當svr已經達到最大處理能力的時候,說明處於一個過載的狀態,服務能力會隨着負載的增高而急劇降低。若是svr過載,則拒絕掉部分正常請求,防止機器被壓垮,依然能對外服務。經過統計svr的被調耗時狀況、worker使用狀況等,斷定是否處於過載狀態。過載保護策略在請求高峯期間起到了保護系統的做用,防止雪崩效應。
下圖就是因過載被拒絕掉的請求:
上一小結中過載保護策略所帶來的問題就是:系統過載返回失敗,前端發消息顯示失敗,顯示紅點,會嚴重影響產品體驗。
發消息是im系統的最基礎的功能,可用性要求達到幾乎100%,因此這個策略確定須要優化。
**解決方案思路就是:**儘管失敗,也返回前端成功,後臺保證最終成功。
爲了保證消息系統的可用性,規避高峯期系統出現過載失敗致使前端出紅點,作了不少優化。
具體策略以下:
1)邏輯層hold住失敗請求,返回前端成功,不出紅點,後端異步重試,直至成功;
2)爲了防止在系統出現大面積故障的時候,重試請求壓滿隊列,只hold住半小時的失敗請求,半小時後新來的請求則直接返回前端失敗;
3)爲了不重試加重系統過載,指數時間延遲重試;
4)複雜的消息鑑權(好友關係,企業關係,集團關係,圈子關係),耗時嚴重,後臺波動容易形成失敗。若是並不是明確鑑權不經過,則冪等重試;
5)爲了防止做惡請求,限制單個用戶和單個企業的請求併發數。例如,單個用戶的消耗worker數超過20%,則直接丟棄該用戶的請求,不重試。
優化後,後臺的波動,前端基本沒有感知。
如下是優化先後的流程對比:
因爲產品形態的緣由,企業微信的消息系統,會依賴不少外部模塊,甚至外部系統。
例如: 與微信消息互通,發送消息的權限須要放到ImUnion去作斷定,ImUnion是一個外部系統,調用耗時較長。
再如: 金融版的消息審計功能,須要把消息同步到審計模塊,增長rpc調用。
再如: 客戶服務的單聊羣聊消息,須要把消息同步到crm模塊,增長rpc調用。爲了不外部系統或者外部模塊出現故障,拖累消息系統,致使耗時增長,則須要系統解耦。
咱們的方案: 與外部系統的交互,全設計成異步化。
思考點: 須要同步返回結果的請求,如何設計成異步化?
例如: 羣聊互通消息需通過ImUnion鑑權返回結果,前端用於展現消息是否成功發送。先讓客戶端成功,異步失敗,則回調客戶端使得出紅點。
若是是非主流程,則異步重試保證成功,主流程不受影響,如消息審計同步功能。那麼,只須要保證內部系統的穩定,發消息的主流程就能夠不受影響。
解耦效果圖:
企業微信的消息類型有多種:
羣聊按羣人數,又分紅3類:
業務繁多: 若是不加以隔離,那麼其中一個業務的波動有可能引發整個消息系統的癱瘓。
重中之重: 須要保證核心鏈路的穩定,就是企業內部的單聊和100人如下羣聊,由於這個業務是最基礎的,也是最敏感的,稍有問題,投訴量巨大。
其他的業務: 互相隔離,減小牽連。按照優先級和重要程度進行隔離,對應的併發度也作了調整,儘可能保證核心鏈路的穩定性。
解耦和隔離的效果圖:
企業微信的羣人數上限是10000,只要羣內每一個人都發一條消息,那麼擴散量就是10000 * 10000 = 1億次調用,很是巨大。10000人投遞完成須要的耗時長,影響了消息的及時性。
既然超大羣擴散寫量大、耗時長,那麼天然會想到:超大羣是否能夠單獨拎出來作成擴散讀呢。
下面分析一下超大羣設計成單副本面臨的難點:
綜上所述: 單副本的方案代價太大。
如下將介紹咱們針對萬人羣聊擴散寫的方案,作的一些優化實踐。
萬人羣的擴散量大,爲了是消息儘量及時到達,使用了多協程去分發消息。可是並非無限制地加大併發度。
爲了不某個萬人羣的高頻發消息,形成對整個消息系統的壓力,消息分發以羣id爲維度,限制了單個羣的分發併發度。消息分發給一我的的耗時是8ms,那麼萬人的整體耗時是80s,併發上限是5,那麼消息分發完成須要16s。16s的耗時,在產品角度來看還、是能夠接受的,大羣對及時性不敏感。同時,併發度控制在合理範圍內。
除了限制單個羣id的併發度,還限制了萬人羣的整體併發度。單臺機,小羣的worker數爲250個,萬人羣的worker數爲30。
萬人羣的頻繁發消息,worker數用滿,致使隊列出現積壓:
因爲併發限制,調用數被壓平,沒有請求無限上漲,系統穩定:
工做場景的聊天,多數是在小羣完成,大羣用於管理員發通知或者老闆發紅包。
大羣消息有一個常見的規律: 平時消息少,會忽然活躍。例如:老闆在羣裏發個大紅包,羣成員起鬨,此時就會產生大量的消息。
消息量上漲、併發度被限制、任務處理不過來,那麼隊列天然就會積壓。積壓的任務中可能存在多條消息須要分發給同一個羣的羣成員。
此時: 能夠將這些消息,合併成一個請求,寫入到消息存儲,消息系統的吞吐量就能夠成倍增長。
在平常的監控中,能夠捕獲到這種場景,高峯能夠同時插入20條消息,對整個系統很友善。
好比: 羣人員變動、羣名稱變更、羣設置變動,都會在羣內擴散一條不可見的控制消息。羣成員收到此控制消息,則向後臺請求同步新數據。
舉個例子: 一個萬人羣,因爲消息過於頻繁,對羣成員形成騷擾,部分羣成員選擇退羣來拒絕消息,假設有1000人選擇退羣。那麼擴散的控制消息量就是1000w,用戶收到控制消息就向後臺請求數據,則額外帶來1000w次的數據請求,形成系統的巨大壓力。
控制消息在小羣是頗有必要的,能讓羣成員實時感知羣信息的變動。
可是在大羣: 羣信息的變動其實不那麼實時,用戶也感受不到。因此結合業務場景,實施降級服務,控制消息在大羣能夠直接丟棄、不分發,減小對系統的調用。
回執消息是辦公場景常常用到的一個功能,能看到消息接受方的閱讀狀態。
一條回執消息的閱讀狀態會被頻繁修改,羣消息被修改的次數和羣成員人數成正比。天天上億條消息,讀寫頻繁,請求量巨大,怎麼保證每條消息在接受雙方的狀態是一致的是一個難點。
消息的閱讀狀態的存儲方式兩個方案。
方案一:
思路: 利用消息存儲,插入一條新消息指向舊消息,此新消息有最新的閱讀狀態。客戶端收到新消息,則用新消息的內容替換舊消息的內容展現,以達到展現閱讀狀態的效果。
優勢: 複用消息通道,增量同步消息就能夠獲取到回執狀態,複用通知機制和收發協議,先後端改造小。
缺點:
方案二:
思路: 獨立存儲每條消息的閱讀狀態,消息發送者經過消息id去拉取數據。
優勢: 狀態一致。
缺點:
企業微信採用了方案一去實現,簡單可靠、改動較小: 存儲冗餘的問題能夠經過LevelDB落盤的時候merge數據,只保留最終狀態那條消息便可;一致性問題下面會介紹如何解決。
上圖是協議流程 (referid:被指向的消息id,senderid:消息發送方的msgid):
1)每條消息都有一個惟一的msgid,只在單個用戶內惟一,kv存儲自動生成的;
2)接收方b已讀消息,客戶端帶上msgid=b1請求到後臺;
3)在接受方b新增一條消息,msgid=b2,referid=b1,指向msgid=b1的消息。並把msgid=b2的消息內容設置爲消息已讀。msgid=b1的消息體,存有發送方的msgid,即senderid=a1;
4)發送方a,讀出msgid=a1的消息體,把b加入到已讀列表,把新的已讀列表保存到消息體中,生成新消息msgid=a2,referid=a1,追加寫入到a的消息流;
5)接收方c已讀同一條消息,在c的消息流走一樣的邏輯;
6)發送方a,讀出msgid=a1的消息體,把c加入到已讀列表,把新的已讀列表保存到消息體中,生成新消息msgid=a3,referid=a1,追加寫入到a的消息流。a3>a2,以msgid大的a3爲最終狀態。
接受方已讀消息,讓客戶端同步感知成功,可是發送方的狀態不必同步修改。由於發送方的狀態修改狀況,接受方沒有感知不到。那麼,能夠採用異步化的策略,下降同步調用耗時。
具體作法是:
客戶端收到大量消息,並非一條一條消息已讀確認,而是多條消息一塊兒已讀確認。爲了提升回執消息的處理效率,能夠對多條消息合併處理。
如上圖所示:
1)X>>A:表示X發了一條消息給A;
2)A合併確認3條消息,B合併確認3條消息。那麼只須要處理2次,就能標誌6條消息已讀;
3)通過mq分發,相同的發送方也能夠合併處理。在發送方,X合併處理2條消息,Y合併處理2條消息,Z合併處理2條消息,則合併處理3次就能標誌6條消息。
通過合併處理,處理效率大大提升。下圖是採集了線上高峯時期的調用數據。能夠看得出來,優化後的效果一共節省了44%的寫入量。
發送方的消息處理方式是先把數據讀起來,修改後從新覆蓋寫入存儲。接收方有多個,那麼就會併發寫發送方數據,避免不了出現覆蓋寫的問題。
流程以下:
處理這類問題,無非就一下幾種辦法。
方案一: 由於併發操做是分佈式,那麼能夠採用分佈式鎖的方式保證一致。操做存儲以前,先申請分佈式鎖。這種方案過重,不適合這種高頻多帳號的場景。
方案二: 帶版本號讀寫。一個帳號的消息流只有一個版本鎖,高頻寫入的場景,很容易產生版本衝突,致使寫入效率低下。
方案三: mq串行化處理。能避免覆蓋寫問題,關鍵是在合併場景起到很好的做用。同一個帳號的請求串行化,就算出現隊列積壓,合併的策略也能提升處理效率。
企業微信採用了方案三,相同id的用戶請求串行化處理,簡單易行,邏輯改動較少。
「撤回消息」 至關於更新原消息的狀態,是否是也能夠經過referid的方式去指向呢?
回執消息分析過: 經過referid指向,必需要知道原消息的msgid。
區別於回執消息: 撤回消息須要修改全部接收方的消息狀態,而不只僅是發送方和單個接收方的。消息擴散寫到每一個接收方的消息流,各自的消息流對應的msgid是不相同的,若是沿用referid的方式,那就須要記錄全部接收方的msgid。
分析: 撤回消息比回執消息簡單的是,撤回消息只須要更新消息的狀態,而不須要知道原消息的內容。接收方的消息的appinfo都是相同的,能夠經過appinfo去作指向。
協議流程:
1)用戶a、b、c,都存在同一條消息,appinfo=s,sendtime=t;
2)a撤回該消息,則在a的消息流插入一條撤回的控制消息,消息體包含{appinfo=s,sendtime=t};
3)客戶端sync到撤回的控制消息,獲取到消息體的appinfo與sendtime,把本地appinfo=s且sendtime=t的原消息顯示爲撤回狀態,並刪除原消息數據。之因此引入sendtime字段,是爲了防止appinfo碰撞,加的雙重校驗;
4)接收方撤回流程和發送方一致,也是經過插入撤回的控制消息。
該方案的優勢明顯,可靠性高,協議簡單。
撤回消息的邏輯示意圖:
企業微信的IM消息架構與微信相似,可是在to B業務場景面臨了一些新的挑戰。結合產品形態、分析策略,經過優化方案,來確保消息系統的可靠性、穩定性、安全性。
企業微信的to B業務繁雜,有不少定製化的需求,消息系統的設計須要考慮通用性和擴展性,以便支持各類需求。例如:撤回消息的方案,能夠適用於消息任何屬性的更新,知足更多場景。(本文同步發佈於:www.52im.net/thread-3631… )