「從零單排canal 05」 server模組原始碼解析

阿丸發表於2020-07-20

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

本文將對canal的server模組進行分析,跟之前一樣,我們帶著幾個問題來看原始碼:

  • CanalServer有幾種使用方式?
  • 控制檯Admin、客戶端client是如何與CanalServer互動的?
  • CanalServerWithNetty和CanalServerWithEmbedded究竟有什麼關係?
  • Canal事件消費的特色協議,非同步流式api(get/ack/rollback協議)的設計是如何實現的?

server模組內的結構如下:

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

主要分為了三個包:

  • admin包:

這個包的CanalAdmin介面定義了canalServer上暴露給canal-admin控制檯使用的一些服務介面。

上一篇deployer模組解析中提到的CanalAdminController就是實現了CanalAdmin介面(把這個介面的實現放在deployer模組是挺奇怪的)。 Admin包中使用了netty作為服務端(CanalAdminWithNetty類中實現),接受控制檯Admin的請求,返回當前canalServer的一些執行狀態。

  • server包:

server模組的核心包,本文重點解析的部分,需要了解CanalServerWithEmbedded 和CanalServerWithNetty。

  • spi包:

定義了canalServer的監控內容 通過spi實現,比如專案中的Prometheus子模組實現了監控能力,我們不展開分析。

1.從CanalServer的架構說起

CanalServer目前支援兩種模式:

  • serverMode = tcp的Server-Client模式
  • serverMode = kafak 或 rocketMQ 的 Server-MQ-Client模式

為了大家能充分理解canalServer的結構,這裡精心製作了一個canalServer的架構圖(如果覺得這圖不錯,給本文點個贊吧)。

1.1 Server-Client模式

架構如圖所示:

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

我們可以清楚的看到Server模組中各個模組的關係與能力:

  • CanalServerWithEmbedde維護了具體的instance任務,負責對binlog進行訂閱、過濾、快取,就是之前的文章介紹過的parser-sink-store的方式。
  • CanalServerWithNetty作為服務端,接收CanalClient的請求,將binlog的訊息傳送給client。
  • CanalAdminWithNetty作為admin的伺服器,接收控制檯Admin的控制操作、查詢狀態操作等,啟停或顯示當前CanalServer以及instance的狀態。

1.2 Server-MQ-Client模式

架構如圖所示:

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

主體部分與Server-client模式一致,主要區別如下:

  • 不需要CanalServerWithNetty,改為CanalMQProducer投遞訊息給訊息佇列
  • 不使用CanalClient,改為MqClient獲取訊息佇列的訊息進行消費

這種模式相比於Server-client模式

  • 下游解耦,利用訊息佇列的特性,可以支援多個客戶端廣播消費、叢集消費、重複消費等
  • 會增加系統的複雜度,增加一些延遲

具體模式的選擇,需要根據具體的使用場景來決定。

2.server包

admin包和spi包都不屬於核心邏輯,因此我們重點關注server包的程式碼。

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

我們看到,server包下面分為了embedded包、exception包、netty包和幾個介面類。

其中,最頂層的設計就要從CanalServer介面入手。

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

它的實現類有兩個,CanalServerWithEmbedded 和 CanalServerWithNetty。

它們之間的區別官方文件給了一些說明。

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

那麼,對於官方文件中提到的Embedded(嵌入式)的自主開發是怎麼使用呢?

跟我們上面提到的Server-Client模式和Server-MQ-Client模式完全不同,採用了一種無server的架構,如下圖所示。

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

我們可以看到,這種模式沒有了Canal-Server,直接在自己的應用中引入canal,然後使用CanalServerWithEmbedded進行資料抓取和訂閱。

當然,這種方式開發成本有點高,一般也不會去這樣使用。

對於CanalServerWithEmbedded 和 CanalServerWithNetty,官方文件裡面實際上沒有解釋的特別到位,只講了區別,沒有講聯絡。

這兩個實現類除了官方文件中說明的區別之外,還有很大的聯絡。

可以看看我們上文介紹的架構圖,對於Server-Client模式下的模組聯絡

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

實際上,真正的執行邏輯是在CanalServerWithEmbedded中的,CanalServerWithNetty中持有了CanalServerWithEmbedded物件,委託embedded進行相關邏輯處理,CanalServerWithNetty更多的作用是充當服務端與CanalClient進行互動。

3. CanalServerWithNetty類

下面,我們先看看CanalServerWithNetty類。

3.1 單例構建

使用 private構造器 + 靜態內部類 來實現一個單例模式,保證了一個CanalServer內部只有一個CanalServerWithNetty。

同時,我們能看到內部持有一個CanalServerWithEmbedded物件,用來處理相關請求,驗證了我們上面的說明。

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

3.2 啟動邏輯 start()

原始碼如下:

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

主要流程如下:

  • 啟動embeddedServer
  • 建立bootstrap例項,設定netty相關配置

引數NioServerSocketChannelFactory也是Netty的API,接受2個執行緒池引數,第一個執行緒池是Accept執行緒池,第二個執行緒池是woker執行緒池,Accept執行緒池接收到client連線請求後,會將代表client的物件轉發給worker執行緒池處理。這裡屬於netty的知識,不熟悉的可以暫時不必深究,簡單認為netty使用執行緒來處理客戶端的高併發請求即可。

  • 構造對應的pipeline,包括解碼處理、身份驗證、建立netty的 seesionHandler(真正處理客戶端請求,seesionHandler的實現是核心邏輯)

pipeline實際上就是netty對客戶端請求的處理器鏈,可以類比JAVA EE程式設計中Filter的責任鏈模式,上一個filter處理完成之後交給下一個filter處理,只不過在netty中,不再是filter,而是ChannelHandler。

  • 啟動netty,監聽port埠,然後客戶端對 這個埠的請求可以被接收到

對於 netty的相關知識 ,本文 不深入展開,簡單理解 為一個高效能伺服器即可,可以監聽 埠請求,並 進行相應的處理。

重點在於sessionHandler的處理。

3.3 邏輯分發SessionHandler類

canalServer的處理邏輯顯然都在sessionHandler裡面,而這個handler在構建時,傳入了embeddedServer。

前面我們提過,serverWithNetty的處理邏輯是委派給embeddedServer的,所以這裡就非常順理成章了,讓handler維護embeddedServer例項,進行邏輯處理。

sessionHandler繼承了netty的SimpleChannelHandler類,重寫了messageReceived方法,接收到不同請求後,委託embeddedServer用不同方法進行處理 。

這個方法裡面的程式碼非常冗長,而本質都是委託給embeddedServer去處理,因此,我們看下主幹邏輯即可。

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

可以看到,根據不同的packet型別,最終都是委託給embeddedServer進行處理,這裡只是做一個邏輯的判斷和分發。

3.4 CanalServerWithNetty小結

到此,我們已經瞭解了CanalServerWithNetty是如何啟動的。

並且,它的主要定位就是充當伺服器,接收客戶端的請求,然後做訊息分發,委託給CanalServerEmbedded進行處理。

下面,我們來看下CanalServerEmbedded的相關實現。

4. CanalServerEmbedded類

4.1 基本認識

  • 非完全單例模式,這裡使用public的構造器,使用者還是有機會自己new物件出來的,應用是用來獨立引入進行開發的時候使用。
  • 維護了instance的物件容器
  • 繼承了CanalServer和CanalService介面
「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

CannalServer介面其實就是就是start()和stop()方法,沒有特別的地方,主要是start()配置了一個MigrateMap.makeComputingMap,

當需要某個instance的時候,就會呼叫apply方法用instanceGenerator建立對應的instance。

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

我們重點看下CanalService介面定義的方法。

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

每個方法的入參都帶來clientIdentity,這個是客戶端的身份標示

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

目前canal只支援一個客戶端對一個instance進行訂閱,clientId全部寫死為1001,據說以後可能會支援多使用者訂閱。

瞭解CanalService定義的方法在CanalServerEmbedded中如何實現,基本也就能看清CanalServerEmbedded的全貌了。

尤其是,你能理解官網wiki中介紹的canal核心功能——非同步消費流式api(get/ack/rollback協議) 設計。

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

4.2 subscribe方法

主要步驟:

  • 根據客戶端標識clientIdentity中的destination,找到對應的instance
  • 通過instance的metaManager記錄下當前這個客戶端在訂閱
  • 通過instace的metaManage獲取當前訂閱binlog的position位置。如果是第一次訂閱,那麼metaManage沒有position資訊,就從eventStore獲取第一個binlog的position,然後更新到metaManager
  • 通知下訂閱關係變化
「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

這裡需要注意一下metaManager,這是一個介面,有多種實現方式,包括基於記憶體、基於檔案、基於記憶體+zookeeper混合、基於zookeeper等,都在meta模組中,這裡就簡單瞭解下概念即可。

  • MemoryMetaManager:位點資訊儲存在記憶體中
  • ZookeeperMetaManage:位點資訊儲存在zk上
  • PeriodMixedMetaManager:前面兩種的混合,儲存在記憶體中,然後位點資訊定期重新整理到zk上
「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

我們在叢集模式下,default-instance.xml使用的是基於PeriodMixedMetaManager的實現。

4.3 unsubscribe方法

這個方法比較簡單,就不放原始碼了。

就是找到instance對應的metaManager,然後呼叫unsubscribe方法取消這個客戶端的訂閱。

需要注意的是,取消訂閱,instance本身仍然是在執行的,可以有新的client來訂閱這個instance。

4.4 getWithoutAck方法

先解釋幾個概念。

我們用的叢集版canalServer,預設是使用PeriodMixedMetaManager來管理位點資訊,也就是MemoryMetaManager + zookeeperMetaManager。

其中,對於客戶端消費instance訊息的情況,內部維護了一個物件MemoryClientIdentityBatch進行記錄

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

回到這個方法來說,這個方法用於客戶端獲取binlog訊息,大致流程如下:

  • 根據clientIdentity的destination獲取對應的instance
  • 獲取到流式資料中的最後一批獲取的位置positionRanges(跟batchId有關聯,就是上面那個map裡面的)
  • 從cananlEventStore裡面獲取binlog,轉化為event。一般是從最後的一個batchId位置開始,如果之前沒有batchId,那麼就從cursor記錄的消費位點開始;如果cursor為空,那隻能從eventStore的第一條訊息開始。
  • event轉化為entry,並生成新的batchId,組合成message返回給客戶端

注意在eventStore獲取event的時候,使用者可以自己設定batchSize和超時時間timeout。為了儘量提高效率,一般一次獲取一批binlog,而不是獲取一條。這個批次的大小(batchSize)由客戶端指定。同時客戶端可以指定超時時間,在超時時間內,如果獲取到了batchSize的binlog,會立即返回。 如果超時了還沒有獲取到batchSize指定的binlog個數,也會立即返回。特別的,如果沒有設定超時時間,如果沒有獲取到binlog也立即返回。具體eventStore的獲取邏輯,我們下次講到這個模組再展開。

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

4.5 get方法

這個方法主要是用於客戶端獲取binlog訊息,與getWithoutAck基本一致。

主要區別在於,客戶端獲取batch後,自動ack,這樣相對來說肯定更快,但是無法保證可靠性。

在專案中看起來暫時沒有使用,我們就不展開了。

4.6 ack方法

進行 batch id 的確認。確認之後,小於等於此 batchId 的 Message 都會被確認。

  • 從metaManager中移除batchId對應的記錄
  • 記錄已經成功消費到的binlog位置,以便下一次獲取的時候可以從這個位置開始
  • 已經ack的資料,在eventStore中清除
「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

4.7 rollback

rollback有兩個方法,回滾所有和回滾指定batchId,不過從原始碼來看,目前回滾指定指定batchId也是回滾所有。

回滾的本質,就是把所有還沒ack的batchId都清空,流式api被get但是還沒ack的訊息會被重新get。

5.canalMQStarter

在第一節的架構模式中我們分析過了,在啟動過程中,如果serverMode選擇tcp,會啟動canalServerWithNetty,如果serverMode選擇了mq,就會啟動cannalMQStarter。

所以從模組組成來說,canalMQStarter跟canalServerWithNetty是比較相似的。

canalMQStarter也是委託embeddedCanal做處理,同時委託CanalMQProducer把訊息投遞到mq叢集。

canalServerWithNetty也是委託embeddedCanal做處理,然後通過netty來跟canal-client做互動。

如果我們以後應用中要內嵌embeddedCanal,完全可以參照canalMQStarter和canalServerWithNetty的模式來寫。

主要組成如下:

  • 工作執行緒池executorService,對每個instance起一個worker執行緒
  • canalMQWorks,記錄了destination(instance的標識)和worker執行緒的關係
  • CanalServerWithEmbedded
  • CanalMQProducer投遞mq訊息
「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

5.1 start方法

這個方法就是前面canalStarter類裡面的start()方法中,對CanalMQStarter.start()的呼叫。

具體做了三件事情:

  • 獲取CanalServerWithEmbedded的單例物件
  • 對應每個instance啟動一個worker執行緒CanalMQRunnable
  • 註冊ShutdownHook,退出時關閉執行緒池和mqProducer
「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

這裡主要看看CanalMQRunnable做了些什麼。

5.2 CanalMQRunnable

這是一個內部類,就是看看worker裡面做了什麼

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

只有一個worker方法,主要邏輯非常清晰:

  • 給自己建立一個身份標識,作為client
  • 根據destination獲取對應instance,如果沒有就sleep,等待產生(比如從別的server那邊HA過來一個instance)
  • 構建一個MQ的destination物件,載入相關mq的配置資訊,用作mqProducer的入參
  • 在embeddedCanal中註冊這個訂閱客戶端
  • 開始執行,並通過embededCanal進行流式get/ack/rollback協議,進行資料消費
「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

「從零單排canal 05」 server模組原始碼解析(1.1.5-sp版本)

 

6.總結

回到開頭的幾個問題,相信文中都已經做了解答。

  • CanalServer有幾種使用方式?

可以獨立部署(推薦),可以使用Server-Client模式 和 Server-MQ-Client模式兩種。

可以內嵌部署開發(embedded,難度較高)。

  • 控制檯Admin、客戶端client是如何與CanalServer互動的?

控制檯Admin通過CanalAdminWithNetty與服務端互動 客戶端client通過CanalServerWithNetty與服務端互動。

  • CanalServerWithNetty和CanalServerWithEmbedded究竟有什麼關係?

CanalServerWithEmbedded是真正核心邏輯(parser-sink-store)處理的地方 。CanalServerWithNetty持有CanalServerWithEmbedded物件,接收client的請求然後轉發給CanalServerWithEmbedded物件處理。

  • Canal事件消費的特色協議,非同步流式api(get/ack/rollback協議)的設計是如何實現的?

CanalServerWithEmbedded整合了CanalService介面,實現了具體的get/ack/rollback協議

 

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

相關文章