这是我参与「第四届青训营 」笔记创作活动的的第6天!
一、本堂课重点内容:
- 实时计算和批式计算的本质区别
- 核心功能 watermark和windown机制
- 3大基本窗口
- 窗口机制的优化和原理
二、详细知识点介绍:
实时计算和批式计算
数据的形式
所有的数据都是以流式的形态产生的,不管是哪里产生的数据,在产生的过程中都是一条条地生成,最后经过了存储和转换处理,形成了各种类型的数据集。如下图所示,根据现实的数据产生方式和数据产生是否含有边界(具有起始点和终止点)角度,将数据分为两种类型的数据集,一种是有界数据集,另外一种是无界数据集。>
比如数据库的表数据就是有界的,flink的流数据就是无界
区别
-
数据时效性不同:流式计算实时、低延迟, 批量计算非实时、高延迟。
-
数据特征不同:流式计算的数据一般是动态的、没有边界的,而批处理的数据一般则是静态数据。
-
应用场景不同:流式计算应用在实时场景,时效性要求比较高的场景,如实时推荐、业务监控...批量计算一般说批处理,应用在实时性要求不高、离线计算的场景下,数据分析、离线报表等。
-
运行方式不同,流式计算的任务持续进行的,批量计算的任务则一次性完成。
流式处理可以用于两种不同场景: 事件流和持续计算。
数据的价值
-
通过大数据处理我们获取了数据的价值,但是数据的价值是恒定不变的吗?显然不是,一些数据在事情发生后不久就有了更高的价值,而且这种价值会随着时间的推移而迅速减少。流处理的关键优势在于它能够更快地提供洞察力,通常在毫秒到秒之间。
-
流式计算的价值在于业务方可在更短的时间内挖掘业务数据中的价值,并将这种低延迟转化为竞争优势。比方说,在使用流式计算的推荐引擎中,用户的行为偏好可以在更短的时间内反映在推荐模型中,推荐模型能够以更低的延迟捕捉用户的行为偏好以提供更精准、及时的推荐。
-
流式计算能做到这一点的原因在于,传统的批量计算需要进行数据积累,在积累到一定量的数据后再进行批量处理;而流式计算能做到数据随到随处理,有效降低了处理延时。
核心功能 watermark和windown机制及state状态
简述
Flink的模范标兵
- Watermark致力于解决数据的乱序和延迟问题(标记乱序数据时间)
- Window尽心尽力提供各种数据切分机制(数据流分块)
- State勤勤恳恳记录中间状态,并在数据恢复等场景承担着重要的角色(缓存window中的数据)
watermark 实时计算中,对数据时间比较敏感,有 EventTime 和 ProcessTime 之分,一般来说 EventTime 是从原始消息中提取出来的,ProcessTime 是 Flink 自己提供的。 在实际应用中,数据源往往很多个且时钟无法严格同步,数据汇集过程中传输的距离和速度也不尽相同,在上游多个节点处理过程的处理速度也有差异,这些因素使得 Event Time 的乱序基本是一个必然现象。
- 数据正序
w的边界时间取决于线右边窗口最大的值
- 数据乱序
w的边界时间取决于 线左边的最小值大于线右边 最大值
关系代码例子
//From EventTimeTrigger.java
@Override
public TriggerResult onElement(Object element, long timestamp, TimeWindow window, TriggerContext ctx) throws Exception {
if (window.maxTimestamp() <= ctx.getCurrentWatermark()) {
// if the watermark is already past the window fire immediately
// 触发窗口计算
return TriggerResult.FIRE;
} else {
// 继续,不触发窗口计算
ctx.registerEventTimeTimer(window.maxTimestamp());
return TriggerResult.CONTINUE;
}
}
3大基本窗口
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进行窗口触发输出。
\
-
比如每隔30秒统计过去1分钟的数据量(基于时间)
-
比如每隔10个元素统计过去100个元素的数据量(基于数量)
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功能。
窗口机制的优化和原理
以下说的所有的高级优化,都只限于在SQL中的window中才有。在DataStream中,用户需要自己通过代码来实现类似的能力。
Mini-batch
一般来讲,Flink的状态比较大一些都推荐使用rocksdb statebackend,这种情况下,每次的状态访问就都需要做一次序列化和反序列化,这种开销还是挺大的。为了降低这种开销,我们可以通过降低状态访问频率的方式来解决,这就是mini-batch最主要解决的问题:即赞一小批数据再进行计算,这批数据每个key的state访问只有一次,这样在单个key的数据比较集中的情况下,对于状态访问可以有效的降低频率,最终提升性能。
这个优化主要是适用于没有窗口的聚合场景,字节内部也扩展了window来支持mini-batch,在某些场景下的测试结果可以节省20-30%的CPU开销。
mini-batch看似简单,实际上设计非常巧妙。假设用最简单的方式实现,那就是每个算子内部自己进行攒一个小的batch,这样的话,如果上下游串联的算子比较多,任务整体的延迟就不是很容易控制。所以真正的mini-batch实现,是复用了底层的watermark传输机制,通过watermark事件来作为mini-batch划分的依据,这样整个任务中不管串联的多少个算子,整个任务的延迟都是一样的,就是用户配置的delay时间。
下面这张图展示的是普通的聚合算子的mini-batch原理,window的mini-batch原理是一样的。
Local-global
local-global优化是分布式系统中典型的优化,主要是可以降低数据shuffle的量,同时也可以缓解数据的倾斜。
所谓的local-global,就是将原本的聚合划分成两阶段,第一阶段先做一个local的聚合,这个阶段不需要数据shuffle,是直接跟在上游算子之后进行处理的;第二个阶段是要对第一个阶段的结果做一个merge(还记得上面说的session window的merge么,这里要求是一样的。如果存在没有实现merge的聚合函数,那么这个优化就不会生效)。
如下图所示,比如是要对数据做一个sum,同样颜色的数据表示相同的group by的key,这样我们可以再local agg阶段对他们做一个预聚合;然后到了global阶段数据倾斜就消除了。
Distinct状态复用
对于distinct的优化,一般批里面的引擎都是通过把它优化成aggregate的方式来处理,但是在流式window中,我们不能直接这样进行优化,要不然算子就变成会下发retract的数据了。所以在流式中,对于count distinct这种情况,我们是需要保存所有数据是否出现过这样子的一个映射。
在SQL中,我们有一种方式可以在聚合函数上添加一些filter,如下面的SQL所示:
像这种情况,我们会对同一个字段用不同的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的实现的
三、实践练习例子:
- 案例一:计算实时抖音DAU曲线
DAU(Daily Active User):指的是每天的去重活跃用户数
输出:每个5s更新一下当前的DAU数值,最终获得一天内的DAU变化曲线
要求:通过上面课程中学到的窗口的功能以及相关的优化,开发一个Flink SQL任务,使得可以高效的计算出来上面要求的实时结果。
(具体内容会在课上展开)
- 案例二:计算大数据任务的资源使用
问题描述:大数据任务(特指离线任务)运行时通常会有多个container启动并运行,每个container在运行结束的时候,YARN会负责将它的资源使用(CPU、内存)情况上报。一般大数据任务运行时间从几分钟到几小时不等。
需求:根据YARN上报的各个container的信息,在任务结束的时候,尽快的计算出一个任务运行所消耗的总的资源。假设前后两个container结束时间差不超过10min。
四、课后个人总结:
- 本章有什么知识点不容易掌握?
- 为什么watermark比event time少几秒