流式计算中的 Window 计算

299 阅读11分钟

这是我参与「第四届青训营 」笔记创作活动的的第6天

回顾

资源任务

批式计算的资源模型是定时调度,流式计算的资源模型为长期持有

数据实时性越高,价值越高

从 T+1 离线计算模型

批处理模型典型数仓架构为T+1架构,即数据计算时天级别的,当天只能看到前一天的计算结果,使用的计算引擎为Hive或者Spark等。计算的时候,数据是完全ready的,输入和输出都是确定性的。

小时级别的批式计算

一天24h,分成24批,技术上可以;但是批式计算除了计算之外,需要每次申请,然后释放,这样的一个周期调度的过程,消耗资源;线上的数仓任务计算时长从几分钟到几小时不等,而且数仓的建模也是建模的,可能有很多层,数据产生到计算完成要求在1h之内,在很多场景中是做不到的。

那么如何做到更实时?

处理时间窗口

实时计算:处理时间窗口 数据实时流动,实时计算,窗口结束直接发送结果,不需要周期调度任务

处理时间vs事件时间

  • 处理时间:数据在流式计算系统中真正处理时所在机器的当前时间
  • 事件时间:数据产生的时间,比如客户端,传感器,后端代码等上报数据的时间

一般处理时间较事件时间有延迟,如何消除?

事件时间窗口

实时计算:事件时间窗口 数据实时进入到真实事件发生的窗口中进行计算,可以有效地处理数据延迟和乱序。

什么时候窗口才算结束呢?

由于不断地数据延迟,所以unpredictable

如何表达实时处理的事件时间的过程中,怎么去表达系统中当前衡量的真实时间,事件时间需要watermark配合处理乱序。

Watermark

在数据中插入一些Watermark,来表示当前的真实数据的时间的时间

在数据存在乱序的时候,watermark就比较重要了,它可以用来在乱序容忍和实时性之间做一个平衡

Watermark

什么是watermark

表示系统认为的当前真实的事件时间

如何产生watermark?

  • SQL order_time减5s作为watermark的值
WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND
  • DataStream

如何传递watermark?

在flink中,watermark需要从上游算子向下游算子进行传递,比方说我们在读取source时,给source带上了watermark,后续要使用source流进行窗口聚合等操作,而一些窗口的触发是需要watermark来参与的(watermark大于窗口的最大时间,触发窗口计算)

传递规则是:下游算子的watermark是上游算子中watermark最小的那一个。这种传递规则是很好理解的,因为watermark标志的是在此之前的数据已经完全到来,所以上游算子中最小的watermark代表着所有上游算子的共识,因为所有上游算子都认为在这个watermark之前的数据已经全部到来了。

开发Flink事件时间窗口程序,为什么有数据输入,却没有输出,原因在于watermark 是否正常?

如何通过Flink UI观察watermark?

典型问题1

watermark的生成方式:

  • 按每个subtask生成:早期版本都是这种机制,典型问题是如果一个source subtask消费多个partition,那么多个partition之间的数据读取可能会加剧乱序程度
  • 按每个partition生成:新版本引入了基于每个分区的watermark生成机制,这种机制可以有效避免上述问题。

典型问题2

部分分区/subtask断流

根据上面提到的watermark传递机制,下游subtask会将上游所有subtask的watermark值的最小值作为自身的watermark值,如果上游有一个subtask的watermark不更新了,则下游的watermark都不更新。

解决方案:idle source 当某个subtask断流超过配置的idle超时时间时,将当前的subtask置为idle,并下发一个idle的状态给下游,下游在计算自身的watermark时,可以忽略掉当前是idle的那些subtask。

典型问题3

迟到数据处理

因为watermark表示当前事件发生的真实时间,那晚于watermark的数据到来时,系统会认为这种数据是迟到数据。

算子自身来决定如何处理迟到数据:

  • window聚合,默认会丢弃迟到数据
  • 双流join,如果是outer join,则可以认为它不能join到任何数据
  • CEP,默认丢弃

window

典型的window:

  1. tumble window(滚动窗口)
  2. sliding window(滑动窗口)
  3. session window(会话窗口)

其它:

  1. 全局window
  2. count window
  3. 累计窗口 ...

基本功能

滚动窗口

窗口划分:

  • 每个key单独划分
  • 每条数据只会属于一个窗口

窗口触发: window结束时间到达的时候一次性触发(是触发不是计算)

滑动窗口

参数除了窗口的size之外,还有滚动时间

窗口划分:

  • 每个key单独划分
  • 每条数据可能会属于多个窗口

窗口触发: window结束时间到达的时候一次性触发

一条数据可能属于多个窗口

会话窗口

与前两种固定窗口(根据数据到来的时间,确定所属窗口)不同, 不能确定数据最终所属的窗口,这能确定现有那一条,因为不可预知后面来的数据。

窗口划分:

  • 每个key单独划分
  • 每条数据会单独划分为一个窗口,如果window之间有交集,则会对窗口进行merge

窗口触发: window结束时间到达的时候一次性触发

迟到数据处理

如何定义迟到 一条数据到来后,会用windowAssigner给它划分一个window,一般时间窗口是一个时间区间,比如[10:00,11:00),如果划分出来的window end比当前的watermark值还小,说明这个窗口已经出发了计算,这条数据会被认为是迟到数据。

什么情况下会产生迟到数据? 只有事件时间下才会有迟到数据。

默认怎么处理:丢弃

处理方式

  1. Allow lateness 需要设置一个允许迟到的时间,设置之后,窗口正常计算结束后,不会马上清理状态,而是会多保留allowLateness这么长时间,在这段时间内如果还有数据到来,则继续之前的状态进行计算 (适用于SQL,DataStream)
  2. SideOutput(侧输出流) 这种方式需要对迟到数据标记一个tag,然后在DataStream上根据这个tag获取到迟到数据流,然后业务层面自行选择进行处理。 (适用于DataStream)

3. 增量计算:

  • 每条数据到来,直接进行计算,window只存储结算结果。比如:计算sum,状态中只需要存储sum的结果,不需要保存每条数据
  • 典型的reduce,aggregate等函数都是增量计算 (SQL中的聚合只有增量计算)
  1. 全量计算:
  • 每条数据到来,会存储到window的state中,等到window出发计算的时候,将所有的数据拿出来一起计算
  • 典型的process函数就是全量计算

通常来讲,window都是在结束的时候才能输出结果,比如1h的tumble window,只有在1小时结束的时候才能统一输出结果。

如果窗口较大,比如1h或者1D,甚至更大,那么计算结果输出的延迟就会较高,失去了实时计算的意义。

EMIT输出指的是,在window没有结束的时候,提前把window计算的部分结果输出出来。

怎么实现?

在DataStream里面可以通过自定义Trigger实现,Trigger的结果可以是:

  • Continue
  • Fire(触发计算,但不是清理)
  • PURGE
  • FIRE_AND_PURGE

SQL也可以使用,配置:

  • table.exec.emit.early-fire.enabled=true
  • table.exec.emit.early-fire.delay={time}

高级优化

前面说到保留状态,有两种选择,内存,外部存储。减少内存的占用使其可控,为了任务稳定,还是放在外部存储比较好,但是写回状态比较频繁,每一次访问都需要进行一次序列化和反序列化,对CPU开销很大。

mini-batch优化

为了应对

  • 解决中间结果偏多
  • 状态访问频繁

minibatch的攒批策略:通过在每个聚合节点注册单独的定时器来实现,时间分配策略采用简单的均分,比如有4个aggregate节点,用户配置10s的minibatch,那么每个节点会分配2.5s,如图

image.png

但是存在以下问题:

  • 用户能容忍10s的延迟,但是真正用来攒批的只有2.5s,攒批效率低,拓扑越复杂,差异越明显
  • 由于上下游的定时器的触发是纯异步的,可能导致上游触发微批的时候,下游也正好触发微批,而处理微批时会一段时间不消费网络数据,导致上游很容易被反压
  • 计时器会引入额外的线程,增加了线程调度和抢锁上的开销

MiniBatch 攒批策略在内存维度是通过统计输入条数,当输入的条数超过用户配置的blink.miniBatch.size时,就会触发批次以防止 OOM。但是 size 参数并不是很好评估,一方面当 size 配的过大,可能会失去保护内存的作用;而当 size 配的太小,又会导致攒批效率降低。

micro-batch攒批策略

为了解决minibatch上述问题,microbatch引入了watermark来控制聚合节点的定时触发功能,用watermark来控制聚合节点的定时触发功能。用 watermark 作为特殊事件插入数据流中将数据流切分成相等时间间隔的一个个批次。实现原理如下所示:

image.png

MicroBatch 会在数据源之后插入一个 MicroBatchAssigner 的节点,用来定时发送 watermark,其间隔是用户配置的延时参数,如10s。那么每隔10s,不管数据源有没有数据,都会发一个当前系统时间戳的 watermark 下去。一个节点的当前 watermark 取自所有 channel 的最小 watermark 值,所以当聚合节点的 watermark 值前进时,也就意味着攒齐了上游的一个批次,我们就可以触发这个批次了。处理完这个批次后,需要将当前 watermark 广播给下游所有 task。当下游 task 收齐上游 watermark 时,也会触发批次。这样批次的触发会从上游到下游逐级触发。

这里将 watermark 作为划分批次的特殊事件是很有意思的一点。Watermark 是一个非常强大的工具,一般我们用来衡量业务时间的进度,解决业务时间乱序的问题。但其实换一个维度,它也可以用来衡量全局系统时间的进度,从而非常巧妙地解决数据划批的问题。

因此与 MiniBatch 策略相比,MicroBatch 具有以下优点:

  1. 相同延时下,MicroBatch 的攒批效率更高,能攒更多的数据。
  2. 由于 MicroBatch 的批次触发是靠事件的,当上游触发时,下游不会同时触发,所以不像 MiniBatch 那么容易引起反压。
  3. 解决数据抖动问题

source:Flink SQL 核心解密 —— 提升吞吐的利器 MicroBatch

local-global倾斜优化

image.png

local类似于mapreduce中的combiner,为了降低shuffle,在shuffle之前先做预聚合,上图如果直接shuffle对于上面聚合节点的压力很大;如果现在有local计算节点,在每个算子shuffle之前先做一次预聚合,这样再shuffle到下游,这个问题就可以得到缓解。

Distinct计算状态复用优化

image.png

image.png

pane优化

image.png

image.png

案例

使用Flink SQL计算抖音的日活曲线

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)

table.exec.emit.early-fire.enabled=true

table.exec.emit.early-fire.delay=5min

问题:所有数据都要在一个subtask中完成窗口计算,无法并行

通过两个阶段聚合来把数据打散,完成第一轮聚合,第二轮聚合只需要对各个分桶的结果求和即可

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)

table.exec.emit.early-fire.enabled=true

table.exec.emit.early-fire.delay=5min

table.exec.window.allow-retract-input=true

计算大数据任务的资源使用

问题描述:

大数据任务(特指离线任务)运行时通常会有多个container启动并运行,每个container在运行结束时,YARN会负责将它的资源使用(CPU,内存)情况上报,一般大数据任务运行时间从几分钟到几小时不等。

需求:

根据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,INTERVAK '10' MINUTE)