RocketMQ 端云一体化设计与实践
簡介:本文首先介紹了端云消息場景一體化的背景,然后重點分析了終端消息場景特點,以及終端消息場景支撐模型,最后對架構和存儲內核進行了闡述。我們期望基于 RocketMQ 統一內核一體化支持終端和服務端不同場景的消息接入目標,以能夠給使用者帶來一體化的價值,如降低存儲成本,避免數據在不同系統間同步帶來的一致性挑戰。
作者 | 悟幻
一體化背景
不止于分發
我們都知道以 RocketMQ 為代表的消息(隊列)起源于不同應用服務之間的異步解耦通信,與以 Dubbo 為代表的 RPC 類服務通信一同承載了分布式系統(服務)之間的通信場景,所以服務間的消息分發是消息的基礎訴求。然而我們看到,在消息(隊列)這個領域,近些年我們業界有個很重要的趨勢,就是基于消息這份數據可以擴展到流批計算、事件驅動等不同場景,如 RocketMQ-streams,Kafka-Streams、Rabbit-Streams 等等。
不止于服務端
傳統的消息隊列 MQ 主要應用于服務(端)之間的消息通信,比如電商領域的交易消息、支付消息、物流消息等等。然而在消息這個大類下,還有一個非常重要且常見的消息領域,即終端消息。消息的本質就是發送和接受,終端和服務端并沒有本質上的大區別。
一體化價值
如果可以有一個統一的消息系統(產品)來提供多場景計算(如 stream、event)、多場景(IoT、APP)接入,其實是非常有價值的,因為消息也是一種重要數據,數據如果只存在一個系統內,可以最大地降低存儲成本,同時可以有效地避免數據因在不同系統間同步帶來的一致性難題。
終端消息分析
本文將主要描述的是終端消息和服務端消息一體化設計與實踐問題,所以首先我們對面向終端的這一大類消息做一下基本分析。
場景介紹
近些年,我們看到隨著智能家居、工業互聯而興起的面向 IoT 設備類的消息正在呈爆炸式增長,而已經發展十余年的移動互聯網的手機 APP 端消息仍然是數量級龐大。面向終端設備的消息數量級比傳統服務端的消息要大很多量級,并仍然在快速增長。
特性分析
盡管無論是終端消息還是服務端消息,其本質都是消息的發送和接受,但是終端場景還是有和服務端不太一樣的特點,下面簡要分析一下:
- 輕量
服務端一般都是使用很重的客戶端 SDK 封裝了很多功能和特性,然而終端因為運行環境受限且龐雜必須使用輕量簡潔的客戶端 SDK。
- 標準協議
服務端正是因為有了重量級客戶端 SDK,其封裝了包括協議通信在內的全部功能,甚至可以弱化協議的存在,使用者無須感知,而終端場景因為要支持各類龐雜的設備和場景接入,必須要有個標準協議定義。
- P2P
服務端消息如果一臺服務器處理失敗可以由另外一臺服務器處理成功即可,而終端消息必須明確發給具體終端,若該終端處理失敗則必須一直重試發送該終端直到成功,這個和服務端很不一樣。
- 廣播比
服務端消息比如交易系統發送了一條訂單消息,可能有如營銷、庫存、物流等幾個系統感興趣,而終端場景比如群聊、直播可能成千上萬的終端設備或用戶需要收到。
- 海量接入
終端場景接入的是終端設備,而服務端接入的就是服務器,前者在量級上肯定遠大于后者。
架構與模型
消息基礎分析
實現一體化前我們先從理論上分析一下問題和可行性。我們知道,無論是終端消息還是服務端消息,其實就是一種通信方式,從通信的層面看要解決的基礎問題簡單總結就是:協議、匹配、觸達。
- 協議
協議就是定義了一個溝通語言頻道,通信雙方能夠聽懂內容語義。在終端場景,目前業界廣泛使用的是 MQTT 協議,起源于物聯網 IoT 場景,OASIS 聯盟定義的標準的開放式協議。
MQTT 協議定義了是一個 Pub/Sub 的通信模型,這個與 RocketMQ 類似的,不過其在訂閱方式上比較靈活,可以支持多級 Topic 訂閱(如 “/t/t1/t2”),可以支持通配符訂閱(如 “/t/t1/+”)
- 匹配
匹配就是發送一條消息后要找到所有的接受者,這個匹配查找過程是不可或缺的。
在 RocketMQ 里面實際上有這個類似的匹配過程,其通過將某個 Queue 通過 rebalance 方式分配到消費組內某臺機器上,消息通過 Queue 就直接對應上了消費機器,再通過訂閱過濾(Tag 或 SQL)進行精準匹配消費者。之所以通過 Queue 就可以匹配消費機器,是因為服務端場景消息并不需要明確指定某臺消費機器,一條消息可以放到任意 Queue 里面,并且任意一臺消費機器對應這個 Queue 都可以,消息不需要明確匹配消費機器。
而在終端場景下,一條消息必須明確指定某個接受者(設備),必須準確找到所有接受者,而且終端設備一般只會連到某個后端服務節點即單連接,和消息產生的節點不是同一個,必須有個較復雜的匹配查找目標的過程,還有如 MQTT 通配符這種更靈活的匹配特性。
- 觸達
觸達即通過匹配查找后找到所有的接受者目標,需要將消息以某種可靠方式發給接受者。常見的觸發方式有兩種:Push、Pull。Push,即服務端主動推送消息給終端設備,主動權在服務端側,終端設備通過 ACK 來反饋消息是否成功收到或處理,服務端需要根據終端是否返回 ACK 來決定是否重投。Pull,即終端設備主動來服務端獲取其所有消息,主動權在終端設備側,一般通過位點 Offset 來依次獲取消息,RocketMQ 就是這種消息獲取方式。
對比兩種方式,我們可以看到 Pull 方式需要終端設備主動管理消息獲取邏輯,這個邏輯其實有一定的復雜性(可以參考 RocketMQ 的客戶端管理邏輯),而終端設備運行環境和條件都很龐雜,不太適應較復雜的 Pull 邏輯實現,比較適合被動的 Push 方式。
另外,終端消息有一個很重要的區別是可靠性保證的 ACK 必須是具體到一個終端設備的,而服務端消息的可靠性在于只要有一臺消費者機器成功處理即可,不太關心是哪臺消費者機器,消息的可靠性 ACK 標識可以集中在消費組維度,而終端消息的可靠性 ACK 標識需要具體離散到終端設備維度。簡單地說,一個是客戶端設備維度的 Retry 隊列,一個是消費組維度的 Retry 隊列。
模型與組件
基于前面的消息基礎一般性分析,我們來設計消息模型,主要是要解決好匹配查找和可靠觸達兩個核心問題。
- 隊列模型
消息能夠可靠性觸達的前提是要可靠存儲,消息存儲的目的是為了讓接受者能獲取到消息,接受者一般有兩種消息檢索維度:
1)根據訂閱的主題 Topic 去查找消息;
2)根據訂閱者 ID 去查找消息。這個就是業界常說的放大模型:讀放大、寫放大。
讀放大:即消息按 Topic 進行存儲,接受者根據訂閱的 Topic 列表去相應的 Topic 隊列讀取消息。
寫放大:即消息分別寫到所有訂閱的接受者隊列中,每個接受者讀取自己的客戶端隊列。
可以看到讀放大場景下消息只寫一份,寫到 Topic 維度的隊列,但接受者讀取時需要按照訂閱的 Topic 列表多次讀取,而寫放大場景下消息要寫多份,寫到所有接受者的客戶端隊列里面,顯然存儲成本較大,但接受者讀取簡單,只需讀取自己客戶端一個隊列即可。
我們采用的讀放大為主,寫放大為輔的策略,因為存儲的成本和效率對用戶的體感最明顯。寫多份不僅加大了存儲成本,同時也對性能和數據準確一致性提出了挑戰。但是有一個地方我們使用了寫放大模式,就是通配符匹配,因為接受者訂閱的是通配符和消息的 Topic 不是一樣的內容,接受者讀消息時沒法反推出消息的 Topic,因此需要在消息發送時根據通配符的訂閱多寫一個通配符隊列,這樣接受者直接可以根據其訂閱的通配符隊列讀取消息。
上圖描述的接受我們的隊列存儲模型,消息可以來自各個接入場景(如服務端的 MQ/AMQP,客戶端的 MQTT),但只會寫一份存到 commitlog 里面,然后分發出多個需求場景的隊列索引(ConsumerQueue),如服務端場景(MQ/AMQP)可以按照一級 Topic 隊列進行傳統的服務端消費,客戶端 MQTT 場景可以按照 MQTT 多級 Topic 以及通配符訂閱進行消費消息。
這樣的一個隊列模型就可以同時支持服務端和終端場景的接入和消息收發,達到一體化的目標。
- 推拉模型
介紹了底層的隊列存儲模型后,我們再詳細描述一下匹配查找和可靠觸達是怎么做的。
上圖展示的是一個推拉模型,圖中的 P 節點是一個協議網關或 broker 插件,終端設備通過 MQTT 協議連到這個網關節點。消息可以來自多種場景(MQ/AMQP/MQTT)發送過來,存到 Topic 隊列后會有一個 notify 邏輯模塊來實時感知這個新消息到達,然后會生成消息事件(就是消息的 Topic 名稱),將該事件推送至網關節點,網關節點根據其連上的終端設備訂閱情況進行內部匹配,找到哪些終端設備能匹配上,然后會觸發 pull 請求去存儲層讀取消息再推送終端設備。
一個重要問題,就是 notify 模塊怎么知道一條消息在哪些網關節點上面的終端設備感興趣,這個其實就是關鍵的匹配查找問題。一般有兩種方式:1)簡單的廣播事件;2)集中存儲在線訂閱關系(如圖中的 lookup 模塊),然后進行匹配查找再精準推送。事件廣播機制看起來有擴展性問題,但是其實性能并不差,因為我們推送的數據很小就是 Topic 名稱,而且相同 Topic 的消息事件可以合并成一個事件,我們線上就是默認采用的這個方式。集中存儲在線訂閱關系,這個也是常見的一種做法,如保存到 Rds、Redis 等,但要保證數據的實時一致性也有難度,而且要進行匹配查找對整個消息的實時鏈路 RT 開銷也會有一定的影響。
可靠觸達及實時性這塊,上圖的推拉過程中首先是通過事件通知機制來實時告知網關節點,然后網關節點通過 Pull 機制來換取消息,然后 Push 給終端設備。Pull+Offset 機制可以保證消息的可靠性,這個是 RocketMQ 的傳統模型,終端節點被動接受網關節點的 Push,解決了終端設備輕量問題,實時性方面因為新消息事件通知機制而得到保障。
上圖中還有一個 Cache 模塊用于做消息隊列 cache,因為在大廣播比場景下如果為每個終端設備都去發起隊列 Pull 請求則對 broker 讀壓力較大,既然每個請求都去讀取相同的 Topic 隊列,則可以復用本地隊列 cache。
- lookup組件
上面的推拉模型通過新消息事件通知機制來解決實時觸達問題,事件推送至網關的時候需要一個匹配查找過程,盡管簡單的事件廣播機制可以到達一定的性能要求,但畢竟是一個廣播模型,在大規模網關節點接入場景下仍然有性能瓶頸。另外,終端設備場景有很多狀態查詢訴求,如查找在線狀態,連接互踢等等,仍然需要一個 KV 查找組件,即 lookup。
我們當然可以使用外部 KV 存儲如 Redis,但我們不能假定系統(產品)在用戶的交付環境,尤其是專有云的特殊環境一定有可靠的外部存儲服務依賴。
這個 lookup 查詢組件,實際上就是一個 KV 查詢,可以理解為是一個分布式內存 KV,但要比分布式 KV 實現難度至少低一個等級。我們回想一下一個分布式 KV 的基本要素有哪些:
如上圖所示,一般一個分布式 KV 讀寫流程是,Key 通過 hash 得到一個邏輯 slot,slot 通過一個映射表得到具體的 node。Hash 算法一般是固定模數,映射表一般是集中式配置或使用一致性協議來配置。節點擴縮一般通過調整映射表來實現。
分布式 KV 實現通常有三個基本關鍵點:
1)映射表一致性
讀寫都需要根據上圖的映射表進行查找節點的,如果規則不一致數據就亂了。映射規則配置本身可以通過集中存儲,或者 zk、raft 這類協議保證強一致性,但是新舊配置的切換不能保證節點同時進行,仍然存在不一致性窗口。
2)多副本
通過一致性協議同步存儲多個備份節點,用于容災或多讀。
3)負載分配
slot 映射 node 就是一個分配,要保證 node 負載均衡,比如擴縮情況可能要進行 slot 數據遷移等。
我們主要查詢和保存的是在線狀態數據,如果存儲的 node 節點宕機丟失數據,我們可以即時重建數據,因為都是在線的,所以不需要考慮多副本問題,也不需要考慮擴縮情況 slot 數據遷移問題,因為可以直接丟失重建,只需要保證關鍵的一點:映射表的一致性,而且我們有一個兜底機制——廣播,當分片數據不可靠或不可用時退化到廣播機制。
架構設計
基于前面的理論和模型分析介紹,我們在考慮用什么架構形態來支持一體化的目標,我們從分層、擴展、交付等方面進行一下描述。
- 分層架構
我們的目標是期望基于 RocketMQ 實現一體化且自閉環,但不希望 Broker 被侵入更多場景邏輯,我們抽象了一個協議計算層,這個計算層可以是一個網關,也可以是一個 broker 插件。Broker 專注解決 Queue 的事情以及為了滿足上面的計算需求做一些 Queue 存儲的適配或改造。協議計算層負責協議接入,并且要可插拔部署。
- 擴展設計
我們都知道消息產品屬于 PaaS 產品,與上層 SaaS 業務貼得最近,為了適應業務的不同需求,我們大致梳理一下關鍵的核心鏈路,在上下行鏈路上添加一些擴展點,如鑒權邏輯這個最偏業務化的邏輯,不同的業務需求都不一樣,又比如 Bridge 擴展,其能夠把終端設備狀態和消息數據與一些外部生態系統(產品)打通。
- 交付設計
好的架構設計還是要考慮最終的落地問題,即怎么交付。如今面臨的現狀是公共云、專有云,甚至是開源等各種環境條件的落地,挑戰非常大。其中最大的挑戰是外部依賴問題,如果產品要強依賴一個外部系統或產品,那對整個交付就會有非常大的不確定性。
為了應對各種復雜的交付場景,一方面會設計好擴展接口,根據交付環境條件進行適配實現;另一方面,我們也會盡可能對一些模塊提供默認內部實現,如上文提到的 lookup 組件,重復造輪子也是不得已而為之,這個也許就是做產品與做平臺的最大區別。
統一存儲內核
前面對整個協議模型和架構進行了詳細介紹,在 Broker 存儲層這塊還需要進一步的改造和適配。我們希望基于 RocketMQ 統一存儲內核來支撐終端和服務端的消息收發,實現一體化的目標。
前面也提到了終端消息場景和服務端一個很大的區別是,終端必須要有個客戶端維度的隊列才能保證可靠觸達,而服務端可以使用集中式隊列,因為消息隨便哪臺機器消費都可以,但是終端消息必須明確可靠推送給具體客戶端。客戶端維度的隊列意味著數量級上比傳統的 RocketMQ 服務端 Topic 隊列要大得多。
另外前面介紹的隊列模型里面,消息也是按照 Topic 隊列進行存儲的,MQTT 的 Topic 是一個靈活的多級 Topic,客戶端可以任意生成,而不像服務端場景 Topic 是一個很重的元數據強管理,這個也意味著 Topic 隊列的數量級很大。
海量隊列
我們都知道像 Kafka 這樣的消息隊列每個 Topic 是獨立文件,但是隨著 Topic 增多消息文件數量也增多,順序寫就退化成了隨機寫,性能下降明顯。RocketMQ 在 Kafka 的基礎上進行了改進,使用了一個 Commitlog 文件來保存所有的消息內容,再使用 CQ 索引文件來表示每個 Topic 里面的消息隊列,因為 CQ 索引數據較小,文件增多對 IO 影響要小很多,所以在隊列數量上可以達到十萬級。然而這終端設備隊列場景下,十萬級的隊列數量還是太小了,我們希望進一步提升一個數量級,達到百萬級隊列數量,我們引入了 Rocksdb 引擎來進行 CQ 索引分發。
Rocksdb 是一個廣泛使用的單機 KV 存儲引擎,具有高性能的順序寫能力。因為我們有了 commitlog 已具備了消息順序流存儲,所以可以去掉 Rocksdb 引擎里面的 WAL,基于 Rocksdb 來保存 CQ 索引。在分發的時候我們使用了 Rocksdb 的 WriteBatch 原子特性,分發的時候把當前的 MaxPhyOffset 注入進去,因為 Rocksdb 能夠保證原子存儲,后續可以根據這個 MaxPhyOffset 來做 Recover 的 checkpoint。我們提供了一個 Compaction 的自定義實現,來進行 PhyOffset 的確認,以清理已刪除的臟數據。
輕量Topic
我們都知道 RocketMQ 中的 Topic 是一個重要的元數據,使用前要提前創建,并且會注冊到 namesrv 上,然后通過 Topicroute 進行服務發現。前面說了,終端場景訂閱的 Topic 比較靈活可以任意生成,如果基于現有的 RocketMQ 的 Topic 重管理邏輯顯然有些困難。我們定義了一種輕量的 Topic,專門支持終端這種場景,不需要注冊 namesrv 進行管理,由上層協議邏輯層進行自管理,broker 只負責存儲。
總結
本文首先介紹了端云消息場景一體化的背景,然后重點分析了終端消息場景特點,以及終端消息場景支撐模型,最后對架構和存儲內核進行了闡述。我們期望基于 RocketMQ 統一內核一體化支持終端和服務端不同場景的消息接入目標,以能夠給使用者帶來一體化的價值,如降低存儲成本,避免數據在不同系統間同步帶來的一致性挑戰。
原文鏈接
本文為阿里云原創內容,未經允許不得轉載。?
總結
以上是生活随笔為你收集整理的RocketMQ 端云一体化设计与实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java应用结构规范
- 下一篇: Apsara Stack 技术百科 |