基於 RocketMQ 的 MQTT 服務架構在小米的實踐

1.jpeg

本文作者:房成進 - 小米高級研發工程師

小米 MQTT應用場景

2.png

小米之家門店的支付通知是小米MQTT落地的重要場景之一,流程如上圖所示。店員通過終端發送下單請求到後端服務,後端在接收到下單請求後,調用支付服務,等待用戶付款。門店終端如何知道本次付款是否成功呢?

我們採用MQTT協議來實現支付消息的通知。支付服務將本次訂單的支付結果發佈到MQTT 服務的一個 Topic中,門店終端與服務保持長連接,訂閱 Topic來實時獲取支付結果,從而進行下一步操作如打印發票等。得益於 TCP長連接和MQTT協議的輕量化,門店終端系統的支付響應能力從 200 毫秒下降至 10 毫秒內,MQTT服務發佈端到訂閱端的平均延時爲2.6ms。

2.png

手機智能製造工廠是小米MQTT落地的另一個核心場景。MQTT主要應用於設備狀態數據採集以及設備控制指令下發。上圖右側爲小米智能製造工廠架構圖。

上行鏈路流程爲:手機生產線上的衆多工業設備會將操作日誌、設備參數、環境參數等通過工業控制層發佈到MQTT服務,MQTT服務的存儲層通過數據集成任務將數據打入大數據系統,進行數據的分析、建模和處理等,最後實現最上層應用工業 BI 和數字孿生的需求。

下行鏈路流程爲:工廠的工作人員通過雲端服務將控制指令下發到MQTT集羣,生產線上的設備與MQTT服務集羣保持長連接,以接受來自雲端的控制指令並執行相應動作。這兩個鏈路對時效性要求很高。目前, MQTT 服務能保證上行和下行鏈路延時在 20ms以內,服務可用性爲99.95%。

小米 MQTT服務架構演進過程

3.png

早期,小米主要基於RocketMQ 社區在 18 年開源的RocketMQ-IoT-Bridge來構建自己的 MQTT 服務。RocketMQ-IoT-Bridge爲單機架構,一是不支持水平擴展,總連接數存在瓶頸,自然無法保證高可用。二是數據無法持久化,只提供內存存儲,一旦重啓服務,必然導致消息丟失。三是隻支持MQTT 協議QoS0,消息存在丟失風險,無法滿足小米的業務要求。如圖所示,服務整體爲單機服務架構,發佈端和訂閱端連接到同一個進程。

4.png

小米基於單機的架構進行了一系列的拓展。高可用方面,從單機變爲分佈式的可擴展架構,連接數從單機的 5 萬變爲可橫向擴展的模式;可靠性方面,在QoS0 的基礎上實現了MQTT協議規定的 QoS 1 和 QoS 2;消費模式方面,除了默認的廣播消費,支持了MQTT5.0新增的共享消費模式,同時還支持了離線消息。

5.png

上圖右側是小米基於 RocketMQ 的分佈式 MQTT 架構圖。最上層爲客戶端,發佈者和訂閱者連接到負載均衡器,這裏使用四層的負載均衡LVS, 主要目的是將請求均攤到各個MQTT Bridge。MQTT Bridge 即MQTT的服務節點,負責連接、訂閱、解析協議和消息轉發。RocketMQ 作爲存儲層,負責持久化消息。類似於存算分離設計,MQTT Bridge 和 RocketMQ 均可獨立水平擴展。

得益於 RocketMQ 從 2020 年開始在小米大規模落地,我們採用RocketMQ來持久化 MQTT 消息。整個發佈訂閱的過程演變成消息從 Bridge發送到RocketMQ,再從RocketMQ消費數據然後推送到訂閱端。每一個MQTT Bridge 內嵌 RocketMQ SDK ,充當 RocketMQ的客戶端,既作爲生產者也作爲消費者。

此外,持久化層支持了小米自研的消息隊列Talos,提供了可插拔模式。根據業務數據的下游使用場景,部署時可靈活選擇任意一個消息隊列作爲持久化層。

6.png

MQTT協議的消息結構和 RocketMQ 的消息結構互相獨立,因此如果將MQTT協議的消息持久化到 RocketMQ 中,必然需要做一定的匹配。MQTT Topic有多級,如圖中T1/T2/T3所示,爲多級樹形結構。將 T1 看作一級 Topic,對應 RocketMQ 中的 Topic T1,則所有發往以 T1 開頭的 MQTT Topic的消息都會持久化到 RocketMQ 的 T1 Topic中。

此時問題演變成如何區分一條消息屬於哪個MQTT Topic,我們選擇將MQTT Topic設爲消息的 tag,MQTT消息中的一些可變 header 直接放在RocketMQ 消息屬性 KV 中,消息體可以直接映射到 RocketMQ消息的 Payload 中,這樣完成了MQTT消息到RocketMQ消息的映射。

7.png

除消息數據之外,元數據是 MQTT 服務非常重要的一部分。MQTT Bridge 中保存了兩類元數據,分別是客戶端元數據和訂閱元數據。客戶端元數據保存了客戶端的連接信息、連接時間、客戶端 ID、Netty channel 等信息,我們實現了可視化的控制檯,支持查詢MQTT服務的連接數,支持通過連接 ID 和客戶端 ID 查詢客戶端的信息。此外,實現了客戶端上下線通知,用戶可以通過訂閱 MQTT  Topic實時獲取到某個客戶端的上線和下線事件。訂閱元數據保存了客戶端和MQTT的映射關係,主要通過Trie樹來保存訂閱關係,可以滿足通配符的方式訂閱,實現快速匹配。Bridge 通過訂閱 Topic找到客戶端,將消息定向推送。

8.png

MQTT協議主要有三個服務質量等級 QoS 0、 QoS 1 和 QoS 2。QoS 0表示消息最多發一次,可能存在丟失消息的情況,性能最好,對於數據可靠性要求不高的業務較爲實用。QoS 1 爲消息保證能至少到達一次,可能會重複,性能相對差一些。QoS 2 爲消息不丟不重,但性能最差。

9.png

上圖爲QoS0的實現流程。QoS 指發送端和接收端之間的消息傳輸質量。發佈消息時,MQTT Bridge 作爲消息的接收端,IoT 設備作爲發佈端。訂閱消息時,MQTT Bridge作爲發佈端,IoT設備作爲接收端。發佈和訂閱是兩個獨立的 QoS 過程,整條鏈路的 QoS 是這兩部分 QoS 的最低值,比如發佈是 QoS 1,訂閱是 QoS 0,則整條鏈路的 QoS 等級就是 0。左側是 QoS 0 發佈的過程。發佈端IoT將消息推送給MQTT Bridge,Bridge 將消息異步推送到 RocketMQ,無需等待響應。圖中兩個箭頭的請求都可能失敗,可能會丟數據,可靠性很低。但由於鏈路短,因此性能較高。

10.png

上圖爲 QoS 1的實現流程。IoT 終端發佈消息之前,會先將其持久化到本地內存裏,Bridge 收到消息之後,將消息異步推送到 RocketMQ,等待消息持久化成功的結果後,再返回pubAck包給IoT,IoT 將內存裏的這條消息刪除。發送的請求可能會失敗,發送端內存裏存儲了消息,因此可以通過重試來實現消息至少被髮一次,但也導致消息可能會重複發送。訂閱端同理。

11.png

QoS 2 的實現流程如上圖。在QoS 1時, Bridge接受到消息後沒有將消息持久化在自己的內存裏,而是直接將消息推送到RocketMQ中。如果發送端一直沒有收到 pubAck 包,則執行重發,重發之後 Bridge無法獲知收到的消息是新消息還是重發消息,會造成消息重複。QoS 2基於 messageID 來實現消息去重。MQTT 協議要求 message ID 可以被重複使用,且有一定範圍,不會一直遞增。所以在利用 messageID 去重的同時,還要保證 messageID 在傳輸過程中不能有重複,用完後必須釋放。

依據這兩點前提,sender在發送消息之前,會將消息持久化在自己的內存裏,再推送給 receiver。receiver 收到消息之後也會放在本地內存裏,返回 PubRec 包給 sender,通知其已經收到消息。如果 sender 一直沒有收到PubRec包,會不斷地重複發送消息。由於receiver 內存裏已經保存了消息,因此可以通過 messageID 來實現消息的去重。發送端在接收到 PubRec 包後發佈PubRel包,通知 receiver 可以清理內存中的消息,也意味着sender已經知道消息已被 receiver 持久化,此時再由 receiver 將消息推給RocketMQ 並等待持久化響應。receiver 發送 PubComp 包給 sender通知其可將PubRel包刪除。上圖中步驟 3.3可能失敗,因此sender必須在內存中緩存PubRel包。上述流程存在兩步確認機制,第一個是保證消息能到達 receiver ;第二個是保證將用過的 messageID 釋放掉,能夠實現 message ID 的重用。

12.png

推拉模型是 MQTT Bridge 實現消息發佈訂閱的核心模型。假設以下場景:有四個訂閱端,其中訂閱端IoT-1和IoT-2分別訂閱了 Topic1/a、Topic1/b,IoT-3和IoT-4分別訂閱了Topic2/ a。第一、二臺設備連接到第一個 Bridge,第三、四臺設備連接到第二個 Bridge。當有新的訂閱關係過來時,會檢查訂閱一級 Topic。上圖中Bridge1 維護的兩個訂閱關係分別是Topic1/a、Topic1/b,它會啓動 RocketMQ的消費任務,從RocketMQ中消費 Topic1 中的數據。消費到的每條消息通過tag判斷屬於哪個 MQTT Topic,再通過匹配樹將消息推送給客戶端。每一個 RocketMQ Topic對應一個拉取消息的任務,而一級 Topic下面可能有很多MQTT Topic,一旦MQTT Topic增多,推送給客戶端的延時就會變高。此外,一級 Topic下可能會存在很多終端,存在大量沒有被訂閱的無用消息。

Topic級別的任務無法爲每個客戶端都維護獨立的 offset 進度。只要 Bridge 接收到客戶端訂閱的請求就會開啓消費線程,Topic沒有訂閱時再將線程停掉。這樣存在的問題是如果長時間沒有消息發佈,但訂閱關係一直存在,會導致線程空轉,存在很大的資源浪費。

13.png

社區在今年 3 月份開源新版MQTT架構,架構中引入了 notify 組件。作用爲通知所有MQTT Bridge 一級Topic中是否有新的消息產生。每一個 Bridge 中都內置 notify 組件,負責啓動針對 RocketMQ一級 Topic的集羣模式消費者,一旦一級 Topic中有消息產生時,notify 組件能夠感知到消息的產生,同時將消息作爲事件廣播給所有Bridge。其他 Bridge 收到消息事件的通知後,會爲連接在這臺 Bridge 上的每個終端開啓獨立拉取任務。拉取時不是拉取一級 Topic中的所有數據,而是通過消費 4.9.3 版本新引入的 LMQ,避免拉取一級 Topic中其他沒有被當前客戶端訂閱的消息,以此避免了讀放大。另外,每個終端獨立的拉取任務可以爲每個終端維護獨立的 offset 進度,方便實現離線消息。

因此,只有新的消息事件到來時,纔會爲終端開啓拉取任務。Topic沒有消息或沒有任何訂閱關係之後,拉取任務將停止。升級後的推拉模型能夠支持離線消息,大幅降低了延時,合理的啓停機制有效避免線程資源的浪費。

14.png

15.png

共享訂閱是MQTT5.0 協議新增的訂閱模式,可以理解爲類似RocketMQ中的集羣模式消費。上圖左側爲簡單的共享隊列實例。IoT 發送幾條消息到 Topic1/a 中,Topic1/a有三個訂閱端,每一條消息只會被推送給其中一個訂閱端,比如IoT-sub-1會收到message1和message4,IoT-sub-1 會收到 message2 和message5,message 會收到message 3和message 6。其實現原理爲:

每個 MQTT Bridge 會啓動一個針對Topic的拉取任務。RocketMQ 本身能夠支持集羣模式,MQTT Bridge又作爲RocketMQ的客戶端,因此可以複用RocketMQ的共享訂閱模型。訂閱端訂閱時以某種特殊方式帶上消費者組名稱,連接到某臺 Bridge 後,該Bridge上就會用消費者組和訂閱的一級 Topic來啓動一個RocketMQ的集羣模式消費者。第二個訂閱端連接了第二臺 Bridge,該Bridge也會啓動一個消費者。只要Bridge 上有終端連接且他們處於一組內並訂閱了同一個 RocketMQ的一級 Topic,則所有符合要求的 Bridge 會組成集羣模式的消費者集羣。有新的消息到達 Topic1 之後,只會被其中一個 Bridge 消費,那麼也只會被連接到該 Bridge 上的 IoT 訂閱端消費到。如果有多個訂閱端同時連到一個 Bridge 上,消息應該推給哪個客戶端呢?我們在MQTT Bridge 內置多種策略,默認選擇輪詢策略。一條消息發到 Bridge 後,Bridge可以輪詢發送給任意一個IoT訂閱端,實現單 Bridge 多訂閱端的共享消費。

未來工作

16.png

未來,小米MQTT的工作將從以下四個方面繼續深入探索:

  • 架構:推拉模型繼續升級完善;

  • 功能:離線消息、保留消息、遺囑消息等功能的完善;

  • 社區:擁抱開源社區,跟隨社區升級RocketMQ端雲一體化的架構;

  • 業務:小米汽車等IoT的場景推廣和應用。

加入 Apache RocketMQ 社區

十年鑄劍,Apache RocketMQ 的成長離不開全球接近 500 位開發者的積極參與貢獻,相信在下個版本你就是 Apache RocketMQ 的貢獻者,在社區不僅可以結識社區大牛,提升技術水平,也可以提升個人影響力,促進自身成長。

社區 5.0 版本正在進行着如火如荼的開發,另外還有接近 30 個 SIG(興趣小組)等你加入,歡迎立志打造世界級分佈式系統的同學加入社區,添加社區開發者微信:rocketmq666 即可進羣,參與貢獻,打造下一代消息、事件、流融合處理平臺。

17.jpeg

微信掃碼添加小火箭進羣

另外還可以加入釘釘羣與 RocketMQ 愛好者一起廣泛討論:

18.png

釘釘掃碼加羣

關注【Apache RocketMQ】公衆號,獲取更多技術乾貨!

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