其它译文可连接如下:
🧩 State & Fault Tolerance。Flink 中的状态及容错
🧩 Flink Streaming (DataStream API) Operators - Windows。介绍 Flink 中的 Window 函数,含 Watermark 的理解
🧩 Flink Streaming (DataStream API) Operators。基于 1.7 官方文档,算子介绍。
DataStream API / Event Time
目录
- Oveiview
- Event Time
- 生成水位线(Generating Watermarks)
- 水位线策略简介(Introduction to Watermark Strategies)
- 使用水位线策略(Using Watermark Strategies)
- 处理空闲 Sources(Dealing With Idle Sources)
- 编写水位线生成器(Writing WatermarkGenerators)
- 水位线策略和 Kafka 连接器(Watermark Strategies and the Kafka Connector)
- 如何操作处理水位线(How Operators Process Watermarks)
- 不推荐使用的 `AssignerWithPeriodicWatermarks` 和 `AssignerWithPunctuatedWatermarks`
- 内置水位线生成器(Builtin Watermark Generators)
- 单调递增时间戳(Monotonously Increasing Timestamps)
- 固定延迟量(Fixed Amount of Lateness)
Oveiview
Event Time
在本节中,您将学习编写时间感知的 Flink 程序。请看下时间流处理(Timely Stream Processing)以了解时间流处理背后的概念。
有关如何在Flink程序中使用时间的知识请参阅窗口和 ProcessFunction。
使用事件时间处理的先决条件是设置正确的时间特性,该设置定义了数据流源的行为方式(例如它们是否将分配时间戳)以及诸如KeyedStream.timeWindow(Time.seconds(30))之类的窗口操作应使用什么时间概念。
您可以使用 StreamExecutionEnvironment.setStreamTimeCharacteristic() 设置时间特征:
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
DataStream<MyEvent> stream = env.addSource(new FlinkKafkaConsumer<MyEvent>(topic, schema, props));
stream
.keyBy( (event) -> event.getUser() )
.timeWindow(Time.hours(1))
.reduce( (a, b) -> a.add(b) )
.addSink(...);
请注意,为了在事件时间中运行此示例,程序需要使用直接为数据定义事件时间并自己发出水位线的源,或者程序必须在源之后注入Timestamp Assigner&Watermark Generator。 这些功能描述了如何访问事件时间戳,以及事件流呈现出何种程度的乱序。
生成水位线(Generating Watermarks)
在本节中,您将了解 Flink 提供的用于处理事件时间时间戳和水位线的 API。 有关事件时间、处理时间和摄取时间的介绍,请参阅introduction to event time。
水位线策略简介(Introduction to Watermark Strategies)
为了使用事件时间 Flink 需要知道事件时间戳,这意味着流中的每个元素都需要分配其事件时间戳。通常这是通过使用 TimestampAssigner 从元素中的某些字段访问或提取时间戳来完成的。
时间戳分配与生成水位线密切相关的,水位线告诉系统事件时间的进展,您可以通过指定 WatermarkGenerator 进行配置。
Flink API 需要一个同时包含 TimestampAssigner 和 WatermarkGenerator 的 WatermarkStrategy,作为 WatermarkStrategy 上的静态方法有许多常见的策略可以直接使用,但用户也可以在需要时构建自己的策略。
为了完整起见,以下是接口:
public interface WatermarkStrategy<T> extends TimestampAssignerSupplier<T>, WatermarkGeneratorSupplier<T>{
/**
* 实例化一个 {@link TimestampAssigner}, 以此分配时间戳策略
*/
@Override
TimestampAssigner<T> createTimestampAssigner(TimestampAssignerSupplier.Context context);
/**
* 实例化一个 WatermarkGenerator ,以此生成水位线策略
*/
@Override
WatermarkGenerator<T> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context);
}
如前所述您通常不会自己实现此接口,而是将WatermarkStrategy上的静态 helper 方法用于常见的水位线策略,或者将自定义的 TimestampAssigner 与 WatermarkGenerator 捆绑在一起。 例如要使用无序水位线和 lambda 函数作为时间戳分配器,请使用以下代码:
WatermarkStrategy
.<Tuple2<Long, String>>forBoundedOutOfOrderness(Duration.ofSeconds(20))
.withTimestampAssigner((event, timestamp) -> event.f0);
指定一个 TimestampAssigner 是可选的,在大多数情况下您实际上并不想指定一个。 例如当使用 Kafka 或 Kinesis 时,您将直接从 Kafka 或 Kinesis 记录中获得时间戳。
我们将在稍后的编写 WatermarkGenerators 中查看 WatermarkGenerator 接口。
注意:时间戳和水位线都指定为自1970-01-01T00:00:00Z 的Java纪元以来的毫秒数。
使用水位线策略(Using Watermark Strategies)
Flink 应用程序中有两个地方可以使用 WatermarkStrategy:1)直接在 source 上,2)在非Source 操作之后。
第一种选择是更可取的,因为它允许source利用水位线逻辑中有关 分片/分区/切分(shards/partitions/splits)的知识,然后 source 通常可以在更精细的级别上跟踪水位线,并且 source 产生的整体水位线将更加准确。直接在 source 代码上指定 WatermarkStrategy 通常意味着您必须使用特定于source的接口(请参阅水位线策略和Kafka连接器)了解如何在Kafka连接器上使用水位线策略和Kafka连接器,以获取有关每个分区水位线如何工作的更多详细信息。
仅当无法直接在source代码上设置策略时,才应使用第二个选项(在任意操作之后设置 WatermarkStrategy):
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
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() )
.timeWindow(Time.seconds(10))
.reduce( (a, b) -> a.add(b) )
.addSink(...);
使用 WatermarkStrategy以这种方式获取一个流并产生带有时间戳记的数据和水位线的新流。如果原始流已经具有时间戳 和/或 水位线,则时间戳分配器将覆盖它们。
处理空闲 Sources(Dealing With Idle Sources)
如果输入的 分片/分区/切分(shards/partitions/splits)中的一个在一段时间内未携带事件,则意味着 WatermarkGenerator也不会获得任何新信息作为水位线基础。我们称其为空闲输入或空闲 Sources,这是一个问题,因为您的某些分区可能仍然承载事件,在那种情况下水位线将被保留,因为它是在所有不同的并行水位线上计算的最小值。
为了解决这个问题,您可以使用 WatermarkStrategy 来检测空闲状态并将输入标记为空闲,WatermarkStrategy为此提供了一个方便的帮手:
WatermarkStrategy
.<Tuple2<Long, String>>forBoundedOutOfOrderness(Duration.ofSeconds(20))
.withIdleness(Duration.ofMinutes(1));
编写水位线生成器(Writing WatermarkGenerators)
TimestampAssigner 是一个简单的函数,可从事件中提取字段,因此我们无需详细查看它们。另一方面 WatermarkGenerator 的编写要复杂一些,我们将在接下来的两节中介绍如何做到这一点,这是 WatermarkGenerator 接口:
/**
* The {@code WatermarkGenerator} 根据时间 或周期性的(以固定的间隔)生成水位线
*
* <p><b>注意:</b> 此 WatermarkGenerator 包含了之前的 {@code AssignerWithPunctuatedWatermarks} 和 {@code AssignerWithPeriodicWatermarks}.
*/
@Public
public interface WatermarkGenerator<T> {
/**
* 每个事件调用,允许水位线生成器检查并记住时间时间戳,或基于事件本身发出水位线
*/
void onEvent(T event, long eventTimestamp, WatermarkOutput output);
/**
* 周期调用,并可能发出新的水位线。
*
* <p>调用此方法和生成水位线的时间间隔取决于 {@link ExecutionConfig#getAutoWatermarkInterval()}.
*/
void onPeriodicEmit(WatermarkOutput output);
}
有两种不同的水位线生成方式:周期和每事件。
周期性生成器通常通过 onEvent() 观察传入的事件,然后在框架调用onPeriodicEmit 时发出水位线。
每事件生成器将查看 onEvent() 中的事件,并等待在流中携带水位线信息的特殊标记事件或标点,当看到这些事件之一时,它将立即发出水位线。通常没事件生成器不会从 onPeriodicEmit() 发出水位线。
接下来,我们将研究如何为每种样式实现生成器。
编写周期性水位线生成器(Writing a Periodic WatermarkGenerator)
周期生成器观察流事件并定期生成水位线(可能取决于流元素或仅基于处理时间)。
通过 ExecutionConfig.setAutoWatermarkInterval(...)定义生成水位线的时间间隔(每n毫秒),生成器的onPeriodicEmit()方法每次都会被调用,如果返回的水位线非空且大于前一个水位线,则将发出新的水位线。
在这里,我们显示了两个使用定期水位线生成的水位线生成器的简单示例,请注意 Flink 附带了 BoundedOutOfOrdernessWatermarks,它是一个 WatermarkGenerator,其工作原理与下面显示的 BoundedOutOfOrdernessGenerator 相似,您可以在此处阅读有关使用它的信息。
/**
* 这个生成器会生成水位线,假设元素的到达顺序不确定,但是只是在一定程度上。
* 特定时间戳 t 的最新元素将在时间戳 t 的最早元素之后最多 n 毫秒到达。
*/
public class BoundedOutOfOrdernessGenerator implements WatermarkGenerator<MyEvent> {
private final long maxOutOfOrderness = 3500; // 3.5秒
private long currentMaxTimestamp;
@Override
public void onEvent(MyEvent event, long eventTimestamp, WatermarkOutput output) {
currentMaxTimestamp = Math.max(currentMaxTimestamp, eventTimestamp);
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// 将水位线作为当前最高时间戳减去无序边界值发出
output.emitWatermark(new Watermark(currentMaxTimestamp - maxOutOfOrderness - 1));
}
}
/**
* 该生成器生成的水位线滞后与处理时间一定量的(fixed amount).
* 它假设元素在有界延迟后到达 Flink。
*/
public class TimeLagWatermarkGenerator implements WatermarkGenerator<MyEvent> {
private final long maxTimeLag = 5000; // 5 seconds
@Override
public void onEvent(MyEvent event, long eventTimestamp, WatermarkOutput output) {
// 不需要做任何事情,因为我们在 processing time 上执行
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
output.emitWatermark(new Watermark(System.currentTimeMillis() - maxTimeLag));
}
}
编写每事件水位线生成器(Writing a Punctuated WatermarkGenerator)
符号水位线生成器(有时也叫做每事件生成器)将观察事件流,并在看到带有水位线信息的特殊元素时发出水位线。
这样,您就可以实现一个符号生成器,该符号生成器在事件表明它带有特定标记时会发出水位线:
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) {
// 不需要做任何处理,因为我们对上述事件作出反应
}
}
注意:可以在每个事件上生成水位线。但是,由于每个水位线都会在下游引起一些计算,因此过多的水位线会降低性能。
水位线策略和 Kafka 连接器(Watermark Strategies and the Kafka Connector)
当使用 Apache Kafka 作为数据源时,每个 Kafka 分区可能都有一个简单的事件时间模式(时间戳增加或有界乱序)。但是,在使用来自 Kafka 的流时,通常会并行使用多个分区,从而将来自分区的事件交错并破坏每个分区的模式(这是 Kafka 的客户端工作方式所固有的)。
在这种情况下,您可以使用 Flink 的可识别 Kafka 分区的水位线。使用该功能,将在Kafka使用者内部针对每个Kafka分区生成水位线,并且每个分区的水位线合并方式与在流 shuffle 时合并水位线的方式相同。
例如,如果事件时间戳严格按照每个 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);

如何操作处理水位线(How Operators Process Watermarks)
通常,要求 operator 在将给定水位线转发给下游之前完全处理给定水位线。例如 WindowOperator 将首先评估应触发的所有窗口,只有在产生了所有由水位线触发的输出之后,水位线本身才会被发送到下游。换句话说,由于水位线的出现而产生的所有元素将在水位线之前发出。
相同的规则适用于 TwoInputStreamOperator。 但是在这种情况下 operators 的当前水位线被定义为其两个输入的最小值。
此行为的详细信息由 OneInputStreamOperator#processWatermark、TwoInputStreamOperator#processWatermark1 和 TwoInputStreamOperator#processWatermark2 方法的实现定义。
不推荐使用的 AssignerWithPeriodicWatermarks 和 AssignerWithPunctuatedWatermarks
在介绍当前的 WatermarkStrategy、TimestampAssigner 和 WatermarkGenerator 的抽象之前,Flink 使用了 AssignerWithPeriodicWatermarks 和 AssignerWithPeriodicWatermarks。您仍会在API中看到它们,但建议使用新接口,因为它们提供了更清晰的关注点分离(separation of concerns)并且还统一了水位线生成的定期和 punctuated 风格。
内置水位线生成器(Builtin Watermark Generators)
如生Generating Watermarks中所述,Flink提供了抽象,允许编程者分配自己的时间戳并发出自己的水位线,更具体地说,可以通过实现 WatermarkGenerator 接口来实现。
为了进一步简化此类任务的编程工作,Flink 附带了一些预先实现的时间戳分配器。本节提供了它们的列表,除了开箱即用的功能外,它们的实现还可以作为自定义实现的示例。
单调递增时间戳(Monotonously Increasing Timestamps)
周期生成水位线的最简单的特殊情况是给定 source 任务看到的时间戳以升序出现。在这种情况下,当前时间戳始终可以充当水位线,因为没有更早的时间戳会到达。
请注意,每个并行数据源任务的时间戳都仅递增,例如如果在一个特定的设置中,一个并行数据源实例读取一个 Kafka 分区,则仅在每个 Kafka 分区内将时间戳递增是必要的。每当对并行流进行shuffle、union、连接或合并时,Flink 的水位线合并机制将生成正确的水位线。
WatermarkStrategy.forMonotonousTimestamps();
固定延迟量(Fixed Amount of Lateness)
周期性水位线生成的另一个示例是水位线在流中看到的最大(事件时间)时间戳落后固定时间量的情况,这种情况涵盖了事先知道流中可能遇到的最大延迟的场景,例如当创建包含带有时间戳的元素的自定义源时,该时间戳会在固定的时间段内传播以进行测试。对于这些情况,Flink 提供了 BoundedOutOfOrdernessWatermarks 生成器,该生成器将 maxOutOfOrderness 作为参数,即在计算给定窗口的最终结果时允许元素延迟被忽略之前的最长时间。延迟对应于 t-t_w 的结果,其中t是元素的(事件时间)时间戳,而 t_w 是先前水位线的时间戳。默认情况下,如果 延迟 > 0,则将元素视为延迟,在为其相应窗口计算作业结果时将忽略该元素。有关使用延迟元素的更多信息,请参见有关允许延迟的文档。
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(10));
本文深入探讨Flink中的水位线机制,包括其在事件时间处理中的作用、不同生成策略及其应用,特别关注周期性和每事件水位线生成器的实现细节。
1811

被折叠的 条评论
为什么被折叠?



