從官方這邊獲悉,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實現訊息的非同步傳送。
核心步驟解讀:
- 訊息傳送執行緒呼叫Commitlog的aysncPutMessage方法寫入訊息。
- Commitlog呼叫submitReplicaRequest方法,將任務提交到GroupTransferService中,並獲取一個Future,實現非同步程式設計。值得注意的是這裡需要等待,待資料成功寫入從節點(內部基於CompletableFuture機制的內部執行緒池ForkJoin)。
- GroupTransferService中對提交的任務依次進行判斷,判斷對應的請求是否已同步到從節點。
- 如果已經複製到從節點,則通過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、調整訊息傳送相關的引數
-
sendMessageThreadPoolNums
Broker端訊息傳送端執行緒池數量,該值在4.9.0版本之前預設為1,新版本調整為作業系統的CPU核數,並且不小於4。
-
useReentrantLockWhenPutMessage
MQ訊息寫入時對記憶體加鎖使用的鎖型別,低版本之前預設為false,表示預設使用自旋鎖;新版本使用ReentrantLock。
自旋主要的優勢是沒有執行緒切換成本,但自旋容易造成CPU的浪費,記憶體寫入大部分情況下是很快,但RocketMQ比較依賴頁快取,如果出現也快取抖動,帶來的CPU浪費是非常不值得,在sendMessageThreadPoolNums設定超過1之後,鎖的型別使用ReentrantLock更加穩定。 -
flushCommitLogTimed
首先我們通過觀察原始碼瞭解一下該引數的含義:
其主要作用是控制刷盤執行緒阻塞等待的方式,低版本flushCommitLogTimed為false,預設使用CountDownLatch,而高版本則直接使用Thread.sleep。猜想的原因是刷盤執行緒比較獨立,無需與其他執行緒進行直接的互動協作,故無需使用CountDownLatch這種專門用來執行緒協作的“外來和尚”。
-
endTransactionThreadPoolNums
主要用於設定事務訊息執行緒池的大小。
新版本主要是可通過調整傳送執行緒池來動態調節事務訊息的值,這個大家可以根據壓測結果動態調整。
文章首發:https://www.codingw.net/posts/fbea8b3.html
一鍵三連(關注、點贊、留言)是對我最大的鼓勵。
掌握一到兩門java主流中介軟體,是敲開BAT等大廠必備的技能,送給大家一個Java中介軟體學習路線,助力大家早日進入網際網路大廠。
最後分享筆者一個硬核的RocketMQ電子書,您將獲得千億級訊息流轉的運維經驗。
獲取方式:RocketMQ電子書。