Flink的端到端的Exactly Once你真的实现对了吗?

很多开发者包括本人也一直以为Flink SQL任务中设置'execution.checkpointing.mode'='EXACTLY_ONCE',就能实现消息的端到端的Exactly Once处理。设置完参数后,鲜有开发者去验证运行的flink job的消息传递语义是否符合预期。接下来通过一个例子进行说明。

需求

公司有个Flink SQL实时任务,Source是mysql cdc表,经过简单计算后Sink到kafka表,这是使用的connector是upsert-kafka。

需求比较简单,读取订单表中商品数量、商品单价,计算总金额,再发送到Kafka。如下图所示

代码实现

最初代码

SET 'execution.checkpointing.mode'='EXACTLY_ONCE';
SET 'execution.checkpointing.interval'='60000';
SET 'pipeline.operator-chaining.enabled'='false';
DROP TABLE IF EXISTS kfk_order;
CREATE TABLE IF NOT EXISTS kfk_order(
	`id` INT NOT NULL,
    `prod_name` STRING,
    `qty` INT,
    `price` DOUBLE,
	`amount` DOUBLE,
    `created_time` TIMESTAMP,
    `updated_tme` TIMESTAMP,
    PRIMARY KEY ( `id` ) NOT ENFORCED
) WITH (
	'connector' = 'upsert-kafka',
	'topic' = 'order',
	'properties.bootstrap.servers' = 'localhost:9092',
	'key.format' = 'json',
	'value.format' = 'json'
);


DROP TABLE IF EXISTS t_order;
CREATE TABLE IF NOT EXISTS t_order (
    `id` INT NOT NULL,
    `prod_name` STRING,
    `qty` INT,
    `price` DOUBLE,
    `created_time` TIMESTAMP,
    `updated_tme` TIMESTAMP,
    PRIMARY KEY ( `id` ) NOT ENFORCED
) WITH (
	'server-id' = '1000',
	'server-time-zone' = 'Asia/Shanghai',
	'scan.incremental.snapshot.enabled' = 'false',
	'connector' = 'mysql-cdc',
	'hostname' = 'localhost',
	'port' = '3306',
	'username' = 'root',
	'password' = '123456',
	'database-name' = 'flink-demo',
	'table-name' = 't_order'
);


INSERT INTO kfk_order
SELECT `id`
    , `prod_name`
    , `qty`
    , `price`
	, `qty` * `price` AS amont
    , `created_time`
    , `updated_tme`
FROM t_order;

Flink UI

Flink任务提交后,在t_order表添加记录,即刻在kafka收到了结果消息,现象不符合我理解的Flink Exactly Once预期。

Exactly Once应该在checkpoint结束后才将结果数据commit到kafka。我们设置的checkpoint间隔是1分钟,但是在kafka妙级接收到消息, 并未做到端到端的Exactly Once。

通过查阅官网和资料,原来Flink的Exactly Once只能保证Flink任务内部算子,kafka的sink是At Leatest Once,所以才出现上文描述的情况。

调整后的代码

SET 'execution.checkpointing.mode'='EXACTLY_ONCE';
SET 'execution.checkpointing.interval'='60000';
SET 'pipeline.operator-chaining.enabled'='false';
DROP TABLE IF EXISTS kfk_order;
CREATE TABLE IF NOT EXISTS kfk_order(
	`id` INT NOT NULL,
    `prod_name` STRING,
    `qty` INT,
    `price` DOUBLE,
	`amount` DOUBLE,
    `created_time` TIMESTAMP,
    `updated_tme` TIMESTAMP,
    PRIMARY KEY ( `id` ) NOT ENFORCED
) WITH (
	'connector' = 'upsert-kafka',
	'topic' = 'order',
	'properties.bootstrap.servers' = 'localhost:9092',
	'sink.delivery-guarantee' = 'exactly-once',
	'sink.transactional-id-prefix' = 'tx',
	'properties.transaction.timeout.ms' = '65000',
	'key.format' = 'json',
	'value.format' = 'json'
);


DROP TABLE IF EXISTS t_order;
CREATE TABLE IF NOT EXISTS t_order (
    `id` INT NOT NULL,
    `prod_name` STRING,
    `qty` INT,
    `price` DOUBLE,
    `created_time` TIMESTAMP,
    `updated_tme` TIMESTAMP,
    PRIMARY KEY ( `id` ) NOT ENFORCED
) WITH (
	'server-id' = '1000',
	'server-time-zone' = 'Asia/Shanghai',
	'scan.incremental.snapshot.enabled' = 'false',
	'connector' = 'mysql-cdc',
	'hostname' = 'localhost',
	'port' = '3306',
	'username' = 'root',
	'password' = '123456',
	'database-name' = 'flink-demo',
	'table-name' = 't_order'
);


INSERT INTO kfk_order
SELECT `id`
    , `prod_name`
    , `qty`
    , `price`
	, `qty` * `price` AS amont
    , `created_time`
    , `updated_tme`
FROM t_order;

配置 'execution.checkpointing.mode'='EXACTLY_ONCE' 的同时。端到端的Exactly once是通过两阶段提交实现的,依赖sink端的事物。所以需要在sink表添加几项关键配置,配置包括消息传递语义与事物相关配置。

'sink.delivery-guarantee' = 'exactly-once',
'sink.transactional-id-prefix' = 'tx',
'properties.transaction.timeout.ms' = '65000',

kafka消息的消费者,也需要配置事物隔离性为读已提交。

总结

Flink要满足端到端的Exactly Once语义,需要Source端,Checkpoint,Sink端的配合。单单配置Checkpoint Mode为Exactly Once不能满足端到端的语义。还需要Source端支持replay,Sink端支持事物。并且Sink端的下游消费者配置读已提交,才能防止脏读。

KafkaFlink实现Exactly-Once语义方面紧密协作,为流式数据处理提供了端到端的精确一次语义保证。这种语义确保每条消息在流处理应用中仅被处理一次,即使在发生故障的情况下也不会丢失或重复数据。 ### KafkaExactly-Once语义 Kafka通过其事务机制和幂等生产者实现Exactly-Once语义。事务机制允许生产者在多个分区上原子性地写入数据,而幂等生产者则确保单个分区内的消息写入是幂等的,即即使消息被多次发送,消费者也只会看到一次。 - **幂等生产者(Idempotent Producer)**:通过在生产者端启用幂等性,Kafka会自动去重重复的消息,从而确保消息在单个分区内的精确一次语义。这种方式适用于简单的用例,不需要复杂的事务管理。 - **事务(Transactions)**:Kafka的事务机制允许生产者在多个分区上进行原子性操作,确保所有写入要么全部成功,要么全部失败。beginTransactions之后的所有写入操作在commitTransaction之后才会对消费者可见,如果发生故障或关闭事务,则所有操作都会被回滚[^4]。 ### FlinkExactly-Once语义 Apache Flink通过其检查点机制(Checkpointing)和状态管理实现Exactly-Once语义。Flink的状态后端和检查点机制确保在故障恢复时能够恢复到一致的状态。 - **检查点机制**:Flink定期创建分布式快照(检查点),记录算子的状态。在发生故障时,Flink会从最近的检查点恢复,并重新处理数据流,确保状态的一致性。 - **TwoPhaseCommitSinkFunction**:Flink提供了TwoPhaseCommitSinkFunction,它与Kafka的事务机制结合使用,实现端到端Exactly-Once语义。该函数在检查点开始时启动事务,并在检查点完成时提交事务。如果检查点失败,则事务会被中止,确保数据的一致性[^3]。 ### KafkaFlink协作实现Exactly-Once语义 为了实现端到端Exactly-Once语义,FlinkKafka需要协同工作。具体流程如下: 1. **开启Kafka事务**:Flink的TwoPhaseCommitSinkFunction会在每个检查点周期开始时启动Kafka事务。 2. **写入Kafka**:Flink将处理后的数据写入Kafka,这些写入操作都在事务的上下文中进行。 3. **提交事务**:当Flink完成检查点并确认所有操作成功时,TwoPhaseCommitSinkFunction会提交Kafka事务,确保所有数据对消费者可见。 4. **故障恢复**:如果在检查点过程中发生故障,TwoPhaseCommitSinkFunction会回滚事务,确保未完成的操作不会对消费者可见。 通过这种方式,FlinkKafka共同确保了从数据读取到处理再到写入的整个过程中,数据仅被处理一次,即使在发生故障的情况下也能保持一致性。 ### 示例代码 以下是一个简单的Flink程序示例,展示了如何使用TwoPhaseCommitSinkFunction将数据写入Kafka实现Exactly-Once语义: ```java import org.apache.flink.api.common.functions.MapFunction; import org.apache.flink.streaming.api.datastream.DataStream; import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer; import org.apache.flink.streaming.connectors.kafka.KafkaTransactionState; import org.apache.flink.streaming.connectors.kafka.TwoPhaseCommitSinkFunction; import java.util.Properties; public class KafkaExactlyOnceExample { public static void main(String[] args) throws Exception { // 创建Flink执行环境 StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); // 设置检查点间隔 env.enableCheckpointing(5000); // 读取输入数据流 DataStream<String> input = env.socketTextStream("localhost", 9999); // 配置Kafka生产者属性 Properties kafkaProps = new Properties(); kafkaProps.setProperty("bootstrap.servers", "localhost:9092"); kafkaProps.setProperty("transaction.timeout.ms", "60000"); // 创建TwoPhaseCommitSinkFunction FlinkKafkaProducer<String> kafkaSink = new FlinkKafkaProducer<>( "output-topic", (key, value) -> new ProducerRecord<>(value.topic(), value.partition(), value.key(), value.value()), kafkaProps, FlinkKafkaProducer.Semantic.EXACTLY_ONCE ); // 将数据流写入Kafka input.map(new MapFunction<String, String>() { @Override public String map(String value) throws Exception { return value; } }).addSink(kafkaSink); // 启动Flink作业 env.execute("Kafka Exactly Once Example"); } } ``` 在上述代码中,`FlinkKafkaProducer`使用了`Semantic.EXACTLY_ONCE`语义,确保数据写入Kafka实现Exactly-Once语义。 ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值