Flink窗口原理详解

Flink原理:窗口原理详解

1 定义

​ Flink 认为 Batch 是 Streaming 的一个特例,所以 Flink 底层引擎是一个流式引擎,在上面实现了流处理和批处理。而窗口(window)就是从 Streaming 到 Batch 的一个桥梁,将无界数据划分成有界数据。我们通过定义一个窗口,收集一批数据,并对这个窗口内的数据进行聚合类的计算。

在这里插入图片描述

2 窗口分类

2.1 Time Window

Flink 中窗口机制和时间类型是完全解耦的,也就是说当需要改变时间类型时不需要更改窗口逻辑相关的代码。

2.1.1 Flink中的时间种类有哪些

​ Flink中的时间与现实世界中的时间是不一致的,在flink中被划分为事件时间,摄入时间,处理时间三种。

​ 如果以EventTime为基准来定义时间窗口将形成EventTimeWindow,要求消息本身就应该携带EventTime

​ 如果以IngesingtTime为基准来定义时间窗口将形成IngestingTimeWindow,以source的systemTime为准。

​ 如果以ProcessingTime基准来定义时间窗口将形成ProcessingTimeWindow,以operator的systemTime为准。

2.1.2 Tumbling Time Window

​ 翻滚窗口能将数据流切分成不重叠的窗口,每一个事件只能属于一个窗口。通过使用 DataStream API,我们可以这样实现:

socketDataStream.keyBy(0).timeWindow(Time.minutes(2));

2.1.3 Sliding Time Window

​ 对于某些应用,它们需要的窗口是不间断的,需要平滑地进行窗口聚合。比如,我们可以每30秒计算一次最近一分钟用户购买的商品总数。这种窗口我们称为滑动时间窗口(Sliding Time Window)。在滑窗中,一个元素可以对应多个窗口。通过使用 DataStream API,我们可以这样实现:

socketDataStream.keyBy(0).timeWindow(Time.minutes(1), Time.seconds(30));

2.2 Count Window

​ Count Window 是根据元素个数对数据流进行分组的。

2.2.1 Tumbling Count Window

​ 当我们想要每100个用户购买行为事件统计购买总数,那么每当窗口中填满100个元素了,就会对窗口进行计算,这种窗口我们称之为翻滚计数窗口(Tumbling Count Window)

socketDataStream.keyBy(0).countWindow(100);

2.2.2 Sliding Count Window

​ 每10个元素计算一次最近100个元素的总和

socketDataStream.keyBy(0).countWindow(100, 10);

2.3 Session Window

计算每个用户在活跃期间总共购买的商品数量,如果用户30秒没有活动则视为会话断开

socketDataStream.keyBy(0).window(ProcessingTimeSessionWindows.withGap(Time.seconds(30)));

3 窗口组件

​ Flink 中定义一个窗口主要需要以下三个组件。

Window Assigner:用来决定某个元素被分配到哪个/哪些窗口中去。

Trigger:触发器。决定了一个窗口何时能够被计算或清除,每个窗口都会拥有一个自己的Trigger。

Evictor:可以译为“驱逐者”。在Trigger触发之后,在窗口被处理之前,Evictor(如果有Evictor的话)会用来剔除窗口中不需要的元素,相当于一个filter,如countTime 中evictor(size) ,其中size 为 保留的元素个数

在这里插入图片描述

​ 大致流程如下:

​ 首先上图中的组件都位于一个算子(window operator)中,数据流源源不断地进入算子,每一个到达的元素都会被交给 WindowAssigner。WindowAssigner 会决定元素被放到哪个或哪些窗口(window),可能会创建新窗口。因为一个元素可以被放入多个窗口中,所以同时存在多个窗口是可能的

​ 每一个窗口都拥有一个属于自己的 Trigger,Trigger上会有定时器,用来决定一个窗口何时能够被计算或清除。每当有元素加入到该窗口,或者之前注册的定时器超时了,那么Trigger都会被调用。Trigger的返回结果可以是

continue(不做任何操作);

fire(处理窗口数据);

purge(移除窗口和窗口中的数据),或者 fire + purge。

​ 一个Trigger的调用结果只是fire的话,那么会计算窗口并保留窗口原样,也就是说窗口中的数据仍然保留不变,等待下次Trigger fire的时候再次执行计算。一个窗口可以被重复计算多次直到它被 purge 了。在purge之前,窗口会一直占用着内存。

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

4 源码分析

4.1 Count Window源码

​ Count Window 是使用三组件的典范,我们可以在 KeyedStream 上创建 Count Window,其源码如下所示:

// tumbling count window
public WindowedStream countWindow(long size) {
    return window(GlobalWindows.create())  // create window stream using GlobalWindows
        .trigger(PurgingTrigger.of(CountTrigger.of(size))); // trigger is window size
}

// sliding count window
public WindowedStream countWindow(long size, long slide) {
    return window(GlobalWindows.create())
        .evictor(CountEvictor.of(size))  // evictor is window size
        .trigger(CountTrigger.of(slide)); // trigger is slide size
}

​ 基于 GlobalWindows 这个 WindowAssigner 来创建的窗口,该assigner会将所有元素都分配到同一个global window中。

​ 翻滚计数窗口并不带evictor,只注册了一个trigger。该trigger是带purge功能的 CountTrigger。也就是说每当窗口中的元素数量达到了 window-size,trigger就会返回fire+purge,窗口就会执行计算并清空窗口中的所有元素,再接着储备新的元素。从而实现了tumbling的窗口之间无重叠。

​ 滑动计数窗口的各窗口之间是有重叠的,但我们用的 GlobalWindows assinger 从始至终只有一个窗口,不像 sliding time assigner 可以同时存在多个窗口。所以trigger结果不能带purge,也就是说计算完窗口后窗口中的数据要保留下来(供下个滑窗使用)。另外,trigger的间隔是slide-size,evictor的保留的元素个数是window-size。也就是说,每个滑动间隔就触发一次窗口计算,并保留下最新进入窗口的window-size个元素,剔除旧元素。

​ 假设有一个滑动计数窗口,每2个元素计算一次最近4个元素的总和,那么窗口工作示意图如下所示

在这里插入图片描述

​ 图中所示的各个窗口逻辑上是不同的窗口,但在物理上是同一个窗口。该滑动计数窗口,trigger的触发条件是元素个数达到2个(每进入2个元素就会触发一次),evictor保留的元素个数是4个,每次计算完窗口总和后会保留剩余的元素。所以第一次触发trigger是当元素5进入,第三次触发trigger是当元素2进入,并驱逐5和2,计算剩余的4个元素的总和(22)并发送出去,保留下2,4,9,7元素供下个逻辑窗口使用。

4.2 Time Window源码

​ 同样的,我们也可以在 KeyedStream 上申请 Time Window,其源码如下所示:

// tumbling time window
public WindowedStream timeWindow(Time size) {
    if (environment.getStreamTimeCharacteristic() == TimeCharacteristic.ProcessingTime) {
        return window(TumblingProcessingTimeWindows.of(size));
    } else {
        return window(TumblingEventTimeWindows.of(size));
    }
}
// sliding time window
public WindowedStream timeWindow(Time size, Time slide) {
    if (environment.getStreamTimeCharacteristic() == TimeCharacteristic.ProcessingTime) {
        return window(SlidingProcessingTimeWindows.of(size, slide));
    } else {
        return window(SlidingEventTimeWindows.of(size, slide));
    }
}

​ 在方法体内部会根据当前环境注册的时间类型,使用不同的WindowAssigner创建window,这里我们主要分析sliding process time window,如下是相关源码:

public class SlidingProcessingTimeWindows extends WindowAssigner {
    private static final long serialVersionUID = 1L;

    private final long size;
    
    private final long slide;
    
    private SlidingProcessingTimeWindows(long size, long slide) {
        this.size = size;
        this.slide = slide;
    }
    
    @Override
    public Collection assignWindows(Object element, long timestamp) {
        timestamp = System.currentTimeMillis();
        List windows = new ArrayList<>((int) (size / slide));
        // 对齐时间戳
        long lastStart = timestamp - timestamp % slide;
        for (long start = lastStart;
            start > timestamp - size;
            start -= slide) {
            // 当前时间戳对应了多个window
            windows.add(new TimeWindow(start, start + size));
        }
        return windows;
    }
      
    @Override
      public Trigger getDefaultTrigger(StreamExecutionEnvironment env) {
    	    return ProcessingTimeTrigger.create();
    }
    ...

}


public class ProcessingTimeTrigger extends Trigger {
    @Override
    // 每个元素进入窗口都会调用该方法
    public TriggerResult onElement(Object element, long timestamp, TimeWindow window, TriggerContext ctx) {
        // 注册定时器,当系统时间到达window end timestamp时会回调该trigger的onProcessingTime方法
        ctx.registerProcessingTimeTimer(window.getEnd());
        return TriggerResult.CONTINUE;
    }

    @Override
    // 返回结果表示执行窗口计算并清空窗口,Flink 1.9.0中只有执行窗口计算,没有情况窗口操作,清空逻辑转而改成注册清空定时器,详情见下文
    public TriggerResult onProcessingTime(long time, TimeWindow window, TriggerContext ctx) {
        return TriggerResult.FIRE_AND_PURGE;
    }
    ...


​ 首先,SlidingProcessingTimeWindows会对每个进入窗口的元素根据系统时间分配到(size / slide)个不同的窗口,并会在每个窗口上根据窗口结束时间注册一个定时器(相同窗口只会注册一份),当定时器超时时意味着该窗口完成了,这时会回调对应窗口的Trigger的onProcessingTime方法,返回FIRE_AND_PURGE,也就是会执行窗口计算并清空窗口。整个过程示意图如下

在这里插入图片描述

5 窗口state

5.1 state结构

Window本身只是一个ID标识符,其内部可能存储了一些元数据,如TimeWindow中有开始和结束时间,但是并不会存储窗口中的元素。窗口中的元素实际存储在 Key/Value State 中,key为Window,value为元素集合(或聚合值)。

窗口在触发前其数据都会保存在state 中, 保证了其容错机制, 对于每条数据的保存都会在WindowOperator#processElement中调用windowState.add(element.getValue()), 那么对于一个keyed window是如何区分各个窗口的数据的呢?在这里可以理解为有一个Map 的数据结构其中key 表示具体的分组key值,Window 表示一个namespace亦即一个具体的window, List表示窗口中的数据,详情参考HeapListState.java

5.2 state清理

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

你可能感兴趣的:(Flink,flink,big,data,大数据)