《流式计算中的 Window 计算》|青训营笔记

148 阅读8分钟

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

本节课程目录:

  1. 概述流式计算和批式计算
  2. Watermark 的含义、生成方法、传递机制等
  3. Window 基本功能和高效优化
  4. 案例分析

1. 概述

1.1 流式计算 VS 批式计算

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

1.png

1.2 批处理

批处理模型典型的数仓架构为T+1架构,即数据计算是天级别的,当天只能看到前一天的计算结果。

通常使用的计算引擎为Hive或者Spark等。计算的时候,数据是完全确定的,输入和输出都是确定性的。

image.jpeg

1.3 处理时间窗口

实时计算:处理时间窗口

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

image-2.jpeg

1.4 处理时间 VS 事件时间

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

事件时间:数据产生的时间,比如客户端、传感器、后段代码等上报数据时的时间。

image-3.jpeg

1.5 事件时间窗口

实时计算:事件时间窗口

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

image-4.jpeg

1.6 Watermark

  • 在数据中插入一些 watermark,来表示当前的真实时间。 image.png
  • 在数据存在乱序的时候,watermark 就比较重要了,它可以用来在乱序容忍和实时性之间做一个平衡。 image-2.png

2. Watermark

2.1 什么是Watermark?

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

2.2 如何产生 Watermark

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

  • SQL image-3.png
  • Datastream image-4.png

2.3 如何传递 Watermark

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

image-5.png

2.4 如何通过 Flink UI 观察 Watermark

image-6.png

image-8.png

2.5 典型问题

2.5.1 Per-partition VS Per-subtask watermark 生成

  • Per-partition watermark 生成

早期版本的机制。典型问题是如果要一个 source subtask 消费多个 partition,那么多个 portition 之间的数据读取可能会加剧乱序程度。

  • Per-subtask watermark 生成 新版本引入了基于每个 partition 单独的 watermark 生成机器,而这种机制可有效避免。

2.5.2 部分 partition/subtask 断流

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

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

2.5.3 迟到数据处理

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

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

  1. Window 集合,默认会丢弃迟到数据
  2. 双流 join ,如果是outer join, 则可以认为它不能join任何属于
  3. CEP,默认丢弃

3. Window

3.1 Window 基本功能

3.1.1 Window 分类

  • TUMBLE Window 滚动窗口

    • 最常见的窗口类型,根据数据的时间划分到它所属的窗口中windowStart = timestamp - timestamp % windowSize,这条数据所属的 window 就是[windowStart, windowStart + windowSize)
    • Flink 中的窗口划分是 key 级别的
    • 每条数据只会属于一个窗口
    • 当 window 结束时间到达的时候,窗口一次性触发对应的输出 image-10.png
  • HOP Window 滑动窗口

    • Flink 中的窗口划分是 key 级别的
    • 每条数据可能会属于多个窗口(取决于窗口定义的大小和滑动)
    • 当 window 结束时间到达的时候,窗口一次性触发对应的输出 image-11.png
  • SESSION Window 会话窗口

    • 这是一个动态merge的过程,一般会设置一个会话的最大的gap,如10min。
    • Flink 中的窗口划分是 key 级别的
    • 每条数据会单独划分为一个窗口,如果窗口之间有交集,则会对其进行merge。
    • 当 window 结束时间到达的时候,窗口一次性触发对应的输出
    • 例如某个 key 来第一条数据的时候,它的 window 就是 [event_time, event_time + gap],当这个 key 来另一条数据的时候,它会立即产生一个窗口,如果这个窗口跟之前的窗口有 overlap 的话,则会将两个窗口进行一个 merge,变成一个更大的窗口,此时需要将之前定义的 timer 取消,再注册一个新的 timer。 image-12.png

3.1.2 迟到数据处理

  • 迟到数据的定义:

Watermark 驱动某个窗口触发输出之后,这个窗口如果后面又来了数据,那这种情况就属于是迟到的数据。(它所属的窗口已经被触发才算迟到)

  • 产生迟到数据的情况

只有事件时间下才会有迟到的数据

  • 迟到数据的数据方式
  1. 使用 Allow lateness 方式,需要设置一个允许迟到的时间,窗口正常计算结束后,不会马上清理状态,而是会多保留允许的时间,若有迟到数据,则继续之前的状态进行计算(适用于 DataStream 和 SQL )
  2. 使用 SideOutput 方式,需要对迟到数据打一个 tag,然后根据 tag 获取到迟到数据,把其转变成一个单独的流,再由用户自己来决定如何处理这部分数据(只有在 DataStream 窗口中才可以使用)
  3. 直接丢弃 image-13.png

3.1.3 增量计算 VS 全量计算

  • 增量计算:

    • 每条数据到来后,直接参与计算,window 只存储计算结果。
    • 典型的 reduce、aggregate 等函数都是增量计算
    • SQL 中主要是窗口聚合,所以都是可以增量计算的 image-14.png
  • 全量计算:

    • 每条数据到来后,先放到一个 buffer 中,会存储到状态里,直到窗口触发输出的时候,才会所有数据拿出来统一进行计算。
    • 典型的 process 函数是全量计算 image-15.png

3.1.4 EMIT 触发

为了获得实时计算输出,使用 EMIT 机制,在 window 没有结束的时候,提前把 window 计算的部分结果输出出来。

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

  • CONTINUE
  • FIRE(触发计算,但是不清理)
  • PURGE
  • FIRE_AND_PURGE

在 SQL 里通过配置可以实现:

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

3.2 Window 高级优化(只限于在 SQL 中的窗口)

3.2.1 Mini - batch 优化

  • 主要解决的问题:只有一小批数据再进行计算,每个 key 的状态访问只有一次,这样在单个 key 的数据比较集中的情况下,对于状态访问可以有效的降低频率,最终提升性能。
  • 主要适用环境:没有窗口的聚合场景。 image-2.png

3.2.2 Local - global 优化(分布式系统中典型的优化)

  • 主要解决的问题:降低了数据 shuffle 的量,同时缓解数据的倾斜。
  • 第一阶段先做一个 local 的聚合,直接跟在上游算子之后进行处理;第二阶段是对第一阶段的结果做一个 merge。 image-3.png

3.2.3 Distinct 状态复用

  • 主要解决的问题:降低状态量
  • 把相同字段的 Distinct 计算用一个 map 的 key 来存储,在 map 的 value 中,用一个 bit vector 来实现就可以把各个状态复用到一起。 image.png

3.2.4 Pane 优化(滑动窗口)

  • 主要解决的问题:降低滑动窗口的状态存储量
  • 将窗口状态划分成更小粒度的 pane,每来一条数据,只更新对应的 pane 的结果就可以了。当窗口需要输出结果的时候,只需要将这个窗口对应的 pane 的结果 merge 起来就可以了。 b25a81a5e39e4b87bee6290f91b2b15f~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png

4. 案例分析

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

image-4.png

  • table.exec.emit.early - fire.enabled = true
  • table.exec.emit.early - fire.delay = 5min
  • table.exec.window.allow - retract - input = true

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

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

  • 问题描述:大数据任务(特指离线任务)运行时通常会有多个container启动并运行,每个container在运行结束的时候,YARN会负责将它的资源使用(CPU、内存)情况上报。一般大数据任务运行时间从几分钟到几小时不等。
  • 需求:根据YARN上报的各个container的信息,在任务结束的时候,尽快的计算出一个任务运行所消耗的总的资源。假设前后两个container结束时间差不超过10min。

image-5.png 通过会话窗口来讲数据划分到一个 window 中,然后再将结果求和即可。