從RocketMQ的Broker原始碼層面驗證一下這兩個點

detectiveHLH發表於2021-03-15

本篇部落格會從原始碼層面,驗證在RocketMQ基礎概念剖析,並分析一下Producer的底層原始碼中提到的結論,分別是:

  • Broker在啟動時,會將自己註冊到所有的NameServer上
  • Broker在啟動之後,會每隔30S向NameServer傳送心跳

之前的文章中,我們知道了RocketMQ中的一些核心概念,例如Broker、NameServer、TopicTag等等。Producer從啟動到傳送訊息的整個過程,從原始碼級別分析了Producer在傳送訊息到Broker的時候,是如何拿到Broker的資料的,如何從多個MessageQueue中選擇對應的Queue傳送訊息。

但是由於篇幅原因,文章開頭提到的兩個已知結論在上篇部落格裡並沒沒有對其進行驗證,這次就從原始碼層面來驗證一下。

一開頭就看到Broker主從架構相關的原始碼

在上篇部落格中提到過,Broker為了保證自身的高可用,會採取一主一從的架構。即使Master Broker因為意外原因掛了,Slave Broker上還有一份完整的資料,Broker可以繼續提供服務。

isEnableDLegerCommitLog中提到的DLeger可以先不管,我們目前只需要知道其預設返回的結果是false。所以Broker首次啟動的時候,就會執行被If包裹住的邏輯。

RocketMQ本身是有主從架構的,但是功能不夠完善,如果Master Broker出現了故障,需要人工的將Slave Broker切換成Master。

就有點類似於手動的將一臺Redis設定成另一臺Redis的Slave節點,如果此時Redis的Master掛了,還需要手動的進行切換一樣。為了解決這個問題,Redis搞出了Sentinel,可以在發生故障的時候自動的實現故障轉移。所以RocketMQ在4.5版本之後推出的Dleger差不多也是這麼個東西,除此之外,Dleger還可以實現多副本。

不使用Dleger時,主從資料如何進行同步

先給出結論,在RocketMQ的主從架構下,主從同步採取的是Slave主動拉取的方式。

如果當前執行註冊的Broker角色是Slave,那就會使用ScheduledExecutorService啟動一個週期性的定時任務,每隔10秒就會去Master同步一次,同步的資料包括Topic的相關配置、Consumer的消費偏移量、延遲訊息的Offset、訂閱組的相關資料和配置。

ScheduledExecutorService的作用和原理下面會做簡單介紹。

首次啟動時強制進行Broker註冊

因為是首次啟動,所以引數forceRegister被直接設定成了true。

使用ScheduledExecutorService啟動定時任務

通過入口進來之後,Broker會啟動一個定時任務,週期性的去註冊。ScheduledExecutorService底層就是一個newSingleThreadScheduledExecutor,只有一個執行緒的執行緒池,其關鍵的引數corePoolSize值為1,然後按照指定的頻率週期性的執行某個任務。

ScheduledExecutorService主要的功能有兩個,分別是:

  • ScheduledExecutorService 以固定的頻率執行任務
  • ScheduledExecutorService 執行完之後,間隔制定的時間後再執行下一個任務

使用scheduleAtFixedRate實現心跳機制

此處我們使用的是scheduleAtFixedRate,如下圖。

至於執行的頻率,我們能夠配置的範圍最大不能超過一分鐘,也就是說這個範圍是在10-60秒之間,預設30秒執行一次,這也就驗證了每30秒,Broker會向NameServer傳送一次心跳。

獲取執行頻率的這個判斷有點意思,甚至看起來有那麼一絲絲簡潔,但是理解其具體可配置的時間範圍可能需要花點時間。在實際業務性程式碼中,個人建議還是不要這麼寫,業務中程式碼的可讀性可維護性我認為是需要放在首位的。

值得注意的是,此處啟動心跳,給了一個10秒的延遲,因為在不使用Dleger的情況下,在之前的邏輯中已經執行過一次註冊了。如果不做延遲,那麼幾乎是同一個時間就會有兩次註冊操作,而這明顯是不符合預期的;同時forceRegister也從true變成了通過函式isForceRegister來進行獲取。

呼叫registerBrokerAll註冊

定時任務註冊完成之後,之後的每次觸發都會執行registerBrokerAll方法來執行註冊,你可能會有疑問,我當前不就是一個Broker嗎,怎麼名字有個字尾All?那是因為NameServer會有多個,Broker啟動的時候會將自己註冊到所有的NameServer上去。當然,口說無憑,我們繼續看下去。

繼續往裡走,如果當前滿足註冊條件,則會實際的執行註冊操作。那具體滿足什麼條件呢?由變數forceRegister和一個needRegister方法來決定,forceRegister預設是true,所以當第一執行這個邏輯的時候是一定會執行註冊操作的。

通過對比資料版本判斷當前Broker是否需要進行註冊

感興趣的話,可以繼續跟隨文章瞭解一下,needRegister是根據什麼來判斷是否需要註冊的。

首先,Broker一旦註冊到了NameServer之後,由於Producer不停的在寫入資料,Consumer也在不停的消費資料,Broker也可能因為故障導致MessageQueue等關鍵路由資訊發生變動,NameServer中的資料和Broker中實際的資料就會不一致,如果不及時更新,Producer拉取到的路由資料就可能有誤。

所以每次定時任務觸發的時候會去對比NameServer和Broker的資料,如果發現資料版本不一致,Broker會重新進行註冊,將最新的資料更新到NameServer。說直白一點,就是做一個資料定時更新。以下紅框中的程式碼就是資料對比的核心程式碼。

當Broker和所有的NameServer節點一一完成資料對比之後,就會進行結果判定,但凡有一個節點資料不一致,都需要進行重新註冊,把最新的資料更新到NameServer,核心判斷邏輯同樣用紅框標出。

至此,其實我們就已經完成了 Broker在啟動的時候會向所有NameServer進行註冊 的驗證。但是由於後續仍然有值得關注發光點,我們繼續後續的原始碼閱讀。

使用CountDownLatch獲取所有註冊非同步任務的返回結果

除此之外,還值得注意的是在needRegister中,對於和多個NameServer的互動,RocketMQ是通過執行緒池非同步實現的,同時使用了CountDownLatch來等待所有的請求結束,返回結果給主執行緒。

既然聊到了CountDownLatch,就順帶提一下。假設我們有5個互不依賴的計算任務,如果快速的計算出結果並返回呢?那當然是5個任務併發執行,這就需要通過新開執行緒實現,結果就無法一起返回了。

而CountDownLatch可以讓主執行緒等待,等待這5個計算任務全部結束之後,喚醒主執行緒再繼續後面的邏輯。這就是CountDownLatch的作用,如果平時只是單純的CRUD功能的話,可能連CountDownLatch是什麼都做不知道,這也是為什麼大廠面試會問這些問題,因為在大廠的複雜業務背景下,你必須要會使用它們。

指定需要註冊之後,接下來就是核心的註冊方法了,核心邏輯由registerBrokerAll來實現。Broker同樣會去每一個NameServer節點上註冊自己,並且為了提前執行的效率,同樣開執行緒採用了非同步的方式。在獲取所有結果時,同樣的使用了CountDownLatch。

使用CopyOnWriteArrayList儲存註冊請求的返回

除此之外,用於儲存註冊結果的列表,使用的是CopyOnWriteArrayList,被面試虐過的同學應該熟悉。我們知道此處開啟了多執行緒去不同的NameServer註冊,寫入註冊結果的時候,多執行緒對同一個列表進行寫入,會產生執行緒安全的問題。

而我們知道ArrayList是非執行緒安全的,這也是為什麼此處要使用CopyOnWriteArrayList來儲存註冊結果。為什麼CopyOnWriteArrayList能夠保證執行緒安全?

這歸功於COW(Copy On Write),讀請求時共用同一個List,涉及到寫請求時,會複製出一個List,並在寫入資料的時候加入獨佔鎖。比起直接對所有操作加鎖,讀寫鎖的形式分離了讀、寫請求,使其互不影響,只對寫請求加鎖,降低了加鎖的消耗,提升了整體操作的併發。

上面併發執行的註冊操作,具體做了哪些事情呢?先看程式碼。

上面就是單個註冊的所有邏輯,可以看到在構建完請求之後,有一個oneway的判斷。

oneway值為false,表示單向通訊,Broker不關心NameServer的返回,也不會觸發任何回撥函式。接下來Broker就會把已經寫進request body的所有資料傳送給NameServer。請求資料統一由一個叫TopicConfigSerializeWrapper的Wrapper給包裹住。

其可以看為兩部分:

  • 存在該Broker節點上的所有Topic的資料
  • 資料版本

然後帶著這些資料,Broker會同步的呼叫invokeSync傳送請求給NameServe,並且在執行之後觸發實現特定功能的回撥函式。

EOF

至此,我們完成了對開篇所提結論的驗證,同時也發現了RocketMQ的主從架構、Master和Slave同步資料的方式、心跳機制的實現等等,也基本從原始碼中看完了Broker啟動的所有流程。看這些老哥寫的原始碼還是挺有意思的,之後有時間隨緣再看看NameServer端相關的原始碼吧。

好了以上就是本篇部落格的全部內容了,如果你覺得這篇文章對你有幫助,還麻煩點個贊關個注分個享留個言

歡迎微信搜尋關注【SH的全棧筆記】,檢視更多相關文章

相關文章