窗口算子WindowOperator是窗口机制的底层实现,它几乎会牵扯到所有窗口相关的知识点,因此相对复杂。本文将以由面及点的方式来分析WindowOperator的实现。首先,我们来看一下对于最常见的时间窗口(包含处理时间和事件时间)其执行示意图:
上图中,左侧从左往右为事件流的方向。方框代表事件,事件流中夹杂着的竖直虚线代表水印,Flink通过水印分配器(TimestampsAndPeriodicWatermarksOperator和TimestampsAndPunctuatedWatermarksOperator这两个算子)向事件流中注入水印。元素在streaming dataflow引擎中流动到WindowOperator时,会被分为两拨,分别是普通事件和水印。
如果是普通的事件,则会调用processElement方法(上图虚线框中的三个圆圈中的一个)进行处理,在processElement方法中,首先会利用窗口分配器为当前接收到的元素分配窗口,接着会调用触发器的onElement方法进行逐元素触发。对于时间相关的触发器,通常会注册事件时间或者处理时间定时器,这些定时器会被存储在WindowOperator的处理时间定时器队列和水印定时器队列中(见图中虚线框中上下两个圆柱体),如果触发的结果是FIRE,则对窗口进行计算。
如果是水印(事件时间场景),则方法processWatermark将会被调用,它将会处理水印定时器队列中的定时器。如果时间戳满足条件,则利用触发器的onEventTime方法进行处理。
而对于处理时间的场景,WindowOperator将自身实现为一个基于处理时间的触发器,以触发trigger方法来消费处理时间定时器队列中的定时器满足条件则会调用窗口触发器的onProcessingTime,根据触发结果判断是否对窗口进行计算。
以上是WindowOperator的常规流程最简单的表述,事实上其逻辑要复杂得多。我们首先分解掉几个内部核心对象,上图中我们看到有两个队列:分别是水印定时器队列和处理时间定时器队列。这里的定时器是什么?它有什么作用呢?接下来我们就来看看它的定义——WindowOperator的内部类Timer。Timer是所有时间窗口执行的基础,它其实是一个上下文对象,封装了三个属性:
- timestamp:触发器触发的时间戳;
- key:当前元素所归属的分组的键;
- window:当前元素所属窗口;
在我们讲解窗口触发器时,我们曾提及过触发器上下文对象,它作为process系列方法参数。在WindowOperator内部我们终于看到了对该上下文对象接口的实现——Context,它主要提供了三种类型的方法:
- 提供状态存储与访问;
- 定时器的注册与删除;
- 窗口触发器process系列方法的包装;
在注册定时器时,会新建定时器对象并将其加入到定时器队列中。等到时间相关的处理方法(processWatermark和trigger)被触发调用,则会从定时器队列中消费定时器对象并调用窗口触发器,然后根据触发结果来判断是否触动窗口的计算。我们选择事件时间的处理方法processWatermark进行分析(处理时间的处理方法trigger跟其类似):
public void processWatermark(Watermark mark) throws Exception {
//定义一个标识,表示是否仍有定时器满足触发条件
boolean fire;
do {
//从水印定时器队列中查找队首的一个定时器,注意此处并不是出队(注意跟remove方法的区别)
Timer<K, W> timer = watermarkTimersQueue.peek();
//如果定时器存在&#