Apache RocketMQ 4.9.1 高效能優化之路

語言: CN / TW / HK
經過社群的投票,Apache RocketMQ 秋天的第一個版本 4.9.1 如約而至,該版本中最值得關注的是高效能優化這塊,針對 Broker 端的效能,特別是對小訊息的生產效能進行了針對性優化,和 4.9.0 版本相比, 小訊息實時生產的 TPS 提升了約 28%。


這一批優化相關的 Pull Request(PR)都掛在  ISSUE2883  下,分為 7 個 PR(A-G),接下來我們來看一下這一批優化的細節,大家也可以到 github 檢視程式碼明細。

A、針對事務訊息的優化


在當前的版本中,事務訊息已經較為成熟,但壓測的時候就會發現,預設的配置下每條訊息都會打出一條日誌:

log.info("Half offset {} has been committed/rolled back", i);

這肯定會影響效能,壓測等大流量場景下甚至會導致災難性影響。所以這個優化最簡單,把這個日誌改成 debug 就可以了。

B、消除不必要的鎖


在 RocketMQ 內部,主從複製和同步刷盤都是多執行緒協作處理的。以主從複製為例(GroupTransferService),訊息處理執行緒(多個)不斷接收訊息,產生待複製的訊息,另外有一個 ServiceThread 單執行緒處理複製結果,可以把前者看做資料生產者,後者看做資料消費者,RocketMQ 使用了雙 Buffer 來達到批量處理的目的。如下圖,消費者正在處理資料的同時,生產者可以不受影響的繼續新增資料,第一階段生產者 Buffer 有 3 條資料,消費者 Buffer 有 2 條資料,由於消費者是單執行緒,沒有別的執行緒跟它競爭,所以它可以批量處理這 2 條資料,完成後它會交換這兩個 Buffer 的引用,於是接下來的第二階段它又可以批量處理 3 條資料。


之前 RocketMQ 在生產者寫入、交換 Buffer 引用、以及內部處理中都使用了多個重量級鎖保證執行緒安全。但實際上只需要在生產執行緒寫入以及交換 Buffer 引用的時候加輕量級自旋鎖就可以,由於這兩個操作都是非常快的,因此可以認為每次加解鎖都只有 2 次 CAS 操作的開銷。


除此之外,WaitNotifyObject 類也進行了優化,減少了需要進入同步程式碼塊的次數。


C、消除主從複製中的陣列拷貝


RocketMQ 使用 mmap 來方法 CommitLog 檔案,其中有一個好處就是 io 操作的時候少了一個記憶體拷貝。但實際上由於工程的複雜性,程式碼中仍然會存在各種各樣的記憶體拷貝,我們優化的目標就是消除那些本來可以避免的複製。

這一次我們就在主從複製這裡找到了一個優化點,有一個 ByteBuffer,要把其中一部分寫到 CommitLog 裡面去,原來的程式碼會建立一個 byte [],然後複製一遍,其實只需要傳入 ByteBuffer.array() 給後續方法,然後指明要複製的起止位置就可以了。這樣優化後我們還節省了這個 byte[] 的建立,原先複製 1G 的 CommitLog 就會有至少 1G 的 byte[] 物件的分配和 gc 開銷,這下也省了。這次修改的部分實際上執行在 Slave 中,但在同步複製的場景下,對訊息傳送的響應時間還是有影響的。


D、優化 Broker 的預設引數


從 RocketMQ4.X 開始引入了自旋鎖並作為預設值,同時將引數 sendMessageThreadPoolNums(出現訊息生產的執行緒數)改為了 1,這樣處理每條訊息寫 CommitLog 的時候可以省下進出重量鎖的開銷。

不過這個地方單執行緒處理,任務有點重,處理訊息的邏輯並不是往 CommitLog 裡面一寫(無法並行)就完事的,還有一些 CPU 開銷比較大的工作,多執行緒處理比較好,經過一些實踐測試,4 個執行緒是比較合理的數值,因此這個引數預設值改為 MIN(邏輯處理器數, 4)。


既然有 4 個執行緒,還用自旋鎖可能就不合適了,因為拿不到鎖的執行緒會讓 CPU 白白空轉。所以 useReentrantLockWhenPutMessage 引數還是改為 true 比較好。


還有個細節,endTransactionThreadPoolNums 這個引數預設設定成了 sendMessageThreadPoolNums 的至少 4 倍,以避免事務訊息量特別大的場景下(比如事務訊息壓測),二階段處理速度趕不上一階段處理速度,進而導致嚴重的問題。


此外,對刷盤相關的引數也進行了調整。預設情況下,RocketMQ 是非同步刷盤,但每次處理訊息都會觸發一個非同步的刷盤請求。這次將 flushCommitLogTimed 這個引數改成 true,也就是定時刷盤(預設每 500ms),可以大幅降低對 IO 壓力,在主從同步複製的場景下,可靠性也不會降低。


E、優化 put message 鎖內操作


寫 CommitLog 只能單執行緒操作,寫之前要先獲取一個鎖,這個鎖也就是影響 RocketMQ 效能最關鍵的一個鎖。理論上這裡只要往 MappedByteBuffer 寫一下就好了,但實踐往往要比理論複雜得多,因為各種原因,這個鎖裡面乾的事情非常的多。


由於當前程式碼的複雜性,這個優化是本批次修改裡面改動最大的,但它的邏輯其實很簡單,就是把鎖內乾的事情,儘量的放到鎖的外面去做,能先準備好的資料就先準備好。它包括了一下改動:

1、將 Buffer 的大部分準備工作(編碼工作)放到了鎖外,提前做好。
2、將 MessageId 的做成了懶初始化(放到鎖外),這個訊息 ID 的生成涉及很多編解碼和資料複製工作,實際上效能開銷相當大。
3、原來鎖內用來查位點雜湊表的 Key 是個拼接出來的字串,這次也改到鎖外先生成好。
4、順便補上了之前遺漏的關於 IPv6 的處理。
5、刪除了無用的程式碼。

F、優化訊息屬性編解碼的效能


MessageDecoder 類中的下面這段程式碼:

public static String messageProperties2String(Map<String, String> properties) {     StringBuilder sb = new StringBuilder();     if (properties != null) {         for (final Map.Entry<String, String> entry : properties.entrySet()) {             final String name = entry.getKey();             final String value = entry.getValue();             if (value == null) {                 continue;             }             sb.append(name);             sb.append(NAME_VALUE_SEPARATOR);             sb.append(value);             sb.append(PROPERTY_SEPARATOR);         }     }     return sb.toString(); }

如果是業務程式碼,這裡看起來似乎沒有什麼問題。但在 TPS 很高的場景下, StringBuilder 預設長度是 16,處理一個正常的訊息,至少會內部擴充套件 2 次,白白產生 2 個物件和 2 次陣列複製。所以優化方案就是先算好需要的長度,建立 StringBuffer 的時候直接就指定好。


這個類中的 string2messageProperties 也進行了優化,用自己的解析代替了 split 呼叫。通過 jmh 進行一下測試,結果如下:


可以看出有了很大的提高。


關於訊息屬性,之前的程式還有一個問題是把一些不需要的屬性也寫到了 CommitLog 裡面(或者也可以說是把不相關的東西放到了訊息屬性裡面)。比如 wait=true 這個屬性,實際上是在訊息處理過程中才用的,不需要持久化,所以這次就想辦法把它從 CommitLog 中刪掉了。遺憾的是沒有一個統一的地方可以一勞永逸的刪掉這個屬性,本次只針對普通訊息進行了刪除。刪掉這個屬性,每個訊息的儲存佔用會減少 10 個位元組,對於小訊息來說,還是挺可觀的。


G、優化訊息 Header 解析的效能


RocketMQ 的通訊協議定義了各種指令,它們的 Header 各不相同,共用了一個通用的解析方法,基於反射來解析和設定訊息 Header。

這裡簡單的針對訊息生產的指令,不再使用共同的這個解析器,而是簡單粗暴的直接一個一個去 set 每一個屬性,這樣這個方法獲得了大約 4 倍效能的提升。

效能測試


現在,我們針對本次優化的成果,進行一次分散式的效能測試。


我們使用 2 臺物理機部署為 Master/Slaver 模式,同步複製,非同步刷盤,其它引數均用預設,分割槽數設定為 18。然後用另外 6 臺伺服器作為 client 同時生產和消費,每個生產者啟動 100 個執行緒同步傳送,訊息體約 300 位元組。


伺服器硬體配置為 2*Xeon(R) Gold 5218,一共 32 核心 64 執行緒,128G 記憶體,Nvme SSD,client 和 server 的 ping 延遲是 0.06ms。


我們還派出了一個神祕的參賽選手,最終待測試的版本包括以下 4 個:

A、4.9.0 版本,使用預設引數。
B、4.9.0 版本,按上面的修改 D 進行引數優化。
C、4.9.1 版本,預設引數。
D、快手內部某版本。


結果如下:


即使按進行過引數優化的 4.9.0 版本作為基線,4.9.1 版本也勝出了 28%,快手內部版本則勝出了 40%。


需要說明的是:

1、由於 OS 虛擬記憶體管理是個很複雜的機制,寫 mmap 的 Byte Buffer 的速度也會存在抖動,所以測試的結果也存在波動。
2、核心引數會對OS的記憶體效能有很大影響,不同硬體、核心可能會有不同的表現,RocketMQ/bin 目錄下的 os.sh 可以作為一個核心引數調整的參考。
3、Nvme SSD 不會是效能瓶頸所在,通過在一個物理機上安裝多個 Broker(改一下埠號和檔案儲存路徑),可以進一步提升 TPS,比如在本次測試的場景下,還是這兩臺物理機,4.9.1 版本每個物理機上 4 個 Broker 混布可以把總 TPS 提升到 60 多萬。
4、壓測的時候 RocketMQ 自身的 benchmark 程式自己也會存在瓶頸,需要多例項執行得出 Broker 的效能,本次測試沒有使用使用這個程式。


總結


效能優化是個長期工作,本批次的優化主要集中在 Broker 的訊息生產鏈路。其他地方也有很多可以優化的點,包括:

  • 消費鏈路
  • Client 的物件建立、資料複製、執行緒切換等
  • 網路通訊和序列化
  • benchmark 程式
即便是生產鏈路也還有很多可以繼續優化的地方,我們會繼續推進這個工作,也歡迎大家一起來貢獻。


作者介紹:
(1)黃理,當前就職於快手,架構師,Apache RocketMQ Commiter,多年 Java 架構和開發經驗,個人技術愛好是效能優化方向。
(2)胡宗棠,當前就職於中國移動雲能力中心,雲原生領域技術專家,Apache RocketMQ Committer,SOFAJRaft Committer,Alibaba/Nacos Committer,熟悉分散式訊息佇列、API 閘道器和分散式事務等中介軟體設計原理、架構以及各種應用場景,具有豐富高效能、高可用和高併發經驗;

加入 Apache RocketMQ 社群


十年鑄劍,Apache RocketMQ 的成長離不開全球接近 500 位開發者的積極參與貢獻,相信在下個版本你就是 Apache RocketMQ 的貢獻者,在社群不僅可以結識社群大牛,提升技術水平,也可以提升個人影響力,促進自身成長。

社群 5.0 版本正在進行著如火如荼的開發,另外還有接近 30 個 SIG(興趣小組)等你加入,新增社群開發者微信:rocketmq666 即可進群,參與貢獻,打造下一代訊息、事件、流超融合處理平臺。

另外還可以加入釘釘群與rocketmq 愛好者一起廣泛討論:


本文分享自微信公眾號 - RocketMQ官微(ApacheRocketMQ)。
如有侵權,請聯絡 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。