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

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

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

一. RabbitMQ架構分析


圖1 AMQP模型git

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

1. RabbitMQ進程模型

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

圖2 RabbitMQ進程模型bash

圖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)機制來確保穩定性。tcp

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個內部隊列包括:q一、q二、delta、q三、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到磁盤後(即從q二、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到磁盤,並且保持這個週期內生產速度不降低。

具體修改可查看:
https://github.com/rabbitmq/rabbitmq-server/compare/stable...javaforfun:stable

圖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操做。

五. 總結

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

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

相關文章
相關標籤/搜索