这是一个示例,说明了为什么我们需要水印以及它们的工作原理。
在此示例中,我们有一个带时间戳的事件流,这些事件的到达顺序有些混乱,如下所示。显示的数字是事件时间时间戳,指示这些事件实际发生的时间。第一个到达的事件发生在时间 4,随后是更早发生的事件(时间 2),依此类推:
··· 23 19 22 24 21 14 17 13 12 15 9 11 7 2 4 →
现在想象一下我们正在尝试创建一个流排序器。这意味着一个应用程序在流到达时处理每个事件,并发出一个包含相同事件的新流,但按时间戳排序。
一些观察结果:
(1) 我们的流排序器看到的第一个元素是 4,但我们不能立即将其作为排序流的第一个元素释放。它可能已无序到达,并且更早的事件可能尚未到达。事实上,我们对这个流的未来有一些神一样的了解,我们可以看到我们的流排序器应该至少等到 2 到达才能产生任何结果。
结论:一些缓冲和一些延迟是必要的。
(2) 如果我们做错了,我们可能会永远等待。首先,我们的应用程序看到了时间 4 的事件,然后看到了时间 2 的事件。时间戳小于 2 的事件会到达吗?或许。也许不会。我们可以永远等待,也永远看不到 1。
结论:最终我们必须勇敢地发出 2 作为排序流的开始。
(3) 我们需要某种策略,定义对于任何给定的带时间戳的事件,何时停止等待较早事件的到达。
这正是水印的作用- 它们定义何时停止等待较早的事件。
Flink 中的事件时间处理取决于水印生成器将特殊的带时间戳的元素插入流中,称为水印.
我们的流排序器什么时候应该停止等待,并推出 2 来启动排序流?当水印到达时时间戳为 2 或更大。
(4) 我们可以想象不同的策略来决定如何生成水印。
我们知道每个事件都会在一定的延迟后到达,并且这些延迟各不相同,因此某些事件比其他事件延迟得更多。一种简单的方法是假设这些延迟受到某个最大延迟的限制。 Flink 将此策略称为有界无序性水印。很容易想象更复杂的水印方法,但对于许多应用程序来说,固定延迟就足够了。
如果你想构建一个像流排序器这样的应用程序,Flink 的KeyedProcessFunction
是正确的构建块。它提供对事件时间计时器(即根据水印到达而触发的回调)的访问,并具有用于管理缓冲事件所需状态的挂钩,直到轮到它们发送到下游为止。