在上篇文章中,我們采用相同的硬件資源分別對 MQTT 消息服務器 EMQ X 和 RabbitMQ 進行了壓力測試。結果表明:在「多對一」 場景中,EMQ X 和 RabbitMQ 相比並沒有太大差別;而在「一對多」場景中,RabbitMQ 則較 EMQ X 產生了較為明顯的差距。
本期文章中我們將對這一結果進行進一步的解析。
造成差距的原因主要有三個:節點間通訊的方式、消息流架構的方式、隊列的使用。
節點間的通訊
RabbitMQ - 委托架構
RabbitMQ 使用了 Erlang 語言的分布式連接,即每個節點之間兩兩互相連接,每個節點用一個單一的鏈接連接着另一個節點。在圖中的情況下,三個節點依次連接;當節點之間需要通信時,一條消息需要通過這個單一鏈接從一個節點發送到另一節點。
在扇出(fan-out)的例子中,正常來講你需要將消息推送到所有節點的隊列上。RabbitMQ 使用的優化方式則是:你的消息只需要發送一次,之后其內置的代理委托框架會將這一條消息派送並且發到其他節點的隊列上。這個過程中,消息是有序發送的,所以保證了消息在不同隊列里都是相同的順序。
但是這個方案也不是十全十美的,因為你會將所有的消息只發送一次,在分發工作都依靠同一個委托進程。而且 RabbitMQ 選擇這個代理進程的策略是根據發布者的哈希算法。所以,當如果你只有一個發布者,所有的消息都會被一直推送到單個的委托代理進程。
EMQ X - Gen_RPC
在 EMQ X 中有個精妙的設計:其不僅存在着分布式連接,還存在着 Gen_RPC。分布連接和 Gen_RPC 各司其職,前者用於交換 Mnesia 的數據信息,后者則只適用於消息的轉發。每當你需要從一個節點向另一個節點發布一個消息的時候,EMQ X 不是重新自動生成新的節點間鏈接(默認 1 個連接),再通過這些新的連接去處理把一個消息從一個節點推送到另一個節點的工作。而是依靠針對此場景特地設計的,專有的 Gen_RPC 連接來處理這個消息推送的工作。所以在扇出(一對多)的例子中,這些鏈接會被完全有效地利用。
但這種設計在網絡分區環境中其性能有可能受到影響,RabbitMQ 節點之間只有一個分布式連接,所以當連接斷開造成腦裂時,愈合修復的工作將會更簡單。
消息流
MQTT 插件
RabbitMQ 在使用 MQTT 插件后會監聽使用 MQTT 協議發布的消息。得到消息之后,消息被解析,之后再通過 AMQP 協議進行轉化,最后才會被發送到 RabbitMQ 上。
如果要發送一條消息,需要經過套接字后進入 mqtt_reader,接下來再進入下圖所示的所有過程。然而如果要在同一條通道里同時接收剛剛發送的這條消息,所有上圖所示的過程則需要反着重新進行一次,包括 mqtt_reader。其中,mqtt_reader 不僅負責了讀,也負責了寫。
AMQP
AMQP 場景則不同,每條消息都被一個 reader 讀取,一個 writer 寫入。這兩條通道讀寫獨立,reader 只負責讀內容,而 writer 只負責寫內容,它們各司其職、相互獨立。而唯一的通道 channel 則是一個主 Erlang 進程,其負責着消息的交換。
可見 RabbitMQ 在 MQTT 場景中存在的明顯的設計問題會導致性能下降,那么如果引入 AMQP 模式的 RabbitMQ 測試用例將會如何呢?將 RabbitMQ 調制成使用 MQTT 插件的和使用單一 AMQP 的模式使用,再對比 EMQ X 在壓力測試下的情況,可以看出 EMQ X 在所有測試中仍是更勝一籌,但總體來說使用 AMQP 模式的 RabbitMQ 要比自己原有的成績更好。
多對一
此場景中 RabbitMQ 與 EMQ X 已經有了接近的性能表現。
一對多
但如果在 fan-out(一對多)場景里,EMQ X 仍然具有顯著優勢,但 RabbitMQ(AMQP)的差距已經明顯縮小。
隊列
以上的測試均使用了 QoS 1 的消息。當發送 QoS 1 的消息時,這些消息每次都要作為可持久化的備份保存在硬盤上。所以隊列空間的使用也尤為重要。
RabbitMQ
RabbitMQ 成熟地使用了一個默認的隊列空間執行方式(可以被替換成其他隊列使用)。這個可變隊列在消息的持久度和給客戶端發送消息的時延里做了均衡。但是在最壞的情況下,一個消息可能會被存入內存。不過這也幫助了 RabbitMQ 在崩潰重啟之后可以讓服務器再上線,並且所有的客戶端還可重連且收到原來持久化的消息。
EMQ X
EMQ X 對隊列的實現方式非常簡單,即在內存中使用了優先隊列。如果發來的消息無法推入接收者的隊列,則這個消息會被丟掉。在 EMQ X 中,只有用一些其他持久化的插件才能使消息持久化保存,這些功能在商業版中提供。
EMQ X 的設計初衷是將接入層獨立,所以將消息持久化的問題留給了后端完成。這一問題在未來具有持久性會話的版本中會解決(persistence session)。
節流
RabbitMQ - 控流
RabbitMQ 采用了一種比較有名的控流機制,它給每一個流程了一個信用值,如下圖所示。假設說我們的服務端接收到了一個消息並由 reader 進行了讀取后,這條消息被送到 channel。這個過程將會消費掉 reader 和 channel 的相應的信用值。這樣一來,就可以通過使兩方信用值保持匹配同步的方法實現不超額的發送了。
這其實是一個不錯的解決方案。設想我們有許多的用戶,即有許多的隊列,每發送一條消息就意味着將要將這條消息分發給許多的隊列,這會嚴重影響 RabbitMQ 實例。然而,這一套流程會阻止 RabbitMQ 再繼續讀區接收緩沖區的消息——因為發送緩沖區已經快滿了!
EMQ X - 限流
EMQ X 的節流主要是靠限制讀取一方的流量去實現的。首先,根據預設,將會一次從套接字內讀取 200 條消息。當這些消息被完全收到了之后才會逐個將他們處理。一旦套接字報告它已經到達了讀取一方的最大限額,它將會檢查有發布者的數量和已經被閱讀的字節數量,並根據這個數值去休眠一段時間。接收緩沖區最終會被填滿,發布者根據 TCP 協議中飛行窗口的要求也將不會再發布任何內容。
總結
以上就是這個橫向評測的結果和分析。最終的贏家很難斷言,但是如果就服務器的性能上來講,EMQ X 肯定是略勝一籌的。不過 RabbitMQ 也有它獨特的優勢。
EMQ X 的設計原則
EMQ X 在設計上,首先分離了前端協議 (FrontEnd) 與后端集成 (Backend),其次分離了消息路由平面 (Flow Plane) 與監控管理平面 (Monitor/Control Plane):
- EMQ X 核心解決的問題:處理海量的並發 MQTT 連接與路由消息。
- 充分利用 Erlang/OTP 平台軟實時、低延時、高並發、分布容錯的優勢。
- 連接 (Connection)、會話 (Session)、路由 (Router)、集群 (Cluster) 分層。
- 消息路由平面 (Flow Plane) 與控制管理平面 (Control Plane) 分離。
- 支持后端數據庫或 NoSQL 實現數據持久化、容災備份與應用集成。
EMQ X 的系統分層
- 連接層 (Connection Layer):負責 TCP 連接處理、 MQTT 協議編解碼。
- 會話層 (Session Layer):處理 MQTT 協議發布訂閱消息交互流程。
- 路由層 (Route Layer):節點內路由派發 MQTT 消息。
- 分布層 (Distributed Layer):分布節點間路由 MQTT 消息。
- 認證與訪問控制 (ACL):連接層支持可擴展的認證與訪問控制模塊。
- 鈎子 (Hooks) 與插件 (Plugins):系統每層提供可擴展的鈎子,支持插件方式擴展服務器。
而 RabbitMQ 則更類似於 Kafka 的消息隊列緩存設計。建議在 IoT 項目中將兩者結合使用。
版權聲明: 本文為 EMQ 原創,轉載請注明出處。
原文鏈接:https://www.emqx.com/zh/blog/emqx-or-rabbitmq-part-2
技術支持:如對本文或 EMQ 相關產品有疑問,可訪問 EMQ 問答社區 https://askemq.com 提問,我們將會及時回復支持。
更多技術干貨,歡迎關注我們公眾號【EMQ 中文社區】。