RocketMq源碼隨筆-高可用HA

RocketMq源碼隨筆-高可用HA

引言

RocketMq在部署的時候對高可用的考慮有兩種模式:一種是消息數據的複製,一種是基於選擇的主節點確定(PS:2021-1-10尚未確定,這部分代碼未看)。

下文是對複製模式的代碼隨筆解讀。

歡迎加入技術交流羣186233599討論交流,也歡迎關注技術公衆號:風火說。

<!--more-->

HAService

putRequest

委託給方法HAService.GroupTransferService#putRequest去實現。

notifyTransferSome

該方法主要是爲了對GroupTransferService中的等待線程執行喚醒。通過CAS操作,將入參的offset的值嘗試寫入push2SlaveMaxOffset,前提是入參的值大於當前值。

一旦CAS成功,則執行方法HAService.GroupTransferService#notifyTransferSome喚醒在GroupTransferService上等待的線程。

如果在循環CAS嘗試中,發現入參的值已經小於等於push2SlaveMaxOffset,則放棄循環,退出方法。

GroupTransferService

在HaService初始化的時候被實例化。類本身繼承了ServiceThread,也是一個後臺運行的線程。在HaService執行start方法的時候被啓動。

GroupTransferService中有讀寫兩個隊列。

putRequest會將請求放入寫隊列中,並且喚醒線程。而線程被喚醒後,會將讀寫隊列互換,這樣就得到了一個只有數據可以被讀取的讀隊列。

遍歷讀隊列的內容,爲每一個元素都執行如下邏輯:

  • 判斷屬性HAService#push2SlaveMaxOffset是否大於等於CommitLog.GroupCommitRequest#nextOffset,將結果聲明爲 transferOK。顯然,前者大於等於後者的時候,意味着數據都已經傳輸到從節點完畢了。
  • 將當前時間加上config.MessageStoreConfig#syncFlushTimeout生命爲waitUntilWhen。
  • 如果transferOK爲false且當前時間未到達waitUntilWhen,這反覆調用方法WaitNotifyObject#waitForRunning執行等待,並且重新爲transferOK賦值。賦值後繼續該流程直到超時或者transferOK爲true。
  • 調用方法CommitLog.GroupCommitRequest#wakeupCustomer喚醒在request上等待的線程。

遍歷結束後,清空讀隊列。

從代碼的內容上來看,這個類本身和數據的同步無關,僅僅起到了等待的作用。

AcceptSocketService

這個類在HaService初始化的時候被實例化,類本身繼承了ServiceThread,也是一個後臺運行的線程。在HaService執行start方法的時候被啓動。

這個類在初始化的時候會使用屬性config.MessageStoreConfig#haListenPort作爲監聽端口,用於監聽從節點接入的請求。

在HaService調用start方法啓動的時候,也會調用本類的beginAccept方法。在這個方法中就會使用NIO的接口,打開一個ServerChannel,並且在對應的端口上監聽接入請求。

run

run方法的實現就是在一個循環中監聽服務通道的accept事件,當有客戶端接入的時候,就包裝爲一個HaConnection對象,並且執行其start方法。

與此同時,將該HaConnection對象加入到HAService#connectionList屬性中。

HaConnection

使用HaService和SocketChannel作爲入參進行初始化。

初始化的時候會對屬性HAService#connectionCount執行加1操作。

然後對SocketChannel進行一系列的基本屬性設置,主要是將其設置爲非阻塞模式,並且後續會使用NIO的接口對這個通道進行讀寫。

HaConnection內部定義了兩個分別用於讀寫的線程對通道上的數據進行處理,分別是WriteSocketService和ReadSocketService。兩個類均繼承了ServiceThread,並且在HaConection的start方法中被啓動。

ReadSocketService

該類會創建一個Selector,監聽給定通道上的讀事件。

該類定義了幾個私有屬性:

  • byteBufferRead,用於在通道上讀取數據。
  • processPosition,用於記錄當前已經處理的數據的偏移量。
  • lastReadTimestamp,用於記錄該通道上最後一次讀取到數據的時間。

該類的run方法的實現邏輯很簡單,在一個死循環中,在selector上執行超時等待。當selector.select方法返回的時候,調用方法processReadEvent處理可能讀取到的數據。

當處理讀取數據錯誤(通道關閉或者發生異常)或者當前時間與上次數據讀取差距過大時(客戶端超時),都會終止循環。

終止循環後會執行以下操作:

  • 調用自身和writeSocketService的makeStop方法,設置停止標誌位。
  • 調用方法HAService#removeConnection將連接刪除。
  • 屬性HAService#connectionCount減1.
  • 執行選擇器取消,通道關閉等操作。

processReadEvent

該方法用於處理在通道上讀取到的數據。首先是聲明瞭一個局部變量readSizeZeroTimes,該變量用於記錄滅有讀取到數據的次數。也就是說在一定次數讀取不到數據的時候,該方法就會結束。這種設計用於盡最大努力來一次性讀取數據。

方法的整體邏輯如下

  • 調用方法java.nio.Buffer#hasRemaining確認byteBufferRead是否沒有剩餘數據。如果沒有剩餘數據,則執行方法java.nio.Buffer#flip恢復到初始可寫狀態。並且將processPosition重新賦值爲0.
  • 執行循環判斷,判斷條件爲java.nio.Buffer#hasRemaining是否爲true。循環內部邏輯如下
    • 執行方法java.nio.channels.SocketChannel#read(java.nio.ByteBuffer)讀取數據到byteBufferRead,並且將讀取到的字節數聲明爲readSize。
    • 如果readSize爲0,則readSizeZeroTimes加1。如果readSizeZeroTimes大於等於3,則結束循環。否則繼續循環。
    • 如果readSize爲-1,則意味着通道關閉,返回false給方法調用者。
    • 如果readSize大於0,則意味着有數據可以讀取並且處理。處理邏輯如下:
      • 將readSizeZeroTimes重新賦值爲0.
      • 將lastReadTimestamp賦值爲當前時間。
      • 如果byteBufferRead的position與processPosition差值小於8則開始下一次循環。否則繼續後續流程。
      • 使用byteBufferRead的position求取最接近其值的8的整倍數的值,聲明爲pos。
      • 使用byteBufferRead,在pos-8的位置讀取一個long變量,聲明爲readOffset。
      • 將processPosition賦值爲pos。
      • 將屬性HAConnection#slaveAckOffset賦值爲readOffset。
      • 如果屬性HAConnection#slaveRequestOffset小於0,則將其賦值爲readOffset.
      • 調用方法HAService#notifyTransferSome將slaveAckOffset作爲參數傳入。這將喚醒在外部等待數據同步的線程。

WriteSocketService

這個類用於向通道中寫入當前commitlog的數據。

對象在初始化的時候會聲明幾個屬性:

  • byteBufferHeader,固定爲12字節大小。前8字節是個long整型數字,代表傳輸偏移量;後4個字節爲int整型數字,代表本次傳輸的內容體長度。
  • nextTransferFromWhere,下一次傳輸的內容在文件上的偏移量。
  • selectMappedBufferResult,根據傳輸偏移量,對選中的文件傳輸區域的包裝對象。
  • lastWriteOver,上一次socket寫出數據是否完畢
  • lastWriteTimestamp,上一次數據寫出時間。

這個類的處理邏輯都在其run方法中。下面來看run方法的具體內容。

run方法的一開始是一個while死循環,用於不斷的等待寫事件的觸發。循環的處理邏輯如下

  • 在selector上執行select超時等待,超時時間爲1秒。方法返回後進入後續流程。
  • 判讀屬性HAConnection#slaveRequestOffset是否等於-1,如果是的話,休眠10毫秒,回頭循環開始。否則進入後續流程。
  • 判斷屬性HAConnection.WriteSocketService#nextTransferFromWhere是否等於-1。等於-1意味着之前沒有傳輸過,需要首先確定從哪一個位置開始啓動傳輸,也就是子流程的作用。如果不等於-1,則直接從上次的位置開始啓動傳輸即可。
    • 判斷屬性HAConnection#slaveRequestOffset是否等於0.如果不是的話,將屬性HAConnection.WriteSocketService#nextTransferFromWhere賦值爲HAConnection#slaveRequestOffset。如果是的話,進入子流程。
      • 調用方法CommitLog#getMaxOffset獲取當前文件上寫入值的最大偏移量。這個偏移量寫入到文件中內容的偏移量。不是刷盤的偏移量。將該偏移量生命爲masterOffset。
      • 將masterOffset進行修正,將masterOffset修正到最接近mappedFileSizeCommitLog整倍數的數字。如果masterOffset小於0,則賦值爲0。
      • 將nextTransferFromWhere重新賦值爲masterOffset。
  • 判斷lastWriteOver是否爲true,代表着上一次數據是否寫出完畢。如果寫出完畢,執行子流程。
    • 獲取當前時間和lastWriteTimestamp的差,聲明爲interval。
    • 如果interval大於配置定義的心跳發送間隔,則使用byteBufferHeader組裝發送數據,偏移量是nextTransferFromWhere,內容體長度是0.調用方法transferData發送數據,並且將結果賦值給lastWriteOver。
    • 如果lastWriteOver爲false,則回到循環開始處,繼續下一次循環。
  • 如果判斷lastWriteOver爲false,則直接執行transferData發送數據,並且將結果賦值給lastWriteOver。如果該結果爲false,則回到循環的開始處,繼續下一次循環。
  • 方法到了這裏,就準備好發送本次要傳輸的文件內容。執行方法DefaultMessageStore#getCommitLogData,以nextTransferFromWhere爲入參,得到類型爲SelectMappedBufferResult的返回結果。結果中包含了從偏移量爲nextTransferFromWhere開始的文件可讀取內容的ByteBuffer對象。將這個結果聲明爲臨時變量selectResult。
  • 如果selectResult不爲null(只有在線程關閉或者偏移量超出文件本身的時候纔會是null),執行下列子流程。
    • 獲取selectResult中的byteBuffer的大小,聲明爲size。如果size大於配置config.MessageStoreConfig#haTransferBatchSize定義的值,則修正到這個值。
    • 將nextTransferFromWhere賦值給臨時變量thisOffset,nextTransferFromWhere自增size。
    • selectResult中的ByteBuffer的大小被重新設置爲size。將selectResult賦值給selectMappedBufferResult。
    • 構建傳輸頭數據,偏移量是thisOffset,也就是上一次的nextTransferFromWhere。傳輸長度是size。
    • 調用transferData方法傳輸數據,並且將結果賦值給lastWriteOver屬性。
  • 如果selectResult爲null,則獲取haService的waitNotifyObject實例,調用其allWaitForRunning方法執行等待。

以上就是整個while循環的邏輯過程。當從循環中退出的時候,就是線程結束的時候,主要是執行一些清理的動作,比如停止自身,停止讀線程,從HaService中移除當前的HaConnection,關閉通道,釋放可能沒有寫完的SelectMappedBufferResult,從HaService的waitNotifyObject中移除自身線程。

HaClient

該類主要是用於向主節點請求數據,本身定義了一些屬性用於控制同步信息。如下:

  • currentReportedOffset。向主節點彙報的當前從節點的偏移量值。在通道建立的時候被設置爲當前提交日誌的寫入偏移量;從節點更新提交日誌完成,準備彙報主節點偏移量前對該值進行更新。
  • dispatchPosition。byteBufferRead中待處理區域的起點偏移量。會在通道關閉,或者byteBufferRead被重分配的時候設置爲0;每當處理完一個消息,dispatchPosition會增加這個消息的長度。
  • lastWriteTimestamp。上一次和主節點發送消息成功的時間點。會在連接創建和向主節點彙報自身偏移量的時候被更新爲當前時間值;會在連接關閉的時候被設置爲0.

該類本身也是個後臺線程,在HaService的start方法中被啓動。來看下run方法的邏輯,如下

  • 通過方法connectMaster判斷是否已經連接上主節點。如果沒有的話,通過waitForRunning方法休眠5秒後繼續循環。
  • 調用方法isTimeToReportOffset判斷當前是否反饋從節點自身的偏移量。如果需要的話,調用方法reportSlaveMaxOffset上報當前自己的偏移量。如果上報失敗,則調用closeMaster方法關閉連接。
  • 在選擇器返回後,調用processReadEvent處理讀取數據。
  • 如果上面處理讀取數據的方法返回false,調用closeMaster方法關閉通道。
  • 調用方法HAClient#reportSlaveMaxOffsetPlus彙報從節點偏移量增長。如果返回false,則回到循環開始,重新下一次循環。
  • 判斷當前時間和HAClient#lastWriteTimestamp的差距是否大於配置MessageStoreConfig#haHousekeepingInterval的值。如果大於,意味着長時間沒有數據交換,關閉當前通道。

connectMaster

如果屬性socketChannel爲null,則嘗試創建連接對象。通過屬性HAService.HAClient#masterAddress獲取主節點地址,創建通道對象,並且在選擇器上註冊這個通道的讀取事件。完成後爲屬性HAService.HAClient#currentReportedOffset賦值,其值通過方法DefaultMessageStore#getMaxPhyOffset來獲得。將lastWriteTimestamp賦值爲當前時間。

返回通道對象是否爲null給調用者。

isTimeToReportOffset

獲取當前時間與lastWriteTimestamp的差距。如果這個差距大於心跳發送間隔,則返回true給調用者。說明此時需要發送心跳信息。

reportSlaveMaxOffset

清空reportOffset,在reportOffset中寫入long整型數字,也就是當前從節點的偏移量。

嘗試將reportOffset的內容寫入通道中,最多嘗試3次。如果在寫入通道過程中出現異常,則返回false。

將lastWriteTimestamp賦值爲當前時間。

返回reportOffset是否已經完全寫入的結果給調用者。

closeMaster

關閉連接,取消當前通道在選擇器上的註冊,將屬性socketChannel設置爲null。將lastWriteTimestamp和dispatchPosition都重置爲0.

processReadEvent

使用byteBufferRead從通道中讀取數據。如果讀取到了數據,就調用方法dispatchReadRequest進行處理。如果沒有讀取到數據,則嘗試再次讀取並且爲當前進行計數。連續三次從通道讀取不到數據,則結束方法。否則嘗試不斷讀取數據。

dispatchReadRequest

該方法實現了對讀取到的數據進行處理的邏輯。總體上來說,是在一個循環中不斷讀取數據並且實現處理。下面我們看下一個循環中的處理邏輯,如下:

  • 判斷byteBufferRead的位置與屬性dispatchPosition的差值是否大於等於12,因爲12是主節點一次發送消息的最小長度。如果沒有的話,就需要進行數據整理,然後可以退出循環了。下面來看下滿足12的情況下,會執行的後續邏輯。
  • 在byteBufferRead的dispatchPosition位置上讀取主節點推送偏移量和本次傳輸內容體長度。分別聲明爲masterPhyOffset和bodySize。
  • 調用方法org.apache.rocketmq.store.DefaultMessageStore#getMaxPhyOffset獲取當前文件的物理偏移量,聲明爲slavePhyOffset。
  • 如果salvephyOffset不爲0,則判斷其是否與masterPhyOffset相等。如果不等的話,就說明主節點要推送的數據與從節點需要接收的數據有偏差,終止流程,並且返回false給調用者。
  • 如果第一步求取的差值大於等於消息頭部和消息體的長度,意味着本次讀取中獲得了一個完整的消息。此時就可以將讀取到的消息體寫入到從節點的本地文件中。
    • 從byteBufferRead中讀取消息內容體,聲明爲bodyData。
    • 調用方法org.apache.rocketmq.store.DefaultMessageStore#appendToCommitLog,以slavePhyOffset和bodyData作爲入參,將數據寫入到提交文件。
    • dispatchPosition增加本次消息的總體長度。
    • 調用方法reportSlaveMaxOffsetPlus上報從節點偏移量新增。如果方法返回false,則結束整個方法,並且返回false。否則繼續流程。
    • 回到循環最開始,執行下一輪循環。
  • 如果第一步的判斷中,可以讀取的內容長度小於12或者本次沒有讀取到完整的消息體,都會走到這一步。判斷byteBufferRead是否有剩餘,如果沒有的話,執行方法reallocateByteBuffer對byteReadBuffer進行整理。
  • 退出當前循環。
  • 返回true到方法調用者。

reportSlaveMaxOffsetPlus

該方法用於彙報從節點的偏移量增長。

  • 通過方法org.apache.rocketmq.store.DefaultMessageStore#getMaxPhyOffset獲取當前的偏移量。聲明爲currentPhyOffset。
  • 如果currentPhyOffset大於屬性currentReportedOffset,則執行子流程邏輯。
    • 爲currentReportedOffset賦值爲currentPhyOffset。
    • 調用方法HAClient#reportSlaveMaxOffset彙報從節點偏移量。
    • 如果彙報失敗,則調用closeMaster方法關閉連接。
  • 如果彙報失敗返回false,其餘情況均返回true。

reallocateByteBuffer

RocketMq這裏代碼中對ByteBuffer的使用感覺比較奇怪,不太適應。其大致的思路是不斷向ByteBuffer中寫入數據,然後處理數據的時候要麼是從絕對偏移量開始,要麼是臨時設置postion,讀取完畢後在恢復。

當不斷從通道中讀取數據後,byteBufferRead的position最終會到達limit的位置。此時就會觸發這個方法。

這個方法會將未處理區域的開始偏移量dispatchPosition到capacity之間的部分寫入到byteBufferReadBackup中。然後讓byteBufferReadBackUp和byteBufferRead互相交換。那麼此時的byteBufferRead又是一個可以繼續寫入的Buffer了。

總結

通過對高可用中涉及到的類的代碼邏輯分析,可以大致梳理出主從角色的設計思路:

  • 主節點:通過HaConnection的ReadSocketService讀取從節點彙報上送偏移量信息。並且更新到org.apache.rocketmq.store.ha.HAConnection#slaveRequestOffset屬性。WriteSocketService會檢查這個屬性,當發現屬性變化的時候就意味着可以從這個地址開始開始進行傳輸數據給從節點。那麼WriteSocketService就會這個偏移量開始不斷傳輸當前提交日誌的內容直到所有內容都傳輸完畢。然後在不斷的循環中一旦發現提交日誌有新的內容就再次傳輸給從節點。
  • 從節點:通過HaClient在死循環中不斷上報自己的偏移量來通知到主節點當前需要同步,然後就可以不斷的接受到主節點的同步信息。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章