字节跳动青训营第4期 第4课 流计算中的Window计算
这是我参与「第四届青训营 」笔记创作活动的第2天
窗口计算机制是流计算中一个比较基础但又必要的机制
[TOC]
概述
流式计算与批式计算的对比
实时性更高的数据,其价值也更高
批式计算
批处理的典型数仓架构为T+1架构,即当天只能看到前一天的数据。
常使用Hive或Spark,数据完全准备好,输入和输出都是确定的。
问题:能否按小时执行批计算,从而达到一个实时性比较高的计算?
理论上是可以实现的,但实际上是不容易实现的,各类资源的调度、数据仓库建模的复杂程度,导致一个计算可能是几分钟或是几个小时,难以控制在一个小时内。
流式计算
处理时间:数据真正被计算引擎处理的时间
事件时间:代码或任务提交的时间
在事件时间的基础上开窗,是有可能存在延迟的,并且会产生一个问题,什么时间算窗口结束?
Watermark就是为了处理这个窗口结束的问题而产生的。
Watermark示意图如下:
如上图,在乱序情况下,W(11)将认为其后不存在比11小的数据,若遇到比11小的数据,则判断为有延迟的数据,进行相应处理即可。
Watermark
什么是Watermark
表示系统认为的当前真实的事件事件。
生成方法
CREATE TABLE Orders {
user BIGINT,
product STRING,
order_time TIMESTAMP(3),
WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND
} WITH { ... };
上述代码实现了根据order_time字段添加watermark,其间隔为5秒。
WatermarkStrategy
.<Tuple2<Long, String>>forBoundedOutOfOrderness(Duration.ofSeconds(20))
.withTimestampAssigner((event, timestamp) -> event.f0);
上述代码实现了根据event.f0字段添加watermark,其间隔为20秒。
此类使用方法虽然简单,但是生产环境中常用的方式。
传递机制
每个算子都从上游的所有Watermark中取最小值作为自己的Watermark,然后传递给下游。
Watermark典型问题
生成方式:Per-partition VS Per-subtask
早期版本的Flink使用subtask的生成方式,但是每个subtask是可以同时使用多个partition的,每个partition的读取速度可能不同,这种方式会加剧数据乱序程度。
新版本的Flink按照partition的方式去生成,可以一定程度环节上述问题。
部分partition/subtask断流
如果上游的算子断流,不再更新,则下游的Watermark都不再更新。
解决方案:Idle source
当某个subtask断流超过Idle阈值,则将其置为Idle,并下发Idle状态给下游,下游在计算自身Watermark时,可以忽略掉状态为Idle的subtask。
迟到数据处理
- Window聚合,默认丢弃迟到数据
- 双流Join,如果是外连接,则可以认为Join不到任何数据
- CEP,默认丢弃
Window
典型窗口包括:
- 滚动窗口 Tumble Window
- 滚动窗口 Slide Window
- 会话窗口 Session Window
其他窗口:
- 全局窗口
- 计数窗口
- 累计窗口
基本功能
Window的使用方式
SELECT
user,
TUMBLE_START(order_time, INTERVAL '1' DAY) AS wStart,
SUM(amount)
FROM Orders
GROUP BY
TUMBLE(order_time, INTERVAL '1' DAY),
user
上述代码实现了一个滚动窗口,根据order_time字段,每相隔1天创建一个window
DataStream<Tuple2<String, Integer>> dataStream = env
.socketTextStream("localhost", 9999)
.flatMap(new Splitter())
.keyBy(value -> value.f0)
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.sum(1);
上述代码实现了一个滚动窗口,根据value.f0字段,每相隔5秒创建一个window
TODO: 添加两种方式的代码并解释
Table API相对SQL API更容易进行功能上的扩展
滚动窗口
窗口划分:根据每个key单独划分,每条数据只属于一个窗口
窗口触发:Window结束时间到达时一次性触发
滑动窗口
在滚动窗口的基础上,窗口和窗口之间可能会重合,一条数据可能会属于多个窗口,例如:11点-13点,12点-14点这样的窗口。
会话窗口
与滚动窗口和滑动窗口不同,会话窗口相对更灵活,可以设置一个session gap在间隔之内的小窗口,可以合并为一个大的会话窗口。
迟到数据的处理
怎么定义迟到?
会给每条数据通过WindowAssigner划分一个window,这个window是一个区间,如果window end比当前watermark小,则认为是迟到数据。
只有事件时间下才有,默认是丢弃处理。
Allow lateness
设置一个允许迟到的时间,在允许迟到的时间内,计算状态会被保存,如果有迟到的数据,则会根据计算状态继续进行计算,相当于对之前的计算结果的修正。
适用于Datastream和SQL
SideOutput 侧输出流
对迟到数据打一个tag,在Datastream上获取到迟到数据流,由业务进行处理
适用于Datastream
增量和全量计算
增量:每条数据到来都会参与计算,window只储存计算结果,不存储数据,典型的reduce,aggregate都是增量计算;SQL的聚合只有增量计算。
全量:每条数据都会存储到window的state,触发计算后,将所有数据拿出来一起计算;process函数是全量计算。
增量是优于全量的,但也必须根据业务情况去选择增量还是全量计算。
EMIT触发
什么是EMIT
当窗口定义的比较长,例如一个小时或一天,计算结果输出的延迟较高,失去了实时计算的意义。
EMIT指的是在window没有结束的时候,提前把window的计算结果输出出来。
如何实现
在Datastream里通过自定义Trigger实现,Trigger的结果可以是:
- CONTINUE
- FIRE(计算但不清理)
- PURGE
- FIRE_AND_PURGE
SQL也可以通过配置的方式使用
高级优化
Mini-Batch优化
主要目的是优化动态表中频繁修改中间状态的场景。
通过Checkpoint或Watermark去调度一个mini batch,新到数据先存入一个mini-batch,当满足一定条件后,将mini batch中的数据一起参与计算。
倾斜优化 local - global
如上图所示,即倾斜优化的基本思想。
对于上图左侧,描述了一个根据key对数字求和的任务,不同底色的数字代表了不同的key,从多个上流获取的数据经过聚合,导致了红色的算子其压力较大,而紫色算子压力较小。
右侧则是倾斜优化的结果,每个上流先进行一个local的聚合,聚合之后再进行global的聚合,从而大大降低了后续算子的压力。
Distinct 计算状态复用
如上图,在实际的生产环境中,有时会需要使用很多的DISTINCT关键字,而SQL又只说明要做什么而不说明具体要怎么做,那么就会浪费很多资源在处理DISTINCT关键字上。
对DISTINCT的优化方案是对其计算状态进行复用,建立一个Map用于存储Distinct计算状态,在需要的时候取出进行应用。
Pane优化
当窗口大,滑动间隔小时,每条数据要参与很多个窗口计算,开销就变得特别大。
先把window划分成更小粒度的Pane,一条数据只属于一个Pane,就像一个微型的滚动窗口,在窗口输出结果时,将所有Pane进行临时合并并输出。
案例分析
使用Flink SQL计算抖音DAU(日活)实时曲线
思路:做一个滚动窗口,使用EMIT机制提前将数据输出
SELECT
COUNT(DISTINCT uid) AS dau
TUMBLE_START(event_time, INTERVAL '1' DAY) AS wstart,
LOCALTIMESTAMP AS current_ts
FROM user_activity
GROUP BY
TUMBLE(event_time, INTERVAL '1' DAY)
基于以上的实现方式,所有数据需要在一个subtask执行,无法并行。
优化思路:把整个计算改变为2个阶段,先根据用户分桶,然后再将每个桶的值进行聚合,即可得到整体的结果。实现了并发的要求,提升了计算的时间。
SELECT
SUM(partial_cnt) AS dau
TUMBLE_START(event_time, INTERVAL '1' DAY) AS wstart,
LOCALTIMESTAMP AS current_ts
FROM (
SELECT
COUNT(DISTINCT uid) AS partial_cnt,
TUMBLE_ROWTIME(event_time, INTERVAL '1' DAY) AS event_time
FROM user_activity
GROUP BY
TUMBLE(event_time, INTERVAL '1' DAY),
MOD(uid, 10000)
)
GROUP BY
TUMBLE(event_time, INTERVAL '1' DAY)
对于优化方案,汇总的SELECT并发为1,但是分成了10000个桶,相当于有10000的并发在执行这个作业
使用Flink SQL进行大数据任务资源使用实时统计分析
思路:建立一个会话窗口,以10分钟为间隔,读取cpu和内存的使用量进行SUM()求和即可。
SELECT
application_id,
SUM(cpu_usage) AS cpu_total,
SUM(memory_usage) AS memory_total
FROM resource_usage
GROUP BY
application_id,
SESSION(event_time, INTERVAL '10' MINUTE)