Flink学习笔记(四)——深入理解Watermark

3,583 阅读4分钟

记录

 上一篇我们主要分析了一下FLink的工作流程。这篇将详细记录一下关于Watermark的理解

产生原因

 Flink流在处理数据时,从事件产生,到经流source,在到operator,中间是有一个过程的,大部分情况,流到operator的数据都是按照事件产生的事件顺序来的,但是也不排除因为网络延迟等原因,导致数据乱序的产生。特别是消费kakfa时,多个分区的消息数据是无序的,这个时候为了保证数据的有序性及计算的及时性,Watermark机制就诞生了。

是什么

 简单来说Watermark就是一种特殊的时间戳,是Flink为了处理EventTime窗口计算设定的一种机制。由source或者自定义的Watermark生成器按照需求生成的一种系统Event,与普通数据流Event一样流转到对应的Operator,接收到Watermark Event的operator不断调整自己管理EventTime clock,Flink保证Watermark单调递增,operator接受到一个Watermark时候就知道次数数据流已经处理到了什么位置也就是时间维度的方式。

生成方式

 在1.10以上版本的Flink中生成水印不在需要去自己实现接口,Flink API提供了一个同时包含TimestampAssigner和WatermarkGenerator的WatermarkStrategy。WatermarkStrategy工具类中提供了很多常用的watermark策略,当然我们也可以在某些场景下构建自己的watermark策略。

public interface WatermarkStrategy<T> extends TimestampAssignerSupplier<T>, WatermarkGeneratorSupplier<T>{

    /**
     * 根据策略实例化一个可分配时间戳的 {@link TimestampAssigner}。
     */
    @Override
    TimestampAssigner<T> createTimestampAssigner(TimestampAssignerSupplier.Context context);

    /**
     * 根据策略实例化一个 watermark 生成器。
     */
    @Override
    WatermarkGenerator<T> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context);
}

 如上所示。通常情况我们不需要实现此接口,而是可以使用WatermarkStrategy工具类中通过的watermark策略或者使用这个工具类将自定义的TimestampAssigner和WatermarkGenerator进行绑定。例如,当我们需要使用有界无序(bounded-out-of-orderness)watermark生成器和一个lambda表达式作为时间戳分配器,如下即可:

WatermarkStrategy
        .<Tuple2<Long, String>>forBoundedOutOfOrderness(Duration.ofSeconds(20))
        .withTimestampAssigner((event, timestamp) -> event.f0);

 其中TimestampAssigner的设置是可选的,通常情况下不需要特别指定。例如使用kafka时就可以直接从数据源中获取到时间戳。

使用策略

 WatermarkStrategy在Flink中有两种使用方式,一种是直接在数据源上使用,另一种是直接在非数据源的操作之后使用。

 推荐使用第一种方式,因为数据源可以利用watermark生成逻辑中有关分片/分区的信息。使用这种方式可以更加精准的跟踪watermark,整体的watermark生成将更加精确,直接在数据源指定watermarkStrategy必须使用特定的数据源接口,例如与kafka链接,使用kafka Connerctor,仅当无法直接在数据源上设置策略是时才使用第二种方式

//第一种 直接在kafka上使用
FlinkKafkaConsumer<MyType> kafkaSource = new FlinkKafkaConsumer<>("myTopic", schema, props);
kafkaSource.assignTimestampsAndWatermarks(
        WatermarkStrategy.
                .forBoundedOutOfOrderness(Duration.ofSeconds(20)));

DataStream<MyType> stream = env.addSource(kafkaSource);
//第二种
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

DataStream<MyEvent> stream = env.readFile(
        myFormat, myFilePath, FileProcessingMode.PROCESS_CONTINUOUSLY, 100,
        FilePathFilter.createDefaultFilter(), typeInfo);

DataStream<MyEvent> withTimestampsAndWatermarks = stream
        .filter( event -> event.severity() == WARNING )
        .assignTimestampsAndWatermarks(<watermark strategy>);

withTimestampsAndWatermarks
        .keyBy( (event) -> event.getGroup() )
        .window(TumblingEventTimeWindows.of(Time.seconds(10)))
        .reduce( (a, b) -> a.add(b) )
        .addSink(...);

 在数据源直接使用时如果因为数据源中的某一个分区/分片在一段时间内未发送事件数据,则意味着watermarkStrategy也不会获得任何数据去生成watermark,在这种情况下可以通过设置有一个空闲时间,当超过这个时间则将这个分片或分区标记为空闲状态。

WatermarkStrategy
        .<Tuple2<Long, String>>forBoundedOutOfOrderness(Duration.ofSeconds(20))
        .withIdleness(Duration.ofMinutes(1));//当时间超过1分钟则设置为空闲状态 

自定义WatermarkGenerator

 除了Flink API中自带的一些已经实现的Watermark策略之外,我们也可以通过WatermarkGenerator接口去自定义实现我们自己的Watermark策略

/**
 * {@code WatermarkGenerator} 可以基于事件或者周期性的生成 watermark。
 *
 * <p><b>注意:</b>  WatermarkGenerator 将以前互相独立的 {@code AssignerWithPunctuatedWatermarks} 
 * 和 {@code AssignerWithPeriodicWatermarks} 一同包含了进来。
 */
@Public
public interface WatermarkGenerator<T> {

    /**
     * 每来一条事件数据调用一次,可以检查或者记录事件的时间戳,或者也可以基于事件数据本身去生成 watermark。
     */
    void onEvent(T event, long eventTimestamp, WatermarkOutput output);

    /**
     * 周期性的调用,也许会生成新的 watermark,也许不会。
     *
     * <p>调用此方法生成 watermark 的间隔时间由 {@link ExecutionConfig#getAutoWatermarkInterval()} 决定。
     */
    void onPeriodicEmit(WatermarkOutput output);
}