流式计算中的Window计算 | 青训营笔记
这是我参与「第四届青训营 」笔记创作活动的的第3天。
一、本堂课重点内容
- 流式计算与批式计算的区别;实时和离线数仓的区别;流式计算中window的定义及挑战
- 实时计算中watermark的概念,以及如何产生、传递
- 介绍三种最基本的window类型及实现原理,结合业务场景提供窗口机制中最核心的优化功能原理
二、详细知识点介绍
1. 流式计算概述
简述流式计算的基本概念,与批式计算相比的难点和挑战
1.1 流式计算和批式计算的对比
- 实时性越高,数据的价值越高
| 项目 | 批式计算 | 流式计算 |
|---|---|---|
| 数据存储 | HDFS、Hive | Kafka、Pulsar |
| 数据时效性 | 天级别 | 分钟级别 |
| 准确性 | 精准 | 精准和时效性取舍 |
| 典型计算引擎 | Hive、Spark、Flink | Flink |
| 计算模型 | Exactly-Once | At-Least-Once/Exactly-Once |
| 资源模型 | 定时调度 | 长期持有 |
| 主要场景 | 离线天级别数据报表 | 实时数仓、实时营销、实时风控 |
1.2 批处理
-
T+1离线计算模型:数据计算是天级别,每天只能看到前一天的计算结果
- 通常使用Hive/Spark,数据和结果是确定的
-
小时级批计算
- 批计算每次需要申请调度资源
- 计算需要时间
1.3 处理时间窗口
- 实时计算:处理时间窗口
- 数据实时流动实时计算
- 窗口结束直接发送结果不需要周期调度任务
1.4 处理时间vs事件时间
- 处理时间:数据在流式计算系统中真正处理时所在机器的当前时间
- 事件时间:数据产生的时间,比如客户端、传感器、后段代码等上报数据的时间
1.5 事件时间窗口
- 实时计算:事件时间窗口
- 数据实时进入到真实时间发生的窗口中进行计算
- 可以有效处理数据延迟和乱序
- 什么时候窗口算结束?
- 引用watermark来表示当前的真实时间
- 数据存在乱序时,可以用来在乱序容忍和实时性间做一个平衡
- 当收到watermark后有比watermark小的数据时认为是延时数据 舍弃
2. Watermark概述
watermark的含义、生成方法、传递机制以及一些典型场景的问题和优化
2.1 Watermark定义
- 当前系统认为的事件时间所在的真实时间
- 当前真实的事件时间
2.2 Watermark产生
- 一般从数据的事件时间来产生
- 产生策略灵活多样:最常见包括使用当前事件时间的时间减去一个固定的Delay,来表示可以容忍多长时间的乱序
SQL:
CREATE TABLE...
order_time TIMESTAMP(3)
WATERMARK FOR order_time AS order_time - IMTERVAL '5' SECOND
DataStream:
WatermarkStrategy
.<Tuple2<Long,String>>forBoundedOutOfOrderness(Duration.ofSeconds(20))
.withTimestampAssigner((event, timestamp)->event.f0);
2.3 Watermark的传递、
上下游task之间有数据传递关系时,上游会将watermark传递给下游;下游收到多个上游发来的watermark后,默认取最小值作为自身的watermark,同时将自己的watermark传递给下游;最后所有的算子都会有自己的watermark。
2.4 Watermark在生产实践中的问题
2.4.1 怎么观察一个任务中Watermark多少,是否正常
通过Flink UI观察算子的watermark和subtask的watermark。
2.4.2 per-partition/per-subtask生成watermark的优缺点
- per-subtask watermark生成:早期版本机制。典型的问题是如果一个source subtask消费多个partition,那么多个partition之间的数据读取(读取速度不一、系统故障、传输延迟等)可能会加剧乱序程度;
- per-partition watermark:引入基于每个partition单独的watermark,有效避免乱序度增加;
2.4.3 如果有部分partition/subtask会断流如何处理
上游subtask的watermark不更新,则下游所有watermark不更新;
- 解决方法:Idle source
- 当某个subtask断流超过配置的idle超时时间时,将当前subtask置为idle,并下发一个idle的状态给下游。
- 下游在计算自身watermark的时候,忽略掉当前是idle的subtask。
2.4.4 算子对于时间晚于watermark的数据的处理
- 迟到数据处理:晚于watermark的数据到来时认为是迟到数据,算子自身来决定如何处理迟到数据
- window聚合:默认丢弃迟到数据
- 双流join:outer-join则不能join到任何数据
- CEP:默认丢弃
3. Window概述
window基本功能和高级优化
3.1 window基本功能
-
window分类
- tumble window 滚动窗口
- sliding window 滑动窗口
- session window 会话窗口
- 其他window:全局window、count window、累计窗口等
-
window使用 SQL:
SUM(amount) FROM ORDERS
GROUP BY
TUMBLE(order_time, INTERVAL ‘1’ DAY)
DataStream:
DataStream<Tuple2<String, Integer>> dataStream = env
.socketTextStream("localhost",9999)
.flatMap(new Splitter())
.keyBy(value ->value.f0)
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.sum(1)
3.1.1 tumble window 滚动窗口
- 窗口划分:每个key单独划分,每条数据只属于一个窗口
- 窗口触发:window结束时间到达时一次性触发
3.1.2 HOP Sliding window 滑动窗口
- 窗口划分:每个key单独划分,每条数据可能属于多个窗口
- 窗口触发:window结束时间到达时一次性触发
3.1.3 session window 会话窗口
- 窗口划分:每个key单独划分,每条数据会单独划分一个窗口,如果window之间有交集,则对窗口进行merge;
- session窗口划分是动态的,可能会与之前的窗口合并
- 窗口触发:window结束时间到达时一次性触发
3.2 迟到数据处理
- 迟到:一条数据到来后会用window assigner给它划分一个window,一般时间窗口是一个时间区间,如果划分出来的window end比当前的watermark小,说明这个窗口已经触发计算了,这条数据就是迟到数据。
- 什么情况下会产生迟到数据:只有事件时间下才会有迟到的数据。
- 迟到数据的默认处理:丢弃
3.2.1 allow lateness
需要设置一个允许迟到的时间。设置之后窗口正常计算结束后不会马上清理状态,而是会多保留allow lateness这么长的时间。在这段时间内如果还有数据到来则继续之前的状态进行计算。
- 适用:DataStream、SQL
3.2.2 SideOutput 侧输出流
这种方式对迟到数据打一个tag,然后在dataStream上根据这个tag获取迟到数据流,然后业务层自行选择进行处理。
- 适用于 DataStream
3.3 增量计算和全量计算
- 增量计算:每条数据到来直接进行计算
- window只存储计算结果,不保留每条数据
- 典型的reduce、aggregate等函数都是增量计算
- SQL中聚合只有增量计算
- 全量计算:每条数据到来后会存储到window的state中。
- window存储所有数据,触发计算时将所有数据拿出一起计算
- 典型的process函数就是全量计算
3.4 EMIT触发
3.4.1 什么是EMIT
- 通常window都是在结束时才输出结果。如果窗口比较大则计算结果输出的延迟就比较高,失去了实时计算的含义。
- EMIT输出指的是在window没有结束时提前把window计算的部分结果输出出来。
3.4.2 怎么实现EMIT
- DataStream中通过自定义Trigger来实现
- Trigger的结果可以是
- CONTINUE
- FIRE(触发计算但不清理)
- PURGE
- FIRE_AND_PURGE
- Trigger的结果可以是
- SQL通过配置:
- table.exec.emit.early-fire.enabled=true
- table.exec.emit.early-fire.delay=(time)
3.5 window offset
windowStart = timeStamp - (timeStamp - offset + windowSize)% windowSize
可以在计算窗口时让窗口有一个偏移。
- DataStream支持offset,SQL不支持。
3.6 window高级优化
3.6.1 mini-batch优化
- RETRACT机制下中间结果偏多输出量大,内存存储到外部状态需要序列化和反序列化消耗CPU
- 多条数据进行一次状态读取、序列化和反序列化、以及写入过程 (攒一批数据然后输出)
- 上下游算子如果都缓存一批进行输出,数据延迟会大大增加
- 利用类watermark传递机制,上游添加mini-batch assigner算子,发送开始mini-batch的信号;下游收到信号后开始对缓存数据进行计算,同时向下游继续发送;
3.6.2 local-global 倾斜优化
- 在shuffle之前,先进行local预聚合,再shuffle到下游全局算子,解决数据倾斜的热点问题
3.6.3 distinct状态复用
COUNT (DISTINCT id) FILTER (WHERE a IN ...)
COUNT (DISTINCT id) FILTER (WHERE b IN ...)
COUNT (DISTINCT id) FILTER (WHERE c IN ...)
COUNT (DISTINCT id) FILTER (WHERE d IN ...)
...
- DISTINCT通常优化为Group by等形式
- 利用FILTER前置过滤DISTINCT
- 复用DISTINCT,把key对应为bit value来确定状态(是否有对应key),从而通过filter复用;
- 降低DISTINCT连续数据的存储量。
3.6.4 滑动窗口pane复用
- 滑动窗口每条数据参与多个窗口,数据开销比较大
- 把滑动窗口划分成更小的pane,从而变成类滚动窗口的形式;每个滑动窗口就是多个pane的聚合,从而数据在每个pane中可以节省访问,降低计算成本。
- 输出时需要临时做一个默指。
三、生产实例分析
1. 计算实时抖音DAU曲线
思路:做一个EMIT的滚动窗口,count DISTINCT uid
单并发:必须聚合到一个节点
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)
两阶段聚合:把数据打散,完成第一轮聚合后对分桶结果求和即可
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) -- 根据uid分为10000个桶
)
GROUP BY TUMBLE(event_time, INTERVAL '1' DAY)
2. 计算大数据任务的资源使用
根据YARN上报的各个container的信息,在任务结束时尽快计算出一个任务占据的总资源,假设前后两个container间隔时间不足10min;
- 典型通过会话窗口将数据划分到一个window中 然后将结果求和
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)
四、课后个人总结
- 复杂知识点
- Watermark机制便于计算迟到信息
- 三种window的定义、使用场景和优化
- 本人总结 本节课主要学习了流式计算中watermark的利用,watermark用于标示迟到数据;同时学习了滚动窗口、滑动窗口和会话窗口的定义、实际运用和优化方法。课程学习的内容相对抽象,需要结合实例进行更深入的学习。