Window | 青训营笔记

61 阅读8分钟

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

1. 三种窗口

1.1 滚动窗口

窗口划分:

  • 每个key单独划分(user1、user2、user3)
  • 每条数据只会属于一个窗口

窗口触发:

  • Window结束时间到达的时候一次性触发
  • 数据进来就可以和之前累加的和进行计算

窗口输出:

  • 在窗口结束的时候才会把结果输出

1.2 滑动窗口

窗口划分:

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

窗口触发:

  • Window结束时间到达的时候一次性触发

1.3 会话窗口

前面两个窗口是固定窗口,数据到来的时候,根据数据的时间就能直接确定它所属的窗口,窗口是不会变的。会话窗口和它们的区别是,数据到来的时候,不能确定数据最终所属的窗口是哪个

窗口划分:

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

窗口触发:

  • Window结束时间到达的时候一次性触发

2. 迟到数据的处理方式

只有事件时间才会产生迟到数据

迟到数据默认处理:丢弃

  • Allow lateness

    • 这种方式需要设置一个允许迟到的时间。设置之后,窗口正常计算结束后,不会马上清理状态,而是会多保留allowLateness这么长时间,在这段时间内,如果还有数据到来,则继续之前状态进行计算。计算后输出结果,覆盖原来的结果
    • 适用于:DataStream、SQL
    • window里面如果开启了allowLateness,就等于是在修正之前的结果,修正的过程就是要有一个retract的过程
  • SideOutput(侧输出流)

    • 这种方式需要对数据做一个tag标记,然后在DataStream上根据这个tag标记来获取迟到数据流,由业务层面自行选择处理

3. 窗口计算的类型

  • 增量计算

    • 每条数据到来,直接进行计算,window只存储计算结果
    • 增量计算函数:reduce()、aggregate()
    • SQL中的聚合只有增量计算
  • 全量计算

    • 每条数据到来,会存储到window的state中。等到window触发计算的时候,将所有数据拿出来一起计算
    • 全量计算函数:process()

4. EMIT触发

通常来讲,window都是在窗口结束的时候才输出结果,如果窗口比较大,就失去了实时计算的意义。

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}

5. Window高级优化

5.1 Mini-batch优化 解决频繁状态访问的问题

场景:FinkSQL里边的动态表的retract输出

每来一条数据,如果数据有更新,会产生一个retract流

例如:Mary,1更新成Mary,2。会retract之前的Mary,1,然后输出更新后的Mary,2

image.png

  • 问题:

    • 把中间的每一条结果都输出出来,输出量会比较大;
    • 像aggregate这种算子是需要保留状态的,大的状态放在外边,状态放在外边会有序列化和反序列化的过程,每次数据来了之后,都需要从外部读取之前的状态,计算,输出,再把结果写到外部,这样,状态的访问程度非常高,CPU的开销非常大。
  • 解决:

    • Mini-batch主要解决中间结果偏多,状态访问比较频繁的问题
    • Mini-batch是这样做的:让算子攒一小批数据,接着读一次状态,处理这一小批数据,输出,写回状态,就做到解决上面的两个问题的能力

image.png

  • 问题引申:

    • 在每一个上下游算子,很多地方都会有一个Mini-batch的过程,如果每一个算子都需要自己攒一批数据,然后输出,最终数据的延迟就是上下游各个算子延迟的总和,这样大大的增加了延迟
  • 解决:

    • 实际生产中,并不是让每一个算子去攒一批数据,而是用类似Watermark的传递机制去做一个全局的协调。
    • 从上游添加一个Mini-batch的assigner,它去统一发起一个指令,表示开始了当前一个新的Mini-batch的信号,这样,每一个下游的算子收到这个信号以后,就把之前缓存的数据计算,输出,写到状态,再往下传递这个信号。等于在一个信号周期内,就可以完成整个链路算子的Mini-batch的计算

5.2 local-global 解决数据倾斜的问题

  • MapReduce中的combiner就相当于local计算

  • 在做local之前,不需要对数据进行Shuffle,它需要做一个预聚合,再把中间结果发到最终的地方去处理

  • 左图中不同颜色代表不同key,红色明显多一些,如果直接进行Shuffle,上面节点的压力就会比较大 如果有一个local的计算节点,可以在每一个算子进行Shuffle之前,先做一个预聚合,如果是求和,在local进行求和以后,再Shuffle到下游,热点问题就会得到一个缓解,这就是local-global的优化

image.png

5.3 distinct 计算状态复用,降低状态存储量

问题:

  • 下图所示的SQL语句,每条数据会先进行filter,再进行左边的聚合计算

  • 在window里面,distinct优化成group by不太现实,因为窗口会变得很复杂

  • 如果distinct不能做SQL本身执行计划的优化,那么最终在window算子里面去处理distinct计算,就需要在状态里边把所有明细的数据都保留下来

  • 知道每一个key来过或者没来过,就可以求得最终的count(distinct)结果,如果每一个distinct都单独存一份状态,这个状态量还是很大的。

image.png

解决:

  • 但是SQL写成上图所示的样子,一个指标有多个filter条件,其实可以让一个指标的多个filter条件复用到同一份状态

  • 比如它是一个map,key是到来的每一条数据,value是一个bool值,value用来标识数据来过或者没来过。如果想要复用它,可以把它定义成一个int或long,用每一个bit去表征它来或没来,这样,一个long就可以表示64个这样的同一个distinct指标的不同的filter条件,这样,复用量是比较可观的

image.png

5.4 Pane 降低滑动窗口的状态存储量

问题:

  • 在滑动窗口里,一条数据可能会属于多个窗口

  • 很多时候,有可能滑动窗口的大小跟滑动步长的比例是比较大的,比如1小时的窗口,1分钟的滑动,这样的话,每条数据需要参与的窗口的计算量是非常大的,计算开销也是比较大的

解决:

  • 给它划分更小的粒度,这个粒度我么称之为Pane,它不是窗口,但最终可以组合成窗口。
  • 比如3小时的窗口,1小时的滑动,我们可以将它划分成1小时的Pane,这样数据来了之后,就可以像普通的滚动窗口一样,一条数据只属于一个key,直接计算就可以,这个计算量和存储量相对来说就比较可控 。
  • 如果这样优化,我们需要在窗口最终输出结果的时候去做一次merge。比如说,9点到12点,我们需要把前三个Pane,merge到一起,把merge后的结果输出出去。也就是说,把计算和状态存储做了一个优化,但输出的时候需要临时做一个merge。这就是所谓的Pane的优化的基本的思路

image.png

6. 案例分析

6.1 需求一:计算抖音的日活曲线

思路:

  • 做一个滚动窗口,用EIMT机制提前把计算结果输出
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

上述SQL语句存在的问题:

  • group by里面只有时间窗口,最终计算必须要有一个单并发的全局的窗口计算,才能够聚合到所有的用户的uid

解决方案:

  • 通过两阶段聚合来把数据打散,完成第一轮聚合,第二轮聚合只需要对各个分桶的结果求和即可
select
    count(distinct uid) 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(even_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 #字节内部添加的参数。
#在原生社区不能跑,因为这个SQL开启了EIMT,但是它会让窗口的输出变成retract的数据,正常的window不允许retract的输入。

此类问题归为倾斜优化或热点优化问题

6.2 需求二:使用FlinkSQL计算大数据任务的资源使用

问题描述:

  • 大数据任务(特指离线任务)运行时通常会有多个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, interval '10'minute)