如果您正在构建实时流式应用程序, 则事件时间处理(Event Time processing)是您迟早要使用的功能之一。由于在大多数实际用例中消息到达了顺序, 因此您所构建的系统应该有某种方式来理解消息可能会迟到并相应地处理它们的事实。在这个博客文章中, 我们将了解为什么需要事件时间处理以及如何在 ApacheFlink 中启用它。
EventTime 是在现实世界中发生事件的时间, ProcessingTime 是Flink system处理该事件的时间。为了理解事件时间处理的重要性, 我们首先要建立一个基于处理时间的系统, 看看它的缺点。
我们将创建一个大小为10秒的滑动窗口(SlidingWindow), 每5秒滑动一次, 在窗口的末尾, 系统将发出在该时间内收到的消息数。一旦您了解了 EventTime 处理对 SlidingWindow 的作用, 就不难理解它是如何为 TumblingWindow 工作的。我们开始吧
ProcessingTime based system
对于本示例, 我们希望消息具有格式值、值为消息的时间戳和时间戳是在源位置生成此消息的时间。由于我们现在正在构建一个基于处理时间的系统, 下面的代码忽略了时间戳部分。
了解消息是否应包含生成时的信息是一个重要方面。Flink或任何其他系统不是一个魔法盒子, 可以莫名其妙地找出它本身。稍后我们将看到, 事件时间处理提取此时间戳信息来处理后期消息。
val text = senv.socketTextStream("localhost", 9999)
val counts = text.map {(m: String) => (m.split(",")(0), 1) }
.keyBy(0)
.timeWindow(Time.seconds(10), Time.seconds(5))
.sum(1)
counts.print
senv.execute("ProcessingTime processing example")
Case 1: Messages arrive without delay
假设源分别在第十三秒、第十三秒和第十六秒中生成了类型 a 的三条消息。(小时和分钟在这里不重要, 因为窗口大小仅为10秒)。
这些消息将按如下方式落入窗口。第十三秒生成的前两条消息将分为 window1 [5s-15s ] 和 window2 [10s-20s], 第十六秒生成的第三个消息将落入 window2 [10s-20s] 和 window3 [15s-25s]。每个窗口发出的最终计数将分别为 (a,2)、(a,3) 和 (a,1)。
此输出可视为预期行为。现在, 我们将看看当一个消息到达系统后迟到时会发生什么。
Case 2: Messages arrive in delay
现在假设其中一条消息 (在第十三秒生成) 以延迟6秒 (第十九秒) 的时间到达, 可能是由于某些网络拥塞造成的。你能猜到这条消息会落入哪个窗口吗?
延迟消息落到窗口2和 3, 因为19在范围10-20 和15-25 之内。它没有在 window2 中对计算产生任何问题 (因为消息无论如何都应该落入该窗口中), 但它影响了 window1 和 window3 的结果。现在, 我们将尝试使用 EventTime 处理来解决这个问题。
EventTime based system
为了启用 EventTime 处理, 我们需要一个时间戳提取器, 从消息中提取事件时间信息。请记住, 消息是格式值、时间戳。extractTimestamp 方法获取时间戳部分并将其返回为 Long。现在忽略 getCurrentWatermark 方法, 我们稍后再来。
class TimestampExtractor extends AssignerWithPeriodicWatermarks[String] with Serializable {
override def extractTimestamp(e: String, prevElementTimestamp: Long) = {
e.split(",")(1).toLong
}
override def getCurrentWatermark(): Watermark = {
new Watermark(System.currentTimeMillis)
}
}
我们现在需要设置这个时间戳提取器, 并将 TimeCharactersistic 设置为 EventTime。其余的代码仍然与 ProcessingTime 的情况相同。
senv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
val text = senv.socketTextStream("localhost", 9999)
.assignTimestampsAndWatermarks(new TimestampExtractor)
val counts = text.map {(m: String) => (m.split(",")(0), 1) }
.keyBy(0)
.timeWindow(Time.seconds(10), Time.seconds(5))
.sum(1)
counts.print
senv.execute("EventTime processing example")
运行上述代码的结果如下图所示。
结果看起来更好, windows 2 和3现在发出了正确的结果, 但 window1 仍然是错误的。Flink没有将延迟的消息分配给窗口 3, 因为它现在检查了消息的事件时间, 并了解它没有落在该窗口中。但为什么不将消息分配给窗口1?。原因是, 当延迟消息到达系统时 (第十九秒), 窗口1的评估已经完成 (第十五秒)。现在让我们尝试使用水印解决此问题。
请注意, 在窗口2中, 延迟的消息仍然放在第十九秒, 而不是在第十三秒 (它的事件时间)。图中的这种描述是有意的, 表示窗口中的消息不会根据事件时间进行排序。(这可能会在将来发生变化)
Watermarks
Watermarks是一个非常重要和有趣的想法, 我会尽量给你一个简短的概述。如果你有兴趣学习更多, 你可以观看这令人敬畏的谈话从谷歌, 也阅读这个博客从 dataArtisans。Watermark实质上是一个时间戳。当Flink中的操作员接收到Watermark时, 它会理解 (假设) 它不会看到比该时间戳早的任何消息。因此, Watermark也可以被认为是告诉Flink它是多远, 在 "EventTime" 的一种方式。
对于这个例子, 把它看作是告诉Flink消息可以延迟多少的一种方式。在最后一次尝试中, 我们将水印设置为当前系统时间。现在, 我们将水印设置为当前时间-5 秒, 它告诉Flink希望消息的最大值5秒的延迟-这是因为每个窗口只有在watermark通过它时才会被评估。因为我们的watermark是当前时间-5 秒, 第一个窗口 [5s-15s] 将被评估仅在第二十秒。同样的窗口 [10s-20s] 将被评估在第二十五秒等。
override def getCurrentWatermark(): Watermark = {
new Watermark(System.currentTimeMillis - 5000)
}
在这里, 我们假设 eventtime 比当前系统时间大5秒, 但情况并非总是如此。在许多情况下, 最好保持迄今为止收到的最大时间戳 (从消息中提取) 并减去预期的延迟。
在进行上述更改后运行代码的结果是:
最后, 我们有正确的结果, 所有的三窗口现在发出计数的预期-这是 (a,2), (a,3) 和 (a,1)。
Allowed Lateness(允许迟到)
在前面的方法中, 我们使用了 "watermark延迟", 直到watermark超过 window_length + 延迟, 窗口才会开火。如果你想容纳后期的事件, 并希望窗口及时开火, 你可以使用允许迟到。如果允许延迟设置, Flink不会丢弃消息, 除非它是过去的 window_end_time + 允许迟到。一旦收到延迟消息, Flink将提取它的时间戳, 并检查它是否在允许迟到范围内, 然后它将检查是否打开窗口 (根据触发器设置)。因此, 请注意, 窗口可能会在这种方法中多次开火,如果您需要恰好一次处理,您可能希望使您的接收器幂等。