「從零單排canal 07」 parser模組原始碼解析

阿丸發表於2020-08-24

基於1.1.5-alpha版本,具體原始碼筆記可以參考我的github:https://github.com/saigu/JavaKnowledgeGraph/tree/master/code_reading/canal

本文將對canal的binlog訂閱模組parser進行分析。

parser模組(綠色部分)在整個系統中的角色如下圖所示,用來訂閱binlog事件,然後通過sink投遞到store.

「從零單排canal 07」 parser模組原始碼解析

 

parser模組應該來說是整個專案裡面比較複雜的模組,程式碼非常多。

因此,本文根據過程中的主線來進行展開分析,從 啟動 開始,進行分析。

如果讀者有其他相關內容不明白的,可以給我留言,我會進行解答或者根據情況再單獨寫相關內容。

模組內的類如下:

「從零單排canal 07」 parser模組原始碼解析

 

重點需要關注幾個核心問題

  • 如何抓取binlog
  • 對binlog訊息處理做了怎樣的效能優化
  • 如何控制位點資訊
  • 如何相容阿里雲RDS的高可用模式下的主備切換問題

1.從啟動進入parser主流程

前面的文章我們已經提到了,instance啟動的是,會按照順序啟動instance的各個模組

「從零單排canal 07」 parser模組原始碼解析

 

parser模組就是在這裡開始的。

這裡需要注意一下,在beforeStartEventParser方法中,啟動了parser的兩個相關元件CanalLogPositionManager 和 CanalHAController,這裡先分別介紹一下。

  • CanalLogPositionManager:管理位點資訊
  • CanalHAController:instance連線源資料庫的心跳檢測,並實現資料庫的HA(如果配置了standby的資料庫)

1.1 位點資訊管理CanalLogPositionManager

我們用的是default-instance.xml的配置,所以實際實現類是FailbackLogPositionManager

「從零單排canal 07」 parser模組原始碼解析

 

這裡構造器有兩個入參,一個是primary的MemoryLogPositionManager,一個是second的MetaLogPositionManager。

前者是記憶體的位點資訊,後者我們我們看一下構造器的metaManager是基於zk的位點資訊管理器。

「從零單排canal 07」 parser模組原始碼解析

 

所以FailbackLogPositionManager邏輯也比較簡單,獲取位點資訊時,先嚐試從記憶體memory中找到lastest position,如果不存在才嘗試找一下zookeeper裡的位點資訊。

「從零單排canal 07」 parser模組原始碼解析

 

1.2 心跳控制器CanalHAController

我們用的是default-instance.xml的配置,所以實際實現類是HeartBeatHAController

「從零單排canal 07」 parser模組原始碼解析

 

HeartBeatHAController裡面沒有特別複雜的邏輯,就是實現了心跳檢測成功的onSuccess方法和onFail方法。另外維護了一個CanalHASwitchable物件,一旦心跳檢測失敗超過一定次數,就執行doSwitch()進行主備切換。

「從零單排canal 07」 parser模組原始碼解析

 

前提是我們要設定了主備資料庫的連線資訊。

這裡的程式碼寫的真的是有點混亂,居然是用MysqlEventParser實現了這個doSwitch()方法。

另外,在MysqlEventParser中,寫了一個MysqlDetectingTimeTask內部類,整合了TimerTask來做定時心跳檢測。

  • 定時去連線資料庫,可以通過select\desc\show\explain等方法做存活檢測
  • 如果檢測成功,就呼叫HeartBeatHAController的onSuccess方法
  • 如果失敗,就HeartBeatHAController的onFail方法
  • 如果失敗超過一定次數,onFail方法中就會呼叫doSwitch方法進行主備切換

2.核心邏輯

從parser.start()進去後,我們就來到了parser的核心。

從default-instance.xml配置檔案看,預設的parser實現是從base-instance.xml的baseEventParser來的,用的是RdsBinlogEventParserProxy類。

「從零單排canal 07」 parser模組原始碼解析

 

我們看下這個類圖的結構。

「從零單排canal 07」 parser模組原始碼解析

 


從這個結構來看,我們從上到下,就能對parser模組的主體邏輯進行抽絲剝繭了。

Let’s go!

2.1 CanalEventParser介面

定義了一個空的介面

2.2 AbstractEventParser抽象類

這個類裡面程式碼非常多,我們重點關注核心流程。

2.2.1 構造器AbstractEventParser()

構造器裡面就只做了一件事情,建立了一個EventTransactionBuffer。

「從零單排canal 07」 parser模組原始碼解析

 

EventTransactionBuffer這個類顧名思義就是一個緩衝buffer,它的作用原始碼裡的註釋也很清楚,它是緩衝event佇列,提供按事務重新整理資料的機制。

那對於這裡構造器中實現的TransactionFlushCallback的flush(List<CanalEntry.Entry> transaction) 方法,肯定就是對於事務中的一系列event,重新整理到store中。

我們可以看下consumeTheEventAndProfilingIfNecessary(transaction)方法,跟我們想的一樣,具體的sink方法放在後面的sink模組再展開分析。

「從零單排canal 07」 parser模組原始碼解析

 

2.2.2 主幹的start()方法

主要做了這些事情:

  • 初始化緩衝佇列transactionBuffer
  • 初始化binlogParser
  • 啟動一個新的執行緒進行核心工作
    • 構造Erosa連線ErosaConnection
    • 利用ErosaConnection啟動一個心跳執行緒
    • 執行dump前的準備工作,檢視資料庫的binlog_format和binlog_row_image,準備一下DatabaseTableMeta
    • findStartPosition獲取最後的位置資訊(挺重要的,具體實現在MysqlEventParser)
    • 構建一個sinkHandler,實現具體的sink邏輯(我們可以看到,裡面就是把單個event事件寫入到transactionBuffer中)
    • 開始dump過程,預設是parallel處理的,需要構建一個MultiStageCoprocessor;如果不是parallel,就直接用sinkHandler處理。內部while不斷迴圈,根據是否parallel,選擇MultiStageCoprocessor或者sinkHandler進行投遞。
  • 如果有異常丟擲,那麼根據異常型別做相關處理,然後退出sink消費,釋放一下狀態,sleep一段時間後重新開始

程式碼很長,邏輯比較清晰,就不貼了。

2.2.3 核心dump過程

dump過程預設是parallel處理的,需要構建一個MultiStageCoprocessor;如果不是parallel,就直接用sinkHandler處理。內部while不斷迴圈,根據是否parallel,選擇MultiStageCoprocessor或者sinkHandler進行投遞。

注意multiStageCoprocessor在這裡start啟動。

程式碼如下

「從零單排canal 07」 parser模組原始碼解析

 

dump方法在MysqlConnection類中實現,主要就是把自己註冊到資料庫作為一個slave,然後獲取binlog變更,具體到協議我們就不展開分析了。

通過fetcher抓取到event,然後呼叫sink投遞到store。

注意,parallel為false的,是單執行緒交給sinkHandler處理,parallel為true的,交給MultiStageCoprocessor的coprocessor.publish(buffer)處理,後面展開分析下並行處理的邏輯。

注意multiStageCoprocessor在這裡publish進行寫入RingBuffer,下文會詳細講下這裡的機制。

「從零單排canal 07」 parser模組原始碼解析

 

2.3 AbstractMysqlEventParser抽象類

這個類比較簡單,就是做了根據配置做了一些物件建立和設定的工作,比如BinlogParser的構建、filter的設定等

2.4 MysqlEventParser實現類

總共有將近1000行程式碼,裡面其實程式碼組織有點混亂。像前面提到的MysqlDetectingTimeTask內部類、HeartBeatHAController的部分方法實現,都是在這個類裡面的。

那拋開這些來說,這個類的主要功能還是在處理根據journalName、position、timestamp等配置查詢對應的binlog位點。

我們選取核心流程裡面的關鍵邏輯 findStartPostion( ) 方法進行分析即可。

這個是AbstractEventParser類中start方法中呼叫的,獲取dump起始位點。

我們預設是使用 非GTID mode記錄位點資訊的,所以直接看下來看下findStartPositionInternal( ) 具體邏輯,這裡可以瞭解到如何正確配置位點資訊:

  • logPositionManager找歷史記錄
  • 如果沒有找到
    • 如果instance沒有配置canal.instance.master.journal.name
      • 如果instance配置了canal.instance.master.timestamp,就按照時間戳去binlog查詢
      • 如果沒有配置timestamp,就返回資料庫binlog最新的位點
    • 如果instance配置了canal.instance.master.journal.name
      • 如果instance配置了canal.instance.master.position,那就根據journalName和position獲取bingo位點資訊
      • 如果配置了timestamp,就用journalName + timestamp形式獲取位點資訊
  • 如果找到了歷史記錄
    • 如果歷史記錄的連線資訊和當前連線資訊一致,那麼判斷下是否有異常,沒有異常就直接返回
    • 如果歷史記錄的連線資訊和當前連線資訊不一致,說明可能發生主備切換,就把歷史記錄的時間戳回退一分鐘,重新查詢

這裡是純if else 流程程式碼,挺長的,就不貼了。

在這個過程中,呼叫了幾個有意思的方法,可以瞭解一下

  • findServerId( ):查詢當前db的serverId資訊,mysql命令為 show variables like 'server_id'
  • findEndPosition():查詢當前的binlog位置,mysql命令為 show master status
  • findStartPosition():查詢當前的binlog位置,mysql命令為 show binlog events limit 1
  • findSlavePosition():查詢當前的slave檢視的binlog位置,mysql命令為 show slave status

2.5 RdsBinlogEventParserProxy實現類

這個類比較簡單,就是canal為阿里雲rds定製的一個代理實現類。

主要解決了雲rds本身高可用架構下,服務端HA切換後導致的binlog位點資訊切換。

所以對於丟擲的異常做了一定的處理,相容了這種服務端HA的情況。

同時,也能滿足rds的備份檔案指定位點開始增量消費的特性。

主要過程如下

  • 如果丟擲了PositionNotFoundException異常,就委託rdsLocalBinlogEventParser進行處理
  • rdsLocalBinlogEventParser會通過下載binlog的oss備份,找到目標binlog檔案和位置
「從零單排canal 07」 parser模組原始碼解析

 

3.事件處理優化 MultiStageCoprocessor

我們前面說過,parallel為false的,是單執行緒交給sinkHandler處理,parallel為true的,交給MultiStageCoprocessor處理。

這裡展開看看並行處理是如何實現的。

實現類是MysqlMultiStageCoprocessor, 看下基本結構,持有了EventTransactionBuffer(前文提到過的儲存事務內多個evnet的buffer)、RingBuffer<MessageEvent>、幾個執行緒池、兩個BatchEventProcessor<MessageEvent>。

這些屬性型別基本是跟Disruptor框架相關。

「從零單排canal 07」 parser模組原始碼解析

 

start()方法裡面對一系列屬性做了初始化配置並進行啟動,要理解這裡的邏輯,其實主要是使用Disruptor框架做的任務佇列。

如果瞭解了Disruptor框架的使用,就能明白這裡所做的任務佇列處理模型了。

start()原始碼如下:

「從零單排canal 07」 parser模組原始碼解析

 

首先,這裡用了Disruptor框架的典型單生產者-多消費者模型。

這裡建立生產者的時候,就建立了RingBuffer和Sequencer,全域性唯一。

上面在dump方法內,訂閱到binlog事件後,通過multiStageCoprocessor的publish方法寫入RingBuffer,作為單一的生產者。

「從零單排canal 07」 parser模組原始碼解析

 

多消費者主要通過Disruptor的Sequencer管理。

Sequencer 介面有兩種實現,SingleProducerSequencer 和 MultiProducerSequencer,分別來處理單個生產者和多個生產者的情況,這裡使用了SingleProducerSequencer。

在 Sequencer 中有一個 next() 方法,就是這個方法來產生 Sequence 中的 value。Sequence 本質上可以認為是一個 AtomicLong,消費者和生產者都會維護自己的 Sequence。

Sequencer 的核心就是解決了兩個問題,第一個是對於所有的消費者,在 RingBuffer 為空時,就不能再從中取資料,對於生產者,新生產的內容不能把未消費的資料覆蓋掉。

「從零單排canal 07」 parser模組原始碼解析

 

上圖中 C 代表消費者,P 代表生產者。

當然,在多消費者模型中,一個關鍵的問題是控制消費者的消費順序。

這裡主要通過消費者之間控制依賴關係其實就是控制 sequence 的大小,如果說 C2 消費者 依賴 C1,那就表示 C2 中 Sequence 的值一定小於等於 C1 的 Sequence。

「從零單排canal 07」 parser模組原始碼解析

 

具體的方法是通過RingBuffer的addGatingSequences( )進行的。

具體Disruptor的原理和使用就不展開說明了,這裡瞭解這些關鍵問題即可。

通過這樣的程式設計模型,parser實現瞭解析器的多階段順序處理。

  • Stage1: 網路接收 (單執行緒),publish投遞到RingBuffer
  • Stage2: 從RingBuffe獲取事件,使用SimpleParserStage進行基本解析 (單執行緒,事件型別、DDL解析構造TableMeta、維護位點資訊)
  • Stage3: 事件深度解析 ,用workpool進行多執行緒, 使用DmlParserStage進行DML事件資料的完整解析
  • Stage4: SinkStoreStage單執行緒投遞到store

SimpleParserStage和SinkStoreStage使用了stageExecutor這個執行緒池進行管理,DmlParserStage使用了workpool進行管理。

這三個類都是MysqlMultiStageCoprocessor的內部類,通過實現OnEvent方法進行邏輯處理,具體處理邏輯就不展開了,大家有興趣可以看下原始碼。

4.總結

這個模組是非常核心的,涉及到了對binlog事件的抓取和處理,以及相關位點資訊的處理。

回頭看開頭幾個問題,相信也都有了答案:

  • 如何抓取binlog

dump方法在MysqlConnection類中實現,主要就是把自己註冊到資料庫作為一個slave,然後獲取binlog變更(具體的協議我們就不展開分析了)。

  • 對binlog訊息處理做了怎樣的效能優化

利用disruptor框架,基於RingBuffer實現了

單執行緒接受 -> 單執行緒解析事件 -> 多執行緒深度解析事件 -> 單執行緒投遞store 這樣的一個流程。

(這裡有點疑惑,單執行緒接受事件後,為什麼需要一個單執行緒先解析一下再多執行緒深度解析,而不是直接多執行緒深度解析?有了解的朋友可以給我留言指點一下,謝謝)

  • 如何控制位點資訊

有多種CanalLogPositionManager可以選擇。

預設採用FailbackLogPositionManager,獲取位點資訊時,先嚐試從記憶體memory中找到lastest position,如果不存在才嘗試找一下zookeeper裡的位點資訊。

  • 如何相容阿里雲RDS的高可用模式下的主備切換問題

RdsBinlogEventParserProxy如果發現了PositionNotFoundException異常,就委託rdsLocalBinlogEventParser通過下載binlog的oss備份,找到目標binlog檔案和位置。

 

都看到最後了,原創不易,點個關注,點個贊吧~
文章持續更新,可以微信搜尋「阿丸筆記 」第一時間閱讀,回覆關鍵字【學習】有我準備的一線大廠面試資料。
知識碎片重新梳理,構建Java知識圖譜:github.com/saigu/JavaK…(歷史文章查閱非常方便)

相關文章