理解Flink窗口机制

窗口介绍

概念

在这里插入图片描述
Flink作为一个流式处理框架,数据是连续不断的,但是有时我们需要做一些聚合类的处理,例如过去1分钟有多少用户点击量。这时就会把源源不断的stream拆成一个个batch。
Flink 认为 Batch 是 Streaming 的一个特例,所以 Flink 底层引擎是一个流式引擎,在上面实现了流处理和批处理。而窗口(window)就是从 Streaming 到 Batch 的一个桥梁,它将流拆分为有限大小的“桶”,将无界的流数据划分为有界的集合,在有界的数据集上进行应用计算,是处理无限流的核心机制。

生命周期

在这里插入图片描述
开始于第一个属于这个窗口的元素到达的时候,结束于第一个不属于这个窗口的元素到达的时候;
Assigner:分配器,负责将元素分配到不同的window。使用窗口时必须设置;
Trigger:触发器,定义何时或什么情况下触发一个window。每个窗口分配器默认都有一个触发器,Trigger上会有定时器,用来决定一个窗口何时能够被计算或清除。例如,多有的event-time窗口分配器都有一个EventTimeTrigger作为默认触发器。该触发器在watermark通过窗口末尾时触发。
如果默认的触发器符合要求则不需要另外定义,如果默认的触发器不符合要求,就可以使用trigger(…)自定义触发器。
Trigger的返回结果可以是continue(不做任何操作)、fire(处理窗口数据)、purge(移除窗口和窗口中的数据)、或者 fire + purge。一个Trigger的调用结果只是fire的话,那么会计算窗口并保留窗口原样,也就是说窗口中的数据仍然保留不变,等待下次Trigger fire的时候再次执行计算。一个窗口可以被重复计算多次直到它被 purge 了。在purge之前,窗口会一直占用着内存。
Evictor:驱逐器,在触发器触发之后以及窗口函数被应用之前或之后可选择的移除元素,会用来遍历窗口中的元素列表,并决定最先进入窗口的多少个元素需要被移除,剔除窗口中不需要的元素,相当于一个filter,剩余的元素会交给用户指定的函数进行窗口的计算。使用Evictor可以防止预聚合,因为窗口的所有元素都必须在应用计算逻辑之前先传给Evictor进行处理。如果没有 Evictor 的话,窗口中的所有元素会一起交给函数进行计算。

分类

按划分方式分类

基于时间划分窗口(time-window )、基于数量划分窗口(count-window)
在这里插入图片描述

按分配器(Assigner)分类

滚动窗口(Tumbling window)

在这里插入图片描述
将数据流切分成长度固定,互不重叠的窗口。每一个元素只能属于一个窗口。
例如统计每一分钟中用户购买的商品的总数,需要将用户的行为事件按每一分钟进行切分,划分为基于时间的滚动窗口。或者想要每100个用户购买行为事件统计购买总数,那么每当窗口中填满100个元素了,划分为基于数量的滚动窗口。示例代码如下:
在这里插入图片描述

滑动窗口(sliding window)

在这里插入图片描述
将数据流切分为长度固定,互相可以重叠,一个元素可能存在于多个窗口。设置窗口的时候需要设置窗口的长度及步长。如果窗口步长小于长度,则两个窗口之间会有重叠部分;如果窗口步长等于长度,则相当于滚动窗口;如果窗口步长大于长度,则不重叠部分的数据丢失。
例如每30秒计算一次最近一分钟用户购买的商品总数,可以使用基于时间的滑动窗口。示例代码如下:
在这里插入图片描述

会话窗口(session window)

在这里插入图片描述
根据活动的事件进行窗口划分,会话窗口长度不固定,互相不重叠,没有固定的开始和结束时间,每个窗口之间的间隔也不一定固定,不活跃的时间长度定义了会话窗口的界限。当会话窗口在一段时间内没有接收到元素时会关闭会话窗口,后续的元素将会被分配到新的会话窗口。
由于会话窗口的开始时间和结束时间取决于接收到的元素,所以窗口分配器无法立即将所有的元素分配到正确的窗口中去。相反,会话窗口分配器最开始时先将每一个元素分配到它自己独有的窗口中去,窗口开始时间是这个元素的时间戳,窗口大小是session gap的大小。接下来,会话窗口分配器会将出现重叠的窗口合并成一个窗口。
使用场景举例,比如需要计算每个用户在活跃期间总共购买的商品数量,如果用户30秒没有活动则视为会话断开,就可以使用会话窗口。示例代码如下:
在这里插入图片描述

全局窗口(global window)

在这里插入图片描述
将所有相同keyed的元素分配到一个窗口里。默认的触发器是NeverTrigger,该触发器从不触发,所以在使用GlobalWindow时必须自定义触发器。示例代码如下:
在这里插入图片描述

自定义窗口(custom window)

自定义分配器、触发器、驱逐器。

基于事件时间的窗口

时间属性

在这里插入图片描述
当基于窗口进行操作时,操作符看的当前时间是由流中元素所携带的信息决定的。流中的每一个元素都必须包含时间戳信息。

机器时间(Processing Time)

事件被算子处理时机器的系统时间
最简单的 “Time” 概念,当流程序在 Processing Time 上运行时,所有基于时间的操作(如时间窗口)将使用当时机器的系统时间,不需要流和机器之间的协调。每小时 Processing Time 窗口将包括在系统时钟指示整个小时之间到达特定操作的所有事件。
例如,如果应用程序在上午 9:15 开始运行,则第一个每小时 Processing Time 窗口将包括在上午 9:15 到上午 10:00 之间处理的事件,下一个窗口将包括在上午 10:00 到 11:00 之间处理的事件。
优点:它提供了最好的性能和最低的延迟。
缺点:在分布式和异步的环境下,Processing Time 不能提供确定性,因为它容易受到事件到达系统的速度(例如从消息队列)、事件在系统内操作流动的速度以及中断的影响。

事件时间(Event Time)

事件发生的时间
一般就是数据本身携带的时间。这个时间通常是在事件到达 Flink 之前就确定的,并且可以从每个事件中获取到事件时间戳。在 Event Time 中,时间取决于数据,而跟其他没什么关系。Event Time 程序必须指定如何生成 Event Time 水印,这是表示 Event Time 进度的机制。
优点:理想状态下,无论事件什么时候到达或者其怎么排序,最后处理 Event Time 将产生完全一致和确定的结果。
缺点:处理 Event Time 时将会因为要等待一些无序事件而产生一些延迟。由于只能等待一段有限的时间,因此就难以保证处理 Event Time 将产生完全一致和确定的结果。

摄入时间(IngestionTime)

事件进入 Flink 任务的时间
在源操作处,每个事件将源的当前时间作为时间戳,并且基于时间的操作(如时间窗口)会利用这个时间戳。以source的系统时间为准。Ingestion Time 在概念上位于 Event Time 和 Processing Time 之间。与ProcessingTime相比可以提供更可预测的结果,因为IngestionTime的时间戳比较稳定(在源处只记录一次),同一数据在流经不同窗口操作时将使用相同的时间戳,而对于ProcessingTime同一数据在流经不同窗口算子会有不同的处理时间戳。
当事件进入source操作符时,source操作符所在机器的机器时间,就是此事件的“摄入时间”(IngestionTime),并同时产生水位线。IngestionTime相当于EventTime和ProcessingTime的混合体。一个事件的IngestionTime其实就是它进入流处理器中的时间。
IngestionTime既有EventTime的执行效率(比较低),又没有EventTime计算结果的准确性。

水位线(watermark)

在这里插入图片描述
在这里插入图片描述

背景

我们知道,流处理从事件产生,到流经source,再到operator,中间是有一个过程和时间的。虽然大部分情况下,流到operator的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络、背压等原因,导致乱序的产生(out-of-order或者说late element)。对于late element,我们不能无限期的等下去,必须要有个机制来保证一个特定的时间后,必须触发window去进行计算。这个特别的机制,就是watermark。

作用

watermark是基于事件时间窗口计算提出的一种机制,是用来衡量EventTime进展,平衡延迟和计算结果的正确性,处理乱序事件的。本质上是一种时间戳,会被插入到流中,是流里的一个特殊的元素。它告诉我们,单调递增,算子接收到一个watermark,则表示所有小于该watermark的时间戳的数据元素都已经到达了,所以watermark可以看作是告诉flink数据流已经处理到什么位置(时间维度)的方式。

产生方式

通常,在接收到source的数据后,应该立刻生成watermark;但是,也可以在source后,应用简单的map或者filter操作,然后再生成watermark。

Punctuated

基于事件产生水位线,数据流中每一个递增的eventTime都产生一个水位线。在实际的生产中,tps很高的场景下会产生大量的水位线,在一定程度上对下游算子造成压力,所以只有在实时性要求很高的场景才会选择这种方式创建水位线。
设定水位线通常需要用到领域知识。举例来说,如果知道事件的迟到时间不会超过5秒,就可以将水位线标记时间设为收到的最大时间戳减去5秒。另一种做法是,采用一个Flink作业监控事件流,学习事件的迟到规律,并以此构建水位线生成模型。

Periodic

周期性地产生水位线,一段时间或者一定条数的记录产生一个水位线。在实际生产中这种方式必须结合时间和累积条数两个维度维护水位线,否则在极端情况下会有很大的延迟。

多流水位线

在这里插入图片描述
flink内部实现每一个流上只能有一个递增的水位线,当出现多流携带EventTime汇聚到一起(group by或union),flink会选择所有流入的EventTime中最小的一个向下游流出。从而保证水位线的单调递增和数据的完整性。

最大延迟时间

背景

窗口计算触发条件是水位线大于等于窗口结束时间(基于时间的滑动、滚动窗口,分配器在最开始就已知每个窗口的结束时间)。默认的,当watermark大于一个窗口的结束时间时,晚到的元素会被丢弃。如果我们想要守护这部分数据,就可以设置最大延迟时间。
在这里插入图片描述

作用

最大延迟时间:在窗口被触发启动后,还可保留该窗口的状态一段时间,可以容忍在彻底删除元素之前依然接收晚到的元素,在这段时间之内到的数据还可以算在这个窗口中,延迟的数据通过outputTag输出。默认的事件时间触发器,处理这种最大延迟时间之内到达的数据,会触发窗口再次计算,即水位线<窗口结束时间+最大延迟时间。
可通过以下视频帮助理解,基于事件时间的窗口,是怎么结合时间+水位线+最大延迟时间进行数据分配的:

20231128_181938

设置方式

设置示例代码在上面视频中有展示。
如果最大延迟时间设置的很大,计算出的结果会更精确,但收到计算结果的速度会很慢,同时系统会缓存大量的数据,对内存造成比较大的压力。同时影响流处理系统的吞吐量。
如果最大延迟时间设置的很小,那么收到计算结果的速度会很快,但可能遗漏数据,收到错误的计算结果。
这恰恰符合现实世界的规律:大部分真实的事件流都是乱序的,并且通常无法了解它们的乱序程度(因为理论上不能预见未来)。水位线+最大延迟时间可以一定程度上直面乱序。

函数计算

函数类型

增量计算

窗口保存一份中间结果数据,每流入一个新元素,新元素与中间结果数据进行聚合计算,生成一个新的中间数据,再保存到窗口中。这样可以大大降低内存的消耗并提升性能。如reduce和aggregate函数。
但是如果用户定义了 Evictor,则不会启用对聚合窗口的优化,因为 Evictor 需要遍历窗口中的所有元素,必须要将窗口中所有元素都存下来。

全量计算

窗口缓存该窗口的所有数据,等到触发条件满足后,对窗口内的全量元素进行计算。如process。

窗口状态

state结构

Window本身只是一个ID标识符,其内部可能存储了一些元数据,如TimeWindow中有开始和结束时间,但是并不会存储窗口中的元素。窗口中的元素实际存储在 Key/Value State 中,key为Window,value为元素集合(或聚合值)。
窗口在触发前其数据都会保存在state 中, 保证了其容错机制, 对于每条数据都会在WindowOperator#processElement中调用windowState.add(element.getValue())进行保存。
窗口的状态大小取决于应用的函数类型。如果应用ReduceFunction或AggregateFunction,则会立即聚合到达的数据,并且窗口仅保存聚合值。如果应用ProcessWindowFunction或WindowFunction,Flink将收集所有输入记录,并在时间超过窗口结束时间时应用该函数。

state清理

窗口中间数据是保存在state中,即内存中。对于已经结束的窗口,这部分数据是无效的,需要被清理掉, WindowOperator中在processElement中会调用registerCleanupTimer方法,注册定时清理窗口数据。数据的清理时间是窗口结束时间+最大延迟时间 , 最大延迟时间在事件时间处理中才有效。此处注册的定时器就是生成一个IntervalTimer放入优先级队列中, 当到达窗口的watermark的大小大于endTime+allowedLateness就会在窗口函数执行之后触发清理操作。

应用

避坑

  1. keyby的状态存储可以理解为(Key, Window) -> List的Map,所以keyby的key一定要是定量,如果key前后发生变化,可能导致无法获取对应的状态,状态无法清理;
  2. 窗口大小设置需要结合实际数据量结合测试评估得出,不然可能会出现背压或者内存占满等情况;
  3. 滑动窗口使用的时候需要谨慎配置,慎用细粒度设置,即window_size/window_slide特别大的滑动窗口。以timeWindow(Time.days(1), Time.minutes(5))为例,Flink会为每个Key维护days(1) / minutes(5) = 288个窗口,总的窗口数量是keys * 288。由于每个窗口会维护单独的状态,并且每个窗口的所有元素都会创建一个副本,这样就会给作业的状态保存以及计算效率带来很大影响。同时可能由于窗口时间长,滑动较为频繁,导致算子计算压力过大,下游算子计算速度抵不上上游数据产生速度,出现背压现象;

调优

  1. 看背压
    通常最后一个背压高的subTask的下游就是job的明显瓶颈之一;
  2. 看checkoint时长
    checkpoint的时长在一定程度上可以影响job的整体吞吐;
  3. 查看关键指标
    通过延迟与吞吐指标可以对任务的性能进行精准的判断;
  4. 资源利用率
    我们进行优化的最终目的是提供资源的利用率,常见的性能问题如下:
    1)JSON序列化与反序列化:常出现在source和sink任务上,在指标上没有体现,容易被忽略;
    2)Map和set的Hash冲突:由于HashMap,HashSet等随着负载因子增高,引起的插入和查询性能下降;
    3)数据倾斜:数据倾斜会导致其中一个或者多个subtask处理的数据量远大于其他节点,造成局部数据延迟;
    4)和低速系统的交互:在实时系统进行高速数据处理时,当涉及到与外部低俗的系统(如Mysql,Hbase等)进行数据交互时;
    5)频繁的GC:因内存或者内存比例分配不合理导致频繁GC, 甚至是TaskManager失联;
    6)大窗口:窗口size大,数据量大,或者是滑动窗口size和step的比值比较大。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值