这是我参与「第四届青训营 」笔记创作活动的第4天
课程目录:
1.概述
2.watermark
3.window
4.案例分析
思维导图
1. 概述
批计算
- 数仓架构为 T + 1 架构
- 通常使用的计算引擎为 Hive 或spark
- 输出输入都是准确性的
- 难以实时
流计算
- 处理时间窗口:窗口结束直接发送结果,不需要周期调度任务
- 处理时间:数据在流式计算系统中真正处理时所在机器的当前时间
- 事件时间:数据产生的时间,比如客户端、传感器、后端代码等上报数据时的时间
处理时间通常会比事件时间有一定的延迟
若实时计算采用事件时间,什么时候窗口才算结束?可能延迟几分钟,也可能延迟几小时
——> 引入WaterMark:在数据中插入一些waterMark,来表示当前的真实时间
- 如w(11)表示这个后面不会比11小的时间,如果有,则认为是迟到的数据,当成其他数据处理或者丢弃即可
WaterMark
当数据乱序时,waterMark就比较重要了,它可以用来乱序容忍和实时性之间做一个平衡
01. 小结:
1. 批式计算一般是 T+1 的数仓架构
2.数据实时性越高,数据的价值越高
3. 实时计算分为处理时间和事件时间
4.事件时间需要 Watermark配合来处理乱序
2. watermark
如何产生
一般是从数据的事件时间来产生,产生策略可以灵活多样,最常见的包括使用当前事件时间的时间减去一个固定的delay,来表示可以可以容忍多长时间的乱序
如何传递
- 上游就会将watermark传递给下游;下游收到多个上游传递过来的watermark后,默认会取其中最小值来作为自身的watermark
- 机制类似于Checkpoint的barrier,由算子去传递、接收
watermark在生产实践中经常遇到的几个问题:
-
怎么观察一个任务中的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仍然可以继续更新。
-
采用idle source处理: 当source断流超过idle的值,就将当前的subtask设置为idle,下游计算waterMark的时候就忽略当前是idle的subtask
- 算子对于时间晚于watermark的数据的处理(迟到数据处理)
- 对于迟到数据,不同的算子对于这种情况的处理可以有不同的实现(主要是根据算子本身的语义来决定的);
- 比如window对于迟到的数据,默认就是丢弃;
- 比如双流join,对于迟到数据,可以认为是无法与之前正常数据join上。
2. Watermark 小结
- 含义:表示系统认为的当前真实时间 2.生成:可以通过 Watermark Generator 来生成
- 传递:取上游所有 subtask 的最小值
- 部分数据断流:Idle Source
- 迟到数据处理:Window 算子是丢弃;Join 算子认为跟之前的数据无法 join 到
3. window
基本功能
典型的window
- TUMBLE Window (滚动窗口)
- 每条数据只属于一个窗口
- HOP Window (滑动窗口)
- 每条数据可能属于多个窗口
- 窗口大小,滑动时间
- 如窗口2个小时,每小时滑动一次——10-12,11-13,12-14点
- SESSION Window (会话窗口)
- 每条数据会单独划分一个窗口,若window之间有交集,则会对窗口进行merge
- 是一个动态merge的过程。一般会设置一个会话的最大的gap,比如10min
- 合并之后会变成一个更大的窗口【10,20】,15分来了一条数据合并——>窗口【10,25】
window的使用
- SQL:在聚合的地方加上tumble(窗口大小时间),即可定义一个窗口
- DataStream:keyby()之后window()
- Flink 中的窗口划分是key级别的
- 窗口触发:window结束时间达到的时候一次性触发
迟到数据怎么处理
- 只有事件时间才会产生迟到数,默认丢弃
- 即数据所处时间在window下,即可认为没有迟到,所属的窗口已经被触发了,才算迟到
1.allow lateness(适用于SQL、DataStream)
- 和waterMark的idle类似,允许一个迟到的时间
- 和retract机制类似,retract不管哪个算子,都要求算子结果输出都有一致性语义
- 开启这个 = 修正之前的结果,修正的过程就是一个retract的过程
2. SideOutput(侧输出流)(适用于DataStream)
- 把迟到的数据转变成一个单独的流,再由用户自己来决定如何处理这部分数据
窗口计算模型
- 增量(更优)
- 来一条数据算一条、只存储计算结果,不需要保存每条数据
- 典型如sum、reduce、aggregate
- SQL的聚合只有增量计算
- 全量
- 作业需要大量缓存,先存储state、等到触发window计算再一次性计算
- 典型的process函数就是全量计算
EMIT触发(大窗口常用)
- 在window没有结束前,可以提前把window计算结果部分输出出来的一种机制
- 这个功能只在SQL中,如果是在DataStream中需要完成类似的功能,需要自己定义一些trigger来做
这种emit的场景就是一个典型的Retract的场景,发送的结果类似于+[1], -[1], +[2], -[2], +[4]这样子。这样才能保证window的输出的最终结果是符合语义的
3.1 小结
- 1. 三种窗口的定义
- 2. 迟到数据:Allow Lateness 、SideOutput
- 3. 增量计算和全量计算模型
- 4. EMIT 触发前提输出窗口的结果
高级优化:
Mini-batch
这个优化主要是适用于没有窗口的聚合场景
-
一般来讲,Flink的状态比较大一些都推荐使用rocksdb statebackend,这种情况下,每次的状态访问就都需要做一次序列化和反序列化,这种开销还是挺大的。为了降低这种开销,我们可以通过降低状态访问频率的方式来解决
-
这就是mini-batch最主要解决的问题:即攒一小批数据再进行计算,这批数据每个key的state访问只有一次,这样在单个key的数据比较集中的情况下,对于状态访问可以有效的降低频率,最终提升性能。
mini-batch看似简单,实际上设计非常巧妙。假设用最简单的方式实现,那就是每个算子内部自己进行攒一个小的batch,这样的话,如果上下游串联的算子比较多,任务整体的延迟就不是很容易控制。
- 所以真正的mini-batch实现,是复用了底层的watermark传输机制,通过watermark事件来作为mini-batch划分的依据,这样整个任务中不管串联多少个算子,整个任务的延迟都是一样的,就是用户配置的delay时间。
倾斜优化:Local-global
- 和MapReduce的combiner机制一样,做处理之前先进行预聚合
- 可以降低数据shuffle的量,同时也可以缓解数据的倾斜
Distinct状态复用
- 对于distinct的优化,一般批里面的引擎都是通过把它优化成aggregate的方式来处理,但是在流式window中,我们不能直接这样进行优化,要不然算子就变成会下发retract的数据了。
- 所以在流式中,对于count distinct这种情况,我们是需要保存所有数据是否出现过这样子的一个映射。
- 在SQL中,我们有一种方式可以在聚合函数上添加一些filter,如下面的SQL所示:
表示先filter再进行左边的聚合计算
Flink里面有,MySQL这些数据库是没有这种写法
像批计算,可以用group代替distinct,但是流式计算的window里是比较难
-
像这种情况,我们会对同一个字段用不同的filter来进行count distinct的计算。如果每个指标都单独用一个map来记录每条数据是否出现过,那状态量是很大的。
-
我们可以把相同字段的distinct计算用一个map的key来存储,在map的value中,用一个bit vector来实现就可以把各个状态复用到一起了。比如一个bigint有64位,可以表示同一个字段的64个filter,这样整体状态量就可以节省很多了。
滑动窗口pane复用
- 滑动窗口如上面所述,一条数据可能会属于多个window。所以这种情况下同一个key下的window数量可能会比较多,比如3个小时的窗口,1小时的滑动的话,每条数据到来会直接对着3个窗口进行计算和更新。这样对于状态访问频率是比较高的,而且计算量也会增加很多。
- 优化方法就是,将窗口的状态划分成更小粒度的pane,比如上面3小时窗口、1小时滑动的情况,可以把pane设置为1h,这样每来一条数据,我们就只更新这条数据对应的pane的结果就可以了。 当窗口需要输出结果的时候,只需要将这个窗口对应的pane的结果merge起来就可以了。
- 注意:这里也是需要所有聚合函数都有merge的实现的
3.2小结:
-
Mini-batch 优化解决频繁访问状态的问题
-
local-global 优化解决倾斜问题
-
Distinct 状态复用降低状态量
-
Pane 优化降低滑动窗口的状态存储
4. 案例分析
案例一:计算实时抖音DAU曲线
DAU(Daily Active User):指的是每天的去重活跃用户数
输出:每个5s更新一下当前的DAU数值,最终获得一天内的DAU变化曲线
要求:通过上面课程中学到的窗口的功能以及相关的优化,开发一个Flink SQL任务,使得可以高效的计算出来上面要求的实时结果。
案例二:计算大数据任务的资源使用
问题描述:大数据任务(特指离线任务)运行时通常会有多个container启动并运行,每个container在运行结束的时候,YARN会负责将它的资源使用(CPU、内存)情况上报。一般大数据任务运行时间从几分钟到几小时不等。
需求:根据YARN上报的各个container的信息,在任务结束的时候,尽快的计算出一个任务运行所消耗的总的资源。假设前后两个container结束时间差不超过10min。
课程总结
1.第一部分介绍了流式计算基本概念,以及和批式计算的区别
2.第二部分介绍了watermark的含义、如何生成、如何传递,以及如何处理部分partition断流的问题
3. 第三部分介绍了三种基本的window的定义,以及迟到数据处理、增量计算VS全量计算、EMIT输出;同时也介绍了local-global优化、mini-batch优化、distinct状态优化、滑动窗口的pane的优化等
4.两个案例介绍滚动窗口、会话窗口,以及两阶段聚合解决倾斜问题