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

學習交流:
(本文已同步發布于:http://www.52im.net/thread-4106-1-1.html)
2、系列文章
本文是系列文章的第9篇,總目錄如下:
3、釘釘IM面臨的巨大技術挑戰
3.1 概述
釘釘作為企業級 IM 領先者,面臨著巨大的技術挑戰。市面上DAU過億的App里,只有釘釘是2B產品,我們不僅需要和其他 2C 產品一樣,支持海量用戶的低時延、高并發、高性能、高可用,還需保證企業級用戶在使用釘釘時能夠提升溝通協同效率。
下圖是概括的是釘釘的主要能力:
3.2 技術挑戰1:ToB與ToC的差異
作為企業級應用,需要保證幫助用戶提升溝通體驗。
ToB 的工作溝通和 ToC 的場景生活溝通存在較大差異, ToC的IM產品比如微信,在有完整的關系鏈后,只需滿足大部分用戶需求即可。
然而微信的很多體驗其實并不友好:比如聊天消息中的視頻圖片在固定時間內沒有打開則會無法下載,卸載重裝之后聊天記錄全部丟失。
而 ToB 場景下:聊天記錄是非常重要的內容,釘釘為保證用戶消息不丟失,提供了多端同步和消息云端存儲的能力,用戶任意換端都能查看完整的聊天記錄。
在工作過程中,大量會議是工作效率殺手,釘釘還提供了已讀、Ding 等效率套件,為工作溝通提供新選項。
3.3 技術挑戰2:安全要求高
在ToB 的工作場景下,用戶對信息安全要求非常高,信息安全是企業的生命線。
釘釘提供了人和組織架構打通的工作群,用戶離開組織后自動退出企業工作群,這樣就很好地保障了企業信息的安全。
同時,在已經支持的全鏈路加密能力上提供了三方加密能力,可以最大程度保障企業用戶的信息安全性。
3.4 技術挑戰3:穩定性要求高
企業用戶對穩定性的要求也非常高,如果釘釘出現故障,深度使用釘釘的企業都會受到巨大影響。
因此,釘釘 IM 系統在穩定性上也做了非常深入的建設,架構上對依賴和流量做了深入治理,核心能力所有依賴都為雙倍。
比如雖然 RocketMQ 已經非常穩定,也沒有發生過故障,但是對 RocketMQ 可能出現故障的產品依然做了很好的保護,使用 RocketMQ 定時消息和堆積能力做熱點治理和流量防護,讓系統面對大規模流量時能從容應對,并且建設了異地多活和可彈性擴縮容能力,疫情期間很好地保證了學生們的在線課堂。
在穩定性機制上,常態化容災演練、突襲演練、自動化健康巡檢等也能很好地保證線上穩定性。比如波浪式流量就是在做斷網演練時發現。
3.5 技術挑戰4:業務多樣性
針對不同行業的業務多樣性,還要盡可能地滿足用戶的通用性需求,比如萬人群、全員群等,目前釘釘已經做到能夠支持 10 萬人級別的群。
更多的業務需求將依賴于我們抽象出的通用開放能力,將 IM 能力盡可能地開放給企業和三方 ISV,使得不同形態的業務都能在釘釘平臺上得到滿足 。
4、消息隊列在釘釘IM系統中的重要作用
4.1 概述
在如此豐富的企業級能力下,釘釘IM要與微信等 ToC 產品一樣,支持億級用戶低時延溝通,系統架構需要具備高并發、高性能、高可用的能力,挑戰非常之大。
IM 本身是異步化溝通系統,與開會或者電話溝通相比,讓溝通雙方異步處理消息能夠減少打斷次數,提升溝通效率。這種異步的特性和消息隊列的能力很契合,消息隊列可以很好地幫助 IM 完成異步化解耦、失敗重試、削峰填谷等能力。
這里,我們以釘釘IM系統最核心的發消息和已讀鏈路簡化流程(如下圖所示),來詳細說明消息隊列在系統里的重要作用。
4.2 發消息鏈路
釘釘IM系統的發消息鏈路流程如下:
- 1)處于登錄狀態的釘釘用戶發送一條消息時,首先會將請求發送到 receiver 應用;
- 2)為保證發消息體驗和成功率,receiver 應用只做這條消息能否發送的校驗,其他如消息入庫、接收者推送等都交由下游應用完成;
- 3)校驗完成之后將消息投遞給消息隊列,成功后即可返回給用戶;
- 4)消息發送成功,processor 會從消息隊列里訂閱到這條消息,并對消息進行入庫處理,再通過消息隊列將消息交給同步服務 syncserver 做處理,將消息同步給在線接收者。

上述過程中,對于不在線的用戶:可以通過消息隊列將消息推給離線 push 系統。離線 push 系統可以對接接蘋果、華為、小米等推送系統進行離線推送。
用戶發消息過程中的每一步,失敗后都可通過消息隊列進行重試處理。如 processor 入庫失敗,可將消息打回消息隊列,繼續回旋處理,達到最終一致。同時,可以在訂閱的過程中對消費限速,避免線上突發峰值給系統帶來災難性的后果。
4.3 消息已讀鏈路
釘釘IM系統的消息已讀鏈路流程如下:
- 1)用戶對一條消息做讀操作后,會發送請求到已讀服務;
- 2)已讀服務收到請求后,直接將請求放到消息隊列進行異步處理,同時可以達到削峰填谷的目的;
- 3)已讀服務處理完之后,將已讀事件推給同步服務,讓同步服務將已讀事件推送給消息發送者。
從上面兩個鏈路可以看出,消息隊列是 IM 系統里非常重要的組成部分。
5、釘釘IM選擇RocketMQ的原因
阿里內部曾有 notify、RocketMQ 兩套應用消息中間件,也有其他基于 MQTT 協議實現的消息隊列,最終都被 RocketMQ 統一。
IM 系統對消息隊列有如下幾個基本要求:
- 1)解耦和削峰填谷(這是消息隊列的基礎能力);
- 2)高性能、低時延;
- 3)高可用性。
對于第 3)點:要求消息隊列的高可用性方面不僅包括系統可用性,也包括數據可用性,要求寫入消息隊列時消息不丟失(釘釘 IM 對消息的保證級別是一條都不丟)。
RocketMQ 經過多次雙 11 考驗,其堆積性能、低時延、高可用已成為業屆標桿,完全符合對消息隊列的要求。
同時它的其他特性也非常豐富,如定時消息、事務消息,能夠以極低的成本實現分布式定時任務,消息可重放和死信隊列提供了后悔藥的能力,比如線上系統出現 bug ,很多消息沒有正確處理,可以通過重置位點、重新消費的方式,訂正之前的錯誤處理。
另外:消息隊列的使用場景非常豐富,RocketMQ 的擴展能力可以在消息發送和消費上做切面處理,實現通用性的擴展封裝,大大降低開發工作量。 Tag & SQL 過濾能讓下游針對性地訂閱定業務需要的消息,無需訂閱整個 topic 里的所有消息,大幅降低下游系統的訂閱壓力。
RocketMQ 至今從未發生故障,集群峰值 TPS 可達 300w/s,從生產到消費時延能夠保證在 10 ms 以內,支持 30 億條消息堆積,核心指標數據表現搶眼,性能異常優秀。
6、RocketMQ的消息必達3重保險
如上圖所示,發消息流程中,很重要的一步是 receiver 應用做完消息能否發送的校驗之后,通過 RocketMQ 將消息投遞給 processor做消息入庫處理。
投遞過程中,將提供三重保險,以保證消息發送萬無一失。
第一重保險:receiver 將消息寫進 RocketMQ 時, RocketMQ SDK 默認會重試五次(每次嘗試不同的 broker ,保障了消息寫失敗的概率非常小)。
第二重保險:寫入 RocketMQ 失敗的情況下,會嘗試以 RPC 形式將消息投遞給 processor 。
第三重保險:如果 RPC 形式也失敗,會嘗試將本地 redoLog 通過 Crontab 任務定時將消息回放到 RocketMQ 里面。
此外,如何在系統異常的情況下做到消息最終一致?
Processor 收到上游投遞的消息時,會嘗試對消息做入庫處理。即使入庫失敗,依然會將消息投給同步服務,將消息下發,保證實時消息收發正常。異常情況時會將消息重新投遞到異常 topic 進行重試,投遞過程中通過設置RocketMQ 定時消息做退避處理,對異常 topic 做限速消費。
重試寫不同的 topic 是為了與正常流量隔離,優先處理正常流量,防止因為異常流量消費而導致真正的線上消息處理被延遲。
另外:Rocket MQ 的一個 broker 默認只有一個 Retry 消息隊列,如果消費失敗量特別大的情況下,會導致下游負載不均,某些機器打死。
此外:如果系統持續發生異常,則會不斷地進行回旋重試,如果不做限速處理,線上容易出現流量疊加,導致整個系統雪崩。
7、RocketMQ的獨門絕技——分布式定時任務
在幾千人的群里發一條消息,假設有 1/4 的成員同時開著聊天窗口,如果不對服務端已讀服務和客戶端需要更新的已讀數做合并處理,更新的 QPS 會高達到 1000/s。釘釘能夠支持十幾萬人的超大群,超大群的活躍對服務端和客戶端都會帶來很大沖擊,而實際上用戶的需求只需實現秒級更新。
針對以上場景:可以利用 RocketMQ 的定時消息能力實現分布式定時任務。
以已讀流程為例(如下圖所示),用戶發起請求時,會將請求放入集中式請求隊列,再通過 RocketMQ 定時消息生成定時任務,比如 5 秒后批量處理。5秒之后,RocketMQ 訂閱到任務觸發消息,將隊列里面所有請求都取出處理。
▲ 用 RocketMQ 實現分布式定時任務的流程原理
我們抽象了一個分布式定時任務的組件,提供了很多其他實時性可達秒級的功能,如萬人群的群狀態更新、消息擴展更新都接入了此組件。通過組件的定時合并處理,大幅降低系統壓力。
如上圖(右邊部分),在一些大群活躍的時間點成功地讓流量下降并保持平穩狀態。
8、釘釘IM使用RocketMQ遇到的技術問題
8.1 概述
RocketMQ 的生產端策略如下:
- 1)生產者獲取到對應 topic 所有 broker 和 Queue 列表,然后輪詢寫入消息;
- 2)消費者端也會獲取到 topic 所有 broker 和Queue列表;
- 3)還需要要從 broker 中獲取所有消費者 IP 列表進行排序(按照配置負載均衡,如哈希、一次性哈希等策略計算出自己應該訂閱哪些 Queue)。

上圖中:ConsumerGroupA的Consumer1被分配到MessageQueue0和MessageQueue1,則它訂閱MessageQueue0和MessageQueue1。
在RocketMQ的使用過程中,我們面臨了諸多問題,下面我們來逐一分享。
8.2 問題1:波浪式流量
我們發現訂閱消息集群滾動時,CPU 呈現波浪式飆升。
經過深入排查發現,斷網演練后進行網絡恢復時,大量 producer 同時恢復工作,同時從第一個 broker 的第一個 Queue 開始寫入消息,生產消息波浪式寫入 RocketMQ ,進而導致消費者端出現波浪式流量。
最終,我們聯系 RocketMQ 開發人員,調整了生產策略,每次生產者發現 broker 數量或狀態發生變化時,都會隨機選取一個初始Queue寫入消息,以此解決問題。
另一個導致波浪式流量的問題是配置問題。
排查線上問題時,從 broker 視角看,每個 broker 的消息量都是平均的,但 consumer 之間流量相差特別大。最終通過在 producer 側嘗試抓包得以定位到問題,是由于 producer 寫入消息時超時率偏高。
梳理配置后發現,是由于 producer 寫入消息時配置超時太短,Rocket MQ 在寫消息時會嘗試多次,比如第一個 broker 寫入失敗后,將直接跳到下一個 broker 的第一個 Queue ,導致每個 broker 的第一個 Queue 消息量特別大,而靠后的 partition 幾乎沒有消息。
8.3 問題2:負載均衡維度太粗
負載均衡只能到Queue維度,導致需要不時地關注 Queue 數量。
比如線上流量增長過快,需要進行擴容,而擴容后發現機器數大于 Queue 數量,導致無論怎么擴容都無法分擔線上流量,最終只能聯系 RocketMQ 運維人員調高 Queue 數量來解決。
雖然調高 Queue 數量能解決機器無法訂閱的問題,但因為負載均衡策略只到 Queue 維度,負載始終無法均衡。從下圖可以看到, consumer 1 訂閱了兩個 Queue 而 consumer 2 只訂閱了一個 Queue。
8.4 問題3:單機夯死導致消息堆積
單機夯死導致消息堆積,這也是負載均衡只能到 Queue 維度帶來的副作用。
比如 Broker A 的 Queue 由 consumer 1 訂閱,出現宿主機磁盤 IO 夯死但與 broker 之間的心跳依然正常,導致 Queue 消息長時間無法訂閱進而影響用戶接收消息。最終只能通過手動介入將對應機器下線來解決。
8.5 問題4:rebalance
Rocket MQ 的負載均衡由 client 自己計算,導致有機器異常或發布時,整個集群狀態不穩定,時常會出現某些 Queue 有多個 consumer 訂閱,而某些 Queue 在幾十秒內沒有 consumer 訂閱的情況。
因而導致線上發布的時候,出現消息亂序或對方已回消息但顯示未讀的情況。
8.6 問題5:C++ SDK 能力缺失
釘釘IM的核心處理模塊Receiver、processor 等應用都是通過 C++ 實現,而RocketMQ 的 C++ SDK 相比于 Java 存在較大缺失。經常出現內存泄漏或 CPU 飆高的情況,嚴重影響線上服務的穩定。
9、釘釘IM與RocketMQ的相互促進
面對以上困擾,在經過過多次討論和共創后,最終孵化出 RocketMQ 5.0 POP 消費模式。
這是 RocketMQ 在實時系統里程碑式的升級,解決了大量實時系統使用 RocketMQ 過程中遇到的問題(如下圖所示)。
1)Pop消費模式下,每一個 consumer 都會與所有 broker 建立長連接并具備消費能力,以 broker 維護整個消息訂閱的負載均衡和位點。重云輕端的模式下,負載均衡、訂閱消息、位點維護都在客戶端完成,而新客戶端只需做長鏈接管理、消息接收,并且通用 gRPC 協議,使得多語言比如 C++、Go、 Python 等語言客戶端都能輕松實現,無需持續投入力去升級維護 SDK 。
2)broker能力升級更簡單。重云輕端很好地解決了客戶端版本升級問題,客戶端改動的可能性和頻率大大降低。以往升級新特性或能力只能推動所有相關 SDK 應用進行升級發布,升級過程中還需考慮新老兼容等問題,工作量極大。而新模式只需升級 broker 即可完成工作。
3)單機夯死消息能繼續被消費。新模式下 consumer 和 broker 進行網狀連接和消息訂閱,由 broker 通過負載均衡策略平均分配消息給 consumer 進行消費,以往宕機夯死導致的 Queue 消息堆積問題也迎刃而解。如果 broker 發現 consumer 長時間沒有進行消息 ACK ,則將不再對其投遞消息,徹底解決單機夯死問題。
4)無需關注partition數量。
5)徹底解決rebalance。
6)負載更均衡。通過新的訂閱模式,不管上游流量如何偏移,只要不超過單個 broker 的容量上限,消費端都能實現真正意義上的負載均衡。
POP 模式消費模式已經在釘釘 IM 場景磨合得非常成熟,在對可用性、性能、時延方面要求非常高的釘釘 IM 系統證明了自己,也證明了不斷升級的 RocketMQ 是即時通訊場景消息隊列的不二選擇。
10、相關資料
[1] 現代IM系統中聊天消息的同步和存儲方案探討
[2] 企業級IM王者——釘釘在后端架構上的過人之處
[3] 深度解密釘釘即時消息服務DTIM的技術設計
[4] 釘釘——基于IM技術的新一代企業OA平臺的技術挑戰(視頻+PPT)
[5] 企業微信的IM架構設計揭秘:消息模型、萬人群、已讀回執、消息撤回等
[6] IM系統的MQ消息中間件選型:Kafka還是RabbitMQ?
(本文已同步發布于:http://www.52im.net/thread-4106-1-1.html)