4、Flink消息乱序-Time与Watermark

1 引言

2 Time Characteristic

2.1 时间语义

2.2 时间特性

3 Timestamp & Watermark

3.1 Timestamp分配和Watermark生成

3.2 Watermark传播

3.3 Watermark处理

4 Table API中的时间

4.1 Table中指定时间列

4.2 时间列和Table操作

5 个人思考

1 引言


FlinkAPI大体上可以划分为三个层次:最底层的ProcessFunction中间一层的DataStream API最上层的SQL/Table API这三层中的每一层都非常依赖于时间属性。时间属性是流处理中最重要的一个方面,是流处理系统的基石之一,贯穿这三层API。在DataStream API这一层中因为封装方面的原因,我们能够接触到时间的地方不是很多,所以我们将重点放在底层的ProcessFunction和最上层的SQL/Table API

https://ververica.cn/wp-content/uploads/2019/09/001-1024x449.png

2 Time Characteristic


2.1 时间语义

在不同的应用场景中时间语义是各不相同的,Flink作为一个先进的分布式流处理引擎,它本身支持不同的时间语义。其核心是Processing TimeEvent Time(Row Time)、Ingest Time,前两类时间主要的不同点如下表所示:

https://ververica.cn/wp-content/uploads/2019/09/002-1024x415.png

Processing Time是来模拟我们真实世界的时间,其实就算是处理数据的节点本地时间,也不一定就是完完全全的我们真实世界的时间,所以说它是用来模拟真实世界的时间。而Event Time是数据世界的时间,就是我们要处理的数据流世界里面的时间。关于他们的获取方式,Process Time是通过直接去调用本地机器的时间,而Event Time则是根据每一条处理记录所携带的时间戳来判定。

这两种时间在Flink内部的处理以及还是用户的实际使用方面,难易程度都是不同的。相对而言的Processing Time处理起来更加的简单,而Event Time要更麻烦一些。而在使用Processing Time的时候,我们得到的处理结果(或者说流处理应用的内部状态)是不确定的。而因为在Flink内部对Event Time 做了各种保障,使用Event Time的情况下,无论重放数据多少次,都能得到一个相对确定可重现的结果。

因此在判断应该使用Processing Time还是Event Time的时候,可以遵循一个原则:当你的应用遇到某些问题要从上一个checkpoint或者savepoint进行重放,是不是希望结果完全相同。如果希望结果完全相同,就只能用Event Time;如果接受结果不同,则可以用Processing TimeProcessing Time的一个常见的用途是,我们要根据现实时间来统计整个系统的吞吐,比如要计算现实时间一个小时处理了多少条数据,这种情况只能使用Processing Time。可从性能、延迟、确定性对比。

2.2 时间特性

时间的一个重要特性是:时间只能递增,不会来回穿越。在使用时间的时候我们要充分利用这个特性。假设我们有这么一些记录,然后我们来分别看一下 Processing Time 还有Event Time对于时间的处理。

  • 对于Processing Time,因为我们是使用的是本地节点的时间(假设这个节点的时钟同步没有问题),我们每一次取到的Processing Time肯定都是递增的,递增就代表着有序,所以说我们相当于拿到的是一个有序数据流
  • 而在用Event Time的时候因为时间是绑定在每一条的记录上的,由于网络延迟、程序内部逻辑、消息重试或者其他一些分布式系统的原因,数据的时间可能会存在一定程度的乱序,比如下图的例子。在Event Time场景下,我们把每一个记录所包含的时间称作Record Timestamp。如果Record Timestamp所得到的时间序列存在乱序,我们就需要去处理这种情况。

https://ververica.cn/wp-content/uploads/2019/09/004.png

如果单条数据之间是乱序,我们就考虑对于整个序列进行更大程度的离散化。简单地讲,就是把数据按照一定的条数组成一些小批次,但这里的小批次并不是攒够多少条就要去处理,而是为了对他们进行时间上的划分。经过这种更高层次的离散化之后,我们会发现最右边方框里的时间就是一定会小于中间方框里的时间,中间框里的时间也一定会小于最左边方框里的时间。

https://ververica.cn/wp-content/uploads/2019/09/005.png

这时在整个时间序列里需要插入一些类似于标志位的特殊的处理数据,这些特殊的处理数据叫做watermark。一个watermark本质上就代表了这个watermark所包含的timestamp数值,表示以后到来的数据已经再也没有小于或等于这个时间的了

3 Timestamp & Watermark


绝大多数的分布式流计算引擎对于数据都是进行了DAG图的抽象,它有自己的数据源,有处理算子,还有一些数据目标端。数据在不同的逻辑算子之间进行流动。watermarktimestamp有自己的生命周期,接下来从watermark和timestamp的产生、他们在不同的节点之间的传播、以及在每一个节点上的处理,这三个方面来展开介绍。

https://ververica.cn/wp-content/uploads/2019/09/006-1024x442.png

3.1 Timestamp分配和Watermark生成

Flink支持两种watermark生成方式

第一种:是在SourceFunction中产生,相当于把整个的timestamp分配和watermark生成的逻辑放在流处理应用的源头。可以在SourceFunction里面通过两个方法产生watermark

  • 通过collectWithTimestamp方法发送一条数据,其中第一个参数就是我们要发送的数据,第二个参数就是这个数据所对应的时间戳;
  • 也可以调用emitWatermark方法去产生一条watermark,表示接下来不会再有时间戳小于等于这个数值记录。

第二种:如果不想在SourceFunction里生成timestamp或者watermark,或者说使用的SourceFunction本身不支持,可以在使用DataStream API的时候指定,调用的DataStream.assignTimestampsAndWatermarks这个方法,能够接收不同的timestampwatermark的生成器。生成器可以分为两类:

  • 第一类是定期生成器;
  • 第二类是根据一些在流处理数据流中遇到的一些特殊记录生成的。

https://ververica.cn/wp-content/uploads/2019/09/007.png

两种生成器的区别主要有三个方面,首先定期生成是现实时间驱动的,这里的定期生成主要是指watermark(因为timestamp是每一条数据都需要有的),即定期会调用生成逻辑去产生一个watermark。而根据特殊记录生成是数据驱动的,即是否生成watermark不是由现实时间来决定,而是当看到一些特殊的记录就表示接下来可能不会有符合条件的数据再发过来了,这个时候相当于每一次分配Timestamp之后都会调用用户实现的watermark生成方法,用户需要在生成方法中去实现watermark的生成逻辑。

在分配timestamp和生成watermark的过程,虽然在SourceFunctionDataStream中都可以指定,但是还是建议生成的工作越靠近DataSource越好。这样会方便让程序逻辑里面更多的operator去判断某些数据是否乱序。Flink内部提供了很好的机制去保证这些timestampwatermark被正确地传递到下游的节点。

3.2 Watermark传播

https://ververica.cn/wp-content/uploads/2019/09/008-1024x474.png

具体的传播策略基本上遵循以下三点:

  1. watermark会以广播的形式在算子之间进行传播。比如说上游的算子,它连接了三个下游的任务,它会把自己当前的收到的watermark以广播的形式传到下游。
  2. 如果在程序里面收到了一个Long.MAX_VALUE这个数值的watermark,就表示对应的那一条流的一个部分不会再有数据发过来了,它相当于就是一个终止标志
  3. 对于单流而言,这个策略比较好理解,而对于有多个输入的算子,watermark的计算就有讲究了,一个原则是:单输入取其大,多输入取小

举个例子,假设这边蓝色的块代表一个算子的一个任务,然后它有三个输入,分别是 W1W2W3,这三个输入可以理解成任何的输入,这三个输入可能是属于同一个流,也可能是属于不同的流。然后在计算watermark的时候,对于单个输入而言是取他们的最大值,因为我们都知道watermark应该遵循一个单调递增的一个原则。对于多输入,它要统计整个算子任务的watermark时,就会取这三个计算出来的watermark的最小值。即一个多个输入的任务,它的watermark受制于最慢的那条输入流。这一点类似于木桶效应,整个木桶中装的水会就是受制于最矮的那块板。

watermark在传播的时候有一个特点,它的传播是幂等的。多次收到相同的watermark,甚至收到之前的watermark都不会对最后的数值产生影响,因为对于单个输入永远取最大的,而对于整个任务永远取一个最小的。

3.3 Watermark处理

https://ververica.cn/wp-content/uploads/2019/09/009-1024x376.png

一个算子的实例在收到watermark的时候 : (1) 要更新当前的算子时间,这样的话在ProcessFunction里方法查询这个算子时间的时候,就能获取到最新的时间。(2) 会遍历计时器队列,这个计时器队列就是timer,你可以同时注册很多timerFlink会把这些Timer按照触发时间放到一个优先队列中。(3) Flink得到一个时间之后就会遍历计时器的队列,然后逐一触发用户的回调逻辑。通过这种方式,Flink的某一个任务就会将当前的watermark发送到下游的其他任务实例上,从而完成整个watermark的传播,从而形成一个闭环。

3.4 乱序处理

1、单输入情况

假如我们设置10s的时间窗口(window),那么0~10s,10~20s都是一个窗口,以0~10s为例,0位start-time,10为end-time。假如有4个数据的event-time分别是8(A),12.5(B),9(C),13.5(D),我们设置Watermarks为当前所有到达数据event-time的最大值减去延迟值3.5秒,即 watermark = eventtime - 3.5s。那么:

  • 当A到达的时候,Watermarks为max{8}-3.5=8-3.5 = 4.5 < 10,不会触发计算
  • 当B到达的时候,Watermarks为max(12.8,5)-3.5=12.5-3.5 = 9 < 10,不会触发计算
  • 当C到达的时候,Watermarks为max(12.5,8,9)-3.5=12.5-3.5 = 9 < 10,不会触发计算
  • 当D到达的时候,Watermarks为max(13.5,12.5,8,9)-3.5=13.5-3.5 = 10 = 10,触发计算

触发计算的时候,会将AC(因为他们都小于10)都计算进去,而BD属于10-20s这个窗口,通过上面这种方式,我们就将迟到的C计算进去了。这里的延迟3.5s是我们假设一个数据到达的时候,比他早3.5s的数据肯定也都到达了,这个是需要根据经验推算的,假如D到达以后有到达了一个E,event-time=6,但是由于0~10的时间窗口已经开始计算了,所以E就丢了。

2、多输入情况

还是上面的例子,假如窗口有多个输入:inputStream-1 和 inputStream-2。watermark的计算及处理如下:

4 Table API中的时间

下面看一看Table/SQL API中的时间。为了让时间参与到Table/SQL这一层的运算中,我们需要提前把时间属性放到表的schema中,这样我们才能够在SQL语句或者Table的一些逻辑表达式里面去使用这些时间去完成需求。

4.1 Table中指定时间列

其实之前社区就怎么在Table/SQL中去使用时间这个问题做过一定的讨论,是把获取当前Processing Time的方法是作为一个特殊的UDF,还是把时间列物化到整个的schema里面,最终采用了后者。我们这里就分开来讲一讲Processing TimeEvent Time在使用的时候怎么在Table中指定。

https://ververica.cn/wp-content/uploads/2019/09/010-1024x487.png

对于Processing Time,我们知道要得到一个Table对象(或者注册一个 Table)有两种手段:可以从一个DataStream转化成一个 Table;直接通过TableSource去生成这么一个Table

对于第一种方法而言,我们只需要在你已有的这些列中(例子中 f1 f2 就是两个已有的列),在最后用列名.proctime”这种写法就可以把最后的这一列注册为一个 Processing Time,以后在写查询的时候就可以去直接使用这一列。如果 Table 是通过 TableSource 生成的,就可以通过实现这一个 DefinedRowtimeAttributes 接口,然后就会自动根据你提供的逻辑去生成对应的 Processing Time

相对而言,在使用 Event Time 时则有一个限制,因为 Event Time 不像 Processing Time 那样是随拿随用。如果你要从 DataStream 去转化得到一个 Table,必须要提前保证原始的 DataStream 里面已经存在了 Record Timestamp watermark。如果你想通过 TableSource 生成的,也一定要保证你要接入的一个数据里面存在一个类型为 long 或者 timestamp 的这么一个时间字段。

具体来说,如果你要从 DataStream 去注册一个表,和 proctime 类似,你只需要加上列名.rowtime”就可以。需要注意的是,如果你要用 Processing Time,必须保证你要新加的字段是整个 schema 中的最后一个字段,而 Event Time 的时候你其实可以去替换某一个已有的列,然后 Flink 会自动的把这一列转化成需要的 rowtime 这个类型。 如果是通过 TableSource 生成的,只需要实现 DefinedRowtimeAttributes 接口就可以了。需要说明的一点是,在 DataStream API 这一侧其实不支持同时存在多个 Event Timerowtime),但是在 Table 这一层理论上可以同时存在多个 rowtime。因为 DefinedRowtimeAttributes 接口的返回值是一个对于 rowtime 描述的 List,即其实可以同时存在多个 rowtime 列,在将来可能会进行一些其他的改进,或者基于去做一些相应的优化。

4.2 时间列和Table操作

指定完了时间列之后,当我们要真正去查询时就会涉及到一些具体的操作。

这里我列举的这些操作都是和时间列紧密相关,或者说必须在这个时间列上才能进行的。比如说“Over 窗口聚合“Group by 窗口聚合这两种窗口聚合,在写 SQL 提供参数的时候只能允许你在这个时间列上进行这种聚合。第三个就是时间窗口聚合,你在写条件的时候只支持对应的时间列。

最后就是排序,我们知道在一个无尽的数据流上对数据做排序几乎是不可能的事情,但因为这个数据本身到来的顺序已经是按照时间属性来进行排序,所以说我们如果要对一个 DataStream 转化成 Table 进行排序的话,你只能是按照时间列进行排序,当然同时你也可以指定一些其他的列,但是时间列这个是必须的,并且必须放在第一位。

https://ververica.cn/wp-content/uploads/2019/09/011-1024x475.png

为什么说这些操作只能在时间列上进行?因为我们有的时候可以把到来的数据流就看成是一张按照时间排列好的一张表,而我们任何对于表的操作,其实都是必须在对它进行一次顺序扫描的前提下完成的。因为大家都知道数据流的特性之一就是一过性,某一条数据处理过去之后,将来其实不太好去访问它。当然因为 Flink 中内部提供了一些状态机制,我们可以在一定程度上去弱化这个特性,但是最终还是不能超越的限制状态不能太大。所有这些操作为什么只能在时间列上进行,因为这个时间列能够保证我们内部产生的状态不会无限的增长下去,这是一个最终的前提。

5 个人思考


1.生成watermark,后边的timeWindow始终无法触发计算

原因:FilterassignTimeStampsAndWatermarks之后chain在了一起,然后有的stream中没有数据,导致下游算子在收到多个上游的watermark对齐后总是0,进而无法触发window计算。

解决方法:

2.处理延迟数据的问题

watermark allowlateness的区别;

方案:watermark + allowlateness + sideOutPut

3.watermark机制详解

[白话解析] Flink的Watermark机制 - 罗西的思考 - 博客园

Flink流的元素统称为StreamElement,具体包括:StreamRecord StreamStatus WaterMark latencyMarker

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值