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