RabbitMQ是一個流行的開源消息隊列系統(tǒng),是AMQP(高級消息隊列協(xié)議)標準的實現(xiàn),由以高性能、健壯、可伸縮性出名的Erlang語言開發(fā),并繼承了這些優(yōu)點。業(yè)界有較多項目使用RabbitMQ,包括OpenStack、Spring、Logstash等。
騰訊云在開發(fā)云消息隊列系統(tǒng)(CMQ)時,對RabbitMQ進行了大量的學習和優(yōu)化,包括瓶頸分析、內(nèi)存管理、參數(shù)調(diào)優(yōu)等。下文結(jié)合Erlang和RabbitMQ架構(gòu)來分析實踐中遇到的問題,并探討相應的優(yōu)化方案。
一. RabbitMQ架構(gòu)分析
圖1 AMQP模型
AMQP是一個異步消息傳遞所使用的應用層協(xié)議規(guī)范,AMQP客戶端能夠無視消息來源任意發(fā)送和接受消息,Broker提供消息的路由、隊列等功能。Broker主要由Exchange和Queue組成:Exchange負責接收消息、轉(zhuǎn)發(fā)消息到綁定的隊列;Queue存儲消息,提供持久化、隊列等功能。AMQP客戶端通過Channel與Broker通信,Channel是多路復用連接中的一條獨立的雙向數(shù)據(jù)流通道。
1. RabbitMQ進程模型
RabbitMQ Server實現(xiàn)了AMQP模型中Broker部分,將Channel和Queue設計成了Erlang進程,并用Channel進程的運算實現(xiàn)Exchange的功能。
圖2 RabbitMQ進程模型
圖2中,tcp_acceptor進程接收客戶端連接,創(chuàng)建rabbit_reader、rabbit_writer、rabbit_channel進程。rabbit_reader接收客戶端連接,解析AMQP幀;rabbit_writer向客戶端返回數(shù)據(jù);rabbit_channel解析AMQP方法,對消息進行路由,然后發(fā)給相應隊列進程。rabbit_amqqueue_process是隊列進程,在RabbitMQ啟動(恢復durable類型隊列)或創(chuàng)建隊列時創(chuàng)建。rabbit_msg_store是負責消息持久化的進程。
在整個系統(tǒng)中,存在一個tcp_accepter進程,一個rabbit_msg_store進程,有多少個隊列就有多少個rabbit_amqqueue_process進程,每個客戶端連接對應一個rabbit_reader和rabbit_writer進程。
2. RabbitMQ流控
RabbitMQ可以對內(nèi)存和磁盤使用量設置閾值,當達到閾值后,生產(chǎn)者將被阻塞(block),直到對應項恢復正常。除了這兩個閾值,RabbitMQ在正常情況下還用流控(Flow Control)機制來確保穩(wěn)定性。
Erlang進程之間并不共享內(nèi)存(binaries類型除外),而是通過消息傳遞來通信,每個進程都有自己的進程郵箱。Erlang默認沒有對進程郵箱大小設限制,所以當有大量消息持續(xù)發(fā)往某個進程時,會導致該進程郵箱過大,最終內(nèi)存溢出并崩潰。
在RabbitMQ中,如果生產(chǎn)者持續(xù)高速發(fā)送,而消費者消費速度較低時,如果沒有流控,很快就會使內(nèi)部進程郵箱大小達到內(nèi)存閾值,阻塞生產(chǎn)者(得益于block機制,并不會崩潰)。然后RabbitMQ會進行page操作,將內(nèi)存中的數(shù)據(jù)持久化到磁盤中。
為了解決該問題,RabbitMQ使用了一種基于信用證的流控機制。消息處理進程有一個信用組{InitialCredit,MoreCreditAfter},默認值為{200, 50}。消息發(fā)送者進程A向接收者進程B發(fā)消息,每發(fā)一條消息,Credit數(shù)量減1,直到為0,A被block住;對于接收者B,每接收MoreCreditAfter條消息,會向A發(fā)送一條消息,給予A MoreCreditAfter個Credit,當A的Credit>0時,A可以繼續(xù)向B發(fā)送消息。
圖3 RabbitMQ生產(chǎn)消息傳輸路徑
可以看出基于信用證的流控最終將消息發(fā)送進程的發(fā)送速度限制在消息處理進程的處理速度內(nèi)。RabbitMQ中與流控有關(guān)的進程構(gòu)成了一個有向無環(huán)圖。
3. amqqueue進程與Paging
如上所述,消息的存儲和隊列功能是在amqqueue進程中實現(xiàn)。為了高效處理入隊和出隊的消息、避免不必要的磁盤IO,amqqueue進程為消息設計了4種狀態(tài)和5個內(nèi)部隊列。
4種狀態(tài)包括:alpha,消息的內(nèi)容和索引都在內(nèi)存中;beta,消息的內(nèi)容在磁盤,索引在內(nèi)存;gamma,消息的內(nèi)容在磁盤,索引在磁盤和內(nèi)存中都有;delta,消息的內(nèi)容和索引都在磁盤。對于持久化消息,RabbitMQ先將消息的內(nèi)容和索引保存在磁盤中,然后才處于上面的某種狀態(tài)(即只可能處于alpha、gamma、delta三種狀態(tài)之一)。
5個內(nèi)部隊列包括:q1、q2、delta、q3、q4。q1和q4隊列中只有alpha狀態(tài)的消息;q2和q3包含beta和gamma狀態(tài)的消息;delta隊列是消息按序存盤后的一種邏輯隊列,只有delta狀態(tài)的消息。所以delta隊列并不在內(nèi)存中,其他4個隊列則是由erlang queue模塊實現(xiàn)。
圖4 內(nèi)部隊列消息傳遞順序
消息從q1入隊,q4出隊,在內(nèi)部隊列中傳遞的過程一般是經(jīng)q1順序到q4。實際執(zhí)行并非必然如此:開始時所有隊列都為空,消息直接進入q4(沒有消息堆積時);內(nèi)存緊張時將q4隊尾部分消息轉(zhuǎn)入q3,進而再由q3轉(zhuǎn)入delta,此時新來的消息將存入q1(有消息堆積時)。
Paging就是在內(nèi)存緊張時觸發(fā)的,paging將大量alpha狀態(tài)的消息轉(zhuǎn)換為beta和gamma;如果內(nèi)存依然緊張,繼續(xù)將beta和gamma狀態(tài)轉(zhuǎn)換為delta狀態(tài)。Paging是一個持續(xù)過程,涉及到大量消息的多種狀態(tài)轉(zhuǎn)換,所以Paging的開銷較大,嚴重影響系統(tǒng)性能。
二. 問題分析
在生產(chǎn)者、消費者均正常情況下,RabbitMQ壓測性能非常穩(wěn)定,保持在一個恒定的速度。當消費者異常或不消費時,RabbitMQ則表現(xiàn)極不穩(wěn)定。
圖5 消息持久化、無消費場景
測試場景如下,exchange和隊列都是持久化的,消息也是持久化的、固定為1K,并且無消費者。如上圖所示,在達到內(nèi)存paging閾值后,生產(chǎn)速率降低,并持續(xù)較長時間。內(nèi)存使用情況表明,在內(nèi)存中的消息數(shù)目只有18M內(nèi)容,其他消息已經(jīng)page到磁盤中,然而進程內(nèi)存仍占用2G。Erlang內(nèi)存使用表明,Queues占用了2G,Binaries占用了2.1G。
該情況說明在消息從內(nèi)存page到磁盤后(即從q2、q3隊列轉(zhuǎn)到delta后),系統(tǒng)中產(chǎn)生了大量的垃圾(garbage),而Erlang VM沒有進行及時的垃圾回收(GC)。這導致RabbitMQ錯誤的計算了內(nèi)存使用量,并持續(xù)調(diào)用paging流程,直到Erlang VM隱式垃圾回收。
三. 內(nèi)存管理優(yōu)化
RabbitMQ內(nèi)存使用量的計算是在memory_monitor進程內(nèi)執(zhí)行的,該進程周期性計算系統(tǒng)內(nèi)存使用量。同時amqqueue進程會周期性拉取內(nèi)存使用量,當內(nèi)存達到paging閾值時,觸發(fā)amqqueue進程進行paging。paging發(fā)生后,amqqueue進程每收到一條新消息都會對內(nèi)部隊列進行page(每次page都會計算出一定數(shù)目的消息存盤)。
該過程可行的優(yōu)化方案是:在amqqueue進程將大部分消息paging到磁盤后,顯式調(diào)用GC,同時將memory_monitor周期設為0.5s、amqqueue拉取周期設為1s,這樣就能夠達到秒級恢復;去掉對每條消息執(zhí)行paging的操作,用amqqueue周期性拉取內(nèi)存使用量的操作來觸發(fā)page,這樣能夠更快將消息paging到磁盤,而且保持這個周期內(nèi)生產(chǎn)速度不下降。
具體修改可查看:
https://github.com/rabbitmq/rabbitmq-server/compare/stable...javaforfun:stable
圖6 paging時主動垃圾回收
從修改后效果可以看出,三次paging都很快結(jié)束,前兩次paging相鄰較近是因為兩個鏡像節(jié)點分別執(zhí)行了paging。
該問題已反饋至RabbitMQ社區(qū):
從圖5中還可以發(fā)現(xiàn),在22:01時生產(chǎn)速度有一個明顯的下降(此時未發(fā)生paging)。通過流控分析,鏈路被block在amqqueue進程;經(jīng)觀察發(fā)現(xiàn)節(jié)點內(nèi)存使用下降了,說明該節(jié)點執(zhí)行了GC。Erlang GC是按進程級別的標記-清掃模式,會將當前進程暫停,直至GC結(jié)束。由于在RabbitMQ中,一個隊列只有一個amqqueue進程,該進程又會處理大量的消息,產(chǎn)生大量的垃圾。這就導致該進程GC較慢,進而流控block上游更長時間。
查看RabbitMQ代碼發(fā)現(xiàn),amqqueue進程的gen_server模型在正常的邏輯中調(diào)用了hibernate,該操作可能導致兩次不必要的GC。優(yōu)化掉hibernate對系統(tǒng)穩(wěn)定性有一些幫助。
對流控可能比較好的優(yōu)化方案是:用多個amqqueue進程來實現(xiàn)一個隊列,這樣可以降低rabbit_channel被單個amqqueue進程block的概率,同時在單隊列的場景下也能更好利用多核的特性。不過該方案對RabbitMQ現(xiàn)有的架構(gòu)改動很大,難度也很大。
四. 參數(shù)調(diào)優(yōu)
RabbitMQ可優(yōu)化的參數(shù)分為兩個部分,Erlang部分和RabbitMQ自身。
IO_THREAD_POOL_SIZE:CPU大于或等于16核時,將Erlang異步線程池數(shù)目設為100左右,提高文件IO性能。
hipe_compile:開啟Erlang HiPE編譯選項(相當于Erlang的jit技術(shù)),能夠提高性能20%-50%。在Erlang R17后HiPE已經(jīng)相當穩(wěn)定,RabbitMQ官方也建議開啟此選項。
queue_index_embed_msgs_below:RabbitMQ 3.5版本引入了將小消息直接存入隊列索引(queue_index)的優(yōu)化,消息持久化直接在amqqueue進程中處理,不再通過msg_store進程。由于消息在5個內(nèi)部隊列中是有序的,所以不再需要額外的位置索引(msg_store_index)。該優(yōu)化提高了系統(tǒng)性能10%左右。
vm_memory_high_watermark:用于配置內(nèi)存閾值,建議小于0.5,因為Erlang GC在最壞情況下會消耗一倍的內(nèi)存。
vm_memory_high_watermark_paging_ratio:用于配置paging閾值,該值為1時,直接觸發(fā)內(nèi)存滿閾值,block生產(chǎn)者。
queue_index_max_journal_entries:journal文件是queue_index為避免過多磁盤尋址添加的一層緩沖(內(nèi)存文件)。對于生產(chǎn)消費正常的情況,消息生產(chǎn)和消費的記錄在journal文件中一致,則不用再保存;對于無消費者情況,該文件增加了一次多余的IO操作。
五. 總結(jié)
RabbitMQ在2007年發(fā)布第一個版本時,只有5000行Erlang代碼,到現(xiàn)在已經(jīng)加入了非常多的特性,但基本架構(gòu)沒有變。從多核的角度看,流控機制和單amqqueue進程之間存在一些沖突,對消費者異常這種場景,還需要從整個架構(gòu)方面做更多優(yōu)化。
除了上述內(nèi)容,RabbitMQ在Cluster、HA、可靠交付、擴展支持等方面也做了大量的工作,這些都值得深入的學習。
核心關(guān)注:拓步ERP系統(tǒng)平臺是覆蓋了眾多的業(yè)務領(lǐng)域、行業(yè)應用,蘊涵了豐富的ERP管理思想,集成了ERP軟件業(yè)務管理理念,功能涉及供應鏈、成本、制造、CRM、HR等眾多業(yè)務領(lǐng)域的管理,全面涵蓋了企業(yè)關(guān)注ERP管理系統(tǒng)的核心領(lǐng)域,是眾多中小企業(yè)信息化建設首選的ERP管理軟件信賴品牌。
轉(zhuǎn)載請注明出處:拓步ERP資訊網(wǎng)http://www.oesoe.com/
本文標題:RabbitMQ進程結(jié)構(gòu)分析與性能調(diào)優(yōu)
本文網(wǎng)址:http://www.oesoe.com/html/support/11121524033.html