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

253 阅读7分钟

字节跳动青训营第4期 第4课 流计算中的Window计算

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

窗口计算机制是流计算中一个比较基础但又必要的机制

[TOC]

概述

流式计算与批式计算的对比

流批对比图

实时性更高的数据,其价值也更高

批式计算

批处理的典型数仓架构为T+1架构,即当天只能看到前一天的数据。

常使用Hive或Spark,数据完全准备好,输入和输出都是确定的。

问题:能否按小时执行批计算,从而达到一个实时性比较高的计算?

理论上是可以实现的,但实际上是不容易实现的,各类资源的调度、数据仓库建模的复杂程度,导致一个计算可能是几分钟或是几个小时,难以控制在一个小时内。

流式计算

处理时间:数据真正被计算引擎处理的时间

事件时间:代码或任务提交的时间

在事件时间的基础上开窗,是有可能存在延迟的,并且会产生一个问题,什么时间算窗口结束?

Watermark就是为了处理这个窗口结束的问题而产生的。

Watermark示意图如下:

Watermark

如上图,在乱序情况下,W(11)将认为其后不存在比11小的数据,若遇到比11小的数据,则判断为有延迟的数据,进行相应处理即可。

Watermark

什么是Watermark

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

生成方法

生成Watermark

CREATE TABLE Orders {
    user BIGINT,
    product STRING,
    order_time TIMESTAMP(3),
    WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND
} WITH { ... };

上述代码实现了根据order_time字段添加watermark,其间隔为5秒。

WatermarkStrategy
    .<Tuple2<Long, String>>forBoundedOutOfOrderness(Duration.ofSeconds(20))
    .withTimestampAssigner((event, timestamp) -> event.f0);

上述代码实现了根据event.f0字段添加watermark,其间隔为20秒。

此类使用方法虽然简单,但是生产环境中常用的方式。

传递机制

传递Watermark

每个算子都从上游的所有Watermark中取最小值作为自己的Watermark,然后传递给下游。

Watermark典型问题

生成方式:Per-partition VS Per-subtask

早期版本的Flink使用subtask的生成方式,但是每个subtask是可以同时使用多个partition的,每个partition的读取速度可能不同,这种方式会加剧数据乱序程度。

新版本的Flink按照partition的方式去生成,可以一定程度环节上述问题。

部分partition/subtask断流

如果上游的算子断流,不再更新,则下游的Watermark都不再更新。

解决方案:Idle source

当某个subtask断流超过Idle阈值,则将其置为Idle,并下发Idle状态给下游,下游在计算自身Watermark时,可以忽略掉状态为Idle的subtask。

迟到数据处理

  1. Window聚合,默认丢弃迟到数据
  2. 双流Join,如果是外连接,则可以认为Join不到任何数据
  3. CEP,默认丢弃

Window

典型窗口包括:

  1. 滚动窗口 Tumble Window
  2. 滚动窗口 Slide Window
  3. 会话窗口 Session Window

其他窗口:

  1. 全局窗口
  2. 计数窗口
  3. 累计窗口

基本功能

Window的使用方式

SELECT
    user,
    TUMBLE_START(order_time, INTERVAL '1' DAY) AS wStart,
    SUM(amount)
FROM Orders
GROUP BY
    TUMBLE(order_time, INTERVAL '1' DAY),
    user

上述代码实现了一个滚动窗口,根据order_time字段,每相隔1天创建一个window

DataStream<Tuple2<String, Integer>> dataStream = env
    .socketTextStream("localhost", 9999)
    .flatMap(new Splitter())
    .keyBy(value -> value.f0)
    .window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
    .sum(1);

上述代码实现了一个滚动窗口,根据value.f0字段,每相隔5秒创建一个window

TODO: 添加两种方式的代码并解释

Table API相对SQL API更容易进行功能上的扩展

滚动窗口

窗口划分:根据每个key单独划分,每条数据只属于一个窗口

窗口触发:Window结束时间到达时一次性触发

滑动窗口

在滚动窗口的基础上,窗口和窗口之间可能会重合,一条数据可能会属于多个窗口,例如:11点-13点,12点-14点这样的窗口。

会话窗口

与滚动窗口和滑动窗口不同,会话窗口相对更灵活,可以设置一个session gap在间隔之内的小窗口,可以合并为一个大的会话窗口。

迟到数据的处理

怎么定义迟到?

会给每条数据通过WindowAssigner划分一个window,这个window是一个区间,如果window end比当前watermark小,则认为是迟到数据。

只有事件时间下才有,默认是丢弃处理。

Allow lateness

设置一个允许迟到的时间,在允许迟到的时间内,计算状态会被保存,如果有迟到的数据,则会根据计算状态继续进行计算,相当于对之前的计算结果的修正。

适用于Datastream和SQL

SideOutput 侧输出流

对迟到数据打一个tag,在Datastream上获取到迟到数据流,由业务进行处理

适用于Datastream

增量和全量计算

增量:每条数据到来都会参与计算,window只储存计算结果,不存储数据,典型的reduce,aggregate都是增量计算;SQL的聚合只有增量计算。

全量:每条数据都会存储到window的state,触发计算后,将所有数据拿出来一起计算;process函数是全量计算。

增量是优于全量的,但也必须根据业务情况去选择增量还是全量计算。

EMIT触发

什么是EMIT

当窗口定义的比较长,例如一个小时或一天,计算结果输出的延迟较高,失去了实时计算的意义。

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

如何实现

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

  • CONTINUE
  • FIRE(计算但不清理)
  • PURGE
  • FIRE_AND_PURGE

SQL也可以通过配置的方式使用

高级优化

Mini-Batch优化

主要目的是优化动态表中频繁修改中间状态的场景。

通过Checkpoint或Watermark去调度一个mini batch,新到数据先存入一个mini-batch,当满足一定条件后,将mini batch中的数据一起参与计算。

倾斜优化 local - global

倾斜优化

如上图所示,即倾斜优化的基本思想。

对于上图左侧,描述了一个根据key对数字求和的任务,不同底色的数字代表了不同的key,从多个上流获取的数据经过聚合,导致了红色的算子其压力较大,而紫色算子压力较小。

右侧则是倾斜优化的结果,每个上流先进行一个local的聚合,聚合之后再进行global的聚合,从而大大降低了后续算子的压力。

Distinct 计算状态复用

Distinct

如上图,在实际的生产环境中,有时会需要使用很多的DISTINCT关键字,而SQL又只说明要做什么而不说明具体要怎么做,那么就会浪费很多资源在处理DISTINCT关键字上。

Distinct 复用Map

对DISTINCT的优化方案是对其计算状态进行复用,建立一个Map用于存储Distinct计算状态,在需要的时候取出进行应用。

Pane优化

当窗口大,滑动间隔小时,每条数据要参与很多个窗口计算,开销就变得特别大。

先把window划分成更小粒度的Pane,一条数据只属于一个Pane,就像一个微型的滚动窗口,在窗口输出结果时,将所有Pane进行临时合并并输出。

案例分析

使用Flink SQL计算抖音DAU(日活)实时曲线

思路:做一个滚动窗口,使用EMIT机制提前将数据输出

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)

基于以上的实现方式,所有数据需要在一个subtask执行,无法并行。

优化思路:把整个计算改变为2个阶段,先根据用户分桶,然后再将每个桶的值进行聚合,即可得到整体的结果。实现了并发的要求,提升了计算的时间。

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)

)
GROUP BY
    TUMBLE(event_time, INTERVAL '1' DAY)

对于优化方案,汇总的SELECT并发为1,但是分成了10000个桶,相当于有10000的并发在执行这个作业

使用Flink SQL进行大数据任务资源使用实时统计分析

场景介绍

思路:建立一个会话窗口,以10分钟为间隔,读取cpu和内存的使用量进行SUM()求和即可。

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)