Flink 1.14测试cdc写入到kafka案例

测试案例

1、遇到的问题

1.1 bug1

io.debezium.DebeziumException: Access denied; you need (at least one of) the REPLICATION SLAVE privilege(s) for this operation Error code: 1227; SQLSTATE: 42000.
	at io.debezium.connector.mysql.MySqlStreamingChangeEventSource.wrap(MySqlStreamingChangeEventSource.java:1146)
	at io.debezium.connector.mysql.MySqlStreamingChangeEventSource$ReaderThreadLifecycleListener.onCommunicationFailure(MySqlStreamingChangeEventSource.java:1185)
	at com.github.shyiko.mysql.binlog.BinaryLogClient.listenForEventPackets(BinaryLogClient.java:973)
	at com.github.shyiko.mysql.binlog.BinaryLogClient.connect(BinaryLogClient.java:606)
	at com.github.shyiko.mysql.binlog.BinaryLogClient$7.run(BinaryLogClient.java:850)
	at java.lang.Thread.run(Thread.java:750)
Caused by: com.github.shyiko.mysql.binlog.network.ServerException: Access denied; you need (at least one of) the REPLICATION SLAVE privilege(s) for this operation
	at com.github.shyiko.mysql.binlog.BinaryLogClient.listenForEventPackets(BinaryLogClient.java:937)
	... 3 more
[ERROR] 2023-04-10 15:21:54,778(28432) --> [Source Data Fetcher for Source: MySQL Source -> Sink kafkaSink (1/1)#11] org.apache.flink.connector.base.source.reader.fetcher.SplitFetcherManager$1.accept(SplitFetcherManager.java:119): Received uncaught exception.  
java.lang.RuntimeException: SplitFetcher thread 0 received unexpected exception while polling the records
	at org.apache.flink.connector.base.source.reader.fetcher.SplitFetcher.runOnce(SplitFetcher.java:150)
	at org.apache.flink.connector.base.source.reader.fetcher.SplitFetcher.run(SplitFetcher.java:105)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:750)
Caused by: com.ververica.cdc.connectors.shaded.org.apache.kafka.connect.errors.ConnectException: An exception occurred in the change event producer. This connector will be stopped.
	at io.debezium.pipeline.ErrorHandler.setProducerThrowable(ErrorHandler.java:42)
	at com.ververica.cdc.connectors.mysql.debezium.task.context.MySqlErrorHandler.setProducerThrowable(MySqlErrorHandler.java:72)
	at io.debezium.connector.mysql.MySqlStreamingChangeEventSource$ReaderThreadLifecycleListener.onCommunicationFailure(MySqlStreamingChangeEventSource.java:1185)
	at com.github.shyiko.mysql.binlog.BinaryLogClient.listenForEventPackets(BinaryLogClient.java:973)
	at com.github.shyiko.mysql.binlog.BinaryLogClient.connect(BinaryLogClient.java:606)
	at com.github.shyiko.mysql.binlog.BinaryLogClient$7.run(BinaryLogClient.java:850)
	... 1 more
Caused by: io.debezium.DebeziumException: Access denied; you need (at least one of) the REPLICATION SLAVE privilege(s) for this operation Error code: 1227; SQLSTATE: 42000.
	at io.debezium.connector.mysql.MySqlStreamingChangeEventSource.wrap(MySqlStreamingChangeEventSource.java:1146)
	... 5 more
Caused by: com.github.shyiko.mysql.binlog.network.ServerException: Access denied; you need (at least one of) the REPLICATION SLAVE privilege(s) for this operation
	at com.github.shyiko.mysql.binlog.BinaryLogClient.listenForEventPackets(BinaryLogClient.java:937)
	... 3 more

上面的报错数据是分布式数据库权限的问题。需要解决权限问题。

bug2

在配置flink kafka producer的EXACTLY_ONCE
flink checkpoint无法触发。
flinkKafkaProducer中配置exactly once,flink开启ck,提交事务失败,其中报错原因是
[INFO ] 2023-04-10 12:37:34,662(142554) --> [Checkpoint Timer] org.apache.flink.runtime.checkpoint.CheckpointCoordinator.onTriggerFailure(CheckpointCoordinator.java:913): Failed to trigger checkpoint for job 80b8184c08504bf8026a8fa4f2e03fb5 because Checkpoint triggering task Source: MySQL Source -> (Sink: Print to Std. Out, Sink kafkaSink) (1/1) of job 80b8184c08504bf8026a8fa4f2e03fb5 is not being executed at the moment. Aborting checkpoint. Failure reason: Not all required tasks are currently running. 

其中flink checkpoint的配置信息

executionEnvironment.getCheckpointConfig().
                setCheckpointStorage(new FileSystemCheckpointStorage("file:///d:/cdc/ck"));
//        executionEnvironment.setStateBackend(new FsStateBackend("hdfs://drmcluster/flink/checkpoints"));
        //开启checkpoint 启用 checkpoint,设置触发间隔(两次执行开始时间间隔)
        executionEnvironment.enableCheckpointing(1000*10L); //测试5秒触发一次 生产环境10分钟
        executionEnvironment.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
//        超时时间,checkpoint没在时间内完成则丢弃
        executionEnvironment.getCheckpointConfig().setCheckpointTimeout(50000L); //10秒
        executionEnvironment.getCheckpointConfig().setMaxConcurrentCheckpoints(2);
        executionEnvironment.getCheckpointConfig().setTolerableCheckpointFailureNumber(1);
        //最小间隔时间(前一次结束时间,与下一次开始时间间隔)
        executionEnvironment.getCheckpointConfig().setMinPauseBetweenCheckpoints(1000);
//        当 Flink 任务取消时,保留外部保存的 checkpoint 信息
        executionEnvironment.getCheckpointConfig().enableExternalizedCheckpoints
                (CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);

在flink 1.14中创建kafkaFink的api发生了改变

Properties properties = new Properties();
        properties.setProperty("transaction.timeout.ms", 1000 * 60 * 2+ "");//设置事务时间 5分钟提交事务
        return KafkaSink.<String>builder()
                .setBootstrapServers("qn-flink01:9092,qn-flink02:9092,qn-flink03:9092")
                .setRecordSerializer(KafkaRecordSerializationSchema.builder()
                        .setTopic(topic)
                        .setValueSerializationSchema(new SimpleStringSchema())
                        .setKeySerializationSchema(new MyKeySerializationSchema())
                        .setPartitioner(new FlinkKafkaPartitioner<String>() {
                            //数据分区,按照scene字段的hash值来分发数据到3个分区
                            @Override
                            public int partition(String record, byte[] key, byte[] value, String targetTopic, int[] partitions) {
                                JSONObject jsonObject = JSONObject.parseObject(record);
                                String afterJson = jsonObject.get("after").toString();
                                Object json = JSONObject.parseObject(afterJson).get(filed);
                                log.info("scene: " + json);
                                return Math.abs(json.hashCode() % partitions.length);
                            }
                        }).build()
                )
                .setDeliverGuarantee(DeliveryGuarantee.EXACTLY_ONCE) //精确一次消费
                .setKafkaProducerConfig(properties)
//                .setTransactionalIdPrefix("scene")
                .build();

在从flik到kafka的端对端的语义的时候:
在这里插入图片描述FLink端到端需要注意的点:

  • Flink任务需要开启checkpoint配置为CheckpointingMode.EXACTLY_ONCE
  • Flink任务FlinkKafkaProducer需要指定参数Semantic.EXACTLY_ONCE
  • Flink任务FlinkKafkaProducer配置需要配置transaction.timeout.ms,checkpoint间隔(代码指定)<transaction.timeout.ms(默认为1小时)<transaction.max.timeout.ms(默认为15分钟)
  • 消费端在消费FlinkKafkaProducer的topic时需要指定isolation.level(默认为read_uncommitted)为read_committed

原文链接:https://blog.youkuaiyun.com/yiweiyi329/article/details/127297375

2、成功的案例

Flink kafka producer的配置是在AT LEAST ONCE的模式,这种情况下,生产者写入的数据会存在重复的情况。

Properties properties = new Properties();
        properties.setProperty("transaction.timeout.ms", 1000 * 60 * 2+ "");//设置事务时间 5分钟提交事务
        return KafkaSink.<String>builder()
                .setBootstrapServers("qn-flink01:9092,qn-flink02:9092,qn-flink03:9092")
                .setRecordSerializer(KafkaRecordSerializationSchema.builder()
                        .setTopic(topic)
                        .setValueSerializationSchema(new SimpleStringSchema())
                        .setKeySerializationSchema(new MyKeySerializationSchema())
                        .setPartitioner(new FlinkKafkaPartitioner<String>() {
                            //数据分区,按照scene字段的hash值来分发数据到3个分区
                            @Override
                            public int partition(String record, byte[] key, byte[] value, String targetTopic, int[] partitions) {
                                JSONObject jsonObject = JSONObject.parseObject(record);
                                String afterJson = jsonObject.get("after").toString();
                                Object json = JSONObject.parseObject(afterJson).get(filed);
                                log.info("scene: " + json);
                                return Math.abs(json.hashCode() % partitions.length);
                            }
                        }).build()
                )
                .setDeliverGuarantee(DeliveryGuarantee.AT_LEAST_ONCE) //精确一次消费
                .setKafkaProducerConfig(properties)
//                .setTransactionalIdPrefix("scene")
                .build();

本地代码在控制台上了实时打印出update、insert、delete的操作日志信息。
update操作:
在这里插入图片描述更新最后一条数据的charge为100:
在这里插入图片描述```json
{
“database”: “zczqdb”,
“before”: {
“is_tax_inclusive”: 0,
“charge”: 13333.0,
“create_time”: 1681143473000,
“treat_shop_id”: “11003”,
“scene”: “3”,
“is_delete”: 0,
“field1”: “”,
“partner_id”: “520181000000”,
“channel_source”: “GRJY(贵人家园)”,
“association_contract”: “”,
“customer_id”: “11003”,
“order_id”: “fc84774d-3031-4511-b99e-5604a7e99a89”,
“accept_time”: 1681143482000,
“status”: 7
},
“after”: {
“is_tax_inclusive”: 0,
“charge”: 100.0,
“create_time”: 1681143473000,
“treat_shop_id”: “11003”,
“scene”: “3”,
“is_delete”: 0,
“field1”: “”,
“partner_id”: “520181000000”,
“channel_source”: “GRJY(贵人家园)”,
“association_contract”: “”,
“customer_id”: “11003”,
“order_id”: “fc84774d-3031-4511-b99e-5604a7e99a89”,
“accept_time”: 1681143482000,
“status”: 7
},
“type”: “update”,
“tableName”: “general_order”
}


delete操作,删除最后一条数据:

```json
{
	"database": "zczqdb",
	"before": {
		"is_tax_inclusive": 0,
		"charge": 100.0,
		"create_time": 1681143473000,
		"treat_shop_id": "11003",
		"scene": "3",
		"is_delete": 0,
		"field1": "",
		"partner_id": "520181000000",
		"channel_source": "GRJY(贵人家园)",
		"association_contract": "",
		"customer_id": "11003",
		"order_id": "fc84774d-3031-4511-b99e-5604a7e99a89",
		"accept_time": 1681143482000,
		"status": 7
	},
	"after": {},
	"type": "delete",
	"tableName": "general_order"
}

3、后面接着完成EXACTLY_ONCE的测试。

<think>我们正在讨论Flink SQL中如何处理乱序数据以确保获取正确结果。结合用户提供的引用和之前的问题,我们可以从以下几个方面展开: 1. **使用Watermark机制处理乱序**:这是Flink的核心机制,通过设置合理的延迟来等待乱序事件。 2. **结合Allowed Lateness和窗口触发机制**:允许窗口在关闭后的一段时间内继续处理迟到数据。 3. **利用状态后端和检查点保证状态一致性**:确保故障恢复后状态正确。 4. **使用SQL中的时间属性和窗口函数**:正确使用Flink SQL中的时间属性(事件时间)和窗口函数。 下面详细展开: ### 一、核心策略:Watermark + Allowed Lateness 在Flink SQL中,处理乱序数据的关键是正确设置事件时间属性和Watermark策略,配合窗口的Allowed Lateness。 #### 1. 定义事件时间属性和Watermark ```sql CREATE TABLE orders ( order_id STRING, product_id INT, amount DOUBLE, order_time TIMESTAMP(3), -- 定义事件时间属性 WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND ) WITH ( ... ); ``` - `WATERMARK`定义了事件时间属性和延迟策略(这里设置5秒延迟,即允许5秒内的乱序)。 #### 2. 使用窗口聚合设置Allowed Lateness ```sql SELECT TUMBLE_START(order_time, INTERVAL '1' MINUTE) AS window_start, product_id, SUM(amount) AS total_amount FROM orders GROUP BY TUMBLE(order_time, INTERVAL '1' MINUTE), product_id ``` 默认情况下,窗口会在Watermark达到窗口结束时间时触发计算关闭。为了处理迟到的数据,我们可以通过设置`GROUP BY`语句的`RETENTION`属性(Flink 1.15+)或者使用`TUMBLE`函数的第三个参数(Flink 1.14及之前版本不支持,但通常通过`TableConfig`设置)来允许延迟。不过,在Flink SQL中,Allowed Lateness通常是通过`TableConfig`设置的: ```java // 在Java代码中设置TableConfig(如果使用SQL Client,可以在配置文件中设置) TableConfig tableConfig = TableEnvironment.getConfig(); tableConfig.set("table.exec.state.ttl", "10 min"); // 状态保留时间,要大于Allowed Lateness ``` 在SQL中,我们可以通过以下方式(Flink 1.15+): ```sql -- 设置Allowed Lateness(示例,具体语法请参考对应版本文档) SET 'table.exec.windowed-allowed-lateness' = '1 min'; ``` 这样,窗口在触发后还会保留1分钟,期间到达的迟到数据会触发窗口的再次计算(输出更新结果)。 #### 3. 处理严重迟到数据 对于超过Allowed Lateness的数据,我们可以将其侧输出到另一条流: ```sql -- 使用侧输出(Flink SQL中通常通过自定义函数或算子实现,这里以Java API为例,SQL中暂不支持直接侧输出) ``` 在Flink SQL中,目前没有直接支持侧输出的语法。通常,我们可以在DataStream API中处理,或者使用`MATCH_RECOGNIZE`进行复杂事件处理。但我们可以通过以下方式间接实现: - 将迟到数据过滤出来,单独处理: ```sql -- 先计算窗口聚合,然后通过时间比较过滤迟到数据 SELECT window_start, product_id, total_amount, CASE WHEN order_time < TUMBLE_START(order_time, INTERVAL '1' MINUTE) + INTERVAL '6' MINUTE THEN 'on_time' ELSE 'late' END AS data_type FROM ( SELECT TUMBLE_START(order_time, INTERVAL '1' MINUTE) AS window_start, product_id, SUM(amount) AS total_amount, -- 注意:这里需要获取每条记录的时间,但聚合后无法获取,所以需要先标记再聚合 ) ``` 但这种方法有局限性。更推荐的方式是使用DataStream API处理,然后转回Table。 ### 二、使用状态TTL和状态清理 设置状态生存时间(TTL)可以防止状态无限增长: ```sql -- 在Flink SQL中,通过TableConfig设置 SET 'table.exec.state.ttl' = '7d'; ``` 这个设置会使得状态在7天后自动清理,适用于窗口保留时间较长的情况。 ### 三、使用ROW_NUMBER()去重 在引用[2]中提到,可以使用ROW_NUMBER()来创建保序任务,确保发送到下游的数据有序。例如,在去重场景中: ```sql SELECT order_id, product_id, amount, order_time FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY order_time DESC) AS rn FROM orders WHERE ... -- 可以过滤掉严重迟到数据 ) WHERE rn = 1 ``` 这样,每个订单取最新的一条,即使有乱序,也能保证最终结果正确。 ### 四、利用CDC源表的快照和增量读取 如果源表是CDC数据(如MySQL binlog),Flink CDC可以确保全量快照和增量数据的顺序性,从而减少乱序影响。 ### 五、监控乱序数据 可以通过指标监控乱序情况: - `numLateRecordsDropped`:被丢弃的迟到记录数(超过Allowed Lateness)。 - 自定义指标:在Watermark生成器中记录延迟情况。 ### 总结:确保正确性的关键点 1. **合理设置Watermark延迟**:根据业务最大乱序时间设置,例如5秒或1分钟。 2. **设置Allowed Lateness**:给予窗口额外的等待时间,更新计算结果。 3. **处理严重迟到数据**:通过侧输出记录或告警。 4. **状态管理**:设置状态TTL,防止状态无限增长。 5. **去重保序**:使用ROW_NUMBER()等函数保证最终结果正确。 > 注意:Flink SQL的语法和配置可能随着版本变化,请参考对应版本的官方文档。 §§相关问题§§ 1.Flink SQL中,如何为不同的事件时间字段设置不同的Watermark策略? 2. 使用Allowed Lateness时,如何避免状态无限增长? 3. 如何监控Flink作业中乱序数据的处理情况(如迟到事件的数量)?</think>在 Flink SQL 中确保乱序数据处理结果的正确性,需要结合**时间语义定义**、**水位线策略**、**窗口机制**和**状态管理**四方面进行配置。以下是具体实践方案: --- ### 一、核心配置步骤 #### 1. **明确定义事件时间属性** ```sql CREATE TABLE orders ( order_id STRING, product_id INT, amount DOUBLE, order_time TIMESTAMP(3), -- 声明事件时间字段 WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND -- 水位线延迟5秒 ) WITH (...); ``` - **关键点**:`WATERMARK` 定义了事件时间的进度,`INTERVAL '5' SECOND` 表示允许5秒内的乱序[^1][^3]。 #### 2. **配置窗口与Allowed Lateness** ```sql SELECT TUMBLE_START(order_time, INTERVAL '1' MINUTE) AS window_start, product_id, SUM(amount) AS total_amount FROM orders GROUP BY TUMBLE(order_time, INTERVAL '1' MINUTE), -- 1分钟滚动窗口 product_id ``` - **添加Allowed Lateness**(Flink 1.15+): ```sql SELECT ... FROM orders GROUP BY TUMBLE( DATA => order_time, SIZE => INTERVAL '1' MINUTE, OFFSET => INTERVAL '0' SECOND, ALLOWED_LATENESS => INTERVAL '2' MINUTE -- 允许迟到2分钟 ), product_id ``` 迟到数据会触发窗口**增量更新**计算结果[^3]。 #### 3. **处理严重迟到数据** 超过 `ALLOWED_LATENESS` 的数据需路由到侧输出流: ```sql -- 创建侧输出表 CREATE TABLE late_events ( order_id STRING, order_time TIMESTAMP(3) ) WITH (...); -- 主查询重定向迟到数据 INSERT INTO late_events SELECT order_id, order_time FROM orders WHERE order_time < CURRENT_WATERMARK(order_time) -- 检测迟到 ``` #### 4. **启用状态一致性保证** ```sql SET 'execution.checkpointing.interval' = '30s'; -- 启用检查点 SET 'state.backend' = 'rocksdb'; -- 推荐状态后端 SET 'table.exec.state.ttl' = '1h'; -- 状态保留时间>窗口大小+延迟 ``` --- ### 二、乱序处理逻辑验证 #### 场景模拟(事件时间顺序) | 事件ID | 订单金额 | 事件时间 | 处理水位线 | |--------|----------|----------------|------------------| | A | 100 | 10:00:00 | 水位线:9:59:55 | | B | 200 | 10:00:10 | 水位线:10:00:05 | | C | 50 | 9:59:50 (迟到) | 水位线:10:00:15 | #### 处理过程: 1. **窗口 [10:00:00, 10:01:00)** 在 `水位线=10:01:05` 时触发计算 - 输出结果:`(A:100 + B:200) = 300` 2. **迟到事件C到达**(时间9:59:50 < 当前水位线10:00:15) - 若在 `ALLOWED_LATENESS(2分钟)` 内:触发增量更新 → `300 + 50 = 350` - 若超时:写入 `late_events` 表[^3] --- ### 三、关键配置项说明 | 配置项 | 作用 | 推荐值 | |----------------------------------|----------------------------------------------------------------------|----------------------| | `watermark` | 定义乱序容忍度 | 业务最大乱序时间+缓冲 | | `allowed_lateness` | 窗口关闭后接受迟到数据的时间 | 2×水位线延迟 | | `table.exec.state.ttl` | 状态保留时间(需 > 窗口大小 + allowed_lateness) | 窗口大小+1小时 | | `execution.checkpointing.mode` | 保证精确一次语义 | `EXACTLY_ONCE` | | `pipeline.auto-watermark-interval` | 水位线发射间隔 | 200ms | --- ### 四、监控与调优建议 1. **监控指标**: - `numLateRecordsDropped`:丢弃的迟到事件数 - `currentWatermark`:当前水位线进度 - `checkpointedSize`:状态大小 2. **动态水位线优化**(处理突发乱序): ```sql WATERMARK FOR order_time AS order_time - INTERVAL '0' SECOND - INTERVAL (CASE WHEN order_id LIKE 'urgent%' THEN '1' ELSE '5' END) SECOND ``` 3. **保序写入下游**: ```sql INSERT INTO kafka_sink SELECT *, ROW_NUMBER() OVER (PARTITION BY window_start ORDER BY order_time) AS rk FROM results WHERE rk = 1 -- 仅发送最新结果 ``` > **总结**:通过 `水位线延迟 + Allowed Lateness + 状态TTL + 检查点` 的组合,可在容忍乱序的同时保证结果正确性。对于金融级场景,建议增加 `ROW_NUMBER() 保序写入` 和 `迟到数据重放机制`[^2][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值