这是我参与「第四届青训营 」笔记创作活动的的第2天
一、概述
数据实时性越高,数据价值越高
-
批处理
- 典型架构为T + 1 数仓架构
- 数据计算是天级别的,只能看到前一天的计算结果
- 典型架构为T + 1 数仓架构
-
处理时间
- 数据在流式计算系统中真正处理时所在机器的当前时间
-
事件时间
- 数据产生的时间,如:客户端、传感器、后端代码等上报数据时的时间。
-
实时计算
-
处理时间窗口
- 数据实时流动, 实时计算, 窗口结束直接发送数据,不需要周期调度任务
-
事件时间窗口
- 数据实时进入到真实事件发生的窗口中进行计算,可以有效的处理数据延迟和乱序
- 判断窗口结束时需要插入一些Watermark配合来处理乱序
-
二、Watermark
-
Watermark定义 表示系统认为的当前真实的事件时间
-
Watermark产生 一般是从数据的事件时间来产生,最常见的产生策略包括使用当前事件时间的时间减去一个固定的delay,来表示可以可以容忍多长时间的乱序。
-
Watermark传递 -- 取上流所有subtask的最小值
- 类似Checkpoint的制作过程,传递就类似于Checkpoint的barrier
- 上下游task之间有数据传输关系的,上游就会将watermark传递给下游;下游收到多个上游传递过来的watermark后,默认会取其中最小值来作为自身的watermark,同时它也会将自己watermark传递给它的下游。
- 经过整个传递过程,最终系统中每一个计算单元就都会实时的知道自身当前的watermark是多少。
典型问题
- 一、Per-partition VS per-subtask watermark生成
- Per-partition 早期机制,典型问题是如果一个source subtask消费多个partition,那么多个partition之间的数据读取可能会加剧乱序程度
- Per-subtask 新版本引入基于每个partition单独watermark生成机制避免上述问题
- 二、部分partition/subtask断流
- 如果上游有一个subtask的watermark不更新,则下游的watermark都不更新
- 解决方案:Idle source
- 当某个subtask断流超过配置的idle超过时间时,将当前subtask置为idle,并下发一个idle的状态给下游。下游在计算自身watermark的时候,可以忽略掉当前是idle的那些subtask
- 三、迟到数据处理
- 迟到数据:晚于watermark的到来的数据
- 算子自身的处理:
- Window算子, CEP(Flink复杂事件)默认丢弃
- 双流join, 如果时outer join,则可以认为它不能join到任何数据
三、Window
3.1 Window基本功能
3.1.1 Window分类
- 典型Window
- Tumble Window(滚动窗口)
- Sliding Window(滑动窗口)
- Session Window(会话窗口)
- 其他Window 全局Window、 Count Window、累计窗口
Flink 中的窗口划分是key级别的
Tumble Window(滚动窗口)
- 窗口划分:每个key单独划分, 每条数据只会属于一个窗口
- 根据数据的时间(可以是处理时间,也可以是事件时间)划分到它所属的窗口中
windowStart = timestamp - timestamp % windowSize,这条数据所属的window就是[windowStart, windowStart + windowSize)
- 根据数据的时间(可以是处理时间,也可以是事件时间)划分到它所属的窗口中
- 窗口触发:window结束时间到达的时候一次性触发
- 目前的实现是给每个window都注册一个timer,通过处理时间或者事件时间的timer来触发window的输出。
- 目前的实现是给每个window都注册一个timer,通过处理时间或者事件时间的timer来触发window的输出。
Sliding Window(滑动窗口)
- 窗口划分:每个key单独划分, 每条数据可能属于多个窗口
- 窗口触发:window结束时间到达的时候一次性触发
Session Window(会话窗口)
- 窗口划分:
- 每个key单独划分
- 每条数据会单独划分窗口,如果window之间有交集。则会对窗口进行merge
- 窗口触发:window结束时间到达的时候一次性触发
3.1.2Window使用
3.1.3 迟到数据处理
- 迟到数据
- 当一条数据到来之后,用windowAssigner给它划分的winodw的window end比watermark值还小,说明窗口触发了计算,则该数据为迟到数据
- 只有事件时间才会有迟到数据
- 默认丢弃迟到数据
- 处理方法
3.1.4 增量计算vs全量运算
- 增量计算
- 每条数据到来,直接进行计算,window只存储计算结果
- e.g. DataStream的reduce、aggregate函数, SQL的聚合
- e.g. DataStream的reduce、aggregate函数, SQL的聚合
- 每条数据到来,直接进行计算,window只存储计算结果
- 全量计算
- 每条数据到来,会用buffer存储到window的state中,等到window触发计算的时候,将所有数据拿出来一起计算
- e.g.DataStream的process函数
- 每条数据到来,会用buffer存储到window的state中,等到window触发计算的时候,将所有数据拿出来一起计算
3.1.5 EMIT触发
- EMIT输出:在window没有结束的时候,提前把window计算的部分结果输出出来
- window一般是结束时才输出结果,如果窗口较大,计算结果输出延迟比较高,就失去了实时计算意义。
- 实现
- DataStream 需要通过自定义Trigger实现
- SQL 通过配置
- table.exec.emit.early-fire.enabled= true
- table.exec.emit.early-fire.delay={time}
- 这种emit的场景就是一个典型的retract的场景,发送的结果类似于+[1], -[1], +[2], -[2], +[4]这样子。这样才能保证window的输出的最终结果是符合语义的
3.1.6 Window Offset
- 在计算窗口时,可以让窗口有一个偏移
window = timestamp - timestamp % windowSize时间戳按照unix timestamp来算的。如果我们要用一个一周的窗口,到周日结束,但计算出时从周四开始。(1970.1.1是周四)
- window公式:
window = timestamp - (timestamp - offset + windowSize) % windowSize - DataStream支持,SQL不支持,字节内部版本扩展支持了SQL的window offset
3.2 Window-高级优化
以下所有的高级优化,都只限于在SQL中的window中才有。在DataStream中,用户需要自己通过代码来实现类似的能力。
Mini-batch优化
- 解决频繁访问状态的问题
- 保存Flink状态时,Flink的状态比较大一些都推荐使用rocksdb statebackend,这种情况下,每次的状态访问就都需要做一次序列化和反序列化,这种开销还是挺大的
- 基本原理:即赞一小批数据再进行计算,这批数据每个key的state访问只有一次,这样在单个key的数据比较集中的情况下,对于状态访问可以有效的降低频率,最终提升性能。
- 假设用最简单的方式实现,那就是每个算子内部自己进行攒一个小的batch,这样的话,如果上下游串联的算子比较多,任务整体的延迟就不是很容易控制。
- 所以真正的mini-batch实现,是复用了底层的watermark传输机制,通过watermark事件来作为mini-batch划分的依据,这样整个任务中不管串联的多少个算子,整个任务的延迟都是一样的,就是用户配置的delay时间。
下面这张图展示的是普通的聚合算子的mini-batch原理,window的mini-batch原理是一样的。
local-global优化---倾斜优化
- 主要是可以降低数据shuffle的量,同时也可以缓解数据的倾斜
- 将原本的聚合划分成两阶段
- 第一阶段先做一个local的聚合,这个阶段不需要数据shuffle,是直接跟在上游算子之后进行处理
- 第二个阶段是要对第一个阶段的结果做一个merge
Distinct状态复用
- 对于distinct的优化,一般批里面的引擎都是通过把它优化成aggregate的方式来处理
- 在流式window中,我们不能直接这样进行优化,要不然算子就变成会下发retract的数据了。所以在流式中,对于count distinct这种情况,我们是需要保存所有数据是否出现过这样子的一个映射。
在SQL中,我们可以在聚合函数上添加一些filter,如下面的SQL所示:
我们可以把相同字段的distinct计算用一个map的key来存储,在map的value中,用一个bit vector来实现就可以把各个状态复用到一起了。比如一个long有64位,可以表示同一个字段的64个filter,这样整体状态量就可以节省很多了。
滑动窗口pane复用
-
降低滑动窗口的状态存储量
-
优化方法:将窗口的状态划分成更小粒度的pane
- 这样每来一条数据,我们就只更新这条数据对应的pane的结果就可以了。当窗口需要输出结果的时候,只需要将这个窗口对应的pane的结果merge起来就可以了。
-
注意:这里也是需要所有聚合函数都有merge的实现的
四、案例分析
需求一:使用Flink SQL计算抖音的DAU曲线
-
DAU(Daily Active User):指的是每天的去重活跃用户数
-
输出:每个5s更新一下当前的DAU数值,最终获得一天内的DAU变化曲线
-
使用滑动窗口 开启EMIT输出
-
问题:所有数据都需要在一个subtask中完成窗口计算,无法并行
-
-
-
加上倾斜优化
- 通过两阶段聚合把数据打散,完成第一轮聚合,第二轮聚合只需对个分桶的结果求和即可
- table.exec.window.allow-retract-input=true 字节内部实现,开启emit后会输出retract,实现window允许retract的输入,将窗口连接起来
需求二:计算大数据任务的资源使用
-
问题描述:大数据任务(特指离线任务)运行时通常会有多个container启动并运行,每个container在运行结束的时候,YARN会负责将它的资源使用(CPU、内存)情况上报。一般大数据任务运行时间从几分钟到几小时不等。
-
需求:根据YARN上报的各个container的信息,在任务结束的时候,尽快的计算出一个任务运行所消耗的总的资源。假设前后两个container结束时间差不超过10min。
-
可以通过会话窗口将数据划分到一个window中,然后再将结果求和