阿里IM技術(shù)分享(九):深度揭密RocketMQ在釘釘IM系統(tǒng)中的應(yīng)用實(shí)踐

本文由釘釘技術(shù)專家尹啟繡分享,有修訂和重新排版。
1、引言
短短的幾年時(shí)間,釘釘便迅速成為一款國民級應(yīng)用,發(fā)展速度堪稱迅猛。
IM作為釘釘最核心的功能,每天需要支持海量企業(yè)用戶的溝通,同時(shí)還通過 PaaS 形式為淘寶、高德等 App 提供基礎(chǔ)的即時(shí)通訊能力,是日均千億級消息量的 IM 平臺。
在釘釘?shù)腎M中,我們通過 RocketMQ實(shí)現(xiàn)了系統(tǒng)解耦、異步削峰填谷,還通過定時(shí)消息實(shí)現(xiàn)分布式定時(shí)任務(wù)等高級特性。同時(shí)與 RocketMQ 深入共創(chuàng),不斷優(yōu)化解決了很多RocketMQ本身的問題,并且孵化出 POP 消費(fèi)模式等新特性,使 RocketMQ 能夠完美支持對性能穩(wěn)定性和時(shí)延要求非常高的 IM 系統(tǒng)。本文將為你分享這些內(nèi)容。

學(xué)習(xí)交流:
- 移動(dòng)端IM開發(fā)入門文章:《新手入門一篇就夠:從零開發(fā)移動(dòng)端IM》
- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK(備用地址點(diǎn)此)
(本文已同步發(fā)布于:http://www.52im.net/thread-4106-1-1.html)
2、系列文章
本文是系列文章的第9篇,總目錄如下:
《阿里IM技術(shù)分享(一):企業(yè)級IM王者——釘釘在后端架構(gòu)上的過人之處》
《阿里IM技術(shù)分享(二):閑魚IM基于Flutter的移動(dòng)端跨端改造實(shí)踐》
《阿里IM技術(shù)分享(三):閑魚億級IM消息系統(tǒng)的架構(gòu)演進(jìn)之路》
《阿里IM技術(shù)分享(四):閑魚億級IM消息系統(tǒng)的可靠投遞優(yōu)化實(shí)踐》
《阿里IM技術(shù)分享(五):閑魚億級IM消息系統(tǒng)的及時(shí)性優(yōu)化實(shí)踐》
《阿里IM技術(shù)分享(六):閑魚億級IM消息系統(tǒng)的離線推送到達(dá)率優(yōu)化》
《阿里IM技術(shù)分享(七):閑魚IM的在線、離線聊天數(shù)據(jù)同步機(jī)制優(yōu)化實(shí)踐》
《阿里IM技術(shù)分享(八):深度解密釘釘即時(shí)消息服務(wù)DTIM的技術(shù)設(shè)計(jì)》
《阿里IM技術(shù)分享(九):深度揭密RocketMQ在釘釘IM系統(tǒng)中的應(yīng)用實(shí)踐》(* 本文)
3、釘釘IM面臨的巨大技術(shù)挑戰(zhàn)
3.1 概述
釘釘作為企業(yè)級 IM 領(lǐng)先者,面臨著巨大的技術(shù)挑戰(zhàn)。市面上DAU過億的App里,只有釘釘是2B產(chǎn)品,我們不僅需要和其他 2C 產(chǎn)品一樣,支持海量用戶的低時(shí)延、高并發(fā)、高性能、高可用,還需保證企業(yè)級用戶在使用釘釘時(shí)能夠提升溝通協(xié)同效率。
下圖是概括的是釘釘?shù)闹饕芰Γ?/strong>

3.2 技術(shù)挑戰(zhàn)1:ToB與ToC的差異
作為企業(yè)級應(yīng)用,需要保證幫助用戶提升溝通體驗(yàn)。
ToB 的工作溝通和 ToC 的場景生活溝通存在較大差異, ToC的IM產(chǎn)品比如微信,在有完整的關(guān)系鏈后,只需滿足大部分用戶需求即可。
然而微信的很多體驗(yàn)其實(shí)并不友好:比如聊天消息中的視頻圖片在固定時(shí)間內(nèi)沒有打開則會無法下載,卸載重裝之后聊天記錄全部丟失。
而 ToB 場景下:聊天記錄是非常重要的內(nèi)容,釘釘為保證用戶消息不丟失,提供了多端同步和消息云端存儲的能力,用戶任意換端都能查看完整的聊天記錄。
在工作過程中,大量會議是工作效率殺手,釘釘還提供了已讀、Ding 等效率套件,為工作溝通提供新選項(xiàng)。
3.3 技術(shù)挑戰(zhàn)2:安全要求高
在ToB 的工作場景下,用戶對信息安全要求非常高,信息安全是企業(yè)的生命線。
釘釘提供了人和組織架構(gòu)打通的工作群,用戶離開組織后自動(dòng)退出企業(yè)工作群,這樣就很好地保障了企業(yè)信息的安全。
同時(shí),在已經(jīng)支持的全鏈路加密能力上提供了三方加密能力,可以最大程度保障企業(yè)用戶的信息安全性。
3.4 技術(shù)挑戰(zhàn)3:穩(wěn)定性要求高
企業(yè)用戶對穩(wěn)定性的要求也非常高,如果釘釘出現(xiàn)故障,深度使用釘釘?shù)钠髽I(yè)都會受到巨大影響。
因此,釘釘 IM 系統(tǒng)在穩(wěn)定性上也做了非常深入的建設(shè),架構(gòu)上對依賴和流量做了深入治理,核心能力所有依賴都為雙倍。
比如雖然 RocketMQ 已經(jīng)非常穩(wěn)定,也沒有發(fā)生過故障,但是對 RocketMQ 可能出現(xiàn)故障的產(chǎn)品依然做了很好的保護(hù),使用 RocketMQ 定時(shí)消息和堆積能力做熱點(diǎn)治理和流量防護(hù),讓系統(tǒng)面對大規(guī)模流量時(shí)能從容應(yīng)對,并且建設(shè)了異地多活和可彈性擴(kuò)縮容能力,疫情期間很好地保證了學(xué)生們的在線課堂。
在穩(wěn)定性機(jī)制上,常態(tài)化容災(zāi)演練、突襲演練、自動(dòng)化健康巡檢等也能很好地保證線上穩(wěn)定性。比如波浪式流量就是在做斷網(wǎng)演練時(shí)發(fā)現(xiàn)。
3.5 技術(shù)挑戰(zhàn)4:業(yè)務(wù)多樣性
針對不同行業(yè)的業(yè)務(wù)多樣性,還要盡可能地滿足用戶的通用性需求,比如萬人群、全員群等,目前釘釘已經(jīng)做到能夠支持 10 萬人級別的群。
更多的業(yè)務(wù)需求將依賴于我們抽象出的通用開放能力,將 IM 能力盡可能地開放給企業(yè)和三方 ISV,使得不同形態(tài)的業(yè)務(wù)都能在釘釘平臺上得到滿足 。
4、消息隊(duì)列在釘釘IM系統(tǒng)中的重要作用
4.1 概述
在如此豐富的企業(yè)級能力下,釘釘IM要與微信等 ToC 產(chǎn)品一樣,支持億級用戶低時(shí)延溝通,系統(tǒng)架構(gòu)需要具備高并發(fā)、高性能、高可用的能力,挑戰(zhàn)非常之大。
IM 本身是異步化溝通系統(tǒng),與開會或者電話溝通相比,讓溝通雙方異步處理消息能夠減少打斷次數(shù),提升溝通效率。這種異步的特性和消息隊(duì)列的能力很契合,消息隊(duì)列可以很好地幫助 IM 完成異步化解耦、失敗重試、削峰填谷等能力。
這里,我們以釘釘IM系統(tǒng)最核心的發(fā)消息和已讀鏈路簡化流程(如下圖所示),來詳細(xì)說明消息隊(duì)列在系統(tǒng)里的重要作用。

?
4.2 發(fā)消息鏈路
釘釘IM系統(tǒng)的發(fā)消息鏈路流程如下:
1)處于登錄狀態(tài)的釘釘用戶發(fā)送一條消息時(shí),首先會將請求發(fā)送到 receiver 應(yīng)用;
2)為保證發(fā)消息體驗(yàn)和成功率,receiver 應(yīng)用只做這條消息能否發(fā)送的校驗(yàn),其他如消息入庫、接收者推送等都交由下游應(yīng)用完成;
3)校驗(yàn)完成之后將消息投遞給消息隊(duì)列,成功后即可返回給用戶;
4)消息發(fā)送成功,processor 會從消息隊(duì)列里訂閱到這條消息,并對消息進(jìn)行入庫處理,再通過消息隊(duì)列將消息交給同步服務(wù) syncserver 做處理,將消息同步給在線接收者。

上述過程中,對于不在線的用戶:可以通過消息隊(duì)列將消息推給離線 push 系統(tǒng)。離線 push 系統(tǒng)可以對接接蘋果、華為、小米等推送系統(tǒng)進(jìn)行離線推送。
用戶發(fā)消息過程中的每一步,失敗后都可通過消息隊(duì)列進(jìn)行重試處理。如 processor 入庫失敗,可將消息打回消息隊(duì)列,繼續(xù)回旋處理,達(dá)到最終一致。同時(shí),可以在訂閱的過程中對消費(fèi)限速,避免線上突發(fā)峰值給系統(tǒng)帶來災(zāi)難性的后果。
4.3 消息已讀鏈路
釘釘IM系統(tǒng)的消息已讀鏈路流程如下:
1)用戶對一條消息做讀操作后,會發(fā)送請求到已讀服務(wù);
2)已讀服務(wù)收到請求后,直接將請求放到消息隊(duì)列進(jìn)行異步處理,同時(shí)可以達(dá)到削峰填谷的目的;
3)已讀服務(wù)處理完之后,將已讀事件推給同步服務(wù),讓同步服務(wù)將已讀事件推送給消息發(fā)送者。
從上面兩個(gè)鏈路可以看出,消息隊(duì)列是 IM 系統(tǒng)里非常重要的組成部分。
5、釘釘IM選擇RocketMQ的原因
阿里內(nèi)部曾有 notify、RocketMQ 兩套應(yīng)用消息中間件,也有其他基于 MQTT 協(xié)議實(shí)現(xiàn)的消息隊(duì)列,最終都被 RocketMQ 統(tǒng)一。
IM 系統(tǒng)對消息隊(duì)列有如下幾個(gè)基本要求:
1)解耦和削峰填谷(這是消息隊(duì)列的基礎(chǔ)能力);
2)高性能、低時(shí)延;
3)高可用性。
對于第?3)點(diǎn):要求消息隊(duì)列的高可用性方面不僅包括系統(tǒng)可用性,也包括數(shù)據(jù)可用性,要求寫入消息隊(duì)列時(shí)消息不丟失(釘釘 IM 對消息的保證級別是一條都不丟)。

RocketMQ 經(jīng)過多次雙 11 考驗(yàn),其堆積性能、低時(shí)延、高可用已成為業(yè)屆標(biāo)桿,完全符合對消息隊(duì)列的要求。
同時(shí)它的其他特性也非常豐富,如定時(shí)消息、事務(wù)消息,能夠以極低的成本實(shí)現(xiàn)分布式定時(shí)任務(wù),消息可重放和死信隊(duì)列提供了后悔藥的能力,比如線上系統(tǒng)出現(xiàn) bug ,很多消息沒有正確處理,可以通過重置位點(diǎn)、重新消費(fèi)的方式,訂正之前的錯(cuò)誤處理。
另外:消息隊(duì)列的使用場景非常豐富,RocketMQ 的擴(kuò)展能力可以在消息發(fā)送和消費(fèi)上做切面處理,實(shí)現(xiàn)通用性的擴(kuò)展封裝,大大降低開發(fā)工作量。 Tag & SQL 過濾能讓下游針對性地訂閱定業(yè)務(wù)需要的消息,無需訂閱整個(gè) topic 里的所有消息,大幅降低下游系統(tǒng)的訂閱壓力。
RocketMQ 至今從未發(fā)生故障,集群峰值 TPS 可達(dá) 300w/s,從生產(chǎn)到消費(fèi)時(shí)延能夠保證在 10 ms 以內(nèi),支持 30 億條消息堆積,核心指標(biāo)數(shù)據(jù)表現(xiàn)搶眼,性能異常優(yōu)秀。

6、RocketMQ的消息必達(dá)3重保險(xiǎn)

如上圖所示,發(fā)消息流程中,很重要的一步是 receiver 應(yīng)用做完消息能否發(fā)送的校驗(yàn)之后,通過 RocketMQ 將消息投遞給 processor做消息入庫處理。
投遞過程中,將提供三重保險(xiǎn),以保證消息發(fā)送萬無一失。
第一重保險(xiǎn):receiver 將消息寫進(jìn) RocketMQ 時(shí), RocketMQ SDK 默認(rèn)會重試五次(每次嘗試不同的 broker ,保障了消息寫失敗的概率非常?。?/p>
第二重保險(xiǎn):寫入 RocketMQ 失敗的情況下,會嘗試以 RPC 形式將消息投遞給 processor 。
第三重保險(xiǎn):如果 RPC 形式也失敗,會嘗試將本地 redoLog 通過 Crontab 任務(wù)定時(shí)將消息回放到 RocketMQ 里面。
此外,如何在系統(tǒng)異常的情況下做到消息最終一致?
Processor 收到上游投遞的消息時(shí),會嘗試對消息做入庫處理。即使入庫失敗,依然會將消息投給同步服務(wù),將消息下發(fā),保證實(shí)時(shí)消息收發(fā)正常。異常情況時(shí)會將消息重新投遞到異常 topic 進(jìn)行重試,投遞過程中通過設(shè)置RocketMQ 定時(shí)消息做退避處理,對異常 topic 做限速消費(fèi)。
重試寫不同的 topic 是為了與正常流量隔離,優(yōu)先處理正常流量,防止因?yàn)楫惓A髁肯M(fèi)而導(dǎo)致真正的線上消息處理被延遲。
另外:Rocket MQ 的一個(gè) broker 默認(rèn)只有一個(gè) Retry 消息隊(duì)列,如果消費(fèi)失敗量特別大的情況下,會導(dǎo)致下游負(fù)載不均,某些機(jī)器打死。
此外:如果系統(tǒng)持續(xù)發(fā)生異常,則會不斷地進(jìn)行回旋重試,如果不做限速處理,線上容易出現(xiàn)流量疊加,導(dǎo)致整個(gè)系統(tǒng)雪崩。
7、RocketMQ的獨(dú)門絕技——分布式定時(shí)任務(wù)
在幾千人的群里發(fā)一條消息,假設(shè)有 1/4 的成員同時(shí)開著聊天窗口,如果不對服務(wù)端已讀服務(wù)和客戶端需要更新的已讀數(shù)做合并處理,更新的 QPS 會高達(dá)到 1000/s。釘釘能夠支持十幾萬人的超大群,超大群的活躍對服務(wù)端和客戶端都會帶來很大沖擊,而實(shí)際上用戶的需求只需實(shí)現(xiàn)秒級更新。
針對以上場景:可以利用 RocketMQ 的定時(shí)消息能力實(shí)現(xiàn)分布式定時(shí)任務(wù)。
以已讀流程為例(如下圖所示),用戶發(fā)起請求時(shí),會將請求放入集中式請求隊(duì)列,再通過 RocketMQ 定時(shí)消息生成定時(shí)任務(wù),比如 5 秒后批量處理。5秒之后,RocketMQ 訂閱到任務(wù)觸發(fā)消息,將隊(duì)列里面所有請求都取出處理。

▲ 用 RocketMQ 實(shí)現(xiàn)分布式定時(shí)任務(wù)的流程原理
我們抽象了一個(gè)分布式定時(shí)任務(wù)的組件,提供了很多其他實(shí)時(shí)性可達(dá)秒級的功能,如萬人群的群狀態(tài)更新、消息擴(kuò)展更新都接入了此組件。通過組件的定時(shí)合并處理,大幅降低系統(tǒng)壓力。
如上圖(右邊部分),在一些大群活躍的時(shí)間點(diǎn)成功地讓流量下降并保持平穩(wěn)狀態(tài)。
8、釘釘IM使用RocketMQ遇到的技術(shù)問題
8.1 概述
RocketMQ 的生產(chǎn)端策略如下:
1)生產(chǎn)者獲取到對應(yīng) topic 所有 broker 和 Queue 列表,然后輪詢寫入消息;
2)消費(fèi)者端也會獲取到 topic 所有 broker 和Queue列表;
3)還需要要從 broker 中獲取所有消費(fèi)者 IP 列表進(jìn)行排序(按照配置負(fù)載均衡,如哈希、一次性哈希等策略計(jì)算出自己應(yīng)該訂閱哪些 Queue)。

上圖中:ConsumerGroupA的Consumer1被分配到MessageQueue0和MessageQueue1,則它訂閱MessageQueue0和MessageQueue1。
在RocketMQ的使用過程中,我們面臨了諸多問題,下面我們來逐一分享。
8.2 問題1:波浪式流量
我們發(fā)現(xiàn)訂閱消息集群滾動(dòng)時(shí),CPU 呈現(xiàn)波浪式飆升。
經(jīng)過深入排查發(fā)現(xiàn),斷網(wǎng)演練后進(jìn)行網(wǎng)絡(luò)恢復(fù)時(shí),大量 producer 同時(shí)恢復(fù)工作,同時(shí)從第一個(gè) broker 的第一個(gè) Queue 開始寫入消息,生產(chǎn)消息波浪式寫入 RocketMQ ,進(jìn)而導(dǎo)致消費(fèi)者端出現(xiàn)波浪式流量。
最終,我們聯(lián)系 RocketMQ 開發(fā)人員,調(diào)整了生產(chǎn)策略,每次生產(chǎn)者發(fā)現(xiàn) broker 數(shù)量或狀態(tài)發(fā)生變化時(shí),都會隨機(jī)選取一個(gè)初始Queue寫入消息,以此解決問題。
另一個(gè)導(dǎo)致波浪式流量的問題是配置問題。
排查線上問題時(shí),從 broker 視角看,每個(gè) broker 的消息量都是平均的,但 consumer 之間流量相差特別大。最終通過在 producer 側(cè)嘗試抓包得以定位到問題,是由于 producer 寫入消息時(shí)超時(shí)率偏高。
梳理配置后發(fā)現(xiàn),是由于 producer 寫入消息時(shí)配置超時(shí)太短,Rocket MQ 在寫消息時(shí)會嘗試多次,比如第一個(gè) broker 寫入失敗后,將直接跳到下一個(gè) broker 的第一個(gè) Queue ,導(dǎo)致每個(gè) broker 的第一個(gè) Queue 消息量特別大,而靠后的 partition 幾乎沒有消息。
8.3 問題2:負(fù)載均衡維度太粗
負(fù)載均衡只能到Queue維度,導(dǎo)致需要不時(shí)地關(guān)注 Queue 數(shù)量。
比如線上流量增長過快,需要進(jìn)行擴(kuò)容,而擴(kuò)容后發(fā)現(xiàn)機(jī)器數(shù)大于 Queue 數(shù)量,導(dǎo)致無論怎么擴(kuò)容都無法分擔(dān)線上流量,最終只能聯(lián)系 RocketMQ 運(yùn)維人員調(diào)高 Queue 數(shù)量來解決。
雖然調(diào)高 Queue 數(shù)量能解決機(jī)器無法訂閱的問題,但因?yàn)樨?fù)載均衡策略只到 Queue 維度,負(fù)載始終無法均衡。從下圖可以看到, consumer 1 訂閱了兩個(gè) Queue 而 consumer 2 只訂閱了一個(gè) Queue。
8.4 問題3:單機(jī)夯死導(dǎo)致消息堆積
單機(jī)夯死導(dǎo)致消息堆積,這也是負(fù)載均衡只能到 Queue 維度帶來的副作用。
比如 Broker A 的 Queue 由 consumer 1 訂閱,出現(xiàn)宿主機(jī)磁盤 IO 夯死但與 broker 之間的心跳依然正常,導(dǎo)致 Queue 消息長時(shí)間無法訂閱進(jìn)而影響用戶接收消息。最終只能通過手動(dòng)介入將對應(yīng)機(jī)器下線來解決。
8.5 問題4:rebalance
Rocket MQ 的負(fù)載均衡由 client 自己計(jì)算,導(dǎo)致有機(jī)器異?;虬l(fā)布時(shí),整個(gè)集群狀態(tài)不穩(wěn)定,時(shí)常會出現(xiàn)某些 Queue 有多個(gè) consumer 訂閱,而某些 Queue 在幾十秒內(nèi)沒有 consumer 訂閱的情況。
因而導(dǎo)致線上發(fā)布的時(shí)候,出現(xiàn)消息亂序或?qū)Ψ揭鸦叵⒌@示未讀的情況。
8.6 問題5:C++ SDK 能力缺失
釘釘IM的核心處理模塊Receiver、processor 等應(yīng)用都是通過 C++ 實(shí)現(xiàn),而RocketMQ 的 C++ SDK 相比于 Java 存在較大缺失。經(jīng)常出現(xiàn)內(nèi)存泄漏或 CPU 飆高的情況,嚴(yán)重影響線上服務(wù)的穩(wěn)定。
9、釘釘IM與RocketMQ的相互促進(jìn)
面對以上困擾,在經(jīng)過過多次討論和共創(chuàng)后,最終孵化出 RocketMQ 5.0 POP 消費(fèi)模式。
這是 RocketMQ 在實(shí)時(shí)系統(tǒng)里程碑式的升級,解決了大量實(shí)時(shí)系統(tǒng)使用 RocketMQ 過程中遇到的問題(如下圖所示)。

1)Pop消費(fèi)模式下,每一個(gè) consumer 都會與所有 broker 建立長連接并具備消費(fèi)能力,以 broker 維護(hù)整個(gè)消息訂閱的負(fù)載均衡和位點(diǎn)。重云輕端的模式下,負(fù)載均衡、訂閱消息、位點(diǎn)維護(hù)都在客戶端完成,而新客戶端只需做長鏈接管理、消息接收,并且通用 gRPC 協(xié)議,使得多語言比如 C++、Go、 Python 等語言客戶端都能輕松實(shí)現(xiàn),無需持續(xù)投入力去升級維護(hù) SDK 。
2)broker能力升級更簡單。重云輕端很好地解決了客戶端版本升級問題,客戶端改動(dòng)的可能性和頻率大大降低。以往升級新特性或能力只能推動(dòng)所有相關(guān) SDK 應(yīng)用進(jìn)行升級發(fā)布,升級過程中還需考慮新老兼容等問題,工作量極大。而新模式只需升級 broker 即可完成工作。
3)單機(jī)夯死消息能繼續(xù)被消費(fèi)。新模式下 consumer 和 broker 進(jìn)行網(wǎng)狀連接和消息訂閱,由 broker 通過負(fù)載均衡策略平均分配消息給 consumer 進(jìn)行消費(fèi),以往宕機(jī)夯死導(dǎo)致的 Queue 消息堆積問題也迎刃而解。如果 broker 發(fā)現(xiàn) consumer 長時(shí)間沒有進(jìn)行消息 ACK ,則將不再對其投遞消息,徹底解決單機(jī)夯死問題。
4)無需關(guān)注partition數(shù)量。
5)徹底解決rebalance。
6)負(fù)載更均衡。通過新的訂閱模式,不管上游流量如何偏移,只要不超過單個(gè) broker 的容量上限,消費(fèi)端都能實(shí)現(xiàn)真正意義上的負(fù)載均衡。
POP 模式消費(fèi)模式已經(jīng)在釘釘 IM 場景磨合得非常成熟,在對可用性、性能、時(shí)延方面要求非常高的釘釘 IM 系統(tǒng)證明了自己,也證明了不斷升級的 RocketMQ 是即時(shí)通訊場景消息隊(duì)列的不二選擇。
10、相關(guān)資料
[1]?現(xiàn)代IM系統(tǒng)中聊天消息的同步和存儲方案探討
[2]?企業(yè)級IM王者——釘釘在后端架構(gòu)上的過人之處
[3]?深度解密釘釘即時(shí)消息服務(wù)DTIM的技術(shù)設(shè)計(jì)
[4]?釘釘——基于IM技術(shù)的新一代企業(yè)OA平臺的技術(shù)挑戰(zhàn)(視頻+PPT)
[5]?企業(yè)微信的IM架構(gòu)設(shè)計(jì)揭秘:消息模型、萬人群、已讀回執(zhí)、消息撤回等
[6]?IM系統(tǒng)的MQ消息中間件選型:Kafka還是RabbitMQ?
(本文已同步發(fā)布于:http://www.52im.net/thread-4106-1-1.html)