RabbitMQ進程結構分析與性能調優
騰訊雲在開發雲消息隊列系統(CMQ)時,對RabbitMQ進行了大量的學習和優化,包括瓶頸分析、內存管理、參數調優等。下文結合Erlang和RabbitMQ架構來分析實踐中遇到的問題,並探討相應的優化方案。
一. RabbitMQ架構分析
圖1 AMQP模型AMQP是一個非同步消息傳遞所使用的應用層協議規範,AMQP客戶端能夠無視消息來源任意發送和接受消息,Broker提供消息的路由、隊列等功能。Broker主要由Exchange和Queue組成:Exchange負責接收消息、轉發消息到綁定的隊列;Queue存儲消息,提供持久化、隊列等功能。AMQP客戶端通過Channel與Broker通信,Channel是多路復用連接中的一條獨立的雙向數據流通道。
1. RabbitMQ進程模型
RabbitMQ Server實現了AMQP模型中Broker部分,將Channel和Queue設計成了Erlang進程,並用Channel進程的運算實現Exchange的功能。
圖2 RabbitMQ進程模型
圖2中,tcp_acceptor進程接收客戶端連接,創建rabbit_reader、rabbit_writer、rabbit_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_reader和rabbit_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發送消息。
圖3 RabbitMQ生產消息傳輸路徑
可以看出基於信用證的流控最終將消息發送進程的發送速度限制在消息處理進程的處理速度內。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狀態的消息;q2和q3包含beta和gamma狀態的消息;delta隊列是消息按序存檔後的一種邏輯隊列,只有delta狀態的消息。所以delta隊列並不在內存中,其他4個隊列則是由erlang queue模塊實現。
圖4 內部隊列消息傳遞順序消息從q1入隊,q4出隊,在內部隊列中傳遞的過程一般是經q1順序到q4。實際執行並非必然如此:開始時所有隊列都為空,消息直接進入q4(沒有消息堆積時);內存緊張時將q4隊尾部分消息轉入q3,進而再由q3轉入delta,此時新來的消息將存入q1(有消息堆積時)。
Paging就是在內存緊張時觸發的,paging將大量alpha狀態的消息轉換為beta和gamma;如果內存依然緊張,繼續將beta和gamma狀態轉換為delta狀態。Paging是一個持續過程,涉及到大量消息的多種狀態轉換,所以Paging的開銷較大,嚴重影響系統性能。
二. 問題分析
在生產者、消費者均正常情況下,RabbitMQ壓測性能非常穩定,保持在一個恆定的速度。當消費者異常或不消費時,RabbitMQ則表現極不穩定。
圖5 消息持久化、無消費場景
測試場景如下,exchange和隊列都是持久化的,消息也是持久化的、固定為1K,並且無消費者。如上圖所示,在達到內存paging閾值後,生產速率降低,並持續較長時間。內存使用情況表明,在內存中的消息數目只有18M內容,其他消息已經page到磁碟中,然而進程內存仍佔用2G。Erlang內存使用表明,Queues佔用了2G,Binaries佔用了2.1G。
該情況說明在消息從內存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到磁碟,而且保持這個周期內生產速度不下降。
具體修改可查看:
rabbitmq/rabbitmq-server圖6 paging時主動垃圾回收
從修改後效果可以看出,三次paging都很快結束,前兩次paging相鄰較近是因為兩個鏡像節點分別執行了paging。
該問題已反饋至RabbitMQ社區:
從圖5中還可以發現,在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操作。
配置參數舉例:
rabbitmqctl set_vm_memory_high_watermark 0.5 // 將rabbitmq佔用機器的內存上限改為0.5
可以通過 rabbitmqctl status 查看結果中的 {vm_memory_high_watermark,0.5}
五. 總結
RabbitMQ在2007年發布第一個版本時,只有5000行Erlang代碼,到現在已經加入了非常多的特性,但基本架構沒有變。從多核的角度看,流控機制和單amqqueue進程之間存在一些衝突,對消費者異常這種場景,還需要從整個架構方面做更多優化。
除了上述內容,RabbitMQ在Cluster、HA、可靠交付、擴展支持等方面也做了大量的工作,這些都值得深入的學習。
推薦閱讀:
※beanstalk和rabbitmq區別?
※C#消息隊列(MQ)零基礎從入門到實戰演練
※RabbitMQ學習心得——RabbitMQ簡介(下)
※RabbitMQ ACK 機制的意義是什麼?
TAG:RabbitMQ |