本章節簡單介紹下在優化RocketMQ過程當中用到的方法和技巧。部分方法在消息領域提高不明顯卻帶來了編碼和運維的複雜度,這類方法雖然最終沒有利用起來,也在下面作了介紹供你們參考。前端
在接觸到內核層面的性能優化以前,Java層面的優化須要先作起來。有時候靈機一動的優化方法須要實現Java程序來進行測試,注意測試的時候須要在排除其餘干擾的同時充分利用JVM的預熱(JIT)特性。推薦使OpenJDK開發的基準測試(Benchmark)工具JMH。node
影響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將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之內(一些超大的共享集羣除外)。bash
對於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應用裏面總會有各式各樣的線程池,運用線程池最須要考慮的兩個因素即是:
關於線程池個數的設置,能夠參考**《Java Concurrency in Practice》**一書中的介紹:
須要注意的是,增長線程數並不是提高性能的萬能藥,且不說多線程帶來的額外性能損耗,大多數業務本質上都是串行的,由一系列並行工做和串行工做組合而成,咱們須要對其進行合適的切分,找出潛在的並行能力。併發是不能突破串行的限制,需遵循Amdahl 定律。
若是線程數設置不合理或者線程池劃分不合理,可能會觀察到虛假競爭,CPU資源利用不高的同時業務吞吐量也上不去。這種狀況也很難經過性能分析工具找出瓶頸,須要對線程模型仔細分析,找出不合理和短板的地方。
事實上,對RocketMQ現存的線程模型進行梳理後,發現了一些不合理的線程數設置,經過對其調優,帶來的性能提高很是可觀。
CPU方面的調優嘗試,主要在於親和性和NUMA。
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對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. 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 複製代碼
你們都知道,操做系統的內存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應用程序使用大頁有如下方法:
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實戰源碼解析文檔》
因爲篇幅限制,我這裏只將此實戰文檔的所含內容所有展示出來了,須要獲取完整文檔用以學習的朋友們能夠關注個人公衆號【風平浪靜如碼】獲取資料!
本文檔分爲兩大部分:
第一節和第二節:基礎知識及生產環境的配置使用
主要包括:消息隊列功能介紹、快速上手 RocketMQ·、小結、RocketMQ 各部分角色介紹、多機集羣配置和部、發送 接收消息示例、經常使用管理命令等
第三節:用適合的方式發送和接收消息
不一樣類型的消費者、類型的生產者、如何存儲隊列位置信息、自定義日誌輸出、小結
第四節:分佈式消息隊列的協調者
NameServer 的功能、各個角色間的交互流程、底層通訊機制、小結
第五節到第八節
第9節到第12節
這幾節是講的RocketMQ的源碼解析內容分別有
因爲篇幅限制,我這裏只將此實戰文檔的所含內容所有展示出來了,須要獲取完整文檔用以學習的朋友們能夠關注個人公衆號【風平浪靜如碼】獲取資料!
以爲寫的還不錯的就點個贊,加個關注唄!點關注,不迷路,持續更新!!!