使用RabbitMQ消息隊列時兩個重要的考慮因素是:吞吐與可靠。有的場景要求高吞吐,有的場景要求高可靠。在系統設計時候如何平衡消息隊列的的吞吐量與可靠性,是使用好RabbitMQ消息隊列的關鍵。 這篇文章列出RabbitMQ的最佳實踐,基於吞吐量與可靠性兩個指標,給出怎么做是好的、怎么做是差的指導,包括隊列大小、常見錯誤、延遲加載隊列、預提取值、連接與通道、集群節點數量等,這些指導都是在實踐中總結出來的。
隊列 Queues
隊列盡可能短
隊列過長的話會占用系統較多內存,RabbitMQ為了釋放內存,會將隊列消息轉儲到硬盤,稱為 page out 。 如果隊列很長,Page out 操作會消耗較長時間,page out 過程中隊列不能處理消息。
隊列過長同時會加長RabbitMQ重啟時間,因為啟動時候需要重建索引。 隊列過長還會導致集群之間節點同步消息時間變長。
啟用 lazy queue 使得性能可預期
RabbitMQ3.6版本引入了 lazy queue 特性, lazy queue 開啟之后隊列中的消息自動存儲到磁盤,消息只在需要的時候才加載到內存中。開啟了 lazy queue 后內存使用量會降低,但是會增加消息處理時延。
在實踐中我們觀察到開啟了 lazy queue 后RabbitMQ集群會更穩定,性能也更可預期。消息不會突然在沒有預警的情況下被寫到磁盤,也不會出現突發性能毛刺。如果你一次批量往隊列寫入大量消息,或者消費者對消息時延不敏感,建議啟動 lazy queue 。
通過 TTL 或 max-length 限制隊列大小
通過設置 TTL 或 max-length 來限制隊列大小,從而讓隊列不超過設定大小。
隊列數量
RabbitMQ中一個隊列對應一個線程,一個隊列的吞吐量大約為50k消息/秒。在多核服務器上,使用多個隊列與消費者可以獲得更好的吞吐量,將隊列數量設置為等於服務器cpu核數將獲得最佳吞吐量。
將隊列分布到不同的CPU核,甚至不同節點
隊列的性能極限是一個CPU核處理能力,因此,將隊列分布到不同的CPU核(集群模式下可以到不同節點),將獲得更好的性能。 RabbitMQ隊列被綁定到第一個節點上,即使創建了集群,所有消息也是被投遞到主隊列所在的節點。你可以手動調整隊列到不同的節點,但是帶來的負面影響是你要管理這個映射關系。
有兩個插件可以輔助實現隊列分布到不同節點或不同CPU和(單節點集群)。
Consistent hash exchange plugin
Consistent hash exchange plugin 插件可以實現 Exchange 按照負載均衡方式投遞消息到隊列中。插件將要投遞消息的 Routing Key 哈希之后找到要投遞的隊列,這種方式能保證同一個 Routing key 的消息總是投遞到同一個隊列。 使用插件時候需要注意,消費者需要在多個隊列上消息分析,不要有遺漏。
RabbitMQ sharding
RabbitMQ sharding 插件自動完成消息的分區,一旦在 Exchange 上定義了分區,插件會在集群的每個節點上創建一個分區隊列;同時RabbitMQ sharding 插件對消費者只提供一個隊列(但是實際后端有多個隊列)。RabbitMQ sharding 插件提供消息生產與消費的中心訪問點,並提供消息跨節點自動分區、管理節點上的隊列等能力。
臨時隊列名字系統自動分配
給隊列取一個有意義的名字很關鍵,生產者與消費者之間通過名字找到隊列。但是對於臨時隊列,名字就交由給系統自動分配。
自動刪除不再使用的隊列
生產者或消費者可能異常退出導致隊列被殘留,大量的殘留隊列會影響RabbitMQ實例的性能。RabbitMQ提供了3種自動刪除隊列的方法。
- 設置隊列的 TTL :如 TTL 為28天的隊列,當持續28天沒有被消費后會被自動刪除
- 配置 auto-delete 隊列: auto-delete 隊列在最后一個消費者取消消費、或鏈接關閉后被刪除
- 配置 exclusive queue: exclusive queue 只能在創建此隊列的 Connection/Channel 中使用,當 Connection/Channel 關閉后隊列被刪除
限制優先隊列數量
每個優先隊列會啟動一個Erlang進程,過多的優先隊列會影響性能,建議數量為5。
連接數與通道數 Connections and channels
每個連接會消耗掉大約100KB的內存(如果使用TLS會更多),成千上萬的連接會導致RabbitMQ負載很高,極端情況會出現內存溢出。AMQP協議引入了Channel概念,一個連接中可以有多個Channel。 建議一個Channel對應一個線程,一個連接對應一個進程,並使用長連接。
不要在多個線程之間共享Channel
很多SDK並未實現Channel的線程安全,因此不要在多個線程之間共享Channel 。
不要頻繁打開與關閉 Channel
同樣是基於性能考慮。
生產者與消費者使用獨立的連接
這么做吞吐量更高。 當生產者發送大量消息時候RabbitMQ會將壓力傳遞到TCP連接上,如果使用同一個連接消費消息可能會得不到確認消息。
大量連接與通道會影響RabbitMQ管理控制台的性能
RabbitMQ會采集每個連接與通道的指標數據並分析,然后在控制台展示,大量的連接與通道會對控制台有較大壓力。
Acknowledgements and Confirms
消息在傳輸過程中可能會丟失(如連接中斷),這時候就需要重傳。確認消息用於告知客戶端與服務端何時重傳消息。客戶端需要發送確認消息當收到消息、或者對於重要消息是消息被處理后。消息確認對性能也有影響,在高吞吐場景下,盡量避免使用手動確認。
對於消費者,一些重要的消息,建議在消息消費邏輯處理完成后才確認,確保消息不丟失。
未確認消息 Unacknowledged messages
所有未確認的消息都存儲在內存中,當有大量的為確認消息時候可能會將內存耗盡。一個高效的限制未確認消息的方法是設置消費者的預提取(prefetch)消息數量。可以參考RabbitMQ的 prefect 機制。
Persistent messages and durable queues
如果消息不允許丟失,需要將隊列設置為 durable ,將消息設置為 persistent 。這種方式消息與隊列都會持久化到硬盤,當然相比於 transient 消息,吞吐量會下降。
Prefetch
prefetch 值用於指定一次發送多少個消息給消費者。RabbitMQ官網對 prefetch 的定義:
prefetch 的目的是使得消費者處於飽和工作狀態,同時又要讓消費者客戶端內存緩存最少,並使得消息呆在隊列中讓其他消費者盡快消費。
RabbitMQ默認不設定消費者內存緩存上限,意思是一次性發送盡量多的消息給消費者,消息在消費者客戶端內存中緩存直到被處理。 Prefetch 限定消費者一次消費的消息數量, 所有 Prefetch 的消息都會從隊列中刪除,其他消費者不再可見。
Prefetch 的值對RabbitMQ的性能有影響。
過小的值會導致RabbitMQ將時間都花費在等待發送消息與正在發送消息過程內。下圖是一個 Prefetch 設置過小,導致時間都花費在網絡傳輸上的例子:消費者處理消息只用了5ms,但是接收消息,確認消息卻耗費了120ms。
過大的值會導致一個消費者取走了所有消息非常繁忙,其他消費者沒有消息可處理空閑等待的現象。
如何設置合適的 prefetch 值
- 消費者很少且消息處理很快:prefetch 設置盡可能大;
- 消費者很多且消息處理很快:prefetch 設置較小;比 “消費者很少且消息處理很快” 場景要小
- 消費者很多且消息處理很慢:prefetch 設置為1;這樣盡可能將消息分布給不同的消費者處理
需要注意的是,如果消費者設置了自動確認消息消費,那么 prefetch 是無效的。
常見的錯誤做法是不設定 prefetch 的值,這種情況下會導致一些消費者撐死,一些消費者餓死。
HiPE
HiPE(High Performance Erlang)開啟之后可以提升吞吐量,負面影響是增加啟動時間;開啟了 HiPE 之后,RabbitMQ會在啟動時候編譯,開啟 HiPE 后性能會有 20%~80% 的提升,啟動時長會增加 1~3 分鍾。
Disable unused plugins
插件會消耗CPU與內存,禁用不需要的插件。
Do not set RabbitMQ Management statistics rate mode to detailed in production
Setting RabbitMQ Management statistics rate mode to detailed has a serious performance impact and should not be used in production.
Use updated RabbitMQ client libraries
確保你使用的SDK是最新的穩定版本。
Use latest stable RabbitMQ and Erlang version
使用最新穩定的RabbitMQ與Erlang版本。
Use TTL with caution
死信投遞與TTL是兩個流行的特性,但是這兩個特性對性能會有影響,在使用時候通常容易忽視這點。
死信投遞
隊列設置了 x-dead-letter-exhcange
屬性將會接收到被拒絕的、或超時的消息。消息設置了 x-dead-letter-routing-key
后 routing key 將會在死信投遞后被改變。
TTL
隊列設置了 x-message-ttl
屬性后,消息將會被從隊列中移除如果在TTL時間內未被消費。