官网博客中: Apache Flink中的端到端精确一次处理概述 对Flink 端到端精确一次处理和两段提交的原理,有详尽的描述

这里要写的是,关于 Flink kafka 端到端精确一次的测试

之前就大概测试过相应内容,应该是测试失败了的,只得到了至少一次的结果(之前的关注点不在这个上面,下面会说明为什么只得到 至少一次 的结果)。

这一次是要做Flink HA 相关的配置,有个重要的点就是任务在异常恢复的时候,是否能保持精确一次,这个关乎线上的数据和我们代码的写法(如果Flink 不能保证精确一次,就需要在代码里添加对应的内容)。

测试的前提当然是,开启了 checkpoint,也设置了checkpoint mode 设为精确一次:

val rock = new RocksDBStateBackend(Common.CHECK_POINT_DATA_DIR)
env.setStateBackend(rock.getCheckpointBackend)
// checkpoint interval 10 minute
env.enableCheckpointing(2 * 60 * 1000)
env.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)

同时 Kafka 的生产者也必须开启精确一次的语义: FlinkKafkaProducer 没有过期的公有构造方法,都需要制定 Kafka 生产者的一致性语义:

public FlinkKafkaProducer(String defaultTopic, KafkaSerializationSchema<IN> serializationSchema, Properties producerConfig, FlinkKafkaProducer.Semantic semantic)
public FlinkKafkaProducer(String defaultTopic, KafkaSerializationSchema<IN> serializationSchema, Properties producerConfig, FlinkKafkaProducer.Semantic semantic, int kafkaProducersPoolSize) 

特别吐槽下,1.10 版本 KafkaSerializationSchema 没有提供对应的实现类,让我这种菜鸟很尴尬

看下我的生产者写法(很挫)

Common.getProp.setProperty("transaction.timeout.ms", 1000*60*2+"")
    val sink = new FlinkKafkaProducer[String](topic+"_out" // 没用了
      , new MyKafkaSerializationSchema[String](topic + "_out") // 指到这里了
      , Common.getProp
      , FlinkKafkaProducer.Semantic.EXACTLY_ONCE)

MyKafkaSerializationSchema 的实现如下:

public class MyKafkaSerializationSchema<T>
        implements KafkaSerializationSchema<T>, KafkaContextAware<T> {
    private String topic;

    public MyKafkaSerializationSchema(String topic) {
        this.topic = topic;
    }

    @Override
    public ProducerRecord<byte[], byte[]> serialize(T element, @Nullable Long timestamp) {

        return new ProducerRecord<>(topic, element.toString().getBytes());
    }

    @Override
    public String getTargetTopic(T element) {
        return null;
    }
}

由于ProducerRecord 必须要指定 topic,但是又获取不到 FlinkKafkaProducer 中指定的topic,就先这样写了

测试代码就简单了:

val topic = "simple_string_kafka"
val source = new FlinkKafkaConsumer[String](topic, new SimpleStringSchema(), Common.getProp)

#开启精确一次语义会报错,必须在kakfa 的 prop 中指定事务过期的时间

Common.getProp.setProperty("transaction.timeout.ms", 1000*60*2+"")
val sink = new FlinkKafkaProducer[String](topic+"_out" // 没用
  , new MyKafkaSerializationSchema[String](topic + "_out") // 指到这里了
  , Common.getProp
  , FlinkKafkaProducer.Semantic.EXACTLY_ONCE)

env.addSource(source)
  .name("source")
  .disableChaining()
  .map(str => {
    str
  })
  .disableChaining()
  .name("map1")
  .map(s => {
    s
  })
  .addSink(sink)
  .name("sink")

代码就这样的,然后就是打包上传服务器。

跑起来就这样的了:

往 source 发送的数据如下:

只有id和create_time 两个字段,id 是个随机的字符串,create_time 是当前的时间戳(每秒一条数据)

接收到 sink topic 的数据如下:

这里的时候,其实是有个疑问的,source 接收到一条数据,直接就写到了sink,我也消费出来了,怎么保证精确一次呢?

kafka 在 0.11 版本之后,增加了对事物的支持,这就是 kafka 端到端一致性的关键。

不然来一条数据,直接就写出去了,下游也消费了,这个时候异常恢复,source是后退了,sink 却没办法后退了。

关键就这 kakfa properties 的这个参数:

// default read_uncommitted
prop.put("isolation.level", "read_committed");

kafka 消费者的隔离级别,默认是 read_uncommitted (读未提交),意思是有消息就读,不管是否提交。

在消费 sink topic 的消费者中添加这个配置,就可以看到,消息不是有一条就输出一条,而是在完成一个checkpoint 后才会输出一批数据,就对应一个checkpoint 时间,flink 处理过的数据,就这样做到 端到端的一致性。

这样Flink 在正常执行的时候,source 消费一条数据,处理后,直接写到sink(也就是kafka),在做checkpoint 的时候,再提交事务,如果这个过程中异常恢复了,source 的offset 直接从 checkpoint 中获取,而之前已经消费过的数据,虽然已经写到kafka,但是没有提交,就这样做到了端到端的一致性。

在 FlinkKafkaProducer 中添加精确一次语义,会到一个异常:

在生产者的 prop 中添加如下配置即可:

Common.getProp.setProperty("transaction.timeout.ms", 1000*60*2+"")

配置合理的超时时间,不能大于kafka 的:transaction.max.timeout.ms 配置(默认 900 秒)