RocketMQ 端雲一體化設計與實踐

簡介:本次分享主要介紹面向設備端消息收發應用場景的架構模型設計,以及如何實現 基於RocketMQ的一體化消息平臺。

作者:悟幻

一體化背景

不止於分發

我們都知道以 RocketMQ 爲代表的消息(隊列)起源於不同應用服務之間的異步解耦通信,與以 Dubbo 爲代表的 RPC 類服務通信一同承載了分佈式系統(服務)之間的通信場景,所以服務間的消息分發是消息的基礎訴求。然而我們看到,在消息(隊列)這個領域,近些年我們業界有個很重要的趨勢,就是基於消息這份數據可以擴展到流批計算、事件驅動等不同場景,如RocketMQ-streams,Kafka-Streams、Rabbit-Streams等等。

不止於服務端

傳統的消息隊列MQ主要應用於服務(端)之間的消息通信,比如電商領域的交易消息、支付消息、物流消息等等。然而在消息這個大類下,還有一個非常重要且常見的消息領域,即終端消息。消息的本質就是發送和接受,終端和服務端並沒有本質上的大區別。

一體化價值

如果可以有一個統一的消息系統(產品)來提供多場景計算(如stream、event)、多場景(IoT、APP)接入,其實是非常有價值的,因爲消息也是一種重要數據,數據如果只存在一個系統內,可以最大地降低存儲成本,同時可以有效地避免數據因在不同系統間同步帶來的一致性難題。

終端消息分析

本文將主要描述的是終端消息和服務端消息一體化設計與實踐問題,所以首先我們對面向終端的這一大類消息做一下基本分析。

場景介紹

近些年,我們看到隨着智能家居、工業互聯而興起的面向IoT設備類的消息正在呈爆炸式增長,而已經發展十餘年的移動互聯網的手機APP端消息仍然是數量級龐大。面向終端設備的消息數量級比傳統服務端的消息要大很多量級,並仍然在快速增長。

特性分析

儘管無論是終端消息還是服務端消息,其本質都是消息的發送和接受,但是終端場景還是有和服務端不太一樣的特點,下面簡要分析一下:

(1)輕量客戶端,服務端一般都是使用很重的客戶端SDK封裝了很多功能和特性,然而終端因爲運行環境受限且龐雜必須使用輕量簡潔的客戶端SDK。

(2) 服務端正是因爲有了重量級客戶端SDK,其封裝了包括協議通信在內的全部功能,甚至可以弱化協議的存在,使用者無須感知,而終端場景因爲要支持各類龐雜的設備和場景接入,必須要有個標準協議定義。

(3)P2P,服務端消息如果一臺服務器處理失敗可以由另外一臺服務器處理成功即可,而終端消息必須明確發給具體終端,若該終端處理失敗則必須一直重試發送該終端直到成功,這個和服務端很不一樣。

(4)廣播比,服務端消息比如交易系統發送了一條訂單消息,可能有如營銷、庫存、物流等幾個系統感興趣,而終端場景比如羣聊、直播可能成千上萬的終端設備或用戶需要收到。

(5)海量接入,終端場景接入的是終端設備,而服務端接入的就是服務器,前者在量級上肯定遠大於後者。

架構與模型

消息基礎分析

實現一體化前我們先從理論上分析一下問題和可行性。我們知道,無論是終端消息還是服務端消息,其實就是一種通信方式,從通信的層面看要解決的基礎問題簡單總結就是:協議、匹配、觸達。

(1)協議

協議就是定義了一個溝通語言頻道,通信雙方能夠聽懂內容語義。在終端場景,目前業界廣泛使用的是MQTT協議,起源於物聯網IoT場景,OASIS聯盟定義的標準的開放式協議。

MQTT協議定義了是一個Pub/Sub的通信模型,這個與RocketMQ類似的,不過其在訂閱方式上比較靈活,可以支持多級Topic訂閱(如 “/t/t1/t2”),可以支持通配符訂閱(如 “/t/t1/+”)

(2)匹配

匹配就是發送一條消息後要找到所有的接受者,這個匹配查找過程是不可或缺的。

在RocketMQ裏面實際上有這個類似的匹配過程,其通過將某個Queue通過rebalance方式分配到消費組內某臺機器上,消息通過Queue就直接對應上了消費機器,再通過訂閱過濾(Tag或SQL)進行精準匹配消費者。之所以通過Queue就可以匹配消費機器,是因爲服務端場景消息並不需要明確指定某臺消費機器,一條消息可以放到任意Queue裏面,並且任意一臺消費機器對應這個Queue都可以,消息不需要明確匹配消費機器。

而在終端場景下,一條消息必須明確指定某個接受者(設備),必須準確找到所有接受者,而且終端設備一般只會連到某個後端服務節點即單連接,和消息產生的節點不是同一個,必須有個較複雜的匹配查找目標的過程,還有如MQTT通配符這種更靈活的匹配特性。

(3)觸達

觸達即通過匹配查找後找到所有的接受者目標,需要將消息以某種可靠方式發給接受者。常見的觸發方式有兩種:Push、Pull。Push,即服務端主動推送消息給終端設備,主動權在服務端側,終端設備通過ACK來反饋消息是否成功收到或處理,服務端需要根據終端是否返回ACK來決定是否重投。Pull,即終端設備主動來服務端獲取其所有消息,主動權在終端設備側,一般通過位點Offset來依次獲取消息,RocketMQ就是這種消息獲取方式。

對比兩種方式,我們可以看到Pull方式需要終端設備主動管理消息獲取邏輯,這個邏輯其實有一定的複雜性(可以參考RocketMQ的客戶端管理邏輯),而終端設備運行環境和條件都很龐雜,不太適應較複雜的Pull邏輯實現,比較適合被動的Push方式。

另外,終端消息有一個很重要的區別是可靠性保證的ACK必須是具體到一個終端設備的,而服務端消息的可靠性在於只要有一臺消費者機器成功處理即可,不太關心是哪臺消費者機器,消息的可靠性ACK標識可以集中在消費組維度,而終端消息的可靠性ACK標識需要具體離散到終端設備維度。簡單地說,一個是客戶端設備維度的Retry隊列,一個是消費組維度的Retry隊列。

模型與組件

基於前面的消息基礎一般性分析,我們來設計消息模型,主要是要解決好匹配查找和可靠觸達兩個核心問題。

(1)隊列模型

消息能夠可靠性觸達的前提是要可靠存儲,消息存儲的目的是爲了讓接受者能獲取到消息,接受者一般有兩種消息檢索維度:1)根據訂閱的主題Topic去查找消息;2)根據訂閱者ID去查找消息。這個就是業界常說的放大模型:讀放大、寫放大。

讀放大:即消息按Topic進行存儲,接受者根據訂閱的Topic列表去相應的Topic隊列讀取消息。寫放大:即消息分別寫到所有訂閱的接受者隊列中,每個接受者讀取自己的客戶端隊列。

可以看到讀放大場景下消息只寫一份,寫到Topic維度的隊列,但接受者讀取時需要按照訂閱的Topic列表多次讀取,而寫放大場景下消息要寫多份,寫到所有接受者的客戶端隊列裏面,顯然存儲成本較大,但接受者讀取簡單,只需讀取自己客戶端一個隊列即可。

我們採用的讀放大爲主,寫放大爲輔的策略,因爲存儲的成本和效率對用戶的體感最明顯。寫多份不僅加大了存儲成本,同時也對性能和數據準確一致性提出了挑戰。但是有一個地方我們使用了寫放大模式,就是通配符匹配,因爲接受者訂閱的是通配符和消息的Topic不是一樣的內容,接受者讀消息時沒法反推出消息的Topic,因此需要在消息發送時根據通配符的訂閱多寫一個通配符隊列,這樣接受者直接可以根據其訂閱的通配符隊列讀取消息。

上圖描述的接受我們的隊列存儲模型,消息可以來自各個接入場景(如服務端的MQ/AMQP,客戶端的MQTT),但只會寫一份存到commitlog裏面,然後分發出多個需求場景的隊列索引(ConsumerQueue),如服務端場景(MQ/AMQP)可以按照一級Topic隊列進行傳統的服務端消費,客戶端MQTT場景可以按照MQTT多級Topic以及通配符訂閱進行消費消息。

這樣的一個隊列模型就可以同時支持服務端和終端場景的接入和消息收發,達到一體化的目標。

(2)推拉模型

介紹了底層的隊列存儲模型後,我們再詳細描述一下匹配查找和可靠觸達是怎麼做的。

上圖展示的是一個推拉模型,圖中的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。

(3)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數據遷移問題,因爲可以直接丟失重建,只需要保證關鍵的一點:映射表的一致性,而且我們有一個兜底機制——廣播,當分片數據不可靠或不可用時退化到廣播機制。

架構設計

基於前面的理論和模型分析介紹,我們在考慮用什麼架構形態來支持一體化的目標,我們從分層、擴展、交付等方面進行一下描述。

(1)分層架構

我們的目標是期望基於RocketMQ實現一體化且自閉環,但不希望Broker被侵入更多場景邏輯,我們抽象了一個協議計算層,這個計算層可以是一個網關,也可以是一個broker插件。Broker專注解決Queue的事情以及爲了滿足上面的計算需求做一些Queue存儲的適配或改造。協議計算層負責協議接入,並且要可插拔部署。

(2)擴展設計

我們都知道消息產品屬於Paas產品,與上層Saas業務貼得最近,爲了適應業務的不同需求,我們大致梳理一下關鍵的核心鏈路,在上下行鏈路上添加一些擴展點,如鑑權邏輯這個最偏業務化的邏輯,不同的業務需求都不一樣,又比如Bridge擴展,其能夠把終端設備狀態和消息數據與一些外部生態系統(產品)打通。

(3)交付設計

好的架構設計還是要考慮最終的落地問題,即怎麼交付。如今面臨的現狀是公共雲、專有云,甚至是開源等各種環境條件的落地,挑戰非常大。其中最大的挑戰是外部依賴問題,如果產品要強依賴一個外部系統或產品,那對整個交付就會有非常大的不確定性。

爲了應對各種複雜的交付場景,一方面會設計好擴展接口,根據交付環境條件進行適配實現;另一方面,我們也會盡可能對一些模塊提供默認內部實現,如上文提到的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統一內核一體化支持終端和服務端不同場景的消息接入目標,以能夠給使用者帶來一體化的價值,如降低存儲成本,避免數據在不同系統間同步帶來的一致性挑戰。

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章