RocketMQ這樣做,壓測後性能提高30%

從官方這邊獲悉,RocketMQ在4.9.1版本中對消息發送進行了大量的優化,性能提升十分顯著,接下來請跟着我一起來欣賞大神們的傑作。

根據RocketMQ4.9.1的更新日誌,我們從中提取到關於消息發送性能優化的Issues:2883,具體優化點如截圖所示:

首先先嚐試對上述優化點做一個簡單的介紹:

  • 對WaitNotifyObject的鎖進行優化(item2)
  • 移除HAService中的鎖(item3)
  • 移除GroupCommitService中的鎖(item4)
  • 消除HA中不必要的數組拷貝(item5)
  • 調整消息發送幾個參數的默認值(item7)
    • sendMessageThreadPoolNums
    • useReentrantLockWhenPutMessage
    • flushCommitLogTimed
    • endTransactionThreadPoolNums
  • 減少瑣的作用範圍(item8-12)

接下來我們逐一來看看其優化點,並簡單加以分析。

通過閱讀相關的變更,優化手段主要包括:

  • 移除不必要的鎖
  • 降低鎖粒度(範圍)
  • 修改消息發送相關參數

接下來根據上述手段,從中挑選具有代表性功能進行詳細剖析,一起領悟Java高併發編程。

1、移除不必要的鎖

本次性能優化,主要針對的是RocketMQ同步複製場景。

我們首先先來簡單介紹一下RocketMQ主從同步在編程方面的技巧。

RocketMQ主節點將消息寫入內存後, 如果採用的是同步複製,需要等待從節點成功寫入後才能向消息發送客戶端返回成功,在代碼編寫方面也極具技巧性,其序列圖入下圖所示:

溫馨提示:在RocketMQ4.7版本開始對消息發送進行了優化,同步消息發送模型引入了jdk的CompletableFuture實現消息的異步發送。

核心步驟解讀:

  1. 消息發送線程調用Commitlog的aysncPutMessage方法寫入消息。
  2. Commitlog調用submitReplicaRequest方法,將任務提交到GroupTransferService中,並獲取一個Future,實現異步編程。值得注意的是這裏需要等待,待數據成功寫入從節點(內部基於CompletableFuture機制的內部線程池ForkJoin)。
  3. GroupTransferService中對提交的任務依次進行判斷,判斷對應的請求是否已同步到從節點。
  4. 如果已經複製到從節點,則通過Future喚醒,並將結果返回給消息發送端。

GroupTransferService代碼如下圖所示:
在這裏插入圖片描述
爲了更加方便大家理解接下來的優化點,首先再總結提煉一下GroupTransferService的設計理念:

  • 首先引入兩個List結合,分別命名爲讀、寫鏈表。
  • 外部調用GroupTransferService的putRequest請求,將存儲在寫鏈表中(requestWrite)。
  • GroupTransferService的run方法從requestRead鏈表中獲取任務,判斷這些任務對應的請求的數據是否成功寫入到從節點。
  • 每當requestRead中沒有數據可讀時,兩個隊列進行交互,從而實現讀寫分離,降低鎖競爭

新版本的優化點主要包括:

  • 更改putRequest的鎖類型,用自旋鎖替換synchronized
  • 去除doWaitTransfer方法中多餘的鎖

1.1 使用自旋鎖替換synchronized

場景分析:正入下圖所示,GroupTransferService向外提供一個接口putRequest用來接受外部的同步任務,需要對線程不安全的ArrayList加鎖進行保護,往ArrayList中添加數據屬於一個內存操作,操作耗時小。

故這裏沒必要採取synchronized這種synchronized,而是可以自旋鎖,自旋鎖的實現非常輕量級,其實現如下圖所示:

整個鎖的實現就只需引入一個AtomicBoolean,加鎖、釋放鎖都是基於CAS操作,非常的輕量,並且自旋鎖不會發生線程切換

1.2 去除多餘的鎖

“鎖”的濫用是一個非常普遍的現象,多線程環境編程是一個非常複雜的交互過程,在編寫代碼過程中我們可能覺得自己無法預知這段代碼是否會被多個線程併發執行,爲了謹慎起見,就直接簡單粗暴的對其進行加鎖,帶來的自然是性能的損耗,這裏將該鎖去除,我們就要結合該類的調用鏈條,判斷是否需要加鎖。

整個GroupTransferService中在多線程環境中運行需要被保護的主要是requestRead與requestWrite集合,引入的鎖的目的也是確保這兩個集合在多線程環境下安全訪問,故我們首先應該梳理一下GroupTransferService的核心方法的運作流程:

doWaitTransfer方法操作的主要對象是requestRead鏈表,而且該方法只會被GroupTransferService線程調用,並且requestRead中方法會在swapRequest中被修改,但這兩個方法是串行執行,而且在同一個線程中,故無需引入鎖,該鎖可以移除。

但由於該鎖被移除,在swapRequests中進行加鎖,因爲requestWrite這個隊列會被多個線程訪問,優化後的代碼如下:

從這個角度來看,其實主要是將鎖的類型由synchronized替換爲更加輕量的自旋鎖。

2、降低鎖的範圍

被鎖包裹的代碼塊是串行執行,即無法併發,在無法避免鎖的情況下,降低鎖的代碼塊,能有效提高併發度,圖解如下:

如果多個線程區訪問lock1,lock2,在lock1中domSomeThing1、domSomeThing2這兩個方法都必須串行執行,而多個線程同時訪問lock2方法,doSomeThing1能被多個線程同時執行,只有doSomething2時才需要串行執行,其整體併發效果肯定是lock2,基於這樣理論:得出一個鎖使用的最佳實踐:被鎖包裹的代碼塊越少越好

在老版本中,消息寫入加鎖的代碼塊比較大,一些可以併發執行的動作也被鎖包裹,例如生成offsetMsgId。
在這裏插入圖片描述

新版本採用函數式編程的思路,只是定義來獲取msgId的方法,在進行消息寫入時並不會執行,降低鎖的粒度,使得offsetMsgId的生成並行化,其編程手段之巧妙,值得我們學習。

3、調整消息發送相關的參數

  1. sendMessageThreadPoolNums

    Broker端消息發送端線程池數量,該值在4.9.0版本之前默認爲1,新版本調整爲操作系統的CPU核數,並且不小於4。

  2. useReentrantLockWhenPutMessage
    MQ消息寫入時對內存加鎖使用的鎖類型,低版本之前默認爲false,表示默認使用自旋鎖;新版本使用ReentrantLock。
    自旋主要的優勢是沒有線程切換成本,但自旋容易造成CPU的浪費,內存寫入大部分情況下是很快,但RocketMQ比較依賴頁緩存,如果出現也緩存抖動,帶來的CPU浪費是非常不值得,在sendMessageThreadPoolNums設置超過1之後,鎖的類型使用ReentrantLock更加穩定。

  3. flushCommitLogTimed
    首先我們通過觀察源碼瞭解一下該參數的含義:

    其主要作用是控制刷盤線程阻塞等待的方式,低版本flushCommitLogTimed爲false,默認使用CountDownLatch,而高版本則直接使用Thread.sleep。猜想的原因是刷盤線程比較獨立,無需與其他線程進行直接的交互協作,故無需使用CountDownLatch這種專門用來線程協作的“外來和尚”。

  4. endTransactionThreadPoolNums

    主要用於設置事務消息線程池的大小。

    新版本主要是可通過調整發送線程池來動態調節事務消息的值,這個大家可以根據壓測結果動態調整。

文章首發:https://www.codingw.net/posts/fbea8b3.html

一鍵三連(關注、點贊、留言)是對我最大的鼓勵

掌握一到兩門java主流中間件,是敲開BAT等大廠必備的技能,送給大家一個Java中間件學習路線,助力大家早日進入互聯網大廠。

Java進階之梯,成長路線與學習資料,助力突破中間件領域

最後分享筆者一個硬核的RocketMQ電子書,您將獲得千億級消息流轉的運維經驗。
在這裏插入圖片描述
獲取方式:RocketMQ電子書

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