1、水位线生成原则
2、水位线生成策略(Watermark Strategies)
在Flink的DataStream API中 , 有 一 个 单 独 用 于 生 成 水 位 线 的 方法:.assignTimestampsAndWatermarks(),它主要用来为流中的数据分配时间戳,并生成水位线来指示事件时间:
// ------------------------------------------------------------------------ // Timestamps and watermarks // ------------------------------------------------------------------------ /** * Assigns timestamps to the elements in the data stream and generates watermarks to signal * event time progress. The given {@link WatermarkStrategy} is used to create a {@link * TimestampAssigner} and {@link WatermarkGenerator}. * * <p>For each event in the data stream, the {@link TimestampAssigner#extractTimestamp(Object, * long)} method is called to assign an event timestamp. * * <p>For each event in the data stream, the {@link WatermarkGenerator#onEvent(Object, long, * WatermarkOutput)} will be called. * * <p>Periodically (defined by the {@link ExecutionConfig#getAutoWatermarkInterval()}), the * {@link WatermarkGenerator#onPeriodicEmit(WatermarkOutput)} method will be called. * * <p>Common watermark generation patterns can be found as static methods in the {@link * org.apache.flink.api.common.eventtime.WatermarkStrategy} class. * * @param watermarkStrategy The strategy to generate watermarks based on event timestamps. * @return The stream after the transformation, with assigned timestamps and watermarks. */ public SingleOutputStreamOperator<T> assignTimestampsAndWatermarks( WatermarkStrategy<T> watermarkStrategy) { final WatermarkStrategy<T> cleanedStrategy = clean(watermarkStrategy); // match parallelism to input, to have a 1:1 source -> timestamps/watermarks relationship // and chain final int inputParallelism = getTransformation().getParallelism(); final TimestampsAndWatermarksTransformation<T> transformation = new TimestampsAndWatermarksTransformation<>( "Timestamps/Watermarks", inputParallelism, getTransformation(), cleanedStrategy); getExecutionEnvironment().addOperator(transformation); return new SingleOutputStreamOperator<>(getExecutionEnvironment(), transformation); }
具体使用时,直接用DataStream调用该方法即可,与普通的transform方法完全一样。
DataStream<Event> eventDStream = eventDataStream.assignTimestampsAndWatermarks( WatermarkStrategy .<Event>forMonotonousTimestamps() //有序流 Watermark 生成策略 .withTimestampAssigner(new SerializableTimestampAssigner<Event>() { @Override//指定时间戳列 public long extractTimestamp(Event element, long recordTimestamp) { return element.timestamp;//recordTimestamp 是毫秒,如果 Event timestamp 是秒 需要*1000 } }));
注意:原始的时间戳只是写入日志数据的一个字段,如果不提取出来并明确把它分配给数据,Flink是无法知道数据真正产生的时间的。当然,有些时候数据源本身就提供了时间戳信息,比如读取Kafka时,我们就可以从Kafka数据中直接获取时间戳,而不需要单独提取字段分配了。.assignTimestampsAndWatermarks()方法需要传入一个WatermarkStrategy作为参数,这就是所谓 的“水位 线生成策略”。WatermarkStrategy中包含 了一个“时间戳 分配器”TimestampAssigner和一个“水位线生成器”WatermarkGenerator。
@Public public interface WatermarkStrategy<T> extends TimestampAssignerSupplier<T>, WatermarkGeneratorSupplier<T> { // ------------------------------------------------------------------------ // Methods that implementors need to implement. // ------------------------------------------------------------------------ /** Instantiates a WatermarkGenerator that generates watermarks according to this strategy. */ @Override WatermarkGenerator<T> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context); /** * Instantiates a {@link TimestampAssigner} for assigning timestamps according to this strategy. */ @Override default TimestampAssigner<T> createTimestampAssigner( TimestampAssignerSupplier.Context context) { // By default, this is {@link RecordTimestampAssigner}, // for cases where records come out of a source with valid timestamps, for example from // Kafka. return new RecordTimestampAssigner<>(); }
3、Flink内置水位线生成器
WatermarkStrategy这个接口是一个生成水位线策略的抽象,让我们可以灵活地实现自己的需求;但看起来有些复杂,如果想要自己实现应该还是比较麻烦的。好在Flink充分考虑到了我们的痛苦,提供了内置的水位线生成器(WatermarkGenerator),不仅开箱即用简化了编程,而且也为我们自定义水位线策略提供了模板。这两个生成器可以通过调用WatermarkStrategy的静态辅助方法来创建。它们都是周期性生成水位线的,分别对应着处理有序流和乱序流的场景。
有序流策略
对于有序流,主要特点就是时间戳单调增长(Monotonously Increasing Timestamps),所以永远不会出现迟到数据的问题。这是周期性生成水位线的最简单的场景,直接调用WatermarkStrategy.forMonotonousTimestamps()方法就可以实现。简单来说,就是直接拿当前最大的时间戳作为水位线就可以了。
//有序流 Watermark 生成策略 forMonotonousTimestamps() DataStream<Event> eventDStream = eventDataStream.assignTimestampsAndWatermarks( WatermarkStrategy .<Event>forMonotonousTimestamps() //有序流 Watermark 生成策略 .withTimestampAssigner(new SerializableTimestampAssigner<Event>() { @Override//指定时间戳列 public long extractTimestamp(Event element, long recordTimestamp) { return element.timestamp;//recordTimestamp 是毫秒,如果 Event timestamp 是秒 需要*1000 } }));
上面代码中我们调用.withTimestampAssigner()方法,将数据中的timestamp字段提取出来,作为时间戳分配给数据元素;然后用内置的有序流水位线生成器构造出了生成策略。这样,提取出的数据时间戳,就是我们处理计算的事件时间。这里需要注意的是,时间戳和水位线的单位,必须都是毫秒。
乱序流
由于乱序流中需要等待迟到数据到齐,所以必须设置一个固定量的延迟时间(Fixed Amount of Lateness)。这时生成水位线的时间戳,就是当前数据流中最大的时间戳减去延迟的结果,相当于把表调慢,当前时钟会滞后于数据的最大时间戳。调用WatermarkStrategy. forBoundedOutOfOrderness()方法就可以实现。这个方法需要传入一个maxOutOfOrderness参数,表示“最大乱序程度”,它表示数据流中乱序数据时间戳的最大差值;如果我们能确定乱序程度,那么设置对应时间长度的延迟,就可以等到所有的乱序数据了。
//无序流 Watermark 生成,forBoundedOutOfOrderness(Duration.ofSeconds(2) 等2s DataStream<Event> eventDStream1 = eventDataStream.//无序流 Watermark 生成 assignTimestampsAndWatermarks( WatermarkStrategy .<Event>forBoundedOutOfOrderness(Duration.ofSeconds(2)) //无序流 Watermark 生成 2s延迟 .withTimestampAssigner(new SerializableTimestampAssigner<Event>() { @Override public long extractTimestamp(Event element, long recordTimestamp) { //指定时间戳列 return element.timestamp; } }));
上面代码中,我们同样提取了timestamp字段作为时间戳,并且以2秒的延迟时间创建了处理乱序流的水位线生成器。事实上,有序流的水位线生成器本质上和乱序流是一样的,相当于延迟设为0的乱序流水位线生成器,两者完全等同:
WatermarkStrategy.forMonotonousTimestamps()
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(0))
这里需要注意的是,乱序流中生成的水位线真正的时间戳,其实是当前最大时间戳–延迟时间–1,这里的单位是毫秒。为什么要减1毫秒呢?我们可以回想一下水位线的特点:时间戳为t的水位线,表示时间戳≤t的数据全部到齐,不会再来了。如果考虑有序流,也就是延迟时间为0的情况,那么时间戳为7秒的数据到来时,之后其实是还有可能继续来7秒的数据的;所以生成的水位线不是7秒,而是6秒999毫秒,7秒的数据还可以继续来。这一点可以在BoundedOutOfOrdernessWatermarks的源码中明显地看到:
@Override public void onPeriodicEmit(WatermarkOutput output) { output.emitWatermark(new Watermark(maxTimestamp - outOfOrdernessMillis - 1)); }
水位线在事件时间的世界里面,承担了时钟的角色。也就是说在事件时间的流中,水位线是唯一的时间尺度。如果想要知道现在几点,就要看水位线的大小。后面讲到的窗口的闭合,以及定时器的触发都要通过判断水位线的大小来决定是否触发。水位线是一种特殊的事件,由程序员通过编程插入的数据流里面,然后跟随数据流向下游流动。水位线的默认计算公式:水位线=观察到的最大事件时间–最大延迟时间–1毫秒。所以这里涉及到一个问题,就是不同的算子看到的水位线的大小可能是不一样的。因为下游的算子可能并未接收到来自上游算子的水位线,导致下游算子的时钟要落后于上游算子的时钟。比如map->reduce这样的操作,如果在map中编写了非常耗时间的代码,将会阻塞水位线的向下传播,因为水位线也是数据流中的一个事件,位于水位线前面的数据如果没有处理完毕,那么水位线不可能弯道超车绕过前面的数据向下游传播,也就是说会被前面的数据阻塞。这样就会影响到下游算子的聚合计算,因为下游算子中无论由窗口聚合还是定时器的操作,都需要水位线才能触发执行。这也就告诉了我们,在编写Flink程序时,一定要谨慎的编写每一个算子的计算逻辑,尽量避免大量计算或者是大量的IO操作,这样才不会阻塞水位线的向下传递。在数据流开始之前,Flink会插入一个大小是负无穷大(在Java中是-Long.MAX_VALUE)的水位线,而在数据流结束时,Flink会插入一个正无穷大(Long.MAX_VALUE)的水位线,保证所有的窗口闭合以及所有的定时器都被触发。对于离线数据集,Flink也会将其作为流读入,也就是一条数据一条数据的读取。在这种情况下,Flink对于离线数据集,只会插入两次水位线,也就是在最开始处插入负无穷大的水位线,在结束位置插入一个正无穷大的水位线。因为只需要插入两次水位线,就可以保证计算的正确,无需在数据流的中间插入水位线了。水位线的重要性在于它的逻辑时钟特性,而逻辑时钟这个概念可以说是分布式系统里面最为重要的概念之一了,理解透彻了对理解各种分布式系统非常有帮助。具体可以参考LeslieLamport的论文。