flink-cdc同步mysql数据到elasticsearch

1,什么是cdc

CDC是(Change Data Capture 变更数据获取)的简称。核心思想是,监测并捕获数据库的变动(包括数据 或 数据表的插入INSERT、更新UPDATE、删除DELETE等),将这些变更按发生的顺序完整记录下来,写入到消息中间件中以供其他服务进行订阅及消费。

2,flink的cdc

cdc项目地址:https://github.com/ververica/flink-cdc-connectors

cdc项目文档:https://ververica.github.io/flink-cdc-connectors/master/

flink-sql项目文档:https://nightlies.apache.org/flink/flink-docs-release-1.13/zh/docs/dev/table/sqlclient/

 

3,环境准备

  • mysql
  • elasticsearch
  • flink on yarn

说明:如果没有安装hadoop,那么可以不用yarn,直接用flink standalone环境吧。

本例使用版本如下:

 下面两个地址下载flink的依赖包,放在lib目录下面。 

  下载地址:

  1、https://repo.maven.apache.org/maven2/com/alibaba/ververica/

  flink-sql-connector-mysql-cdc-1.4.0.jar

  此仓库提供的最新版本为1.4.0,如需新版本可自行编译或者去https://mvnrepository.com/下载。

  2、https://repo.maven.apache.org/maven2/org/apache/flink/

  flink-sql-connector-elasticsearch7_2.11-1.13.5.jar

  小坑:此处使用的是es7,由于本地环境是es8导致无法创建索引,又重新安装es7测试成功。

 

4,启动flink

启动flink集群

./start-cluster.sh

启动成功的话,可以在 http://localhost:8081/ 访问到 Flink Web UI,如下所示:

 启动flink sql-client

./sql-client.sh
不加任何参数进入交互式界面。
./sql-client.sh -f /tmp/aa.sql
-f:就是接sql文件。即不用进行交互式查询,这里注意:aa.sql文件里的insert语句会被分开成一个个job。

如果想要在一个job里提交就要注意写法,即:
在1.15.0以前语法:
BEGIN STATEMENT SET;
-- one or more INSERT INTO statements
{ INSERT INTO|OVERWRITE <select_statement>; }+
END;

自定义job名称: set pipeline.name = totalTask;

启动成功后,可以看到如下的页面:

 

5,数据同步初始化

1)mysql数据库原始表

CREATE TABLE `product_view` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`product_id` int(11) NOT NULL,
`server_id` int(11) NOT NULL,
`duration` int(11) NOT NULL,
`times` varchar(11) NOT NULL,
`time` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `time` (`time`),
KEY `user_product` (`user_id`,`product_id`) USING BTREE,
KEY `times` (`times`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 样本数据
INSERT INTO `product_view` VALUES ('1', '1', '1', '1', '120', '120', '2020-04-24 13:14:00');
INSERT INTO `product_view` VALUES ('2', '1', '1', '1', '120', '120', '2020-04-24 13:14:00');
INSERT INTO `product_view` VALUES ('3', '1', '1', '3', '120', '120', '2020-04-24 13:14:00');
INSERT INTO `product_view` VALUES ('4', '1', '1', '2', '120', '120', '2020-04-24 13:14:00');
INSERT INTO `product_view` VALUES ('5', '8', '1', '1', '120', '120', '2020-05-14 13:14:00');
INSERT INTO `product_view` VALUES ('6', '8', '1', '2', '120', '120', '2020-05-13 13:14:00');
INSERT INTO `product_view` VALUES ('7', '8', '1', '3', '120', '120', '2020-04-24 13:14:00');
INSERT INTO `product_view` VALUES ('8', '8', '1', '3', '120', '120', '2020-04-23 13:14:00');
INSERT INTO `product_view` VALUES ('9', '8', '1', '2', '120', '120', '2020-05-13 13:14:00');

2)flink 创建source数据库关联表

CREATE TABLE product_view_source (
`id` int,
`user_id` int,
`product_id` int,
`server_id` int,
`duration` int,
`times` string,
`time` timestamp,
PRIMARY KEY (`id`) NOT ENFORCED
) WITH (
'connector' = 'mysql-cdc',
'hostname' = '10.34.100.209',
'port' = '3306',
'username' = 'root',
'password' = '123',
'database-name' = 'flinkcdc_test',
'table-name' = 'product_view',
'server-id' = '5401'
);

这样,我们在flink-sql client操作这个表相当于操作mysql里面的对应表。

3)flink 创建sink,数据库关联表elasticsearch

CREATE TABLE product_view_sink(
`id` int,
`user_id` int,
`product_id` int,
`server_id` int,
`duration` int,
`times` string,
`time` timestamp,
PRIMARY KEY (`id`) NOT ENFORCED
) WITH (
'connector' = 'elasticsearch-7',
'hosts' = 'http://10.34.100.156:9200',
'index' = 'product_view_index'
);

这样,es里的product_view_index这个索引在数据同步时会被自动创建,如果想指定一些属性,可以提前手动创建好索引。往product_view_sink里面插入数据,可以发现es中已经有数据了。

查看flink创建的表

 查看flink表数据

select * from product_view_source;

 

select * from product_view_sink;

 由此可见,sink不能直接使用sql查询。

4)建立同步任务

insert into product_view_sink select * from product_view_source;

这个时候是可以退出flink sql-client的,然后进入flink web-ui,可以看到mysql表数据已经同步到elasticsearch中了,对mysql进行插入删除更新,elasticsearch都是同步更新的。

查看任务

 查看es数据

 

6,数据实时同步

1)新增记录

mysql数据库插入一条记录

INSERT INTO `product_view` VALUES ('10', '8', '1', '2', '120', '120', '2020-05-13 13:14:00');

查询es,新增一条记录

 

2)删除记录

mysql数据库删除一条记录

DELETE FROM `product_view` where id=10;

查询es,减少一条记录

 3)更新记录

es原始记录

 mysql更新一条记录

UPDATE `product_view` SET user_id=100,product_id=101 WHERE id=2;

变更后es记录

 

 4)修改表结构

1、flink-sql不支持alter语句,因此flink-sql创建的source,sink表也不支持结构的修改。

2、当mysql源表增加列时,flink创建的source,sink表结构都不会发生改变,job会忽略新增的列仍继续执行同步任务,不会报错。

3、当mysql源表删除job中正在同步的列时,job会报如下错误:time是手动删除的字段。并且手动增加time列后job也不能恢复正常。

 

7,故障恢复

1)背景

当提交的job出错时会导致同步任务中断,重启job会导致同步任务从头开始,如果源表数据量巨大会耗费大量时间和资源,如何能以最小的代价快速恢复呢?这就要用到flink的重启策略和checkpoint。

2)相关说明

官方文档:https://nightlies.apache.org/flink/flink-docs-release-1.13/zh/docs/dev/execution/task_failure_recovery/

Flink 作业如果没有定义重启策略,则会遵循集群启动时加载的默认重启策略。 如果提交作业时设置了重启策略,该策略将覆盖掉集群的默认策略。

通过 Flink 的配置文件 flink-conf.yaml 来设置默认的重启策略。配置参数 restart-strategy 定义了采取何种策略。 如果没有启用 checkpoint,就采用“不重启”策略。如果启用了 checkpoint 且没有配置重启策略,那么就采用固定延时重启策略fixed-delay,默认值:重启次数Integer.MAX_VALUE ,重启间隔1s。

3)重启场景模拟

不开启checkpoint就不会开启重启策略。

提交job后关掉mysql,job报如下错误:

 开启checkpoint就会自动开启默认的重启策略。

设置命令:


//每 3 秒做一次 checkpoint,用于测试,生产配置建议5到10分钟
set execution.checkpointing.interval = 3s;

提交job后关掉mysql,job一只在重启,重启mysql后job自动恢复running状态。

 4)checkpoint模拟

当任务失败并且重启也失败(job彻底挂掉)或任务被取消掉,此时可以从checkpoint中快速恢复最近的状态。

默认checkpoint的存储有3种方式

1、内存中,当job失败无法从checkpoint中恢复,感觉没啥鸟用。

2、持久化保存checkpoint,最好使用hdfs,我这里为了方便采用的是本地目录存储。

3、rockdb数据库,没有研究,有兴趣的朋友自行研究。

设置命令:

// 设置本地存储目录
set state.checkpoints.dir = file:///tmp/checkpoint-dir;
//当作业手动取消时,将会保留作业的 Checkpoint 状态信息。注意,这种情况下,需要手动清除该作业保留的 Checkpoint 状态信息,否则这些状态信息将永远保留在外部的持久化存储中。
//为方便测试,本例采用此种方式
set execution.checkpointing.externalized-checkpoint-retention=RETAIN_ON_CANCELLATION;
//仅当作业失败时,作业的 Checkpoint 才会被保留用于任务恢复。当作业取消时,Checkpoint 状态信息会被删除,因此取消任务后,不能从 Checkpoint 位置进行恢复任务。
set execution.checkpointing.externalized-checkpoint-retention=DELETE_ON_CANCELLATION;
// 重置指定参数
reset execution.checkpointing.externalized-checkpoint-retention;
// 设置并行度
set parallelism.default = 1;
// 重置全部参数
reset;

提交job,手动取消job后,查看最近的checkpoint。

 从此最近的checkpoint文件中可恢复job

设置命令:

//设置要恢复的checkpoint
set 'execution.savepoint.path'='file:/tmp/checkpoint-dir/0bdf06a6e699f87b6703a39df6ea8b5d/chk-3';
//执行原来的任务sql,是所有的sql,包括建表语句。最好提交刚开始提交任务时就用文件的方式,这样方便恢复。
insert into product_view_sink select a2.* from product_view_source a1 inner join product_view_source2 a2 on a1.id=a2.id;

 从checkpoint中恢复时,新提交的任务checkpoint状态变化如下:

表示从要恢复的job的id=118号开始恢复,新job的id=旧job的结束id+新job已创建的id,123=118+5

 实操模拟:

  1、源mysql中的数据如下:

   2、开启checkpoint,设置存储目录,保留取消任务时checkpoint文件,提交job。

   3、查询es中同步的数据,和mysql中记录完全一样

   4、取消job,删除es中的索引

 

   4、修改、添加mysql中数据

   5、flink-sql设置恢复checkpoint,并重新提交任务

   6、观察es中的数据发现取消job前的数据没有了,只有取消后新增的2条数据和修改的那条数据。这说明不是从头开始同步的,是从checkpoint中恢复出来的。

 

8,任务文件模板

1)任务模板

提交任务,最好以文件的方式提交,方便后续job的恢复。

-- 设置job名称
set pipeline.name = totalTask;
-- 设置checkpoint时间间隔
set execution.checkpointing.interval = 300s;
-- 设置checkpoint持久化目录
set state.checkpoints.dir = file:///tmp/checkpoint-dir;
-- 设置当取消job时仍保留checkpoint文件
set execution.checkpointing.externalized-checkpoint-retention=RETAIN_ON_CANCELLATION;


建表语句source表
建表语句sink表

BEGIN STATEMENT SET;
insert同步语句
END; 

9,断点续传

job失败后恢复步骤如下:

1、找到最新的checkpoint;
2、set 'execution.savepoint.path'='xxxxx';
3、执行本原来任务sql文件;

10,遇到的问题

1)slots不足

flink默认taskmanager.numberOfTaskSlots=1即只能运行一个子任务,一般设置为机器的CPU核心数。

2)内存不足

任务失败,可能由于内存不足导致,一般可调节flink-config.yaml以下配置,如内存需求很大可以按照b来配置。

a.最简单的总内存配置,会按照默认比例给具体的内存项分配。

b.给具体的内存项分配

jobmanager.memory.flink.size: 2048m

taskmanager.memory.task.heap.size : 2048m
taskmanager.memory.managed.size : 2048m

 

 

3)重复server-id

 

1、前提:单个提交job任务,即每个insert语句形成一个job,就是一个同步任务。

结论:通过实践可知,所有具有相同server-id的source表,只能选择其中一个且被一个job使用。实用性很差,只是测试时踩的坑记录一下。

场景:假如source1和source2表具有相同的server-id,如果job1中使用了source1(不能同时使用source1和source2),那其他job就不能在用source1、source2了。

分析:先提交一个job1并且已经在同步了,此时如果提交的job2中有source表与job1中source表有相同的server-id,或job2中使用和job1中重复的source表,那job2也从job1已经读到的binlog位置开始读就会有问题,直接报如下错误。

2、前提:批量提交同步任务,即将多个insert语句放在一起形成一个job,一个insert对应一个同步任务,一个job包含多个同步任务。

结论:通过实践可知,不同的同步任务(即不同的insert语句)可以使用同一个source表,但不建议共享,可能造成数据丢失。但一个同步任务不能使用相同的server-id的source表。

场景:假如source1和source2表具有相同的server-id,如果任务1使用source1(不能同时使用source1和source2),其他任务还可以使用source1。

分析:同时提交任务1和任务2并且都使用到了source1,等于2个任务共同维护source1的binlog状态,此时可能导致某个任务从错误的binlog位置读取数据,从而导致数据丢失。

最佳实践:一个同步任务中(一个insert语句)使用到的每个source表都对应一个不同server-id。同一个source表如在多个job或任务中使用,就在每个job或每个任务中设置不同的表名及server-id。这样对于相同的source表在每个job或任务中都各自维护一份binlog状态了。

举例说明:如order表需要在3个job或同步任务中使用,job1中name=order1,server-id=5401;job2中name=order2,server-id=5402;job3中name=order3,server-id=5403;这样3个job就会各自维护各自的关于order表的binlog状态。

4)数据不一致

问题:同步完成后通过es查询索引的数据量和mysql查询的数据量不一致。

问题定位:通过在flink的sql-client.sh查询发现和mysql查询的数据不一致,定位到是flink的sql和mysql的sql语法存在区别。

当where 条件中同时包含 OR 和 IS NULL 的 SQL时,IS NULL条件会失效 。 例如: select * from product01_source where (test_char is NULL or test_char = '2'); 这样的结果筛选出来不会包含test_char  is null的结果,当把test_char  = '2'去掉只有test_char  is null时这样的结果是正确的。也就是说OR和IS NULL不可以同时使用。
场景模拟
mysql查询select * from product01 where (test_char = '2' or test_char is null);

 flink的sql-client.sh查询select * from product01_source where (test_char is NULL or test_char = '2');

 

 通过以上模拟,可以发现mysql查询的结果和flink查询的结果不一致。

问题解决:最初是想通过union来合并两个条件的结果来解决,但总感觉还有其他解决方法,最终发现是flink的版本bug导致,将原来的flink-1.13.6换成1.14.6,并将flink-sql-connector-mysql-cdc-1.4.0.jar换成flink-sql-connector-mysql-cdc-2.3.0.jar,问题得以解决。

问题链接:https://issues.apache.org/jira/browse/FLINK-22015

 遇到的新问题:flink升级为1.14.6版本后虽然解决了数据一致性的问题,但是当提交多个复杂(join的表多)的job的时候,自测的时候提交2-3个时就会卡死,一直提交不了。报连接池连接不可用超时。

 问题解决:给source表加上   'connection.pool.size' = '200'  参数,增加连接数量即可。

总结:至此可正常提交job并保证数据的一致性。

posted @ 2023-08-10 20:32  技术人的菜园子  阅读(908)  评论(0编辑  收藏  举报