一、問題
線上RocketMQ 集群,偶爾報錯如下:
(1)[REJECTREQUEST]system busy, start flow control for a while
(2)[TIMEOUT_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: 206ms, size of queue: 5
二、調優歷程
Google資料
翻閱github 和 stackoverflow,在stackoverflow 上找到該問題:https://stackoverflow.com/questions/47749906/rocketmq-throw-exception-timeout-clean-queuebroker-busy-start-flow-control-f
按照以上方式增加配置文件參數:
sendMessageThreadPoolNums=64
useReentrantLockWhenPutMessage=true
之后重啟觀察,報錯確實變少了,但量大了后,還有報錯。期間對sendMessageThreadPoolNums參數做過多次調整,32 、64、128 都試過,發現在 4核CPU, 30G內存的機器上,配置sendMessageThreadPoolNums超過64 反倒表現的更不好。本人最后選擇sendMessageThreadPoolNums=32。具體多少合適,最好通過壓測評估。
擼源碼
此時還會報 broker busy,實在沒辦法,只能看源碼,尤其對報錯前后相關代碼多次閱讀,基本了解了報錯原因。
兩個報錯的代碼如下:
通過閱讀源碼,得出結果:mq busy是因為線程等待時間,超過了waitTimeMillsInSendQueue的值,該值默認 200ms。
所以還需分析,到底是什么原因,導致線程在等待,並超過了200ms,有兩種可能:
1. mq消息消費速度慢,消息堆積,導致消息寫入內存變慢,超過了200ms
2. GC時JVM STW(stop the world) ,導致超時
之后對 JVM 進行了調優,選用 G1回收器,並且觀察 gc 時間,full GC 基本沒有了,新生代gc 時間控制在50ms 之內,而且頻率不高。
JVM 優化后,還是偶爾有 broker busy.
配置優化
查看rocketmq 日志,存儲層日志store.log:
可以看出,在消息發送時,主從同步時,連接從服務器等待超時,自動斷了連接。
如果是同步雙寫模式,master會等待slave也寫成功才會給客戶端返回成功。這個也會耗時,性能不好,建議使用異步。
優化建議:主從同步使用異步,配置brokerRole=ASYNC_MASTER。
到此,優化配置:
#主從同步模式
brokerRole=ASYNC_MASTER
#消息發送隊列等待時間,默認200
waitTimeMillsInSendQueue=400
#發送消息的最大線程數,默認1,因為服務器配置不同,具體多少需要壓測后取最優值
sendMessageThreadPoolNums=32
#發送消息是否使用可重入鎖(4.1版本以上才有)
useReentrantLockWhenPutMessage=true
到此,MQ 基本穩定,連續幾個月再沒有報錯。
后來MQ集群內存快不夠了,擴了集群,擴為之前2倍節點。
擴容后過段時間,又出現了 以上兩個錯, broker busy 報錯居多。。。
此時第一感覺是新增的機器問題,通過Falcon監控,觀察分析后,發現報錯期間,CPU、內存、IO 等都無異常,有部分報錯時間點 swap 波動較大。
當內存不夠用時,rocketMQ 會使用交換區,使用交換區性能較差,這里就不對swap做過多解釋了。
對Linux內核參數調優:
查看 cat /proc/sys/vm/swappiness
設置 swappiness = 1
減少使用交換區,提升性能。
優化該參數后,報錯減少。
后來上線異步消息,並發增高,隨着使用方量級增長,broker busy 還是會出現。
在和 system busy 和 broker busy 的斗爭中,我對RocketMQ的了解也在加深。
對於RocketMQ 集群,和其他使用者也有過交流,總之經驗是:多個小集群 優於 一個大集群。
調優時,多關注磁盤IO 和 內存使用情況及釋放時間點,重點關注以下指標:
三、最終的參數調優方案
機器配置參考:CPU 4核 \ 內存30G
RocketMQ 的配置
#主從異步復制
brokerRole=ASYNC_MASTER
#異步刷盤
flushDiskType=ASYNC_FLUSH
#線上關閉自動創建topic
autoCreateTopicEnable=false
#發送消息的最大線程數,默認1
sendMessageThreadPoolNums=32
#使用可重入鎖
useReentrantLockWhenPutMessage=true
#發送消息線程等待時間,默認200ms
waitTimeMillsInSendQueue=1000
#開啟臨時存儲池
transientStorePoolEnable=true
#開啟Slave讀權限(分擔master 壓力)
slaveReadEnable=true
#關閉堆內存數據傳輸
transferMsgByHeap=false
#開啟文件預熱
warmMapedFileEnable=true
JVM
-server -Xms10g -Xmx10g
-XX:+UseG1GC -XX:MaxGCPauseMillis=80 -XX:G1HeapRegionSize=16m
-XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30
-XX:SoftRefLRUPolicyMSPerMB=0 -XX:SurvivorRatio=8
-verbose:gc -Xloggc:/dev/shm/mq_gc_%p.log -XX:+PrintGCDetails
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m
-XX:-OmitStackTraceInFastThrow -XX:+AlwaysPreTouch -XX:MaxDirectMemorySize=15g
-XX:-UseLargePages -XX:-UseBiasedLocking -XX:+PrintGCApplicationStoppedTime
-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1
-XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCDateStamps
-XX:+PrintAdaptiveSizePolicy
Linux
- vm.extra_free_kbytes,告訴VM在后台回收(kswapd)啟動的閾值與直接回收(通過分配進程)的閾值之間保留額外的可用內存。RocketMQ使用此參數來避免內存分配中的長延遲。(與具體內核版本相關)
- vm.min_free_kbytes,如果將其設置為低於1024KB,將會巧妙的將系統破壞,並且系統在高負載下容易出現死鎖。
- vm.max_map_count,限制一個進程可能具有的最大內存映射區域數。RocketMQ將使用mmap加載CommitLog和ConsumeQueue,因此建議將為此參數設置較大的值。(agressiveness --> aggressiveness)
- vm.swappiness,定義內核交換內存頁面的積極程度。較高的值會增加攻擊性,較低的值會減少交換量。建議將值設置為10來避免交換延遲。
- File descriptor limits,RocketMQ需要為文件(CommitLog和ConsumeQueue)和網絡連接打開文件描述符。我們建議設置文件描述符的值為655350。
- Disk scheduler,RocketMQ建議使用I/O截止時間調度器,它試圖為請求提供有保證的延遲。
1. 減少使用交換區
swappiness = 1
2. Disk scheduler使用DeadLine IO調度器
查看IO調度器:
#查看機器整體
dmesg | grep -i scheduler
#查看某個磁盤的調度器
cat /sys/block/vda/queue/scheduler
修改IO調度器:
echo deadline > /sys/block/vda/queue/scheduler
注意:Linux version 3.10 以上的版本,會出現修改不成功。
如下:
如果你的Linux 服務器,查看磁盤IO調度器發現是 none, none 意思是不使用 IO調度器,一般都是開啟了 blk-mq, 查看/sys/block/vda/ 下,如果有mq目錄,說明使用的是blk-mq機制。
Blk-mq
Linux 3.13引入了塊層的新框架 blk-mq(多隊列塊IO排隊機制),並且在3.16內核中已具有完整的功能。Blk-mq 通過8插槽服務器上的高性能閃存設備(例如PCIe SSD)允許超過1500萬IOPS。
Blk-mq無縫集成到Linux存儲堆棧中。它為設備驅動程序提供了基本功能,用於將I / O查詢映射到多個隊列。任務分布在多個線程中,因此分布到多個CPU內核(每個內核軟件隊列)。兼容Blk-mq的驅動程序通知blk-mq設備支持多少個並行硬件隊列(作為硬件調度隊列注冊的一部分的提交隊列的數量)。基於Blk-mq的設備驅動程序會繞過以前的Linux I / O調度程序。根據Linux I / O調度程序(基於request_fn的方法,請參閱Linux I / O堆棧圖),使用前一個塊I / O層的所有設備驅動程序將繼續獨立於blk-mq用作基於請求的驅動程序。
所以,如果你的MQ服務器硬盤是SSD 或者 內核為 3.10以上的,理論上使用blk-mq性能將會更高,不需要修改。
Netty
相關配置參數
1. jvm參數加:-Dio.netty.recycler.maxCapacity.default=0
2. jvm參數去掉-XX:+DisableExplicitGC
Netty性能問題
1.查閱netty相關資料,在netty的github上找到了一個issue #4147,大致可以看出,netty實現的Recycler並不保證池的大小,也就是說有多少對象就往池中加入多少對象,從而可能撐滿服務器。通過在jvm啟動時加入-Dio.netty.recycler.maxCapacity.default=0參數來關閉Recycler池,前提:netty version>=4.0。所以可懷疑為內存泄露!
千萬不要開啟-XX:+DisableExplicitGC!因為netty要做System.gc()操作,而System.gc()會對直接內存進行一次標記回收,如果通過DisableExplicitGC禁用了,會導致netty產生的對象爆滿
2.Netty里四種主力的ByteBuf,
其中UnpooledHeapByteBuf 底下的byte[]能夠依賴JVM GC自然回收;而UnpooledDirectByteBuf底下是DirectByteBuffer,如Java堆外內存掃盲貼所述,除了等JVM GC,最好也能主動進行回收;而PooledHeapByteBuf 和 PooledDirectByteBuf,則必須要主動將用完的byte[]/ByteBuffer放回池里,否則內存就要爆掉。所以,Netty ByteBuf需要在JVM的GC機制之外,有自己的引用計數器和回收過程。
在DirectByteBuffer中,首先向Bits類申請額度,Bits類有一個全局的 totalCapacity變量,記錄着全部DirectByteBuffer的總大小,每次申請,都先看看是否超限 -- 堆外內存的限額默認與堆內內存(由-XMX 設定)相仿,可用 -XX:MaxDirectMemorySize 重新設定。
如果已經超限,會主動執行Sytem.gc(),期待能主動回收一點堆外內存。然后休眠一百毫秒,看看totalCapacity降下來沒有,如果內存還是不足,就拋出大家最頭痛的OOM異常。
心得:
MQ 調優過程,需要關注 disk 、mem 、IO、CPU 等方面指標,尤其要關注 iowait , 磁盤iowait 會直接影響性能。
發送消息線程數並不是越多越好,太多的線程,CPU 上下文切換也是很大的性能損耗。
需要對Linux 有較為深刻的了解,其中涉及 頁緩存、缺頁中斷、零拷貝、預讀、回寫、內存映射、IO調度器。
其他技術:並發處理、鎖機制、異步、池化技術、堆外內存、Netty 等相關技術。
只有對以上這些技術都有深刻理解,才能很好的理解RocketMQ,並對其做出合理的優化。
參考資料
Apache RocketMQ開發者指南
RocketMQ單機存儲原理
調整 Linux I/O 調度器優化系統性能
Linux Blk-mq 機制