flink-kafka端到端精准一次性

本文介绍了如何通过Flink与Kafka的整合实现端到端精准一次性。关键在于producer的事务支持、consumer的读已提交模式以及checkpoint机制。测试过程中,观察到当设置为READ_COMMITTED模式时,数据会在checkpoint后才被处理,确保精准一次。建议在具备幂等性的下游和无幂等性需求的业务场景中谨慎使用。同时,文章还提到了KafkaProducer分区策略和Flink版本中的相关问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

flink-kafka端到端精准一次性

  • producer有事务
//FlinkKafkaProducer 默认不读取全局配置而是写死默认值AT_LEAST_ONCE 在创建KafkaProducer时要指定时间语义 详见: new FlinkKafkaProducer<>()
Optional<FlinkFixedPartitioner<String>> customPartitioner = Optional.of(new FlinkFixedPartitioner<>());
            flinkKafkaProducer = new FlinkKafkaProducer<>(topic,
                    new SimpleStringSchema(),
                    producerProp,
                    customPartitioner.orElse(null),
                    FlinkKafkaProducer.Semantic.EXACTLY_ONCE,
                    DEFAULT_KAFKA_PRODUCERS_POOL_SIZE);
//生产者的事务超时属性 使用EXACTLY_ONCE需要增加
producerProp.put(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG, 1000 * 60 * 5);
//设置事务ID,这里用了类名做唯一ID
producerProp.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, getClassName());
//开启幂等性
producerProp.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,"true");
producerProp.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION,"5");
  • consumer 有偏移量 且设置读已提交
//设置为读已提交
consumerProp.setProperty(ConsumerConfig.ISOLATION_LEVEL_CONFIG,"read_committed");
  • 中间有checkpoint 所以flink整合kafka 可以实现完整的精准一次性
  • 注意: flink-kafka精准一次的致命弱点: 在进行checkpoint时才会将数据提交到kafka,导致 流处理会转换成微批处理(微批的大小为checkpoint的间隔时间)

代码样例

pom.xml

  • 特别注意如果是本地只用导入flink-connector-kafka_2.12即可
  • 如果是flink集群上需要导入以下三个包才能正常使用kafka 因为flink-connector-kafka包中把后面两个排除掉了
    • flink-connector-kafka_2.12-1.12.0
    • flink-connector-kafka-base_2.12-1.12.0
    • kafka-clients-2.2.0
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-kafka_2.12</artifactId>
    <version>${flink.version}</version>
</dependency>
<!--1.12.0需要在服务器上引入如下两个jar包-->
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-base</artifactId>
    <version>1.12.0</version>
</dependency>
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>2.4.1</version>
</dependency>
本地测试依赖
<!--flink通用连接kafka 可以在写代码时不用指定版本 2.12代表scala版本-->
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-kafka_2.12</artifactId>
    <version>${flink.version}</version>
</dependency>

KafkaConsumerApp关键代码


        env.setParallelism(1);
        env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
        env.getCheckpointConfig().setCheckpointInterval(TimeUnit.MINUTES.toMillis(1));
        Properties consumerProp = new Properties();
        consumerProp.setProperty(ConsumerConfig.ISOLATION_LEVEL_CONFIG, IsolationLevel.READ_COMMITTED.toString().toLowerCase(Locale.ROOT));
        FlinkKafkaConsumer<String> kafkaComsumer = KafkaUtil.getKafkaComsumer("sleepy.test.lq_test2",consumerProp);
        DataStreamSource<String> source = env.addSource(kafkaComsumer);
        source.map(item-> {
            log.debug("KafkaConsumerApp: " + item);
            return item;
        });
        source.addSink(KafkaUtil.getKafkaProducer("sleepy.test.lq_test3"));
        execute();

KafkaProducerApp 关键代码

        env.setParallelism(1);
        env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
        env.getCheckpointConfig().setCheckpointInterval(TimeUnit.MINUTES.toMillis(1));
        FlinkKafkaConsumer<String> kafkaComsumer = KafkaUtil.getKafkaComsumer("sleepy.test.lq_test");
        DataStreamSource<String> source = env.addSource(kafkaComsumer);
        Properties producerProp = new Properties();
        producerProp.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, getClassName());
        producerProp.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,"true");
        producerProp.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION,"5");


        FlinkKafkaProducer<String> kafkaProducer = KafkaUtil.getKafkaProducer("sleepy.test.lq_test2",producerProp);
        source.addSink(kafkaProducer);
        execute();

测试步骤

  1. 打开KafkaConsumerApp, 再打开KafkaProducerApp
  2. 向topic:sleepy.test.lq_test发送数据KafkaProducerApp接受到数据后会以事务的方式发送到topic:sleepy.test.lq_test2(先发送未提交的数据,checkpoint时再执行提交)
  3. KafkaConsumerApp会收到topic:sleepy.test.lq_test2中未提交的数据, 但是我们设置的模式是READ_COMMITTED(读已提交)则数据不会往下面发,也不会处理,等到上游KafkaProducerApp执行checkpoint时会将数据提交,此时KafkaConsumerApp才会处理数据并且打印debug日志最后发往topic:sleepy.test.lq_test3
  4. 如果KafkaConsumerApp设置的模式为READ_UNCOMMITTED(读未提交)则会及时的处理消息,未提交的消息也会被消费到. 此时无法保证两个flink程序整条链路的精准一次性.

源码说明 FlinkKafkaProducer 110行

public enum Semantic {

		/**
		 * Semantic.EXACTLY_ONCE the Flink producer will write all messages in a Kafka transaction that will be
		 * committed to Kafka on a checkpoint.
		 *
		 * <p>In this mode {@link FlinkKafkaProducer} sets up a pool of {@link FlinkKafkaInternalProducer}. Between each
		 * checkpoint a Kafka transaction is created, which is committed on
		 * {@link FlinkKafkaProducer#notifyCheckpointComplete(long)}. If checkpoint complete notifications are
		 * running late, {@link FlinkKafkaProducer} can run out of {@link FlinkKafkaInternalProducer}s in the pool. In that
		 * case any subsequent {@link FlinkKafkaProducer#snapshotState(FunctionSnapshotContext)} requests will fail
		 * and {@link FlinkKafkaProducer} will keep using the {@link FlinkKafkaInternalProducer}
		 * from the previous checkpoint.
		 * To decrease the chance of failing checkpoints there are four options:
		 * <li>decrease number of max concurrent checkpoints</li>
		 * <li>make checkpoints more reliable (so that they complete faster)</li>
		 * <li>increase the delay between checkpoints</li>
		 * <li>increase the size of {@link FlinkKafkaInternalProducer}s pool</li>
		 */
		EXACTLY_ONCE,

总结建议

  • 实时数仓最好使用AT_LEAST_ONCE+下游幂等性来保证端到端的精准一次.这样可以最大程度保证实时性.
  • 整条链路使用精准一次性,可以在以下情况同时满足时使用.但需要提前说明利弊
    1. 没有幂等性保证
    2. 下游无法找出唯一键去重
    3. 业务要求数据的精准一次

其他注意事项

  • KafkaProducer 默认是按照Task的ID%分区数 来向下游进行消息发送的. 如果上游只有一个分区内有数据则只会发往下游topic的一个分区中, 可以使用自定义分区器,来指定数据随机发往下游某个分区
public static FlinkKafkaPartitioner<String> getRandomPartitioner(){
    FlinkKafkaPartitioner<String> flinkKafkaPartitioner = new FlinkKafkaPartitioner<String>() {
        @Override
        public int partition(String record, byte[] key, byte[] value, String targetTopic, int[] partitions) {
            return partitions[RandomUtils.nextInt(0, partitions.length)];

        }
    };
    return flinkKafkaPartitioner;
}
public static FlinkKafkaProducer<String> getKafkaProducer(String topic, Properties prop,FlinkKafkaPartitioner<String> flinkKafkaPartitioner) {
    //如果传入了配置则对默认配置进行覆盖
    if (prop != null) {
        producerProp.putAll(prop);
    }
    FlinkKafkaProducer<String> flinkKafkaProducer;
    if(flinkKafkaPartitioner==null){
        flinkKafkaProducer=new FlinkKafkaProducer<>(topic,
            new SimpleStringSchema(),
            producerProp);
    }else{
        flinkKafkaProducer= new FlinkKafkaProducer<>(topic,
            new SimpleStringSchema(),
            producerProp, Optional.of(flinkKafkaPartitioner));
    }

    return flinkKafkaProducer;
}
  • 2.4.1的kafkaCliet向低版本kafka 发送数据如果使用KafkaSerializationSchema 会导致数据无法发送进去
  • flink1.11.1 有bug KafkaProducer 因为没有重写open方法往kafka下游发送 只会发往固定的0分区, flink-1.11.3已修复
  • SimpleStringSchema使用注意无法处理null, 源码如下
    @Override
	public String deserialize(byte[] message) {
		return new String(message, charset);
	}
	@Override
	public byte[] serialize(String element) {
		return element.getBytes(charset);
	}

解决方法-改造后

public static class StringSchema extends SimpleStringSchema {
    @Override
    public String deserialize(byte[] message) {
        return message != null ? new String(message, StandardCharsets.UTF_8) : "";
    }
}
<think>嗯,用户深入询问了KafkaFlink在数据中台架构中实现Exactly-Once的原理。从对话历史看,用户对数据中台的技术实现有相当专业的了解,之前已经讨论过Kafka在数据中台中的核心作用,现在聚焦在事务一致性这个高阶话题上。用户提供的引用资料很有价值,主要说明了三点:1)Kafka0.11+的事务支持是实现EE的基础[^1][^2];2)Flink实现EE需要sink端配合[^3];3)消费端也需要事务支持[^4]。这说明用户已经做了功课,需要的是系统化的原理剖析。这个问题可以拆解为三个层面来讲:首先是Flink内部检查点机制如何保障状态一致性,这是基石;其次要说明Kafka作为消息系统如何参与分布式事务;最后需要解释数据中台场景下这种机制如何落地。用户特别强调"架构"视角,所以应该突出组件间的协作关系。注意到用户上次询问过"端到端一致性",这次EE实现其实是延续话题。在数据中台语境下,需要强调SchemaRegistry的作用——它保存的数据格式定义也是事务一致性的组成部分,虽然引用里没提这点,但根据之前的讨论应该补充。用户提供的引用[^3]中那个支持列表特别重要,要重点解释为什么有些sink只能做到at-least-once。不过技术细节容易枯燥,可以用那个Kafka生产者的配置案例来具象化,这样用户能立即联想到实际配置场景。最后那个自动偏移提交的对比很关键,用户引用[^2]提到了checkpoint禁用时的行为差异,这个对比能帮助理解EE和ALO的本质区别。可以用一个简单表格归纳,避免技术叙述过于密集。</think> 在数据中台架构中,KafkaFlink协同实现端到端的Exactly-Once语义需通过**分布式事务机制**完成,核心原理分为以下三部分: --- ### **一、Flink检查点机制(Checkpoint)** Flink通过**分布式快照**保障内部状态一致性: 1. **屏障传播** JobManager周期性广播**Barrier**信号,当算子接收到所有输入的Barrier后,将自身状态快照写入**持久化存储(如HDFS)**[^2] $$ \text{Checkpoint}_n = \bigoplus_{i=1}^{k} S_i \quad (S_i\text{为算子状态}) $$ 2. **异步快照** 状态备份与数据处理并行执行,避免阻塞 3. **精准一次恢复** 故障时从最近完整Checkpoint恢复,重放未确认数据 > ✅ **作用范围**:Flink计算拓扑内部状态一致性 --- ### **二、Kafka事务生产与消费** #### 1. **生产者端(写入Sink)** - Kafka ≥0.11支持事务API - Flink Kafka Sink通过事务ID管理写入批次: ```java // KafkaSink配置事务参数 KafkaSink.<String>builder() .setProperty("transactional.id", "flink-job-001") // 唯一ID .setProperty("enable.idempotence", "true") // 启用幂等 .build(); ``` - **两阶段提交逻辑**: - **预提交**:数据写入Kafka但标记为`未提交` - **Checkpoint完成**:FlinkKafka发送`commitTransaction()`[^1] #### 2. **消费者端(Source)** - Flink Kafka Consumer绑定到Checkpoint: ```java env.addSource(new FlinkKafkaConsumer<>( "input_topic", new JSONKeyValueDeserializationSchema(), props).setCommitOffsetsOnCheckpoints(true) // 偏移量随Checkpoint提交 ); ``` - 偏移量提交由Flink控制,替代Kafka自动提交[^2] > ✅ **协作机制**:Offset提交与Checkpoint绑定,实现读取位置一致性 --- ### **三、端到端事务链路(数据中台场景)** ```mermaid flowchart TD A[业务数据源] --> B(Kafka Topic A) B --> C{Flink Job} C -->|1. 消费Topic A| D[计算状态] D -->|2. 写入结果| E(Kafka Topic B) subgraph 事务控制点 C -- Checkpoint触发--> F[持久化状态/Offset] F -- 提交信号--> E end ``` 1. **原子性保障** - 当Checkpoint完成时: - Topic A偏移量提交 - Topic B事务提交 - 状态快照持久化 **三者原子执行**,否则整体回滚 2. **故障恢复流程** - 重启后加载最近Checkpoint $C_n$ - 重置Topic A消费偏移量至 $C_n$ 记录位置 - 回滚Topic B中$C_n$后写入的数据 3. **数据中台关键配置** ```properties # Flink检查点配置 execution.checkpointing.interval: 5000 # 5秒触发Checkpoint execution.checkpointing.mode: EXACTLY_ONCE # Kafka生产者 isolation.level: read_committed # 只读已提交数据 ``` > 🔐 **完整性与效率平衡**:事务周期与Checkpoint间隔需权衡,过短增加开销,过长扩大故障恢复范围 --- ### **与传统机制的对比** | **机制** | 偏移量提交方式 | 数据重复风险 | 故障恢复粒度 | |------------------|------------------------|--------------|--------------------| | 无Checkpoint | Kafka自动提交 | 高(至少一次)| 偏移量区间 | | Checkpoint+ALO | Flink同步提交 | 中(至少一次)| 单条数据 | | **Checkpoint+EE**| 事务绑定提交 | **零** | **原子事务批次** | --- ### **典型问题解决方案** > **Q:事务ID冲突导致数据丢失?** > A:为每个Flink Job配置唯一`transactional.id`,Kafka根据ID保留未完成事务数据[^1] > **Q:Checkpoint超时影响吞吐?** > A:调整`state.backend`为RocksDB,增量快照可提升效率: > ```python > env.set_state_backend(RocksDBStateBackend()) > ``` > **Q:下游非事务系统如何保障一致性?** > A:通过**两阶段提交抽象**实现: > - 实现`TwoPhaseCommitSinkFunction`接口 > - 示例代码参考:[Flink官方文档](https://nightlies.apache.org/flink/flink-docs-release-1.18/docs/dev/datastream/fault-tolerance/exactly_once/) ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值