课程资料
完整手册:https://bytedance.feishu.cn/docx/doxcnECGEFkCKYqbxaDipK1qrVf
学员手册:https://juejin.cn/post/7123908203590451207/#heading-0
课程视频:https://live.juejin.cn/4354/yc_Window
课程PPT:https://bytedance.feishu.cn/file/boxcn5expS9gYOnxpZUBayXPwVg
一、流/批计算概述
- 批式计算一般是T+1的数仓架构
- 数据实时性越高,数据的价值越高
- 实时计算分为处理时间和事件时间
- 事件时间需要Watermark配合来处理乱序
二、Watermark
2.1 概念
- Watermark定义:当前系统认为的事件时间所在的真实时间
- Watermark产生:一般是从数据的事件时间来产生,产生策略可以灵活多样,最常见的包括使用当前事件时间的时间减去一个固定的delay,来表示可以可以容忍多长时间的乱序
- Watermark传递:这个类似于上节课中介绍的Checkpoint的制作过程,传递就类似于Checkpoint的barrier,上下游task之间有数据传输关系的,上游就会将watermark传递给下游;下游收到多个上游传递过来的watermark后,默认会取其中最小值来作为自身的watermark,同时它也会将自己watermark传递给它的下游。经过整个传递过程,最终系统中每一个计算单元就都会实时的知道自身当前的watermark是多少
2.2 经典问题
如何观察一个任务中的watermark是多少,是否是正常的
- 一般通过Flink Web UI上的信息来观察当前任务的watermark情况
- 这个问题是生产实践中最容易遇到的问题,大家在开发事件时间的窗口任务的时候,经常会忘记了设置watermark,或者数据太少,watermark没有及时的更新,导致窗口一直不能触发
Per-partition|Per-subtask生成watermark的优缺点
- 在Flink里早期都是per-subtask的方式进行watermark的生成,这种方式比较简单。但是如果每个source task如果有消费多个partition的情况的话,那多个partition之间的数据可能会因为消费的速度不同而最终导致数据的乱序程度增加
- 后期(上面图中)就逐步的变成了per-partition的方式来产生watermark,来避免上面的问题
如果有部分partition/subtask会断流,应该如何处理
- 数据断流是很常见的问题,有时候是业务数据本身就有这种特点,比如白天有数据,晚上没有数据。在这种情况下,watermark默认是不会更新的,因为它要取上游subtask发来的watermark中的最小值。此时我们可以用一种IDLE状态来标记这种subtask,被标记为这种状态的subtask,我们在计算watermark的时候,可以把它先排除在外。这样就可以保证有部分partition断流的时候,watermark仍然可以继续更新
算子对于时间晚于watermark的数据的处理
- 对于迟到数据,不同的算子对于这种情况的处理可以有不同的实现(主要是根据算子本身的语义来决定的)
- 比如window对于迟到的数据,默认就是丢弃;比如双流join,对于迟到数据,可以认为是无法与之前正常数据join上
三、Window
3.1 基本功能
TUMBLE Window(滚动窗口)
这是最常见的窗口类型,就是根据数据的时间(可以是处理时间,也可以是事件时间)划分到它所属的窗口中windowStart = timestamp - timestamp % windowSize
,这条数据所属的window就是[windowStart, windowStart + windowSize)
在我们使用window的过程中,最容易产生的一个疑问是,window的划分是subtask级别的,还是key级别的。这里大家要记住,Flink 中的窗口划分是key级别的。 比如下方的图中,有三个key,那每个key的窗口都是单独的。所以整个图中,一种存在14个窗口。
窗口的触发,是时间大于等于window end的时候,触发对应的window的输出(计算有可能提前就增量计算好了),目前的实现是给每个window都注册一个timer,通过处理时间或者事件时间的timer来触发window的输出。
HOP Window(滑动窗口)
了解了上面的TUMBLE窗口的基本原理后,HOP窗口就容易理解了。上面的TUMBLE窗口是每条数据只会落在一个窗口中。在HOP窗口中,每条数据是可能会属于多个窗口的(具体属于多少,取决于窗口定义的大小和滑动),比如下图中假设滑动是1h的话,那窗口大小就是2h,这种情况每条数据会属于两个窗口。除了这一点之外,其它的基本跟HOP窗口是类似的,比如也是key级别划分窗口,也是靠timer进行窗口触发输出。
SESSION Window(会话窗口)
会话窗口跟上面两种窗口区别比较大,上面两个窗口的划分,都是根据当前数据的时间就可以直接确定它所属的窗口。会话窗口的话,是一个动态merge的过程。一般会设置一个会话的最大的gap,比如10min。
那某个key下面来第一条数据的时候,它的window就是 [event_time, event_time + gap),当这个key后面来了另一条数据的时候,它会立即产生一个窗口,如果这个窗口跟之前的窗口有overlap的话,则会将两个窗口进行一个merge,变成一个更大的窗口,此时需要将之前定义的timer取消,再注册一个新的timer。
所以会话窗口要求所有的聚合函数都必须有实现merge
迟到数据处理
根据上面说到的watermark原理,watermark驱动某个窗口触发输出之后,这个窗口如果后面又来了数据,那这种情况就属于是迟到的数据了。(注意,不是数据的时间晚于watermark就算是迟到,而是它所属的窗口已经被触发了,才算迟到)。
对于迟到的数据,我们现在有两种处理方式:
- 使用side output方式,把迟到的数据转变成一个单独的流,再由用户自己来决定如何处理这部分数据
- 直接drop掉
注意:side output只有在DataStream的窗口中才可以用,在SQL中目前还没有这种语义,所以暂时只有drop这一个策略。
增量计算 VS 全量计算
这个问题也是使用窗口的时候最典型的问题之一。先定义一下:
- 增量计算:每条数据到来后,直接参与计算(但是还不需要输出结果)
- 全量计算:每条数据到来后,先放到一个buffer中,这个buffer会存储到状态里,直到窗口触发输出的时候,才把所有数据拿出来统一进行计算
在SQL里面,主要是窗口聚合,所以都是可以增量计算的,也就是每条数据来了之后都可以直接进行计算,而不用把数据都存储起来。举个例子,比如要做sum
计算,那每来一条数据,就直接把新的数据加到之前的sum
值上即可,这样我们就只需要存储一个sum
值的状态,而不需要存储所有buffer的数据,状态量会小很多。
DataStream里面要用增量计算的话,需要用reduce
/aggregate
等方法,就可以用到增量计算。如果用的是process
接口,这种就属于是全量计算。
EMIT触发
上面讲到,正常的窗口都是窗口结束的时候才会进行输出,比如一个1天的窗口,只有到每天结束的时候,窗口的结果才会输出。这种情况下就失去了实时计算的意义了。
那么EMIT触发就是在这种情况下,可以提前把窗口内容输出出来的一种机制。比如我们可以配置一个1天的窗口,每隔5s输出一次它的最新结果,那这样下游就可以更快的获取到窗口计算的结果了。
这个功能只在SQL中,如果是在DataStream中需要完成类似的功能,需要自己定义一些trigger来做。
上节课中,有讲到retract机制,这里需要提一下,这种emit的场景就是一个典型的retract的场景,发送的结果类似于+[1], -[1], +[2], -[2], +[4]这样子。这样才能保证window的输出的最终结果是符合语义的。
Window Offset
按照上面提到的,滚动窗口的计算方式是:windowStart = timestamp - timestamp % windowSize
[windowStart, windowStart + windowSize),这个时间戳是按照unix timestamp来算的。比如我们要用一个一周的窗口,想要的是从周一开始,到周日结束,但是按照上面这种方式计算出来的窗口的话,就是从周四开始的(因为1970年1月1日是周四)。
那么window offset的功能就是可以在计算窗口的时候,可以让窗口有一个偏移。所以最终计算window的公式就变成了:windowStart = timestamp - (timestamp - offset + windowSize) % windowSize
DataStream原生就是支持offset的,但是SQL里并不支持,字节内部版本扩展支持了SQL的window offset功能。
3.2 Window高级优化
以下说的所有的高级优化,都只限于在SQL中的window中才有。在DataStream中,用户需要自己通过代码来实现类似的能力。
- Mini-batch
- Local-global
- Distinct状态复用
- 滑动窗口pane复用