【RabbitMQ】RabbitMQ 進程結構分析與性能調優

原文鏈接:RabbitMQ 進程結構分析與性能調優

RabbitMQ是一個流行的開源消息隊列系統,是AMQP(高級消息隊列協議)標準的實現,由以高性能、健壯、可伸縮性出名的Erlang語言開發,並繼承了這些優點。業界有較多項目使用RabbitMQ,包括OpenStack、Spring、Logstash等。

騰訊雲在開發雲消息隊列系統(CMQ)時,對RabbitMQ進行了大量的學習和優化,包括瓶頸分析、內存管理、參數調優等。下文結合Erlang和RabbitMQ架構來分析實踐中遇到的問題,並探討相應的優化方案。

一、RabbitMQ 架構分析

AMQP 是一個異步消息傳遞所使用的應用層協議規範,AMQP 客戶端能夠無視消息來源任意發送和接受消息,Broker 提供消息的路由、隊列等功能。Broker 主要由 Exchange 和 Queue 組成:Exchange 負責接收消息、轉發消息到綁定的隊列,提供持久化、隊列等功能。AMQP 客戶端通過 Channel 與 Broker 通信,Channel 是多路複用連接中的一條獨立的雙向數據流通道。

1. RabbitMQ 的進程模型

RabbitMQ Server實現了AMQP模型中Broker部分,將 Channel 和 Queue 設計成了 Erlang 進程,並用 Channel 進程的運算實現 Exchange 的功能。

上圖中,tcp_acceptor 進程接收客戶端連接,創建 rabbit_readerrabbit_writerrabbit_channel進程。rabbit_reader 接收客戶端連接,解析 AMQP 幀;rabbit_writer 向客戶端返回數據;rabbit_channel 解析 AMQP 方法,對消息進行路由,然後發給相應隊列進程。

rabbit_amqqueue_process 是隊列進程,在 RabbitMQ 啓動(恢復 durable 類型隊列)或創建隊列時創建。rabbit_msg_store 是負責消息持久化的進程。

在整個系統中,存在一個 tcp_accepter 進程,一個 rabbit_msg_store 進程,有多少個隊列就有多少個 rabbit_amqqueue_process 進程,每個客戶端連接對應一個 rabbit_readerrabbit_writer 進程。

2. RabbitMQ 流控

RabbitMQ 可以對內存和磁盤使用量設置閾值,當達到閾值後,生產者將被阻塞(block),直到對應項恢復正常。除了這兩個閾值,RabbitMQ 在正常情況下還用流控(Flow Control)機制來確保穩定性。

Erlang 進程之間並不共享內存(binaries類型除外),而是通過消息傳遞來通信,每個進程都有自己的進程郵箱。Erlang 默認沒有對進程郵箱大小設限制,所以當有大量消息持續發往某個進程時,會導致該進程郵箱過大,最終內存溢出並崩潰。

在 RabbitMQ 中,如果生產者持續高速發送,而消費者消費速度較低時,如果沒有流控,很快就會使內部進程郵箱大小達到內存閾值,阻塞生產者(得益於 block 機制,並不會崩潰)。然後 RabbitMQ 會進行 page 操作,將內存中的數據持久化到磁盤中。

爲了解決該問題,RabbitMQ 使用了一種基於信用證的流控機制。消息處理進程有一個信用組 {InitialCredit, MoreCreditAfter},默認值爲 {200, 50}。消息發送者進程 A 向接收者進程 B 發送消息,每發一條消息,Credit 數量減 1,直到爲 0,A 被 block 住;對於接收者 B,每接收 MoreCreditAfter 條消息,會向 A 發送一條消息,給予 A MoreCreditAfter 個 Credit,當 A 的 Credit > 0 時,A 可以繼續向 B 發送消息。

可以看出基於信用證的流控最終將消息發送進程的發送速度限制在消息處理進程的處理速度內。RabbitMQ 中與流控有關的進程構成了一個有向無環圖。

3. amqqueue 進程與 Paging

如上所述,消息的存儲和隊列功能是在 amqqueue 進程中實現。爲了高效處理入隊和出隊的消息、避免不必要的磁盤 IO,amqqueue 進程爲消息設計了 4 種狀態和 5 個內部隊列。

4 種狀態包括:alpha,消息的內容和索引都在內存中;beta,消息的內容在磁盤,索引在內存;gamma,消息的內容在磁盤,索引在磁盤和內存中都有;delta,消息的內容和索引都在磁盤。對於持久化消息,RabbitMQ 先將消息的內容和索引保存在磁盤中,然後才處於上面的某種狀態(即只可能處於 alpha、gamma、delta 三種狀態之一)。

5 個內部隊列包括:q1、q2、delta、q3、q4。q1 和 q4 隊列中只有 alpha 狀態的消息;delta 隊列是消息按序存盤後的一種邏輯隊列,只有 delta 狀態的消息。所以 delta 隊列並不在內存中,其他 4 個隊列則是由 Erlang queue 模塊實現的。

消息從 q1 入隊,q4 出隊,在內部隊列中傳遞的過程一般是經 q1 到 q4。實際執行並非如此:開始時所有隊列都爲空,消息直接進入 q4(沒有消息堆積時);內存緊張時將 q4 隊尾部分消息傳入 q3,進而再由 q3 傳入 delta,此時新來的消息將存入 q1(有消息堆積時)。

Paging 就是在內存緊張時觸發的,paging 將大量 alpha 狀態的消息轉換爲 beat 和 gamma;如果內存依然緊張,繼續將beta 和 gamma 狀態轉換爲 delta 狀態。Paging 是一個持續過程,涉及到大量消息的多種狀態轉換,所以 Paging 的開銷較大,嚴重影響系統性能。

二、問題分析

在生產者、消費者均正常情況下,RabbitMQ 壓測性能非常穩定,保持在一個恆定的速度。當消費者異常或不消費時,RabbitMQ 則表現極不穩定。

測試場景如下,exchange 和隊列都是持久化的,消息也是持久化的、固定爲 1k,並且無消費者。如上圖所示,在達到內存 paging 閾值後,生產速率降低,並持續較長時間。內存情況表明,在內存中的消息數目只有 18M 內容,其他消息已經 page 到磁盤中,然而進程內存仍佔用 2G。Erlang 內存使用表明,Queue 佔用了 2G,Binaries 佔用了 2.1 G。

該情況說明在消息從內存page到磁盤後(即從 q2、q3 隊列轉到 delta 後),系統中產生了大量的垃圾(garbage),而 Erlang VM 沒有進行及時的垃圾回收(GC)。這導致 RabbitMQ 錯誤地計算了內存使用量,並持續調用 paging 流程,直到 Erlang VM 隱式垃圾回收。

三、內存管理優化

RabbitMQ 內存使用量的計算是在memory_monitor進程內執行的,該進程週期性計算系統內存使用量。同時amqqueue進程會週期性拉取內存使用量,當內存達到 paging 閾值時,觸發 amqqueue 進程進行 paging。paging 發生後,amqqueue 進程每收到一條消息都會對內部隊列進行 page(每次 page 都會計算出一定數目的消息存盤)。

該過程可行的優化方案是:在 amqqueue 進程將大部分消息paging到磁盤後,顯式調用GC,同時將memory_monitor週期設爲0.5s、amqqueue拉取週期設爲1s,這樣就能夠達到秒級恢復;去掉對每條消息執行paging的操作,用amqqueue週期性拉取內存使用量的操作來觸發page,這樣能夠更快將消息paging到磁盤,而且保持這個週期內生產速度不下降。

從修改後效果可以看出,三次paging都很快結束,前兩次paging相鄰較近是因爲兩個鏡像節點分別執行了paging。

(注:目前版本好像解決了這個問題)

從前文圖中還可以發現,在22:01時生產速度有一個明顯的下降(此時未發生paging)。通過流控分析,鏈路被block在amqqueue進程;經觀察發現節點內存使用下降了,說明該節點執行了GC。Erlang GC是按進程級別的標記-清掃模式,會將當前進程暫停,直至GC結束。由於在RabbitMQ中,一個隊列只有一個amqqueue進程,該進程又會處理大量的消息,產生大量的垃圾。這就導致該進程GC較慢,進而流控block上游更長時間。

查看RabbitMQ代碼發現,amqqueue進程的gen_server模型在正常的邏輯中調用了hibernate,該操作可能導致兩次不必要的GC。優化掉hibernate對系統穩定性有一些幫助。

對流控可能比較好的優化方案是:用多個amqqueue進程來實現一個隊列,這樣可以降低rabbit_channel被單個amqqueue進程block的概率,同時在單隊列的場景下也能更好利用多核的特性。不過該方案對RabbitMQ現有的架構改動很大,難度也很大。

四、參數調優

RabbitMQ可優化的參數分爲兩個部分,Erlang部分RabbitMQ自身

  • IO_THREAD_POOL_SIZE:CPU大於或等於16核時,將Erlang異步線程池數目設爲100左右,提高文件IO性能。

  • hipe_compile:開啓Erlang HiPE編譯選項(相當於Erlang的jit技術),能夠提高性能20%-50%。在Erlang R17後HiPE已經相當穩定,RabbitMQ官方也建議開啓此選項。

  • queue_index_embed_msgs_below:RabbitMQ 3.5版本引入了將小消息直接存入隊列索引(queue_index)的優化,消息持久化直接在amqqueue進程中處理,不再通過msg_store進程。由於消息在5個內部隊列中是有序的,所以不再需要額外的位置索引(msg_store_index)。該優化提高了系統性能10%左右。

  • vm_memory_high_watermark:用於配置內存閾值,建議小於0.5,因爲Erlang GC在最壞情況下會消耗一倍的內存。

  • vm_memory_high_watermark_paging_ratio:用於配置paging閾值,該值爲1時,直接觸發內存滿閾值,block生產者。

  • queue_index_max_journal_entries:journal文件是queue_index爲避免過多磁盤尋址添加的一層緩衝(內存文件)。對於生產消費正常的情況,消息生產和消費的記錄在journal文件中一致,則不用再保存;對於無消費者情況,該文件增加了一次多餘的IO操作。

五、其他優化和注意的地方

1、一定要注意避免觸發流控,增加消費者,提高消費者的消費能力;

2、消息的大小會影響消息的發送速率;

3、消費者的預取參數的大小對消費者的消費性能影響很大;

  • prefetch設置的過大,可能導致消費者處理不過來,堆積在本地緩存區,導致消息處理延遲過長。
  • prefetch設置的過小,會導致消費者不能充分工作。

六、總結

RabbitMQ在2007年發佈第一個版本時,只有5000行Erlang代碼,到現在已經加入了非常多的特性,但基本架構沒有變。從多核的角度看,流控機制和單amqqueue進程之間存在一些衝突,對消費者異常這種場景,還需要從整個架構方面做更多優化。

除了上述內容,RabbitMQ在Cluster、HA、可靠交付、擴展支持等方面也做了大量的工作,這些都值得深入的學習。

參考:

https://www.rabbitmq.com/admin-guide.html
https://github.com/rabbitmq/rabbitmq-server/issues/101
http://prog21.dadgum.com/16.html
http://www.erlang.org/doc/man/gen_server.html
http://docs.basho.com/riak/latest/ops/tuning/erlang/
http://www.erlang.org/doc/efficiency_guide/introduction.html

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