ITPub博客

首页 > 数据库 > 数据库开发技术 > Flink的mysql CDC,好使不?

Flink的mysql CDC,好使不?

原创 数据库开发技术 作者:安瑞哥是码农 时间:2023-11-07 20:55:45 0 删除 编辑


这些天,一直都折腾,看可以通过哪些手段来导mysql表的数据源到各个目标数据库中,为了摸清楚当下大概有哪些导入方式,找出我认为最靠谱,最方便的,可谓煞费苦心。


前两篇文章分别介绍了用dataX和CK外部表的玩法,通过基于我的应用场景实践,详细说明了这两种数据导入方式谁更靠谱。


那么今天咱再来试试用Flink的CDC方案,用它来读取mysql表,并把数据写入到Doris,看整个过程有哪些坑,以及是否方便、靠谱。


其实从Flink官网来看,flink读取mysql,还有一种jdbc的方式,只不过CDC这个概念被Flink宣传的过于火热,咱还是决定从它先开始。



0. 环境准备


既然要用Flink CDC这个功能(至于啥是CDC,网上遍地的资料,这里暂不解释),那高低得看一眼CDC的官网,看看我们在使用这玩意时,需要注意哪些内容。


Flink CDC官网地址(注意:它跟Flink官网是分开的):https://ververica.github.io/flink-cdc-connectors/master/content/about.html


这个官网最大的优点就是:可以不用梯子。


由于我当前开发环境Flink用的1.15版本,所以我第一个关心的问题就是,CDC作为一个额外的flink生态组件,我该选哪个版本?


不出所料,官网给出了详细的版本兼容性列表,因此我的1.15可以选择的CDC版本有两个:2.3跟2.4,既然是测试,这里我选择最新的2.4版本。


于是,我需要在我的IDE开发环境里,引入对应的pom依赖:



关于本次测试需要的所有组件版本,如下表所示:


组件名称
版本
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来说,太低了,抛出了如下的异常:



果然,想使用这玩意,也不是谁都配的,回头再去确认了一下官方文档,需要mysql版本不能低于5.6(那低版本的数据想用Flink同步咋整呢?可能需要试试JDBC方案)。



于是就只能去部署一个更高版本的mysql实例,于是我就果断下了个mysql8给部署起来了。


还是拿我之前的上网日志数据,根据数据特点先建表:


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

至于为什么这个Primary key是这么多的字段组合,原因在于如果不这样,容易造成因为主键重复导致数据写入失败。


随后,我用load data语法,将一个本地数据文件给导入到该表中,在导入中发现,即便上面这个Primary key的组合实际内容并没有重复,但还是报了一些重复主键的错误,所以我怀疑,它这个组合键判断是否重复的依据,是用的hash值。


为了快速达到测试效果,我只往这张表里先写入了1百多万数据:



当然,还需要在该数据库创建一个,外部可以连接到该表的一个外部用户,并对其赋予相关的权限,过于简单的内容,这里就不赘述了。



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的类查找功能,在当前项目瞅一眼,这个类,它到底有没有?


不仅有,而且有3个jar包都包含这个类(1个 hive-exec 包,2个 guava 包),所以你是不是就此判断,是因为这3个jar包冲突导致的这个报错呢?


其实不然,要知道,这里查找出的结果,是当前软件工程中,所有module的,也就是说,咱们的目标module(flink CDC所在的module),是不是真有这些jar包,还不一定,还得进一步确认。


果然一查发现,在flink cdc这个module下,hive-exec 这个包它就没有,而 guava 的包也只有个低版本的。


那根据我的经验判断,应该是当前module缺少高版本的 guava 包导致的报错(毕竟这个破包已经在我的多个项目中留下了案底,所以印象深刻),那既然是需要高版本,自然就想到要把这个低版本的guava给先排除掉。


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


干掉低版本的,添加高版本的

(PS:大家可能会觉得,这看起来挺简单的嘛,但如果要是你在开发时遇到了这个问题,你能做到这么头脑清醒,思维缜密吗)


2.2 坑2


继续启动代码,又抛出下面这个错误:



从错误提示来看,好像是某个数据格式不规范而导致的问题。


于是呢,为了确定这一点,我就新建了张表结构一样的空表,再次启动程序发现,还是报这个错。


于是我开始怀疑是表字段问题,就又开始逐个排查,从建一个字段的表,逐渐增加到建包含所有字段的表,居然,又都不报错了。


然后我在新表中写入数据,再次启动程序,发现也正常了。


最后,我再次让程序读取最开始报错的那张表,之前的错误居然消失了,要知道,我期间没有做任何的修改,就离谱。


合着试一圈下来,是在玩我呢!


经过多次反复的尝试,我很怀疑这玩意就是flink CDC的bug,偶发,没有特定规律。



3.  CDC的写入逻辑验证


填完上面的坑之后,再次启动程序,就能很顺利将mysql中的数据全部导入到Doris的目标表中了。


可以看到,已经把之前写入到mysql中的所有数据,一条不落的都导入过来了(对比上面mysql数据量的截图),而且速度很快。


起初,我原本想着,既然是CDC,程序是不是只会捕获变化(新增)的数据才对,至于存量的历史数据(导入程序启动之前就存在的数据),可能不会导入。


但是经过验证,Flink CDC它做到了,通过查看程序运行的日志可以发现,它之所以能做到这一点,跟它读取的是binlog日志密切相关。


CDC程序运行过程的日志

为了进一步验证这一点,我把mysql的binlog功能给关了试试,于是我在配置文件中加入这个:


然后重启mysql,结果发现很快,CDC程序就报错了:


所以说明,mysql CDC能使用的前提是:必须要开启binlog功能。


为了进一步验证CDC程序能感知binlog的哪些操作,我又分别对mysql的源表做了update、insert和delete操作,咱分别都来看一下,这些操作会对Doris目的表有哪些影响?


先看update:


用update对mysql源表,其中的两条记录的某个字段值做个修改。


然后再来看Doris目标表的记录数有没有变化:


可以看到,数据如期增加了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的,但限于文章篇幅,今天就到这里。


下一篇,咱接着聊spark structured streaming 在这方面的表现怎么样...



你可以添加我的私人微信,拉你入技术讨论群,跟一群热爱技术的小伙伴一起成长...


原文作者:安瑞哥是码农;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2022-02-16

  • 博文量
    228
  • 访问量
    115420