Flink内核原理与实现
上QQ阅读APP看书,第一时间看更新

4.3 窗口原理与机制

窗口算子负责处理窗口,数据流源源不断地进入算子,每一个数据元素进入算子时,首先会被交给WindowAssigner。WindowAssigner决定元素被放到哪个或哪些窗口,在这个过程中可能会创建新窗口或者合并旧的窗口。在Window Operator中可能同时存在多个窗口,一个元素可以被放入多个窗口中。

数据进入窗口时,分配窗口和计算的逻辑如图4-3所示。

图4-3 Window原理

此处需要注意,Window本身只是一个ID标识符,其内部可能存储了一些元数据,如TimeWindow中有开始和结束时间,但是并不会存储窗口中的元素。窗口中的元素实际存储在Key/Value State中,Key为Window,Value为数据集合(或聚合值)。为了保证窗口的容错性,Window实现依赖Flink的State机制,State的介绍参见本书相关章节。

每一个窗口都拥有一个属于自己的Trigger,Trigger上有定时器,用来决定一个窗口何时能够被计算或清除。每当有元素被分配到该窗口,或者之前注册的定时器超时时,Trigger都会被调用。

Trigger被触发后,窗口中的元素集合就会交给Evictor(如果指定了的话)。Evictor主要用来遍历窗口中的元素列表,并决定最先进入窗口的多少个元素需要被移除。剩余的元素会交给用户指定的函数进行窗口的计算。如果没有Evictor的话,窗口中的所有元素会一起交给函数进行计算。

计算函数收到窗口的元素(可能经过了Evictor的过滤),计算出窗口的结果值,并发送给下游。窗口的结果值可以是一个也可以是多个。DataStream API上可以接收不同类型的计算函数,包括预定义的sum()、min()、max(),以及ReduceFunction、FoldFunction和WindowFunction。WindowFunction是最通用的计算函数,其他的预定义函数基本上都是基于该函数实现的。

Flink对一些聚合类的窗口计算(如sum和min)做了优化,因为聚合类的计算不需要将窗口中的所有数据都保存下来,只需要保存一个中间结果值就可以了。每个进入窗口的元素都会执行一次聚合函数并修改中间结果值,这样可以大大降低内存的消耗并提升性能。但是如果用户定义了Evictor,则不会启用对聚合窗口的优化,因为Evictor需要遍历窗口中的所有元素,必须将窗口中的所有元素都存下来。

4.3.1 WindowAssigner

WindowAssigner用来决定某个元素被分配到哪个/哪些窗口中去。SessionWindowAssigner比较特殊,因为Session Window无法事先确定窗口的范围,是动态改变的。

Flink中有两套WindowAssigner体系,分别位于Streaming中和Blink中。Streaming中的WindowAssigner为DataStream API服务,Blink中的WindowAssigner为基于Blink Planner的Flink SQL服务。

Streaming中的WindowAssigner类体系如图4-4所示。

图4-4 Streaming中的WindowAssigner类体系

Blink中的WindowAssigner体系如图4-5所示。

图4-5 Blink中的WindowAssigner体系

4.3.2 WindowTrigger

Trigger触发器决定了一个窗口何时能够被计算或清除,每一个窗口都拥有一个属于自己的Trigger,Trigger上会有定时器,用来决定一个窗口何时能够被计算或清除。每当有元素加入该窗口,或者之前注册的定时器超时时,Trigger都会被调用。

Trigger触发的结果如下。

1)Continue:继续,不做任何操作。

2)Fire:触发计算,处理窗口数据。

3)Purge:触发清理,移除窗口和窗口中的数据。

4)Fire + Purge:触发计算+清理,处理数据并移除窗口和窗口中的数据。

当数据到来的时候,调用Trigger判断是否需要触发计算,如果调用结果只是Fire,则计算窗口并保留窗口原样,窗口中的数据不清理,数据保持不变,等待下次触发计算的时候再次执行计算。窗口中的数据会被反复计算,直到触发结果清理。在清理之前,窗口和数据不会释放,所以窗口会一直占用内存。

Trigger触发流程如下。

1)当Trigger Fire时,窗口中的元素集合会交给Evictor(如果已经指定了)。Evictor主要用来遍历窗口中的元素列表,并决定最先进入窗口的多少个元素需要被移除。剩余的元素会交给用户指定的函数进行窗口的计算。如果没有Evictor,窗口中的所有元素会一起交给函数进行计算。

2)计算函数收到窗口的元素(可能经过了Evictor的过滤),计算出窗口的结果值,并发送给下游。窗口的结果值可以是一个也可以是多个。DataStream API上可以接收不同类型的计算函数,包括预定义的sum()、min()、max(),以及ReduceFunction、FoldFunction和WindowFunction。WindowFunction是最通用的计算函数,其他预定义的函数基本都是基于该函数实现的。

3)Flink对于一些聚合类的窗口计算(如sum和min)做了优化,因为聚合类的计算不需要将窗口中的所有数据都保存下来,只需要保存一个result值就可以了。每个进入窗口的元素都会执行一次聚合函数并修改result值,这样可以大大降低内存的消耗并提升性能。但是如果用户定义了Evictor,则不会启用对聚合窗口的优化,因为Evictor需要遍历窗口中的所有元素,必须将窗口中所有元素都存下来。

Streaming模块Trigger类体系如图4-6所示。

图4-6 Streaming模块Trigger类体系

Blink SQL模块Trigger体系如图4-7所示:

图4-7 Blink SQL模块Trigger体系

在抽象类Trigger中定义了Trigger的行为,Trigger的关键行为分为两类:触发逻辑的判断和合并。

在大数据中有3种典型延迟计算:

1)基于数据记录个数的触发:即等待Window中的数据达到一定个数,则触发窗口的计算,在类体系中对应的是CountTrigger。

2)基于处理时间的触发:在处理时间维度判断哪些窗口需要触发,对应的是ProcessingTimeTrigger。

3)基于事件时间的触发:使用Watermark机制触发。

Trigger接口的定义如代码清单4-2所示。

代码清单4-2 Trigger接口

4.3.3 WindowEvictor

Evictor可以理解为窗口数据的过滤器,Evictor可在Window Function执行前或后,从Window中过滤元素。Flink内置了3种窗口数据过滤器,如图4-8所示。

图4-8 Evictor类体系

1)CountEvictor: 计数过滤器。在Window中保留指定数量的元素,并从窗口头部开始丢弃其余元素。

2)DeltaEvictor: 阈值过滤器。本质上来说就是一个自定义规则,计算窗口中每个数据记录,然后与一个事先定义好的阈值做比较,丢弃超过阈值的数据记录。

3)TimeEvictor: 时间过滤器。保留Window中最近一段时间内的元素,并丢弃其余元素。

4.3.4 Window函数

数据经过WindowAssigner之后,已经被分配到不同的Window中,接下来,要通过窗口函数对窗口内的数据进行处理。窗口函数主要分为两种。

1. 增量计算函数

增量计算指的是窗口保存一份中间数据,每流入一个新元素,新元素都会与中间数据两两合一,生成新的中间数据,再保存到窗口中,如ReduceFunction、AggregateFunction、FoldFunction。

增量计算的优点是数据到达后立即计算,窗口只保存中间结果,计算效率高,但是增量计算函数计算模式是事先确定的,能够满足大部分的计算需求,对于特殊业务需求可能无法满足。

2. 全量计算函数

全量计算指的是先缓存该窗口的所有元素,等到触发条件后对窗口内的所有元素执行计算。Flink内置的ProcessWindowFunction就是全量计算函数,通过全量缓存,实现灵活计算,计算效率比增量聚合稍低,毕竟要占用更多的内存。