一、水印策略介绍
为了处理event time,Flink需要知道事件时间戳,这意味着流中的每个元素都需要分配其事件时间戳。这通常是通过使用TimestampAssigner从元素的某个字段(访问/提取)时间戳来完成的。
时间戳分配与生成水印是同步进行的,它告诉系统事件时间上的进展。您可以通过指定水印生成器来配置它。
Flink API需要一个包含TimestampAssigner和水印生成器的水印策略。许多常用的策略都可以作为水印策略的静态方法开箱即用,但用户也可以在需要时构建自己的策略。
为了完整起见,下面是接口:
public interface WatermarkStrategy<T> extends TimestampAssignerSupplier<T>, WatermarkGeneratorSupplier<T>{
/**
* Instantiates a {@link TimestampAssigner} for assigning timestamps according to this
* strategy.
*/
@Override
TimestampAssigner<T> createTimestampAssigner(TimestampAssignerSupplier.Context context);
/**
* Instantiates a WatermarkGenerator that generates watermarks according to this strategy.
*/
@Override
WatermarkGenerator<T> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context);
}
正如前面提到的,您通常不自己实现这个接口,而是使用水印策略上的静态helper方法来实现公共水印策略,或者将自定义TimestampAssigner与水印生成器捆绑在一起。例如,要使用限定范围外的水印和lambda函数作为时间戳分配器,可以使用以下方法:
WatermarkStrategy
.<Tuple2<Long, String>>forBoundedOutOfOrderness(Duration.ofSeconds(20))
.withTimestampAssigner((event, timestamp) -> event.f0);
指定TimestampAssigner是可选的,而且在大多数情况下,您实际上并不想指定一个。例如,当使用Kafka或Kinesis时,您将直接从Kafka/Kinesis记录中获得时间戳。
注意:从Java纪元1970-01-01T00:00:00Z开始,时间戳和水印都指定为毫秒。
二、使用水印策略
在Flink应用程序中,有两个地方可以使用水印策略:1)直接在源上使用,2)在非源操作之后使用。
第一种方法更好,因为它允许源利用关于水印逻辑中的碎片/分区/分割的知识。然后,源通常可以在更细的水平上跟踪水印,而源产生的整体水印将更加准确。直接在源上指定水印策略通常意味着你必须使用一个源特定的接口/参考水印策略
第二个选项(在任意操作之后设置水印策略)只有在无法直接在源上设置策略时才应该使用:
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(...);
这种方法使用水印策略,获取一个流并生成一个带有时间戳元素和水印的新流。如果原始流已经有时间戳和/或水印,时间戳分配者将覆盖它们。
三、处理空闲资源
如果某个输入的split /partitions/shards在一段时间内不携带事件,这意味着水印生成器也不会获得任何可以作为水印基础的新信息。我们称之为空闲输入或空闲源。这是一个问题,因为您的某些分区可能仍然携带事件。在这种情况下,水印将被保留,因为它是在所有不同的并行水印上计算的最小值。
为了解决这个问题,您可以使用一个水印策略来检测空闲性并将一个输入标记为空闲。水印策略为此提供了一个方便的助手:
WatermarkStrategy
.<Tuple2<Long, String>>forBoundedOutOfOrderness(Duration.ofSeconds(20))
.withIdleness(Duration.ofMinutes(1));
四、写入水印生成器
TimestampAssigner是一个从事件中提取字段的简单函数,因此我们不需要详细查看它们。另一方面,水印生成器编写起来有点复杂,我们将在接下来的两部分中查看如何实现。这是水印生成器接口:
/**
* The {@code WatermarkGenerator} generates watermarks either based on events or
* periodically (in a fixed interval).
*
* <p><b>Note:</b> This WatermarkGenerator subsumes the previous distinction between the
* {@code AssignerWithPunctuatedWatermarks} and the {@code AssignerWithPeriodicWatermarks}.
*/
@Public
public interface WatermarkGenerator<T> {
/**
* Called for every event, allows the watermark generator to examine and remember the
* event timestamps, or to emit a watermark based on the event itself.
*/
void onEvent(T event, long eventTimestamp, WatermarkOutput output);
/**
* Called periodically, and might emit a new watermark, or not.
*
* <p>The interval in which this method is called and Watermarks are generated
* depends on {@link ExecutionConfig#getAutoWatermarkInterval()}.
*/
void onPeriodicEmit(WatermarkOutput output);
}
有两种不同的风格的水印产生:周期和标点符号。
周期生成器通常通过onEvent()观察传入的事件,然后在框架调用onPeriodicEmit()时发出水印。
穿孔生成器将查看onEvent()中的事件,并等待流中携带水印信息的特殊标记事件或标点符号。当它看到这些事件之一,它立即发出水印。通常,带有标点符号的生成器不会从onPeriodicEmit()中发出水印。
接下来,我们将了解如何为每种样式实现生成器。
五、编写周期性水印生成器
周期生成器周期性地观察流事件并生成水印(可能取决于流元素,或纯粹基于处理时间)。
水印生成的时间间隔(每n毫秒)是通过ExecutionConfig.setAutoWatermarkInterval(…)定义的。每次都会调用生成器的onPeriodicEmit()方法,如果返回的水印是非空的并且比之前的水印大,则会发出新的水印。
这里我们展示了两个简单的例子,水印生成器使用周期性水印生成。注意,Flink附带了boundedoutoforderness水印,这是一个水印生成器,其工作方式与下面所示的BoundedOutOfOrdernessGenerator类似。你可以在这里了解如何使用它。
/**
* This generator generates watermarks assuming that elements arrive out of order,
* but only to a certain degree. The latest elements for a certain timestamp t will arrive
* at most n milliseconds after the earliest elements for timestamp t.
*/
public class BoundedOutOfOrdernessGenerator implements WatermarkGenerator<MyEvent> {
private final long maxOutOfOrderness = 3500; // 3.5 seconds
private long currentMaxTimestamp;
@Override
public void onEvent(MyEvent event, long eventTimestamp, WatermarkOutput output) {
currentMaxTimestamp = Math.max(currentMaxTimestamp, eventTimestamp);
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// emit the watermark as current highest timestamp minus the out-of-orderness bound
output.emitWatermark(new Watermark(currentMaxTimestamp - maxOutOfOrderness - 1));
}
}
/**
* This generator generates watermarks that are lagging behind processing time by a fixed amount.
* It assumes that elements arrive in Flink after a bounded delay.
*/
public class TimeLagWatermarkGenerator implements WatermarkGenerator<MyEvent> {
private final long maxTimeLag = 5000; // 5 seconds
@Override
public void onEvent(MyEvent event, long eventTimestamp, WatermarkOutput output) {
// don't need to do anything because we work on processing time
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
output.emitWatermark(new Watermark(System.currentTimeMillis() - maxTimeLag));
}
}
六、编写一个标点水印生成器
标点水印生成器将观察事件流,并在看到携带水印信息的特殊元素时发出水印。
这是如何你可以实现一个标点生成器,发出水印时,事件表明,它携带一定的标记:
public class PunctuatedAssigner implements WatermarkGenerator<MyEvent> {
@Override
public void onEvent(MyEvent event, long eventTimestamp, WatermarkOutput output) {
if (event.hasWatermarkMarker()) {
output.emitWatermark(new Watermark(event.getWatermarkTimestamp()));
}
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// don't need to do anything because we emit in reaction to events above
}
}
注意:可以在每个事件上生成水印。但是,由于每个水印都会引起下游的一些计算,过多的水印会降低性能。
七、水印策略和Kafka连接器
当使用Apache Kafka作为数据源时,每个Kafka分区可能都有一个简单的事件时间模式(升序时间戳或有界的外部长度)。然而,当使用来自Kafka的流时,常常会并行使用多个分区,交叉使用来自分区的事件并破坏每个分区的模式(这是Kafka客户端工作的固有方式)。
在这种情况下,您可以使用Flink的kafka分区感知水印生成。使用该特性,水印在Kafka消费者内部生成,每个Kafka分区,每个分区的水印被合并,就像在流变换中合并水印一样。
例如,如果每个Kafka分区的事件时间戳是严格升序的,那么使用升序时间戳水印生成器生成每个分区的水印将得到完美的整体水印。注意,在示例中我们没有提供TimestampAssigner,而是使用Kafka记录本身的时间戳。
下面的插图展示了如何使用每个kafka分区的水印生成,以及在这种情况下水印是如何通过流数据流传播的。
FlinkKafkaConsumer<MyType> kafkaSource = new FlinkKafkaConsumer<>("myTopic", schema, props);
kafkaSource.assignTimestampsAndWatermarks(
WatermarkStrategy.
.forBoundedOutOfOrderness(Duration.ofSeconds(20)));
DataStream<MyType> stream = env.addSource(kafkaSource);
八、操作者如何处理水印
作为一般规则,操作人员需要完全处理给定的水印,然后再将其转发到下游。例如,WindowOperator将首先评估所有应该被触发的窗口,只有在生成由水印触发的所有输出之后,水印本身才会被发送到下游。换句话说,由于水印的出现而产生的所有元素将在水印之前发出。
同样的规则也适用于TwoInputStreamOperator。但是,在这种情况下,该操作符的当前水印被定义为它的两个输入的最小值。
这个行为的细节是由一个inputstreamoperator # process水印、两个oinputstreamoperator #processWatermark1和两个oinputstreamoperator #processWatermark2方法的实现定义的。
九、The Deprecated AssignerWithPeriodicWatermarks and AssignerWithPunctuatedWatermarks
在介绍当前的水印策略抽象、TimestampAssigner和水印生成器之前,Flink使用了带有周期性水印的AssignerWithPeriodicWatermarks和带有周期性水印的AssignerWithPeriodicWatermarks。您仍将在API中看到它们,但建议使用新接口,因为它们提供了更清晰的关注点分离,还统一了水印生成的周期和标点风格。