一篇文章讓你玩轉高性能下的RocketMQ消息中間件!(附資料分享)

RocketMQ高性能優化探索

本章節簡單介紹下在優化RocketMQ過程中用到的方法和技巧。部分方法在消息領域提升不明顯卻帶來了編碼和運維的複雜度,這類方法雖然最終沒有利用起來,也在下面做了介紹供大家參考。

Java篇

在接觸到內核層面的性能優化之前,Java層面的優化需要先做起來。有時候靈機一動的優化方法需要實現Java程序來進行測試,注意測試的時候需要在排除其他干擾的同時充分利用JVM的預熱(JIT)特性。推薦使OpenJDK開發的基準測試(Benchmark)工具JMH

  • JVM停頓

影響Java應用性能的頭號大敵便是JVM停頓,說起停頓,大家耳熟能詳的便是GC階段的STW(Stop the World),除了GC,還有很多其他原因,如下圖所示。

當懷疑我們的Java應用受停頓影響較大時,首先需要找出停頓的類型,下面一組JVM參數可以輸出詳細的安全點信息:

-XX:+LogVMOutput -XX:LogFile=/dev/shm/vm.log 
-XX:+PrintGCApplicationStoppedTime -XX:+PrintSafepointStatistics  
-XX:PrintSafepointStatisticsCount=1 -XX:+PrintGCApplicationConcurrentTime

在RocketMQ的性能測試中,發現存在大量的RevokeBias停頓,偏向鎖主要是消除無競爭情況下的同步原語以提高性能,但考慮到RocketMQ中該場景比較少,便通過-XX:-UseBiasedLocking關閉了偏向鎖特性。

停頓有時候會讓我們的StopWatch變得很不精確,有一段時間經常被StopWatch誤導,觀察到一段代碼耗時異常,結果花時間去優化也沒效果,其實不是這段代碼耗時,只是在執行這段代碼時發生了停頓。停頓和動態編譯往往是性能測試的兩大陷阱。

  • GC

GC將Java程序員從內存管理中解救了出來,但也對開發低延時的Java應用帶來了更多的挑戰。對GC的優化個人認爲是一項調整參數的工作,垃圾收集方面最值得關注的兩個性能屬性爲吞吐量和延遲,對GC進行優化往往是尋求吞吐量和延遲上的折衷,沒辦法魚和熊掌兼得。

RocketMQ通過GC調優後最終採取的GC參數如下所示,供大家參考。

-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集羣採用的也是這一組參數,基本上GC時間能控制在20ms以內(一些超大的共享集羣除外)。

對於G1,官方推薦使用該-XX:MaxGCPauseMillis設置目標暫停時間,不要手動指定-Xmn和-XX:NewRatio,但我們在實測中發現,如果指定過小的目標停頓時間(10ms),G1會將新生代調整爲很小,導致YGC更加頻繁,老年代用得更快,所有還是手動指定了-Xmn爲4g,在GC頻率不高的情況下完成了10ms的目標停頓時間,這裏也說明有時候一些通用的調優經驗並不適用於所有的產品場景,需要更多的測試才能找到最合適的調優方法,往往需要另闢蹊徑。

同時也分享下我們在使用CMS時遇到的一個坑,-XX:UseConcMarkSweepGC在使用CMS收集器的同時默認在新生代使用ParNew, ParNew並行收集垃圾使用的線程數默認值更機器cpu數(<8時)或者8+(ncpus-8)*5/8,大量垃圾收集線程同時運行會帶來大量的停頓導致毛刺,可以使用-XX:ParallelGCThreads指定並行線程數。

還有避免使用finalize()方法來進行資源回收,除了不靠譜以爲,會加重GC的壓力,原因就不贅述了。

另外,我們也嘗試了Azul公司的商業虛擬機Zing,Zing採用了C4垃圾收集器,但Zing的長處在於GC的停頓時間不隨堆的增長而變長,特別適合於超大堆的應用場景,但RocketMQ使用的堆其實較小,大多數的內存需要留給PageCache,所以沒有采用Zing。我這裏有一份MetaQ在Zing下的測試報告,感興趣的可以聯繫我,性能確實不錯。

  • 線程池

Java應用裏面總會有各式各樣的線程池,運用線程池最需要考慮的兩個因素便是:

  1. 線程池的個數,避免設置過多或過少的線程池數,過少會導致CPU資源利用率不夠吞吐量低,過多的線程池會帶來更多的同步原語、上下文切換、調度等方面的性能損失。
  2. 線程池的劃分,需要根據具體的業務或者模塊做詳細的規劃,線程池往往也起到了資源隔離的作用,RocketMQ中曾有一個重要模塊和一個非重要模塊共享一個線程池,在去年雙十一的壓測中,非重要模塊因壓力大佔據了大部分的線程池資源,導致重要模塊的業務發生飢餓,最終導致了無法恢復的密集FGC。

關於線程池個數的設置,可以參考**《Java Concurrency in Practice》**一書中的介紹:

需要注意的是,增加線程數並非提升性能的萬能藥,且不說多線程帶來的額外性能損耗,大多數業務本質上都是串行的,由一系列並行工作和串行工作組合而成,我們需要對其進行合適的切分,找出潛在的並行能力。併發是不能突破串行的限制,需遵循Amdahl 定律

如果線程數設置不合理或者線程池劃分不合理,可能會觀察到虛假競爭,CPU資源利用不高的同時業務吞吐量也上不去。這種情況也很難通過性能分析工具找出瓶頸,需要對線程模型仔細分析,找出不合理和短板的地方。

事實上,對RocketMQ現存的線程模型進行梳理後,發現了一些不合理的線程數設置,通過對其調優,帶來的性能提升非常可觀。

CPU篇

CPU方面的調優嘗試,主要在於親和性和NUMA。

  • CPU親和性

CPU親和性是一種調度屬性,可以將一個線程”綁定” 到某個CPU上,避免其在處理器之間頻繁遷移。

同時,有一個開源的Java庫可以支持在Java語言層面調用API完成CPU親和性綁定。該庫給出了Thread如何綁定CPU,如果需要對線程池裏面的線程進行CPU綁定,可以自定義ThreadFactory來完成。

我們通過對RocketMQ中核心線程進行CPU綁定發現效果不明顯,考慮到會引入第三方庫便放棄了此方法。推測效果不明顯的原因是我們在覈心鏈路上已經使用了無鎖編程,避免上下文切換帶來的毛刺現象。

上下文切換確實是比較耗時的,同時也具有毛刺現象,下圖是我們通過LockSupport.unpark/park來模擬上下文切換的測試,可以看出切換平均耗時是微妙級,但偶爾也會出現毫秒級的毛刺。

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

此外有一個內核配置項isolcpus,可以將一組CPU在系統中孤立出來,默認是不會被使用的,該參數在GRUB中配置重啓即可。CPU被隔離出來後可以通過CPU親和性綁定或者taskset/numactl來分配任務到這些CPU以達到最優性能的效果。

  • NUMA

對於NUMA,大家的態度是褒貶不一,在數據庫的場景忠告一般是關掉NUMA,但通過了解了NUMA的原理,覺得理論上NUMA對RocketMQ的性能提升是有幫助的。

前文提到了併發的調優是不能突破Amdahl 定律的,總會有串行的部分形成短板,對於CPU來講也是同樣的道理。隨着CPU的核數越來越多,但CPU的利用率卻越來越低,在64核的物理機上,RocketMQ只能跑到2500%左右。這是因爲,所有的CPU都需要通過北橋來讀取內存,對於CPU來說內存是共享的,這裏的內存訪問便是短板所在。爲了解決這個短板,NUMA架構的CPU應運而生。

如下圖所示,是兩個NUMA節點的架構圖,每個NUMA節點有自己的本地內存,整個系統的內存分佈在NUMA節點的內部,某NUMA節點訪問本地內存的速度(Local Access)比訪問其它節點內存的速度(Remote Access)快三倍。

RocketMQ通過在NUMA架構上的測試發現有20%的性能提升,還是比較可觀的。特別是線上物理機大都支持NUMA架構,對於兩個節點的雙路CPU,可以考慮按NUMA的物理劃分虛擬出兩個Docker進行RocketMQ部署,最大化機器的性能價值。

感興趣的同學可以測試下NUMA對自家應用的性能影響,集團機器都從BIOS層面關閉了NUMA,如果需要測試,按如下步驟打開NUMA即可:

**1.**打開BIOS開關:

打開方式跟服務器相關。

**2.**在GRUB中配置開啓NUMA

vi /boot/grub/grub.conf
添加boot參數:numa=on

**3.**重啓

**4.**查看numa node個數

numactl --hardware
如果看到了>1個節點,即爲支持NUMA

內存篇

可以將Linux內存分爲以下三類:

  • 頁錯誤

我們知道,爲了使用更多的內存地址空間切更加有效地管理存儲器,操作系統提供了一種對主存的抽象概念——虛擬存儲器(VM),有了虛擬存儲器,就必然需要有從虛擬到物理的尋址。進程在分配內存時,實際上是通過VM系統分配了一系列虛擬頁,此時並未涉及到真正的物理頁的分配。當進程真正地開始訪問虛擬內存時,如果沒有對應的物理頁則會觸發缺頁異常,然後調用內核中的缺頁異常處理程序進行的內存回收和分配。

頁錯誤分爲兩種:

  1. Major Fault, 當需要訪問的內存被swap到磁盤上了,這個時候首先需要分配一塊內存,然後進行disk io將磁盤上的內容讀回道內存中,這是一系列代價比較昂貴的操作。
  2. Minor Fault, 常見的頁錯誤,只涉及頁分配。

爲了提高訪存的高效性,需要觀察進程的頁錯誤信息,以下命令都可以達到該目的:

1. ps -o min_flt,maj_flt <PID>
2. sar -B

如果觀察到Major Fault比較高,首先要確認系統參數vm.swappiness是否設置恰當,建議在機器內存充足的情況下,設置一個較小的值(0或者1),來告訴內核儘可能地不要利用磁盤上的swap區域,0和1的選擇原則如下:

切記不要在2.6.32以後設置爲0,這樣會導致內核關閉swap特性,內存不足時不惜OOM也不會發生swap,前端時間也碰到過因swap設置不當導致的故障。

另一方面,避免觸發頁錯誤,內存頻繁的換入換出,還有以下手段可以採用:

1.-XX:+AlwaysPreTouch,顧名思義,該參數爲讓JVM啓動時將所有的內存訪問一遍,達到啓動後所有內存到位的目的,避免頁錯誤。
**2.**對於我們自行分配的堆外內存,或者mmap從文件映射的內存,我們可以自行對內存進行預熱,有以下四種預熱手段,第一種不可取,後兩種是最快的。

**3.**即使對內存進行了預熱,當內存不夠時,後續還是會有一定的概率被換出,如果希望某一段內存一直常駐,可以通過mlock/mlockall系統調用來將內存鎖住,推薦使用JNA來調用這兩個接口。不過需要注意的是內核一般不允許鎖定大量的內存,可通過以下命令來增加可鎖定內存的上限。

echo '* hard memlock      unlimited' >> /etc/security/limits.conf
echo '* soft memlock      unlimited' >> /etc/security/limits.conf
  • Huge Page

大家都知道,操作系統的內存4k爲一頁,前文說到Linux有虛擬存儲器,那麼必然需要有頁表(Page Table)來存儲物理頁和虛擬頁之間的映射關係,CPU訪問存時首先查找頁表來找到物理頁,然後進行訪存,爲了提高尋址的速度,CPU裏有一塊高速緩存名爲ranslation Lookaside Buffer (TLB),包含部分的頁表信息,用於快速實現虛擬地址到物理地址的轉換。

但TLB大小是固定的,只能存下小部分頁表信息,對於超大頁表的加速效果一般,對於4K內存頁,如果分配了10GB的內存,那麼頁表會有兩百多萬個Entry,TLB是遠遠放不下這麼多Entry的。可通過cpuid查詢TLB Entry的個數,4K的Entry一般僅有上千個,加速效果有限。

爲了提高TLB的命中率,大多數CPU支持大頁,大頁分爲2MB和1GB,1GB大頁是超大內存的不二選擇,可通過grep pdpe1gb /proc/cpuinfo | uniq查看CPU是否支持1GB的大頁。

開啓大頁需要配置內核啓動參數,hugepagesz=1GB hugepages=10,設置大頁數量可通過內核啓動參數hugepages或者/proc/sys/vm/nr_hugepages進行設置。

內核開啓大頁過後,Java應用程序使用大頁有以下方法:

  • 對於堆內存,有JVM參數可以用:-XX:+UseLargePages
  • 如果需要堆外內存,可以通過mount掛載hugetlbfs,mount -t hugetlbfs hugetlbfs /hugepages,然後通過mmap分配大頁內存。

可以看出使用大頁比較繁瑣的,Linux提供透明超大頁面 (THP)。THP 是可自動創建、管理和使用超大頁面。可通過修改文件/sys/kernel/mm/transparent_hugepage/enabled來關閉或者打開THP。

但大頁有一個弊端,如果內存壓力大,需要換出時,大頁會先拆分成小頁進行換出,需要換入時再合併爲大頁,該過程會加重CPU的壓力。

網卡篇

網卡性能診斷工具是比較多的,有ethtool, ip, dropwatch, netstat等,RocketMQ嘗試了網卡中斷和中斷聚合兩方面的優化手段。

  • 網卡中斷

這方面的優化首先便是要考慮是否需要關閉irqbalance,它用於優化中斷分配,通過自動收集系統數據來進行中斷負載,同時還會綜合考慮節能等因素。但irqbalance有個缺點是會導致中斷自動漂移,造成不穩定的現象,在高性能的場合建議關閉。

關閉irqbalance後,需要對網卡的所有隊列進行CPU綁定,目前的網卡都是由多隊列組成,如果所有隊列的中斷僅有一個CPU進行處理,難以利用多核的優勢,所以可以對這些網卡隊列進行CPU一一綁定。

這部分優化對RocketMQ的小消息性能提升有很大的幫助。

  • 中斷聚合

中斷聚合的思想類似於Group Commit,避免每一幀的到來都觸發一次中斷,RocketMQ在跑到最大性能時,每秒會觸發近20000次的中斷,如果可以聚合一部分,對性能還是有一定的提升的。

可以通過ethtool設置網卡的rx-frames-irq和rx-usecs參數來決定湊齊多少幀或者多少時間過後才觸發一次中斷,需要注意的是中斷聚合會帶來一定的延遲。

總結

目前RocketMQ最新的性能基準測試中,128字節小消息TPS已達47W,如下圖所示:

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

共同學習,資料分享

大多數人學習面臨的痛點

實戰經驗缺乏

很多人學習一門技術,更多的是看視頻看書,純理論學習。背概念,缺乏真實的實戰。很多同學看過不少RocketMQ博客或視頻,理論知識豐富。但我們實際工作中會遇到的問題是各種各樣的,缺少實戰,當真正碰到問題就不知道如何運用所學知識去解決。

純技術晦澀難懂,甚至作者刻意將問題困難化

市面上真正適合學習的RocketMQ 資料太少,有的書或資料雖然講得比較深入,但是語言晦澀難懂,大多數人看完這些書基本都是從入門到放棄。學透RocketMQ 難道就真的就沒有一種適合大多數同學的方法嗎?

這次我針對RocketMQ技術知識難點特地分享一份PDF文檔《RocketMQ實戰源碼解析文檔》

由於篇幅限制,我這裏只將此實戰文檔的所含內容全部展現出來了,需要獲取完整文檔用以學習的朋友們可以關注我的公衆號【風平浪靜如碼】獲取資料!

本文檔分爲兩大部分:

  1. **第一部分是 RocketMQ 實戰,**包括第1—8章這是本文檔的主體內容,可快速用好RocketMQ這個分佈式消息隊列
  2. **第二部分是源碼分析,**包括第9到13章當有特殊的業務需求,需要更改或擴展 RocketMQ 現有功能的時候,這部分內容能幫助讀者快速熟悉源碼,找到要下手更改的地方,快速實現想要的功能

第一節和第二節:基礎知識及生產環境的配置使用

**主要包括:**消息隊列功能介紹、快速上手 RocketMQ·、小結、RocketMQ 各部分角色介紹、多機集羣配置和部、發送 接收消息示例、常用管理命令等

第三節:用適合的方式發送和接收消息

不同類型的消費者、類型的生產者、如何存儲隊列位置信息、自定義日誌輸出、小結

第四節:分佈式消息隊列的協調者

NameServer 的功能、各個角色間的交互流程、底層通信機制、小結


第五節到第八節

  • 消息隊列的核心機
  • 制可靠性優先的使用場
  • 景吞吐量優先的使用場
  • 景和其他系統交互

第9節到第12節

這幾節是講的RocketMQ的源碼解析內容分別有

由於篇幅限制,我這裏只將此實戰文檔的所含內容全部展現出來了,需要獲取完整文檔用以學習的朋友們可以關注我的公衆號【風平浪靜如碼】獲取資料!

覺得寫的還不錯的就點個贊,加個關注唄!點關注,不迷路,持續更新!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章