基於1.1.5-alpha版本,具體原始碼筆記可以參考我的github:https://github.com/saigu/JavaKnowledgeGraph/tree/master/code_reading/canal
本文將對canal的binlog訂閱模組parser進行分析。
parser模組(綠色部分)在整個系統中的角色如下圖所示,用來訂閱binlog事件,然後通過sink投遞到store.
parser模組應該來說是整個專案裡面比較複雜的模組,程式碼非常多。
因此,本文根據過程中的主線來進行展開分析,從 啟動 開始,進行分析。
如果讀者有其他相關內容不明白的,可以給我留言,我會進行解答或者根據情況再單獨寫相關內容。
模組內的類如下:
重點需要關注幾個核心問題
- 如何抓取binlog
- 對binlog訊息處理做了怎樣的效能優化
- 如何控制位點資訊
- 如何相容阿里雲RDS的高可用模式下的主備切換問題
1.從啟動進入parser主流程
前面的文章我們已經提到了,instance啟動的是,會按照順序啟動instance的各個模組
parser模組就是在這裡開始的。
這裡需要注意一下,在beforeStartEventParser方法中,啟動了parser的兩個相關元件CanalLogPositionManager 和 CanalHAController,這裡先分別介紹一下。
- CanalLogPositionManager:管理位點資訊
- CanalHAController:instance連線源資料庫的心跳檢測,並實現資料庫的HA(如果配置了standby的資料庫)
1.1 位點資訊管理CanalLogPositionManager
我們用的是default-instance.xml的配置,所以實際實現類是FailbackLogPositionManager
這裡構造器有兩個入參,一個是primary的MemoryLogPositionManager,一個是second的MetaLogPositionManager。
前者是記憶體的位點資訊,後者我們我們看一下構造器的metaManager是基於zk的位點資訊管理器。
所以FailbackLogPositionManager邏輯也比較簡單,獲取位點資訊時,先嚐試從記憶體memory中找到lastest position,如果不存在才嘗試找一下zookeeper裡的位點資訊。
1.2 心跳控制器CanalHAController
我們用的是default-instance.xml的配置,所以實際實現類是HeartBeatHAController
HeartBeatHAController裡面沒有特別複雜的邏輯,就是實現了心跳檢測成功的onSuccess方法和onFail方法。另外維護了一個CanalHASwitchable物件,一旦心跳檢測失敗超過一定次數,就執行doSwitch()進行主備切換。
前提是我們要設定了主備資料庫的連線資訊。
這裡的程式碼寫的真的是有點混亂,居然是用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類。
我們看下這個類圖的結構。
從這個結構來看,我們從上到下,就能對parser模組的主體邏輯進行抽絲剝繭了。
Let’s go!
2.1 CanalEventParser介面
定義了一個空的介面
2.2 AbstractEventParser抽象類
這個類裡面程式碼非常多,我們重點關注核心流程。
2.2.1 構造器AbstractEventParser()
構造器裡面就只做了一件事情,建立了一個EventTransactionBuffer。
EventTransactionBuffer這個類顧名思義就是一個緩衝buffer,它的作用原始碼裡的註釋也很清楚,它是緩衝event佇列,提供按事務重新整理資料的機制。
那對於這裡構造器中實現的TransactionFlushCallback的flush(List<CanalEntry.Entry> transaction) 方法,肯定就是對於事務中的一系列event,重新整理到store中。
我們可以看下consumeTheEventAndProfilingIfNecessary(transaction)方法,跟我們想的一樣,具體的sink方法放在後面的sink模組再展開分析。
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啟動。
程式碼如下
dump方法在MysqlConnection類中實現,主要就是把自己註冊到資料庫作為一個slave,然後獲取binlog變更,具體到協議我們就不展開分析了。
通過fetcher抓取到event,然後呼叫sink投遞到store。
注意,parallel為false的,是單執行緒交給sinkHandler處理,parallel為true的,交給MultiStageCoprocessor的coprocessor.publish(buffer)處理,後面展開分析下並行處理的邏輯。
注意multiStageCoprocessor在這裡publish進行寫入RingBuffer,下文會詳細講下這裡的機制。
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檔案和位置
3.事件處理優化 MultiStageCoprocessor
我們前面說過,parallel為false的,是單執行緒交給sinkHandler處理,parallel為true的,交給MultiStageCoprocessor處理。
這裡展開看看並行處理是如何實現的。
實現類是MysqlMultiStageCoprocessor, 看下基本結構,持有了EventTransactionBuffer(前文提到過的儲存事務內多個evnet的buffer)、RingBuffer<MessageEvent>、幾個執行緒池、兩個BatchEventProcessor<MessageEvent>。
這些屬性型別基本是跟Disruptor框架相關。
start()方法裡面對一系列屬性做了初始化配置並進行啟動,要理解這裡的邏輯,其實主要是使用Disruptor框架做的任務佇列。
如果瞭解了Disruptor框架的使用,就能明白這裡所做的任務佇列處理模型了。
start()原始碼如下:
首先,這裡用了Disruptor框架的典型單生產者-多消費者模型。
這裡建立生產者的時候,就建立了RingBuffer和Sequencer,全域性唯一。
上面在dump方法內,訂閱到binlog事件後,通過multiStageCoprocessor的publish方法寫入RingBuffer,作為單一的生產者。
多消費者主要通過Disruptor的Sequencer管理。
Sequencer 介面有兩種實現,SingleProducerSequencer 和 MultiProducerSequencer,分別來處理單個生產者和多個生產者的情況,這裡使用了SingleProducerSequencer。
在 Sequencer 中有一個 next() 方法,就是這個方法來產生 Sequence 中的 value。Sequence 本質上可以認為是一個 AtomicLong,消費者和生產者都會維護自己的 Sequence。
Sequencer 的核心就是解決了兩個問題,第一個是對於所有的消費者,在 RingBuffer 為空時,就不能再從中取資料,對於生產者,新生產的內容不能把未消費的資料覆蓋掉。
上圖中 C 代表消費者,P 代表生產者。
當然,在多消費者模型中,一個關鍵的問題是控制消費者的消費順序。
這裡主要通過消費者之間控制依賴關係其實就是控制 sequence 的大小,如果說 C2 消費者 依賴 C1,那就表示 C2 中 Sequence 的值一定小於等於 C1 的 Sequence。
具體的方法是通過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…(歷史文章查閱非常方便)