流计算中的window计算| 青训营笔记

93 阅读9分钟

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

一、概述


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

image.png

  • 批处理

    • 典型架构为T + 1 数仓架构
      • 数据计算是天级别的,只能看到前一天的计算结果
  • 处理时间

    • 数据在流式计算系统中真正处理时所在机器的当前时间
  • 事件时间

    • 数据产生的时间,如:客户端、传感器、后端代码等上报数据时的时间。
  • 实时计算

    • 处理时间窗口

      • 数据实时流动, 实时计算, 窗口结束直接发送数据,不需要周期调度任务
    • 事件时间窗口

      • 数据实时进入到真实事件发生的窗口中进行计算,可以有效的处理数据延迟和乱序
      • 判断窗口结束时需要插入一些Watermark配合来处理乱序

二、Watermark


  • Watermark定义 表示系统认为的当前真实的事件时间

  • Watermark产生 一般是从数据的事件时间来产生,最常见的产生策略包括使用当前事件时间的时间减去一个固定的delay,来表示可以可以容忍多长时间的乱序。 image.png

  • Watermark传递 -- 取上流所有subtask的最小值

    • 类似Checkpoint的制作过程,传递就类似于Checkpoint的barrier
    • 上下游task之间有数据传输关系的,上游就会将watermark传递给下游;下游收到多个上游传递过来的watermark后,默认会取其中最小值来作为自身的watermark,同时它也会将自己watermark传递给它的下游。
    • 经过整个传递过程,最终系统中每一个计算单元就都会实时的知道自身当前的watermark是多少。

    image.png

典型问题

  • 一、Per-partition VS per-subtask watermark生成
    • Per-partition 早期机制,典型问题是如果一个source subtask消费多个partition,那么多个partition之间的数据读取可能会加剧乱序程度
    • Per-subtask 新版本引入基于每个partition单独watermark生成机制避免上述问题
  • 二、部分partition/subtask断流
    • 如果上游有一个subtask的watermark不更新,则下游的watermark都不更新
    • 解决方案:Idle source
      • 当某个subtask断流超过配置的idle超过时间时,将当前subtask置为idle,并下发一个idle的状态给下游。下游在计算自身watermark的时候,可以忽略掉当前是idle的那些subtask
  • 三、迟到数据处理
    • 迟到数据:晚于watermark的到来的数据
    • 算子自身的处理:
      • Window算子, CEP(Flink复杂事件)默认丢弃
      • 双流join, 如果时outer join,则可以认为它不能join到任何数据

三、Window


3.1 Window基本功能

3.1.1 Window分类

  • 典型Window
    • Tumble Window(滚动窗口)
    • Sliding Window(滑动窗口)
    • Session Window(会话窗口)
  • 其他Window 全局Window、 Count Window、累计窗口

Flink 中的窗口划分是key级别的

Tumble Window(滚动窗口)

  • 窗口划分:每个key单独划分, 每条数据只会属于一个窗口
    • 根据数据的时间(可以是处理时间,也可以是事件时间)划分到它所属的窗口中windowStart = timestamp - timestamp % windowSize,这条数据所属的window就是[windowStart, windowStart + windowSize)
  • 窗口触发:window结束时间到达的时候一次性触发
    • 目前的实现是给每个window都注册一个timer,通过处理时间或者事件时间的timer来触发window的输出。 image.png

Sliding Window(滑动窗口)

  • 窗口划分:每个key单独划分, 每条数据可能属于多个窗口
  • 窗口触发:window结束时间到达的时候一次性触发 image.png

Session Window(会话窗口)

  • 窗口划分:
    • 每个key单独划分
    • 每条数据会单独划分窗口,如果window之间有交集。则会对窗口进行merge
  • 窗口触发:window结束时间到达的时候一次性触发 image.png

3.1.2Window使用

image.png

3.1.3 迟到数据处理

  • 迟到数据
    • 当一条数据到来之后,用windowAssigner给它划分的winodw的window end比watermark值还小,说明窗口触发了计算,则该数据为迟到数据
  • 只有事件时间才会有迟到数据
  • 默认丢弃迟到数据
  • 处理方法 image.png

3.1.4 增量计算vs全量运算

  • 增量计算
    • 每条数据到来,直接进行计算,window只存储计算结果
      • e.g. DataStream的reduce、aggregate函数, SQL的聚合 image.png
  • 全量计算
    • 每条数据到来,会用buffer存储到window的state中,等到window触发计算的时候,将所有数据拿出来一起计算
      • e.g.DataStream的process函数
      • image.png

3.1.5 EMIT触发

  • EMIT输出:在window没有结束的时候,提前把window计算的部分结果输出出来
    • window一般是结束时才输出结果,如果窗口较大,计算结果输出延迟比较高,就失去了实时计算意义。
  • 实现
    • DataStream 需要通过自定义Trigger实现
    • SQL 通过配置
      • table.exec.emit.early-fire.enabled= true
      • table.exec.emit.early-fire.delay={time}
  • 这种emit的场景就是一个典型的retract的场景,发送的结果类似于+[1], -[1], +[2], -[2], +[4]这样子。这样才能保证window的输出的最终结果是符合语义的

3.1.6 Window Offset

  • 在计算窗口时,可以让窗口有一个偏移
    • window = timestamp - timestamp % windowSize 时间戳按照unix timestamp来算的。如果我们要用一个一周的窗口,到周日结束,但计算出时从周四开始。(1970.1.1是周四)
  • window公式:window = timestamp - (timestamp - offset + windowSize) % windowSize
  • DataStream支持,SQL不支持,字节内部版本扩展支持了SQL的window offset

3.2 Window-高级优化

以下所有的高级优化,都只限于在SQL中的window中才有。在DataStream中,用户需要自己通过代码来实现类似的能力。

Mini-batch优化

  • 解决频繁访问状态的问题
  • 保存Flink状态时,Flink的状态比较大一些都推荐使用rocksdb statebackend,这种情况下,每次的状态访问就都需要做一次序列化和反序列化,这种开销还是挺大的
  • 基本原理:即赞一小批数据再进行计算,这批数据每个key的state访问只有一次,这样在单个key的数据比较集中的情况下,对于状态访问可以有效的降低频率,最终提升性能。
    • 假设用最简单的方式实现,那就是每个算子内部自己进行攒一个小的batch,这样的话,如果上下游串联的算子比较多,任务整体的延迟就不是很容易控制。
    • 所以真正的mini-batch实现,是复用了底层的watermark传输机制,通过watermark事件来作为mini-batch划分的依据,这样整个任务中不管串联的多少个算子,整个任务的延迟都是一样的,就是用户配置的delay时间。

下面这张图展示的是普通的聚合算子的mini-batch原理,window的mini-batch原理是一样的。

image.png

local-global优化---倾斜优化

  • 主要是可以降低数据shuffle的量,同时也可以缓解数据的倾斜
  • 将原本的聚合划分成两阶段
    • 第一阶段先做一个local的聚合,这个阶段不需要数据shuffle,是直接跟在上游算子之后进行处理
    • 第二个阶段是要对第一个阶段的结果做一个merge

image.png

Distinct状态复用

  • 对于distinct的优化,一般批里面的引擎都是通过把它优化成aggregate的方式来处理
  • 在流式window中,我们不能直接这样进行优化,要不然算子就变成会下发retract的数据了。所以在流式中,对于count distinct这种情况,我们是需要保存所有数据是否出现过这样子的一个映射。

在SQL中,我们可以在聚合函数上添加一些filter,如下面的SQL所示:

image.png

我们可以把相同字段的distinct计算用一个map的key来存储,在map的value中,用一个bit vector来实现就可以把各个状态复用到一起了。比如一个long有64位,可以表示同一个字段的64个filter,这样整体状态量就可以节省很多了。

image.png

滑动窗口pane复用

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

  • 优化方法:将窗口的状态划分成更小粒度的pane

    • 这样每来一条数据,我们就只更新这条数据对应的pane的结果就可以了。当窗口需要输出结果的时候,只需要将这个窗口对应的pane的结果merge起来就可以了。
  • 注意:这里也是需要所有聚合函数都有merge的实现的

image.png

四、案例分析


需求一:使用Flink SQL计算抖音的DAU曲线

  • DAU(Daily Active User):指的是每天的去重活跃用户数

  • 输出:每个5s更新一下当前的DAU数值,最终获得一天内的DAU变化曲线

  • 使用滑动窗口 开启EMIT输出

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

    • image.png

  • 加上倾斜优化

    • 通过两阶段聚合把数据打散,完成第一轮聚合,第二轮聚合只需对个分桶的结果求和即可
    • table.exec.window.allow-retract-input=true 字节内部实现,开启emit后会输出retract,实现window允许retract的输入,将窗口连接起来 image.png

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

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

  • 需求:根据YARN上报的各个container的信息,在任务结束的时候,尽快的计算出一个任务运行所消耗的总的资源。假设前后两个container结束时间差不超过10min。

  • 可以通过会话窗口将数据划分到一个window中,然后再将结果求和 image.png