时间语义之水位线(Watermask)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


介绍

什么是水位线

在事件时间语义下,我们不依赖系统时间,而是基于数据自带的时间戳去定义了一个时钟,
用来表示当前时间的进展。于是每个并行子任务都会有一个自己的逻辑时钟,它的前进是靠数
据的时间戳来驱动的。
我们可以把时钟也以数据的形式传递出去,告诉下游任务当前时间的进展;而且这个时钟
的传递不会因为窗口聚合之类的运算而停滞。一种简单的想法是,在数据流中加入一个时钟标
记,记录当前的事件时间;这个标记可以直接广播到下游,当下游任务收到这个标记,就可以
更新自己的时钟了。由于类似于水流中用来做标志的记号,在Flink中,这种用来衡量事件时
间(Event Time)进展的标记,就被称作“水位线”(Watermark)。
具体实现上,水位线可以看作一条特殊的数据记录,它是插入到数据流中的一个标记点,
主要内容就是一个时间戳,用来指示当前的事件时间。而它插入流中的位置,就应该是在某个
数据到来之后;这样就可以从这个数据中提取时间戳,作为当前水位线的时间戳了。
在这里插入图片描述

1. 有序流中的水位线

在理想状态下,数据应该按照它们生成的先后顺序、排好队进入流中;而在实际应用中,
如果当前数据量非常大,可能会有很多数据的时间戳是相同的,这时每来一条数据就提取时间
戳、插入水位线就做了大量的无用功。所以为了提高效率,一般会每隔一段时间生成一个水位
线,这个水位线的时间戳,就是当前最新数据的时间戳,如图所示。所以这时的水位线,
其实就是有序流中的一个周期性出现的时间标记。
在这里插入图片描述

2. 乱序流中的水位线

在分布式系统中,数据在节点间传输,会因为网络传输延迟的不确定性,导致顺序发生改
变,这就是所谓的“乱序数据”。
在这里插入图片描述
对于连续数据流,我们插入新的水位线时,要先判断一下时间戳是否比之前的大,否则就
不再生成新的水位线,如图所示。也就是说,只有数据的时间戳比当前时钟大,才能推动
时钟前进,这时才插入水位线。
在这里插入图片描述

如果考虑到大量数据同时到来的处理效率,我们同样可以周期性地生成水位线。这时只需
要保存一下之前所有数据中的最大时间戳,需要插入水位线时,就直接以它作为时间戳生成新
的水位线,如图所示。
在这里插入图片描述
为了让窗口能够正确收集到迟到的数据,我们可以等上2秒;也就是用当前已有数据的最
大时间戳减去2秒,就是要插入的水位线的时间戳,如图所示。
在这里插入图片描述

如果仔细观察就会看到,这种“等2秒”的策略其实并不能处理所有的乱序数据。所以我
们可以试着多等几秒,也就是把时钟调得更慢一些。最终的目的,就是要让窗口能够把所有迟
到数据都收进来,得到正确的计算结果。对应到水位线上,其实就是要保证,当前时间已经进
展到了这个时间戳,在这之后不可能再有迟到数据来了。
下面是一个示例,我们可以使用周期性的方式生成正确的水位线。
在这里插入图片描述

3. 水位线的特性

现在我们可以知道,水位线就代表了当前的事件时间时钟,而且可以在数据的时间戳基础
上加一些延迟来保证不丢数据,这一点对于乱序流的正确处理非常重要。
我们可以总结一下水位线的特性:
⚫ 水位线是插入到数据流中的一个标记,可以认为是一个特殊的数据
⚫ 水位线主要的内容是一个时间戳,用来表示当前事件时间的进展
⚫ 水位线是基于数据的时间戳生成的
⚫ 水位线的时间戳必须单调递增,以确保任务的事件时间时钟一直向前推进
⚫ 水位线可以通过设置延迟,来保证正确处理乱序数据
⚫ 一个水位线Watermark(t),表示在当前流中事件时间已经达到了时间戳t, 这代表t之
前的所有数据都到齐了,之后流中不会出现时间戳t’ ≤ t的数据
水位线是Flink流处理中保证结果正确性的核心机制,它往往会跟窗口一起配合,完成对
乱序数据的正确处理。

如何生成水位线

1. 生成水位线的总体原则

如果我们希望计算结果能更加准确,那可以将水位线的延迟设置得更高一些,等待的时间
越长,自然也就越不容易漏掉数据。不过这样做的代价是处理的实时性降低了,我们可能为极
少数的迟到数据增加了很多不必要的延迟。
如果我们希望处理得更快、实时性更强,那么可以将水位线延迟设得低一些。这种情况下,
可能很多迟到数据会在水位线之后才到达,就会导致窗口遗漏数据,计算结果不准确。对于这
些 “漏网之鱼”,Flink 另外提供了窗口处理迟到数据的方法,我们会在后面介绍。当然,如
果我们对准确性完全不考虑、一味地追求处理速度,可以直接使用处理时间语义,这在理论上
可以得到最低的延迟。
所以Flink中的水位线,其实是流处理中对低延迟和结果正确性的一个权衡机制,而且把
控制的权力交给了程序员,我们可以在代码中定义水位线的生成策略。

2. 水位线生成策略(Watermark Strategies)

在 Flink 的 DataStream API 中 , 有 一 个 单独用于生成水位线的方法:
86
assignTimestampsAndWatermarks(),它主要用来为流中的数据分配时间戳,并生成水位线来指
示事件时间。
具体使用时,直接用DataStream调用该方法即可。

val stream = env.addSource(new ClickSource) 
val withTimestampsAndWatermarks =  
stream.assignTimestampsAndWatermarks(<watermark strategy>) 

assignTimestampsAndWatermarks()方法需要传入一个 WatermarkStrategy 作为参数,这就是
所谓的“水位线生成策略”。WatermarkStrategy中包含了一个“时间戳分配器”TimestampAssigner
和一个“水位线生成器”WatermarkGenerator。

public interface WatermarkStrategy<T>  
extends TimestampAssignerSupplier<T>, 
WatermarkGeneratorSupplier<T>{ 
@Override 
TimestampAssigner<T> 
createTimestampAssigner(TimestampAssignerSupplier.Context context); 
@Override 
WatermarkGenerator<T> 
createWatermarkGenerator(WatermarkGeneratorSupplier.Context context); 
} 

⚫ TimestampAssigner:主要负责从流中数据元素的某个字段中提取时间戳,并分配给
元素。时间戳的分配是生成水位线的基础。
⚫ WatermarkGenerator:主要负责按照既定的方式,基于时间戳生成水位线。在
WatermarkGenerator 接口中,主要又有两个方法:onEvent()和onPeriodicEmit()。
⚫ onEvent:每个事件(数据)到来都会调用的方法,它的参数有当前事件、时间戳,
以及允许发出水位线的一个WatermarkOutput,可以基于事件做各种操作
⚫ onPeriodicEmit:周期性调用的方法,可以由WatermarkOutput发出水位线。周期时间
为处理时间,可以调用环境配置的 setAutoWatermarkInterval()方法来设置,默认为
200ms。

env.getConfig.setAutoWatermarkInterval(60 * 1000L) 

3. Flink 内置水位线生成器

Flink 提供了内置的水位线生成器(WatermarkGenerator),不仅开箱即用简化了编程,而
且也为我们自定义水位线策略提供了模板。
这两个生成器可以通过调用WatermarkStrategy 的静态辅助方法来创建。它们都是周期性
生成水位线的,分别对应着处理有序流和乱序流的场景。
(1)有序流
对于有序流,主要特点就是时间戳单调增长(Monotonously Increasing Timestamps),所以
永远不会出现迟到数据的问题。这是周期性生成水位线的最简单的场景,直接调用
87
88

WatermarkStrategy.forMonotonousTimestamps()方法就可以实现。简单来说,就是直接拿当前最
大的时间戳作为水位线就可以了。

stream.assignTimestampsAndWatermarks( 
      WatermarkStrategy.forMonotonousTimestamps[Event]() 
        .withTimestampAssigner( 
          new SerializableTimestampAssigner[Event] { 
            override def extractTimestamp(element: Event, recordTimestamp: Long): 
Long = element.timestamp 
          } 
        ) 
    ) 

上面代码中我们调用withTimestampAssigner()方法,将数据中的timestamp字段提取出来,
作为时间戳分配给数据元素;然后用内置的有序流水位线生成器构造出了生成策略。这样,提
取出的数据时间戳,就是我们处理计算的事件时间。
这里需要注意的是,时间戳和水位线的单位,必须都是毫秒。
(2)乱序流
由于乱序流中需要等待迟到数据到齐,所以必须设置一个固定量的延迟时间(Fixed
Amount of Lateness)。这时生成水位线的时间戳,就是当前数据流中最大的时间戳减去延迟的
结果,相当于把表调慢,当前时钟会滞后于数据的最大时间戳。调用WatermarkStrategy.
forBoundedOutOfOrderness()方法就可以实现。这个方法需要传入一个maxOutOfOrderness参
数,表示“最大乱序程度”,它表示数据流中乱序数据时间戳的最大差值;如果我们能确定乱序
程度,那么设置对应时间长度的延迟,就可以等到所有的乱序数据了。
代码示例如下:

import java.time.Duration 
 
import com.atguigu.chapter05.{ClickSource, Event} 
import org.apache.flink.api.common.eventtime.{SerializableTimestampAssigner, 
WatermarkStrategy} 
import org.apache.flink.streaming.api.scala._ 
 
 
object OutOfOrdernessTest { 
  def main(args: Array[String]): Unit = { 
    val env = StreamExecutionEnvironment.getExecutionEnvironment 
 
    env 
      .addSource(new ClickSource) 
      //插入水位线的逻辑 
      .assignTimestampsAndWatermarks( 
      //针对乱序流插入水位线,延迟时间设置为5s 
      WatermarkStrategy 
        .forBoundedOutOfOrderness[Event](Duration.ofSeconds(5)) 
.withTimestampAssigner( 
new SerializableTimestampAssigner[Event] { 
// 指定数据中的哪一个字段是时间戳 
override def extractTimestamp(element: Event, recordTimestamp: Long): 
Long = element.timestamp 
} 
) 
) 
.print() 
env.execute() 
} 
} 

上面代码中,我们同样提取了timestamp字段作为时间戳,并且以5秒的延迟时间创建了
处理乱序流的水位线生成器。
事实上,有序流的水位线生成器本质上和乱序流是一样的,相当于延迟设为0的乱序流水
位线生成器,两者完全等同:

WatermarkStrategy.forMonotonousTimestamps() 
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(0)) 

4. 自定义水位线策略

一般来说,Flink 内置的水位线生成器就可以满足应用需求了。不过有时我们的业务逻辑
可能非常复杂,这时对水位线生成的逻辑也有更高的要求,我们就必须自定义实现水位线策略
WatermarkStrategy 了。
在WatermarkStrategy 中,时间戳分配器TimestampAssigner 都是大同小异的,指定字段提
取时间戳就可以了;而不同策略的关键就在于 WatermarkGenerator 的实现。整体说来,Flink
有两种不同的生成水位线的方式:一种是周期性的(Periodic),另一种是断点式的(Punctuated)。
(1)周期性水位线生成器(Periodic Generator)
周期性生成器一般是通过onEvent()观察判断输入的事件,而在onPeriodicEmit()里发出水
位线。
(2)断点式水位线生成器(Punctuated Generator)
断点式生成器会不停地检测 onEvent()中的事件,当发现带有水位线信息的特殊事件时,
就立即发出水位线。一般来说,断点式生成器不会通过onPeriodicEmit()发出水位线。

5. 在自定义数据源中发送水位线

我们也可以在自定义的数据源中抽取事件时间,然后发送水位线。这里要注意的是,在自
定义数据源中发送了水位线以后,就不能再在程序中使用assignTimestampsAndWatermarks 方
法 来生成水位线了。在自定义数据源中生成水位线和在程序中使用
assignTimestampsAndWatermarks 方法生成水位线二者只能取其一。

水位线的传递

在“重分区”(redistributing)的传输模式下,一个任务有可能会收到来自不同分区上游子
任务的数据。而不同分区的子任务时钟并不同步,所以同一时刻发给下游任务的水位线可能并
不相同。这说明上游各个分区处理得有快有慢,进度各不相同,这时我们应该以最慢的那个时
钟,也就是最小的那个水位线为准。
在这里插入图片描述
水位线在上下游任务之间的传递,非常巧妙地避免了分布式系统中没有统一时钟的问题,
每个任务都以“处理完之前所有数据”为标准来确定自己的时钟,就可以保证窗口处理的结果
总是正确的。对于有多条流合并之后进行处理的场景,水位线传递的规则是类似的。

  • 17
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值