RocketMQ高性能優(yōu)化探索
本章節(jié)簡單介紹下在優(yōu)化RocketMQ過程中用到的方法和技巧。部分方法在消息領(lǐng)域提升不明顯卻帶來了編碼和運(yùn)維的復(fù)雜度,這類方法雖然最終沒有利用起來,也在下面做了介紹供大家參考。
Java篇
在接觸到內(nèi)核層面的性能優(yōu)化之前,Java層面的優(yōu)化需要先做起來。有時候靈機(jī)一動的優(yōu)化方法需要實現(xiàn)Java程序來進(jìn)行測試,注意測試的時候需要在排除其他干擾的同時充分利用JVM的預(yù)熱(JIT)特性。推薦使OpenJDK開發(fā)的基準(zhǔn)測試(Benchmark)工具JMH。
- JVM停頓
影響Java應(yīng)用性能的頭號大敵便是JVM停頓,說起停頓,大家耳熟能詳?shù)谋闶荊C階段的STW(Stop the World),除了GC,還有很多其他原因,如下圖所示。

當(dāng)懷疑我們的Java應(yīng)用受停頓影響較大時,首先需要找出停頓的類型,下面一組JVM參數(shù)可以輸出詳細(xì)的安全點信息:
-XX:+LogVMOutput -XX:LogFile=/dev/shm/vm.log
-XX:+PrintGCApplicationStoppedTime -XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1 -XX:+PrintGCApplicationConcurrentTime
在RocketMQ的性能測試中,發(fā)現(xiàn)存在大量的RevokeBias停頓,偏向鎖主要是消除無競爭情況下的同步原語以提高性能,但考慮到RocketMQ中該場景比較少,便通過-XX:-UseBiasedLocking關(guān)閉了偏向鎖特性。
停頓有時候會讓我們的StopWatch變得很不精確,有一段時間經(jīng)常被StopWatch誤導(dǎo),觀察到一段代碼耗時異常,結(jié)果花時間去優(yōu)化也沒效果,其實不是這段代碼耗時,只是在執(zhí)行這段代碼時發(fā)生了停頓。停頓和動態(tài)編譯往往是性能測試的兩大陷阱。
- GC
GC將Java程序員從內(nèi)存管理中解救了出來,但也對開發(fā)低延時的Java應(yīng)用帶來了更多的挑戰(zhàn)。對GC的優(yōu)化個人認(rèn)為是一項調(diào)整參數(shù)的工作,垃圾收集方面最值得關(guān)注的兩個性能屬性為吞吐量和延遲,對GC進(jìn)行優(yōu)化往往是尋求吞吐量和延遲上的折衷,沒辦法魚和熊掌兼得。
RocketMQ通過GC調(diào)優(yōu)后最終采取的GC參數(shù)如下所示,供大家參考。
-server -Xms8g -Xmx8g -Xmn4g
-XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25
-XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0
-XX:SurvivorRatio=8 -XX:+DisableExplicitGC
-verbose:gc -Xloggc:/dev/shm/mq_gc_%p.log -XX:+PrintGCDetails
-XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime
-XX:+PrintAdaptiveSizePolicy
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m
可以看出,我們最終全部切換到了G1,16年雙十一線上MetaQ集群采用的也是這一組參數(shù),基本上GC時間能控制在20ms以內(nèi)(一些超大的共享集群除外)。
對于G1,官方推薦使用該-XX:MaxGCPauseMillis設(shè)置目標(biāo)暫停時間,不要手動指定-Xmn和-XX:NewRatio,但我們在實測中發(fā)現(xiàn),如果指定過小的目標(biāo)停頓時間(10ms),G1會將新生代調(diào)整為很小,導(dǎo)致YGC更加頻繁,老年代用得更快,所有還是手動指定了-Xmn為4g,在GC頻率不高的情況下完成了10ms的目標(biāo)停頓時間,這里也說明有時候一些通用的調(diào)優(yōu)經(jīng)驗并不適用于所有的產(chǎn)品場景,需要更多的測試才能找到最合適的調(diào)優(yōu)方法,往往需要另辟蹊徑。
同時也分享下我們在使用CMS時遇到的一個坑,-XX:UseConcMarkSweepGC在使用CMS收集器的同時默認(rèn)在新生代使用ParNew, ParNew并行收集垃圾使用的線程數(shù)默認(rèn)值更機(jī)器cpu數(shù)(<8時)或者8+(ncpus-8)*5/8,大量垃圾收集線程同時運(yùn)行會帶來大量的停頓導(dǎo)致毛刺,可以使用-XX:ParallelGCThreads指定并行線程數(shù)。
還有避免使用finalize()方法來進(jìn)行資源回收,除了不靠譜以為,會加重GC的壓力,原因就不贅述了。
另外,我們也嘗試了Azul公司的商業(yè)虛擬機(jī)Zing,Zing采用了C4垃圾收集器,但Zing的長處在于GC的停頓時間不隨堆的增長而變長,特別適合于超大堆的應(yīng)用場景,但RocketMQ使用的堆其實較小,大多數(shù)的內(nèi)存需要留給PageCache,所以沒有采用Zing。我這里有一份MetaQ在Zing下的測試報告,感興趣的可以聯(lián)系我,性能確實不錯。
- 線程池
Java應(yīng)用里面總會有各式各樣的線程池,運(yùn)用線程池最需要考慮的兩個因素便是:
- 線程池的個數(shù),避免設(shè)置過多或過少的線程池數(shù),過少會導(dǎo)致CPU資源利用率不夠吞吐量低,過多的線程池會帶來更多的同步原語、上下文切換、調(diào)度等方面的性能損失。
- 線程池的劃分,需要根據(jù)具體的業(yè)務(wù)或者模塊做詳細(xì)的規(guī)劃,線程池往往也起到了資源隔離的作用,RocketMQ中曾有一個重要模塊和一個非重要模塊共享一個線程池,在去年雙十一的壓測中,非重要模塊因壓力大占據(jù)了大部分的線程池資源,導(dǎo)致重要模塊的業(yè)務(wù)發(fā)生饑餓,最終導(dǎo)致了無法恢復(fù)的密集FGC。
關(guān)于線程池個數(shù)的設(shè)置,可以參考《Java Concurrency in Practice》一書中的介紹:

需要注意的是,增加線程數(shù)并非提升性能的萬能藥,且不說多線程帶來的額外性能損耗,大多數(shù)業(yè)務(wù)本質(zhì)上都是串行的,由一系列并行工作和串行工作組合而成,我們需要對其進(jìn)行合適的切分,找出潛在的并行能力。并發(fā)是不能突破串行的限制,需遵循Amdahl 定律。
如果線程數(shù)設(shè)置不合理或者線程池劃分不合理,可能會觀察到虛假競爭,CPU資源利用不高的同時業(yè)務(wù)吞吐量也上不去。這種情況也很難通過性能分析工具找出瓶頸,需要對線程模型仔細(xì)分析,找出不合理和短板的地方。
事實上,對RocketMQ現(xiàn)存的線程模型進(jìn)行梳理后,發(fā)現(xiàn)了一些不合理的線程數(shù)設(shè)置,通過對其調(diào)優(yōu),帶來的性能提升非??捎^。
CPU篇
CPU方面的調(diào)優(yōu)嘗試,主要在于親和性和NUMA。
- CPU親和性
CPU親和性是一種調(diào)度屬性,可以將一個線程”綁定” 到某個CPU上,避免其在處理器之間頻繁遷移。
同時,有一個開源的Java庫可以支持在Java語言層面調(diào)用API完成CPU親和性綁定。該庫給出了Thread如何綁定CPU,如果需要對線程池里面的線程進(jìn)行CPU綁定,可以自定義ThreadFactory來完成。
我們通過對RocketMQ中核心線程進(jìn)行CPU綁定發(fā)現(xiàn)效果不明顯,考慮到會引入第三方庫便放棄了此方法。推測效果不明顯的原因是我們在核心鏈路上已經(jīng)使用了無鎖編程,避免上下文切換帶來的毛刺現(xiàn)象。
上下文切換確實是比較耗時的,同時也具有毛刺現(xiàn)象,下圖是我們通過LockSupport.unpark/park來模擬上下文切換的測試,可以看出切換平均耗時是微妙級,但偶爾也會出現(xiàn)毫秒級的毛刺。

通過Perf也觀察到unpark/park也確實能產(chǎn)生上下文切換。

此外有一個內(nèi)核配置項isolcpus,可以將一組CPU在系統(tǒng)中孤立出來,默認(rèn)是不會被使用的,該參數(shù)在GRUB中配置重啟即可。CPU被隔離出來后可以通過CPU親和性綁定或者taskset/numactl來分配任務(wù)到這些CPU以達(dá)到最優(yōu)性能的效果。
- NUMA
對于NUMA,大家的態(tài)度是褒貶不一,在數(shù)據(jù)庫的場景忠告一般是關(guān)掉NUMA,但通過了解了NUMA的原理,覺得理論上NUMA對RocketMQ的性能提升是有幫助的。
前文提到了并發(fā)的調(diào)優(yōu)是不能突破Amdahl 定律的,總會有串行的部分形成短板,對于CPU來講也是同樣的道理。隨著CPU的核數(shù)越來越多,但CPU的利用率卻越來越低,在64核的物理機(jī)上,RocketMQ只能跑到2500%左右。這是因為,所有的CPU都需要通過北橋來讀取內(nèi)存,對于CPU來說內(nèi)存是共享的,這里的內(nèi)存訪問便是短板所在。為了解決這個短板,NUMA架構(gòu)的CPU應(yīng)運(yùn)而生。
如下圖所示,是兩個NUMA節(jié)點的架構(gòu)圖,每個NUMA節(jié)點有自己的本地內(nèi)存,整個系統(tǒng)的內(nèi)存分布在NUMA節(jié)點的內(nèi)部,某NUMA節(jié)點訪問本地內(nèi)存的速度(Local Access)比訪問其它節(jié)點內(nèi)存的速度(Remote Access)快三倍。

RocketMQ通過在NUMA架構(gòu)上的測試發(fā)現(xiàn)有20%的性能提升,還是比較可觀的。特別是線上物理機(jī)大都支持NUMA架構(gòu),對于兩個節(jié)點的雙路CPU,可以考慮按NUMA的物理劃分虛擬出兩個Docker進(jìn)行RocketMQ部署,最大化機(jī)器的性能價值。
感興趣的同學(xué)可以測試下NUMA對自家應(yīng)用的性能影響,集團(tuán)機(jī)器都從BIOS層面關(guān)閉了NUMA,如果需要測試,按如下步驟打開NUMA即可:
1.打開BIOS開關(guān):
打開方式跟服務(wù)器相關(guān)。
2.在GRUB中配置開啟NUMA
vi /boot/grub/grub.conf
添加boot參數(shù):numa=on
3.重啟
4.查看numa node個數(shù)
numactl --hardware
如果看到了>1個節(jié)點,即為支持NUMA
內(nèi)存篇
可以將Linux內(nèi)存分為以下三類:

- 頁錯誤
我們知道,為了使用更多的內(nèi)存地址空間切更加有效地管理存儲器,操作系統(tǒng)提供了一種對主存的抽象概念——虛擬存儲器(VM),有了虛擬存儲器,就必然需要有從虛擬到物理的尋址。進(jìn)程在分配內(nèi)存時,實際上是通過VM系統(tǒng)分配了一系列虛擬頁,此時并未涉及到真正的物理頁的分配。當(dāng)進(jìn)程真正地開始訪問虛擬內(nèi)存時,如果沒有對應(yīng)的物理頁則會觸發(fā)缺頁異常,然后調(diào)用內(nèi)核中的缺頁異常處理程序進(jìn)行的內(nèi)存回收和分配。
頁錯誤分為兩種:
- Major Fault, 當(dāng)需要訪問的內(nèi)存被swap到磁盤上了,這個時候首先需要分配一塊內(nèi)存,然后進(jìn)行disk io將磁盤上的內(nèi)容讀回道內(nèi)存中,這是一系列代價比較昂貴的操作。
- Minor Fault, 常見的頁錯誤,只涉及頁分配。
為了提高訪存的高效性,需要觀察進(jìn)程的頁錯誤信息,以下命令都可以達(dá)到該目的:
1. ps -o min_flt,maj_flt <PID>
2. sar -B
如果觀察到Major Fault比較高,首先要確認(rèn)系統(tǒng)參數(shù)vm.swappiness是否設(shè)置恰當(dāng),建議在機(jī)器內(nèi)存充足的情況下,設(shè)置一個較小的值(0或者1),來告訴內(nèi)核盡可能地不要利用磁盤上的swap區(qū)域,0和1的選擇原則如下:

切記不要在2.6.32以后設(shè)置為0,這樣會導(dǎo)致內(nèi)核關(guān)閉swap特性,內(nèi)存不足時不惜OOM也不會發(fā)生swap,前端時間也碰到過因swap設(shè)置不當(dāng)導(dǎo)致的故障。
另一方面,避免觸發(fā)頁錯誤,內(nèi)存頻繁的換入換出,還有以下手段可以采用:
1.-XX:+AlwaysPreTouch,顧名思義,該參數(shù)為讓JVM啟動時將所有的內(nèi)存訪問一遍,達(dá)到啟動后所有內(nèi)存到位的目的,避免頁錯誤。
2.對于我們自行分配的堆外內(nèi)存,或者mmap從文件映射的內(nèi)存,我們可以自行對內(nèi)存進(jìn)行預(yù)熱,有以下四種預(yù)熱手段,第一種不可取,后兩種是最快的。

3.即使對內(nèi)存進(jìn)行了預(yù)熱,當(dāng)內(nèi)存不夠時,后續(xù)還是會有一定的概率被換出,如果希望某一段內(nèi)存一直常駐,可以通過mlock/mlockall系統(tǒng)調(diào)用來將內(nèi)存鎖住,推薦使用JNA來調(diào)用這兩個接口。不過需要注意的是內(nèi)核一般不允許鎖定大量的內(nèi)存,可通過以下命令來增加可鎖定內(nèi)存的上限。
echo '* hard memlock unlimited' >> /etc/security/limits.conf
echo '* soft memlock unlimited' >> /etc/security/limits.conf
- Huge Page
大家都知道,操作系統(tǒng)的內(nèi)存4k為一頁,前文說到Linux有虛擬存儲器,那么必然需要有頁表(Page Table)來存儲物理頁和虛擬頁之間的映射關(guān)系,CPU訪問存時首先查找頁表來找到物理頁,然后進(jìn)行訪存,為了提高尋址的速度,CPU里有一塊高速緩存名為ranslation Lookaside Buffer (TLB),包含部分的頁表信息,用于快速實現(xiàn)虛擬地址到物理地址的轉(zhuǎn)換。
但TLB大小是固定的,只能存下小部分頁表信息,對于超大頁表的加速效果一般,對于4K內(nèi)存頁,如果分配了10GB的內(nèi)存,那么頁表會有兩百多萬個Entry,TLB是遠(yuǎn)遠(yuǎn)放不下這么多Entry的。可通過cpuid查詢TLB Entry的個數(shù),4K的Entry一般僅有上千個,加速效果有限。
為了提高TLB的命中率,大多數(shù)CPU支持大頁,大頁分為2MB和1GB,1GB大頁是超大內(nèi)存的不二選擇,可通過grep pdpe1gb /proc/cpuinfo | uniq查看CPU是否支持1GB的大頁。
開啟大頁需要配置內(nèi)核啟動參數(shù),hugepagesz=1GB hugepages=10,設(shè)置大頁數(shù)量可通過內(nèi)核啟動參數(shù)hugepages或者/proc/sys/vm/nr_hugepages進(jìn)行設(shè)置。
內(nèi)核開啟大頁過后,Java應(yīng)用程序使用大頁有以下方法:
- 對于堆內(nèi)存,有JVM參數(shù)可以用:-XX:+UseLargePages
- 如果需要堆外內(nèi)存,可以通過mount掛載hugetlbfs,
mount -t hugetlbfs hugetlbfs /hugepages,然后通過mmap分配大頁內(nèi)存。
可以看出使用大頁比較繁瑣的,Linux提供透明超大頁面 (THP)。THP 是可自動創(chuàng)建、管理和使用超大頁面??赏ㄟ^修改文件/sys/kernel/mm/transparent_hugepage/enabled來關(guān)閉或者打開THP。
但大頁有一個弊端,如果內(nèi)存壓力大,需要換出時,大頁會先拆分成小頁進(jìn)行換出,需要換入時再合并為大頁,該過程會加重CPU的壓力。
網(wǎng)卡篇
網(wǎng)卡性能診斷工具是比較多的,有ethtool, ip, dropwatch, netstat等,RocketMQ嘗試了網(wǎng)卡中斷和中斷聚合兩方面的優(yōu)化手段。
- 網(wǎng)卡中斷
這方面的優(yōu)化首先便是要考慮是否需要關(guān)閉irqbalance,它用于優(yōu)化中斷分配,通過自動收集系統(tǒng)數(shù)據(jù)來進(jìn)行中斷負(fù)載,同時還會綜合考慮節(jié)能等因素。但irqbalance有個缺點是會導(dǎo)致中斷自動漂移,造成不穩(wěn)定的現(xiàn)象,在高性能的場合建議關(guān)閉。
關(guān)閉irqbalance后,需要對網(wǎng)卡的所有隊列進(jìn)行CPU綁定,目前的網(wǎng)卡都是由多隊列組成,如果所有隊列的中斷僅有一個CPU進(jìn)行處理,難以利用多核的優(yōu)勢,所以可以對這些網(wǎng)卡隊列進(jìn)行CPU一一綁定。
這部分優(yōu)化對RocketMQ的小消息性能提升有很大的幫助。
- 中斷聚合
中斷聚合的思想類似于Group Commit,避免每一幀的到來都觸發(fā)一次中斷,RocketMQ在跑到最大性能時,每秒會觸發(fā)近20000次的中斷,如果可以聚合一部分,對性能還是有一定的提升的。
可以通過ethtool設(shè)置網(wǎng)卡的rx-frames-irq和rx-usecs參數(shù)來決定湊齊多少幀或者多少時間過后才觸發(fā)一次中斷,需要注意的是中斷聚合會帶來一定的延遲。
總結(jié)
目前RocketMQ最新的性能基準(zhǔn)測試中,128字節(jié)小消息TPS已達(dá)47W,如下圖所示:

高性能的RocketMQ可應(yīng)用于更多的場景,能接管和替代Kafka更多的生態(tài),同時可以更大程度上承受熱點問題,在保持高性能的同時,RocketMQ在低延遲方面依然具有領(lǐng)先地位,如下圖所示,RocketMQ僅有少量10~50ms的毛刺延遲,Kafka則有不少500~1s的毛刺。

共同學(xué)習(xí),資料分享
大多數(shù)人學(xué)習(xí)面臨的痛點
實戰(zhàn)經(jīng)驗缺乏
很多人學(xué)習(xí)一門技術(shù),更多的是看視頻看書,純理論學(xué)習(xí)。背概念,缺乏真實的實戰(zhàn)。很多同學(xué)看過不少RocketMQ博客或視頻,理論知識豐富。但我們實際工作中會遇到的問題是各種各樣的,缺少實戰(zhàn),當(dāng)真正碰到問題就不知道如何運(yùn)用所學(xué)知識去解決。
純技術(shù)晦澀難懂,甚至作者刻意將問題困難化
市面上真正適合學(xué)習(xí)的RocketMQ 資料太少,有的書或資料雖然講得比較深入,但是語言晦澀難懂,大多數(shù)人看完這些書基本都是從入門到放棄。學(xué)透RocketMQ 難道就真的就沒有一種適合大多數(shù)同學(xué)的方法嗎?
這次我針對RocketMQ技術(shù)知識難點特地分享一份PDF文檔《RocketMQ實戰(zhàn)源碼解析文檔》
由于篇幅限制,我這里只將此實戰(zhàn)文檔的所含內(nèi)容全部展現(xiàn)出來了,需要獲取完整文檔用以學(xué)習(xí)的朋友們可以進(jìn)Q群:909666042 免費獲?。?/strong>
本文檔分為兩大部分:
- 第一部分是 RocketMQ 實戰(zhàn),包括第1—8章這是本文檔的主體內(nèi)容,可快速用好RocketMQ這個分布式消息隊列
- 第二部分是源碼分析,包括第9到13章當(dāng)有特殊的業(yè)務(wù)需求,需要更改或擴(kuò)展 RocketMQ 現(xiàn)有功能的時候,這部分內(nèi)容能幫助讀者快速熟悉源碼,找到要下手更改的地方,快速實現(xiàn)想要的功能
第一節(jié)和第二節(jié):基礎(chǔ)知識及生產(chǎn)環(huán)境的配置使用
主要包括:消息隊列功能介紹、快速上手 RocketMQ·、小結(jié)、RocketMQ 各部分角色介紹、多機(jī)集群配置和部、發(fā)送 接收消息示例、常用管理命令等

第三節(jié):用適合的方式發(fā)送和接收消息
不同類型的消費者、類型的生產(chǎn)者、如何存儲隊列位置信息、自定義日志輸出、小結(jié)

第四節(jié):分布式消息隊列的協(xié)調(diào)者
NameServer 的功能、各個角色間的交互流程、底層通信機(jī)制、小結(jié)

第五節(jié)到第八節(jié)
- 消息隊列的核心機(jī)
- 制可靠性優(yōu)先的使用場
- 景吞吐量優(yōu)先的使用場
- 景和其他系統(tǒng)交互


第9節(jié)到第12節(jié)
這幾節(jié)是講的RocketMQ的源碼解析內(nèi)容分別有


由于篇幅限制,我這里只將此實戰(zhàn)文檔的所含內(nèi)容全部展現(xiàn)出來了,需要獲取完整文檔用以學(xué)習(xí)的朋友們可以909666042 免費獲取!
