Flink的mysql CDC,好使不?

大資料技術前線發表於2023-11-09

來源:安瑞哥是碼農

這些天,一直都折騰,看可以透過哪些手段來導mysql表的資料來源到各個目標資料庫中,為了摸清楚當下大概有哪些匯入方式,找出我認為最靠譜,最方便的,可謂煞費苦心。


前兩篇文章分別介紹了用dataX和CK外部表的玩法,透過基於我的應用場景實踐,詳細說明了這兩種資料匯入方式誰更靠譜。


那麼今天我們再來試試用Flink的CDC方案,用它來讀取mysql表,並把資料寫入到Doris,看整個過程有哪些坑,以及是否方便、靠譜。


其實從Flink官網來看,flink讀取mysql,還有一種jdbc的方式,只不過CDC這個概念被Flink宣傳的過於火熱,我們還是決定從它先開始。



0. 環境準備


既然要用Flink CDC這個功能(至於啥是CDC,網上遍地的資料,這裡暫不解釋),那高低得看一眼CDC的官網,看看我們在使用這玩意時,需要注意哪些內容。


Flink CDC官網地址(注意:它跟Flink官網是分開的):


這個官網最大的優點就是:可以不用梯子。


由於我當前開發環境Flink用的1.15版本,所以我第一個關心的問題就是,CDC作為一個額外的flink生態元件,我該選哪個版本?


Flink的mysql CDC,好使不?

不出所料,官網給出了詳細的版本相容性列表,因此我的1.15可以選擇的CDC版本有兩個:2.3跟2.4,既然是測試,這裡我選擇最新的2.4版本。


於是,我需要在我的IDE開發環境裡,引入對應的pom依賴:


Flink的mysql CDC,好使不?


關於本次測試需要的所有元件版本,如下表所示:


元件名稱
版本
Flink
1.15.3
Flink CDC
2.4.0
mysql
5.5、8.0
Doris
2.0



1.  建立mysql資料來源


本來我的叢集有臺機器已經部署了mysql,這個mysql是CentOS7官方預設源自帶的,版本為5.5,一開始想著直接就在這個mysql上做測試得了。


但是,我簡單寫了個demo跑一下發現,當前版本的mysql對於Flink的CDC來說,太低了,丟擲瞭如下的異常:


Flink的mysql CDC,好使不?


果然,想使用這玩意,也不是誰都配的,回頭再去確認了一下官方文件,需要mysql版本不能低於5.6(那低版本的資料想用Flink同步咋整呢?可能需要試試JDBC方案)。


Flink的mysql CDC,好使不?


於是就只能去部署一個更高版本的mysql例項,於是我就果斷下了個mysql8給部署起來了。


還是拿我之前的上網日誌資料,根據資料特點先建表:


CREATE TABLE `test02` (
  `client_ip` varchar(50NOT NULL,
  `domain` varchar(100NOT NULL,
  `time` varchar(20NOT NULL,
  `target_ip` varchar(50NOT NULL,
  `rcode` int NOT NULL,
  `query_type` int NOT NULL,
  `authority_record` text,
  `add_msg` text,
  `dns_ip` varchar(50DEFAULT NULL,
  PRIMARY KEY (`client_ip`,`domain`,`time`,`target_ip`,`rcode`,`query_type`)

至於為什麼這個Primary key是這麼多的欄位組合,原因在於如果不這樣,容易造成因為主鍵重複導致資料寫入失敗。


隨後,我用load data語法,將一個本地資料檔案給匯入到該表中,在匯入中發現,即便上面這個Primary key的組合實際內容並沒有重複,但還是報了一些重複主鍵的錯誤,所以我懷疑,它這個組合鍵判斷是否重複的依據,是用的hash值


為了快速達到測試效果,我只往這張表裡先寫入了1百多萬資料:


Flink的mysql CDC,好使不?


當然,還需要在該資料庫建立一個,外部可以連線到該表的一個外部使用者,並對其賦予相關的許可權,過於簡單的內容,這裡就不贅述了。



2. 匯入有哪些坑


為了讓程式碼更加簡潔方便,這裡用Flink的SQL API,不得不說,是真香。


package com.anryg.mysql_cdc

import java.time.Duration

import org.apache.flink.contrib.streaming.state.EmbeddedRocksDBStateBackend
import org.apache.flink.streaming.api.CheckpointingMode
import org.apache.flink.streaming.api.environment.CheckpointConfig.ExternalizedCheckpointCleanup
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment

/**
  * @DESC: 用Flink CDC讀取mysql寫Doris表
  * @Auther: Anryg
  * @Date: 2023/11/6 20:02
  */

object FlinkSQLFromMysql2Doris {

    def main(args: Array[String]): Unit = {
        val env = StreamExecutionEnvironment.getExecutionEnvironment

        env.enableCheckpointing(10000L)/**這個必須加上,否則資料入不了庫*/
        //env.setParallelism(args(0).toInt)

        env.setStateBackend(new EmbeddedRocksDBStateBackend(true)) //新的設定state backend的方式
        env.getCheckpointConfig.setCheckpointStorage("hdfs://192.168.211.106:8020/tmp/flink_checkpoint/FlinkSQLFromMysql2Doris")
        env.getCheckpointConfig.setExternalizedCheckpointCleanup(ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION//設定checkpoint記錄的保留策略
        env.getCheckpointConfig.setAlignedCheckpointTimeout(Duration.ofMinutes(1L))
        env.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)

        val tableEnv = StreamTableEnvironment.create(env)


        /**第一步:讀取mysql資料來源*/
        tableEnv.executeSql(
            """
             Create table data_from_mysql(
             |`client_ip` STRING,
             |`domain` STRING,
             |`time` STRING,
             |`target_ip` STRING,
             |`rcode` int,
             |`query_type` int,
             |`authority_record` STRING,
             |`add_msg` STRING,
             |`dns_ip` STRING,
             |PRIMARY KEY(`client_ip`,`domain`,`time`,`target_ip`,`rcode`,`query_type`) NOT ENFORCED
             |)
             |with(
             |'connector' = 'mysql-cdc',
             |'hostname' = '192.168.221.173',
             | 'port' = '3306',
             | 'username' = '****',
             | 'password' = '****',
             | 'database-name' = 'test',
             | 'table-name' = 'test02'                      //確定文字資料來源的分隔符
             |)
            "
"".stripMargin)


        /**第二步:建立Doris對映表*/
        tableEnv.executeSql(
            s"""
               |CREATE TABLE data_from_flink01 (
               |`client_ip` STRING,
               |domain STRING,
               |`time` STRING,
               |target_ip STRING,
               |rcode INT,
               |query_type INT,
               |authority_record STRING,
               |add_msg STRING,
               |dns_ip STRING
               |)
               |    WITH (
               |      'connector' = 'doris',
               |      'fenodes' = '192.168.221.173:8030',
               |      'table.identifier' = 'example_db.data_from_flink01',
               |      'username' = '***',
               |      'password' = '***',
               |     'sink.label-prefix' = 'load01'  //匯入標籤字首,每次要不一樣
               |)
            "
"".stripMargin)

        /**第三步:資料寫入到Doris表中*/
        tableEnv.executeSql(
            """
              |INSERT INTO data_from_flink01
              |select
              |*
              |from
              |data_from_mysql
            "
"".stripMargin)

    }
}

程式碼邏輯雖然非常簡單,但是這玩意跑起來到底有沒有坑呢?


那必須有。


2.1 坑1


從我這些年的開發經驗來看,幾乎但凡你要往原本已經相對成熟的軟體專案中,再次引入新的pom依賴,那勢必會將原本寧靜祥和的依賴環境,掀起一場血雨腥風。


具體表現就是:新程式碼各種報錯,老程式碼也可能跑不利索。


先來看引入Flink CDC依賴之後,新程式碼拋的第一個異常:


Caused by: java.lang.ClassNotFoundException: com.google.common.collect.RangeSet
 at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
 at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
 at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
 at java.lang.ClassLoader.loadClass(ClassLoader.java:351)

說沒有找到 com.google.common.collect.RangeSet 這個類。


既然這樣,那就只能用老辦法來排除了。


先利用IDEA的類查詢功能,在當前專案瞅一眼,這個類,它到底有沒有?


Flink的mysql CDC,好使不?

不僅有,而且有3個jar包都包含這個類(1個 hive-exec 包,2個 guava 包),所以你是不是就此判斷,是因為這3個jar包衝突導致的這個報錯呢?


其實不然,要知道,這裡查詢出的結果,是當前軟體工程中,所有module的,也就是說,我們們的目標module(flink CDC所在的module),是不是真有這些jar包,還不一定,還得進一步確認。


果然一查發現,在flink cdc這個module下,hive-exec 這個包它就沒有,而 guava 的包也只有個低版本的。


Flink的mysql CDC,好使不?

那根據我的經驗判斷,應該是當前module缺少高版本的 guava 包導致的報錯(畢竟這個破包已經在我的多個專案中留下了案底,所以印象深刻),那既然是需要高版本,自然就想到要把這個低版本的guava給先排除掉。


於是,先排除低版本的guava,再引入一個高版本的guava。


Flink的mysql CDC,好使不?

幹掉低版本的,新增高版本的

PS:大家可能會覺得,這看起來挺簡單的嘛,但如果要是你在開發時遇到了這個問題,你能做到這麼頭腦清醒,思維縝密嗎


2.2 坑2


繼續啟動程式碼,又丟擲下面這個錯誤:


Flink的mysql CDC,好使不?


從錯誤提示來看,好像是某個資料格式不規範而導致的問題。


於是呢,為了確定這一點,我就新建了張表結構一樣的空表,再次啟動程式發現,還是報這個錯。


於是我開始懷疑是表欄位問題,就又開始逐個排查,從建一個欄位的表,逐漸增加到建包含所有欄位的表,居然,又都不報錯了。


然後我在新表中寫入資料,再次啟動程式,發現也正常了。


最後,我再次讓程式讀取最開始報錯的那張表,之前的錯誤居然消失了,要知道,我期間沒有做任何的修改,就離譜。


合著試一圈下來,是在玩我呢


經過多次反覆的嘗試,我很懷疑這玩意就是flink CDC的bug,偶發,沒有特定規律。



3.  CDC的寫入邏輯驗證


填完上面的坑之後,再次啟動程式,就能很順利將mysql中的資料全部匯入到Doris的目標表中了。


Flink的mysql CDC,好使不?

可以看到,已經把之前寫入到mysql中的所有資料,一條不落的都匯入過來了(對比上面mysql資料量的截圖),而且速度很快。


起初,我原本想著,既然是CDC,程式是不是隻會捕獲變化(新增)的資料才對,至於存量的歷史資料(匯入程式啟動之前就存在的資料),可能不會匯入。


但是經過驗證,Flink CDC它做到了,透過檢視程式執行的日誌可以發現,它之所以能做到這一點,跟它讀取的是binlog日誌密切相關。


Flink的mysql CDC,好使不?

CDC程式執行過程的日誌

為了進一步驗證這一點,我把mysql的binlog功能給關了試試,於是我在配置檔案中加入這個:


Flink的mysql CDC,好使不?

然後重啟mysql,結果發現很快,CDC程式就報錯了:


Flink的mysql CDC,好使不?

所以說明,mysql CDC能使用的前提是:必須要開啟binlog功能


為了進一步驗證CDC程式能感知binlog的哪些操作,我又分別對mysql的源表做了update、insert和delete操作,我們分別都來看一下,這些操作會對Doris目的表有哪些影響?


先看update:


用update對mysql源表,其中的兩條記錄的某個欄位值做個修改。


Flink的mysql CDC,好使不?

然後再來看Doris目標表的記錄數有沒有變化:


Flink的mysql CDC,好使不?

可以看到,資料如期增加了2條(因為Doris這邊建立的是Duplicate模型表,所以是新增,不是覆蓋)。


再看insert:


接下來測試了新增記錄的情況,往mysql表寫入若干條記錄,程式能如期識別到並寫入到Doris目標表中(過於簡單,就不截圖了)。


最後看delete:


但是有意思的是,當我在源表中嘗試把之前修改過的2條記錄給刪除掉,結果你猜怎麼著,Doris目標表依然新增了那兩條我想要刪除的記錄。


也就是說:


對於Flink 的mysql CDC來說,除了對源表的select外,其他的如insert、update、delete 操作,都會觸發程式對目標表的新增資料。


所以保險起見,為了防止資料重複,對於承接CDC結果的目的表,最好使用去重表模型。



最後


從測試的結果來看,mysql的CDC能很好的滿足我們的資料匯入要求,只不過,並不是所有版本的mysql都有這個資格(需要5.6及以上版本)。


而且從實踐來看,對Flink mysql CDC的使用也並不沒有任何門檻,這裡面的一些坑,甚至隱藏的bug,都要求開發者有一定的問題排查和解決能力。


本來還想著基於同樣的場景,來對比一下spark的,但限於文章篇幅,今天就到這裡。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70027827/viewspace-2993610/,如需轉載,請註明出處,否則將追究法律責任。

相關文章