flink-window 原理

flink-window

https://ci.apache.org/projects/flink/flink-docs-release-1.13/docs/dev/datastream/operators/windows/

参考资料:http://wuchong.me/blog/2016/06/06/flink-internals-session-window/

flink 是基于 Streaming 的世界观来处理 Batch 数据,而 window 就是 Streaming 与 Batch 的桥梁

window 的分类

event time(事件时间:事件发生时的时间)

ingestion time(摄取时间:事件进入流处理系统的时间)

processing time(处理时间:消息被计算处理的时间)

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

window 方法接收的输入是一个 WindowAssigner
WindowAssigner 负责将每条输入的数据分发到正确的 window 中(一条数据可能同时分发到多个 Window 中)Flink提供了几种通用的 WindowAssigner:
    tumbling window(窗口间的元素无重复)
    sliding window(窗口间的元素可能重复)
    session window 以及 global window
    如果需要自己定制数据分发策略,则可以实现一个 class,继承自 WindowAssigner



WindowAssigner
    *assignWindows() 窗口分配器
    *getDefaultTrigger() 默认触发器	
    *getWindowSerializer() 窗口序列化器
    *boolean isEventTime() 是否为事件时间
    窗口分配器内容 一些元数据信息
    *class WindowAssignerContext {public abstract long getCurrentProcessingTime();}

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

Trigger
    * onElement() 每次往 window 增加一个元素的时候都会触发
    * onEventTime() 当 event-time timer 被触发的时候会调用
    * onProcessingTime() 当 processing-time timer 被触发的时候会调用
    * onMerge() 对两个 trigger 的 state 进行 merge 操作 用来处理 session window
    * clear() window 销毁的时候被调用
    
  上面的接口中前三个会返回一个 TriggerResult,TriggerResult 有如下几种可能的选择:
    * CONTINUE 不做任何事情
    * FIRE 触发 window
    * PURGE 清空整个 window 的元素并销毁窗口
    * FIRE_AND_PURGE 触发窗口,然后销毁窗口

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

Evictor
    *evictBefore() 窗口函数调用前调用
    *evictAfter() 窗口函数执行完后调用
    驱逐器的内容
    interface EvictorContext {
        *getCurrentProcessingTime()
        *getMetricGroup()
        *getCurrentWatermark()
    }

window 原理

每条进入窗口的元素都会交由 WindowAssigner 处理
WindowAssigner 会决定元素被放到那个或哪些窗口 一个元素可以放入多个窗口
window 本身就是一个ID标识 并不存储窗口中的元素 内部可能存储一些元数据 如 TimeWindow 中有开始和结束时间
窗口中的元素实际存储在 Key/Value State 中,key为Window,value为元素集合(或聚合值)

每个 window 都有一个 Trigger 用来决定窗口何时被触发或清除
Trigger被调用:1.有元素加入当前窗口 2.之前注册的定时器到期了
Trigger的返回结果:
	continue 不做任何操作
	fire 处理窗口数据
		计算窗口并保留窗口原样,也就是说窗口中的数据仍保留不变,下次 Trigger fire 的时候再次参与计算
	purge 移除窗口和窗口中的数据
    	一个窗口可以被重复计算多次直到它被 purge 了 在purge之前,窗口会一直占用着内存
	fire + purge 触发并清除窗口

当 Trigger fire 了,窗口中的元素集合就会交给 Evictor(如果指定了的话)
Evictor: 遍历窗口中的元素列表
		 移除最先进入窗口的多少个元素
	     剩余的元素交由指定的函数进行窗口的计算
	     如果没有 Evictor 窗口中的所有元素会一起交给指定的函数进行计算
计算函数收到窗口的元素(可能经过了 Evictor 的过滤),计算出窗口结果并发送给下游

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



第一个函数是申请翻滚计数窗口,参数为窗口大小。第二个函数是申请滑动计数窗口,参数分别为窗口大小和滑动大小。它们都是基于 GlobalWindows 这个 WindowAssigner 来创建的窗口,该assigner会将所有元素都分配到同一个global window中,所有GlobalWindows的返回值一直是 GlobalWindow 单例。基本上自定义的窗口都会基于该assigner实现。

翻滚计数窗口并不带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个元素,剔除旧元素。

Count Window

// tumbling count window 滚动计数窗口
public WindowedStream<T, KEY, GlobalWindow> 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<T, KEY, GlobalWindow> 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 会将所有元素都分配到同一个 GlobalWindow 中
GlobalWindows*assignWindows() return Collections.singletonList(GlobalWindow.get())
GlobalWindow*get() return INSTANCE
	private static final GlobalWindow INSTANCE = new GlobalWindow()

滚动计数窗口无重叠
滚动计数窗口不带 evictor,只注册了一个带 purge 功能的 CountTrigger trigger(PurgingTrigger.of(CountTrigger.of(size)))
也就是说每当窗口中的元素数量达到了 size,trigger 就会返回 fire+purge 窗口就会执行计算并清空窗口中的所有元素,再接着储备新的元素。从而实现了 tumbling 窗口之间无重叠
	//通过构造器注入一个 CountTrigger 并 将其赋值给 nestedTrigger 属性
    PurgingTrigger*PurgingTrigger(Trigger<T, W> nestedTrigger) {
        this.nestedTrigger = nestedTrigger;
    }
    PurgingTrigger*onElement(){
    	//调用 CountTrigger 的 onElement 方法 并接收返回值
        TriggerResult triggerResult = nestedTrigger
            .onElement(element, timestamp, window, ctx);
        //如果 窗口达到触发条件 则触发并清空窗口 而实现了 tumbling 窗口之间无重叠
        return 
            triggerResult.isFire() ? TriggerResult.FIRE_AND_PURGE : triggerResult;
    }
    CountTrigger*onElement(){
    	//获取当前窗口中的元素数量
        ReducingState<Long> count = ctx.getPartitionedState(stateDesc);
        count.add(1L);
        //如果 窗口中的元素数量 大于等于 设置的窗口数量  fire 窗口 否则 continue  
        if (count.get() >= maxCount) {
            count.clear();
            return TriggerResult.FIRE;
        }
        return TriggerResult.CONTINUE;
    }

滑动计数窗口的各窗口之间是有重叠
因为使用的是 GlobalWindow 所有的元素都会分配到一个窗口 
所以 Trigger 不能带 purge .trigger(CountTrigger.of(slide))  
窗口计算完后窗口中的数据需要保留下来给下个滑动窗口计算
trigger 的间隔是 slide
evictor 的保留的元素个数是 size。也就是说,每个滑动间隔就触发一次窗口计算,并保留下最新进入窗口的size个元素,剔除旧元素
CountEvictor*evictBefore() { if (!doEvictAfter) {evict(elements, size, ctx); }}
CountEvictor*evict() {
        if (size <= maxCount) {
            return;
        } else {
            int evictedCount = 0;
            //使用迭代器将最先加入的几个元素删除
            for (Iterator<TimestampedValue<Object>> iterator = elements.iterator();
                    iterator.hasNext(); ) {
                iterator.next();
                evictedCount++;
                if (evictedCount > size - maxCount) {
                    break;
                } else {
                    iterator.remove();
                }
            }
        }
    }



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

Time Window

tumbling time window
.window(TumblingEventTimeWindows.of(size))
.window(TumblingProcessingTimeWindows.of(size)) 
sliding time window
.window(SlidingEventTimeWindows.of(size, slide)) 
.window(SlidingProcessingTimeWindows.of(size, slide)) 
public <W extends Window> WindowedStream<T, KEY, W> window(
    WindowAssigner<? super T, W> assigner) {
    return new WindowedStream<>(this, assigner);
}


// tumbling time window
.window(TumblingEventTimeWindows.of(size)) or
.window(TumblingProcessingTimeWindows.of(size)) 
public WindowedStream<T, KEY, TimeWindow> timeWindow(Time size) {
  if (environment.getStreamTimeCharacteristic() == TimeCharacteristic.ProcessingTime) {
    return window(TumblingProcessingTimeWindows.of(size));
  } else {
    return window(TumblingEventTimeWindows.of(size));
  }
}


// sliding time window
.window(SlidingEventTimeWindows.of(size, slide)) or 
.window(SlidingProcessingTimeWindows.of(size, slide)) 
public WindowedStream<T, KEY, TimeWindow> timeWindow(Time size, Time slide) {
  if (environment.getStreamTimeCharacteristic() == TimeCharacteristic.ProcessingTime) {
    return window(SlidingProcessingTimeWindows.of(size, slide));
  } else {
    return window(SlidingEventTimeWindows.of(size, slide));
  }
}




TumblingProcessingTimeWindows.of(size)
TumblingEventTimeWindows.of(size)

SlidingProcessingTimeWindows.of(size, slide)
SlidingEventTimeWindows.of(size, slide)

ProcessingTimeTrigger
EventTimeTrigger


TumblingProcessingTimeWindows.of(size)
ProcessingTimeTrigger
//分配 window 
TumblingProcessingTimeWindows*assignWindows{
    //获取指定的操作时间  Processing Time 就是 系统当前运行时间
    final long now = context.getCurrentProcessingTime();
    if (staggerOffset == null) {
    	//从窗口标记器获取窗口偏移量
        //aligned 对齐的(默认) OL
        //random 随机的 (0,windowSize)间的随机值
        //natueal 自然的 窗口开始时间和当前处理时间之间的差值作为偏移量
        staggerOffset =windowStagger
        		.getStaggerOffset(context.getCurrentProcessingTime(), size);
    }
    long start =TimeWindow.getWindowStartWithOffset(
   					 now, (globalOffset + staggerOffset) % size, size);
   						//now - (now - 0 + size) % size = now-now%size
    //相同窗口只会创建一个窗口
    return Collections.singletonList(new TimeWindow(start, start + size));
}
// 每个元素进入窗口都会调用该方法
ProcessingTimeTrigger* onElement(){
	//注册窗口最大时间-1  end - 1  
    ctx.registerProcessingTimeTimer(window.maxTimestamp());
    return TriggerResult.CONTINUE;
}
//触发窗口 返回结果表示执行窗口计算并清空窗口
ProcessingTimeTrigger*onProcessingTime() return TriggerResult.FIRE;
//清除触发器
ProcessingTimeTrigger*clear() ctx.deleteProcessingTimeTimer(window.maxTimestamp());



SlidingEventTimeWindows.of(size, slide)
EventTimeTrigger
SlidingEventTimeWindows*assignWindows() {
    if (timestamp > Long.MIN_VALUE) {
    	//在 size 大小的时间段内 创建 size / slide 个窗口
        List<TimeWindow> windows = new ArrayList<>((int) (size / slide));
        //获取窗口的开始时间 timestamp-(timestamp-0+slide)%slide=timestamp-timestamp%slide
        long lastStart = TimeWindow.getWindowStartWithOffset(timestamp, offset, slide);
        for (long start = lastStart; start > timestamp - size; start -= slide) {
        	windows.add(new TimeWindow(start, start + size));
        }
        return windows;
    } else {
        throw new RuntimeException(...)
    }
}
//注册触发器
EventTimeTrigger*onElement(){
	// end - 1 <= watermark 
    if (window.maxTimestamp() <= ctx.getCurrentWatermark()) {
        // if the watermark is already past the window fire immediately
        return TriggerResult.FIRE;
    } else {
        ctx.registerEventTimeTimer(window.maxTimestamp());
        return TriggerResult.CONTINUE;
    }
}
//触发窗口
EventTimeTrigger* onProcessingTime() return TriggerResult.CONTINUE
EventTimeTrigger*onEventTime
	return time == window.maxTimestamp() ? TriggerResult.FIRE : TriggerResult.CONTINUE
//清除触发器
EventTimeTrigger*clear() ctx.deleteEventTimeTimer(window.maxTimestamp())


SlidingEventTimeWindows 对每个进入窗口的元素根据 event time 时间分配到(size / slide)个不同的窗口
并在每个窗口上根据窗口结束时间注册一个定时器(相同窗口只会注册一份),当定时器超时时意味着该窗口完成了,这时会回调对应窗口的Trigger的onProcessingTime方法

Session Windows

session window 的创建非常的灵活 可以在必要时合并两个或多个窗口
为了实现其功能 flink api 拓展了 MergingWindowAssigner 窗口分配器 用来决定哪些窗口是可以合并的
并且在 Trigger 中添加了 onMerge 方法用来响应发生窗口合并之后对 trigger的相关动作

SessionWindows assigner  会为每个进入的元素分配一个窗口 [timestamp, timestamp+sessionGap)




当第三个元素进入时,分配到的窗口与现有的两个窗口发生了叠加




由于支持了窗口的合并,WindowAssigner可以合并这些窗口
它会遍历现有的窗口,并告诉系统哪些窗口需要合并成新的窗口。
Flink 会将这些窗口进行合并,合并的主要内容有两部分:
1. 需要合并的窗口的底层状态的合并(也就是窗口中缓存的数据,或者对于聚合窗口来说是一个聚合值)
2. 需要合并的窗口的Trigger的合并(比如对于EventTime来说,会删除旧窗口注册的定时器,并注册新窗口的定时器)




需要注意的是,对于每一个新进入的元素,都会分配一个属于该元素的窗口,都会检查并合并现有的窗口
在触发窗口计算之前,每一次都会检查该窗口是否可以和其他窗口合并,直到 trigger 触发后,会将该窗口从窗口列表中移除。
对于 event time 来说,窗口的触发是要等到大于窗口结束时间的 watermark 到达,当watermark没有到,窗口会一直缓存着。所以基于这种机制,可以做到对乱序消息的支持。



EventTimeSessionWindows.withGap()
EventTimeSessionWindows.withDynamicGap((element) -> {
	// determine and return session gap
})
ProcessingTimeSessionWindows.withGap
ProcessingTimeSessionWindows.withDynamicGap((element) -> {
	// determine and return session gap
})


MergingWindowAssigner
	// 需要合并的 windows 合并窗口的方法 callback
	*mergeWindows(Collection<W> windows, MergeCallback<W> callback)
	//合并窗口的方法 merge
	*interface MergeCallback{void merge(Collection<W> toBeMerged, W mergeResult)}
	


核心方法:WindowOperator*processElement

WindowOperator*processElement(StreamRecord<IN> element) throws Exception {
    // 为每个元素创建一个 window  new TimeWindow(timestamp, timestamp + sessionTimeout)
    final Collection<W> elementWindows = windowAssigner
    							.assignWindows(element.getValue(), 	
                                               element.getTimestamp(), 
                                               windowAssignerContext);

    final K key = this.<K>getKeyedStateBackend().getCurrentKey();
	//判断是否是 session window 如果是需要判断是否需要特殊处理
    if (windowAssigner instanceof MergingWindowAssigner) {
        // 对于session window 的特殊处理
        MergingWindowSet<W> mergingWindows = getMergingWindowSet();

        for (W window : elementWindows) {
            // 加入新窗口 可能会发生合并
            // 如果会合并 调用 MergeFunction.merge 方法进行合并 
            // 返回的 actualWindow 即为合并后的窗口
            // 该方法中主要是更新trigger, 合并旧窗口中的状态到新窗口中
            // 如果不会合并 actualWindow 就为新添加的窗口
            W actualWindow =
            mergingWindows.addWindow(
                window,
                new MergingWindowSet.MergeFunction<W>() {
                    @Override
                    public void merge(
                        W mergeResult,
                        Collection<W> mergedWindows,
                        W stateWindowResult,
                        Collection<W> mergedStateWindows) throws Exception {

                            triggerContext.key = key;
                            triggerContext.window = mergeResult;

                            // 根据新窗口的结束时间注册新的定时器
                            triggerContext.onMerge(mergedWindows);

                            // 删除旧窗口注册的定时器
                            for (W m : mergedWindows) {
                                triggerContext.window = m;
                                triggerContext.clear();
                                deleteCleanupTimer(m);
                            }

                            // 合并旧窗口中的状态到新窗口中
                            windowMergingState
                            	.mergeNamespaces(stateWindowResult, mergedStateWindows);
                    }
                });

            // drop if the window is already late
            if (isWindowLate(actualWindow)) {
                mergingWindows.retireWindow(actualWindow);
                continue;
            }

            isSkippedElement = false;

            // 取 actualWindow 对应的用来存状态的窗口
            W stateWindow = mergingWindows.getStateWindow(actualWindow);
            if (stateWindow == null) {
                throw new IllegalStateException(
                "Window " + window + " is not in in-flight window set.");
            }

            // 将新进入的元素数据加入到新窗口(或者说合并后的窗口)中对应的状态中
            windowState.setCurrentNamespace(stateWindow);
            windowState.add(element.getValue());

            triggerContext.key = key;
            triggerContext.window = actualWindow;

            // 检查是否需要 fire or purge
            TriggerResult triggerResult = triggerContext.onElement(element);
            if (triggerResult.isFire()) {...}
            if (triggerResult.isPurge()) {...}
            //清除注册的定时器
            registerCleanupTimer(actualWindow);
        }
        // need to make sure to update the merging state in state
        mergingWindows.persist();
    }
    else { //普通window assigner的处理 }
}

processElement的代码,首先根据 window assigner 为新进入的元素分配窗口集合
对于 MergingWindowAssigner 分配的窗口,取出当前的 MergingWindowSet。
对于每个分配到的窗口,调用 MergingWindowSet 的 addWindow() 将其加入到 MergingWindowSet中,由MergingWindowSet 维护窗口与状态窗口之间的关系,并在需要窗口合并的时候,合并状态和trigger
然后根据映射关系,取出结果窗口对应的状态窗口,根据状态窗口取出对应的状态。将新进入的元素数据加入到该状态中。最后,根据trigger结果来对窗口数据进行处理,对于session window来说,这里都是不进行任何处理的。真正对窗口处理是由定时器超时后对完成的窗口调用processTriggerResult。



MergingWindowSet
Map<W, W> mapping 保存窗口状态 将其他窗口 merge 到该窗口的状态上
Map<W, W> initialMapping 创建MergingWindowSet时的映射 用它来决定是否需要对状态修改
ListState<Tuple2<W, W>> state 映射关系的容错

MergingWindowSet*addWindow(W newWindow, MergeFunction<W> mergeFunction){
    List<W> windows = new ArrayList<>();
    windows.addAll(this.mapping.keySet());
    windows.add(newWindow);
    //窗口的合并结果
    final Map<W, Collection<W>> mergeResults = new HashMap<>();
    //调用 TimeWindow 的 mergeWindows 方法 合并窗口
    windowAssigner.mergeWindows(
        windows,
        new MergingWindowAssigner.MergeCallback<W>() {
            @Override
            public void merge(Collection<W> toBeMerged, W mergeResult) {
                //将 toBeMerged 窗口集合 合并进 mergeResult
                mergeResults.put(mergeResult, toBeMerged);
            }
        });
    W resultWindow = newWindow;
    boolean mergedNewWindow = false;
    // perform the merge
    for (Map.Entry<W, Collection<W>> c : mergeResults.entrySet()) {
        W mergeResult = c.getKey();
        //需要合并的窗口的集合
        Collection<W> mergedWindows = c.getValue();
        // if our new window is in the merged windows make the merge result the
        // result window
        if (mergedWindows.remove(newWindow)) {
            mergedNewWindow = true;
            resultWindow = mergeResult;
        }
        // pick any of the merged windows and choose that window's state window
        // as the state window for the merge result
        W mergedStateWindow = this.mapping.get(mergedWindows.iterator().next());

        // figure out the state windows that we are merging
        List<W> mergedStateWindows = new ArrayList<>();
        for (W mergedWindow : mergedWindows) {
            W res = this.mapping.remove(mergedWindow);
            if (res != null) { mergedStateWindows.add(res);}
    	}

        this.mapping.put(mergeResult, mergedStateWindow);

        // don't put the target state window into the merged windows
        mergedStateWindows.remove(mergedStateWindow);

        // don't merge the new window itself, it never had any state associated with it
        // i.e. if we are only merging one pre-existing window into itself
        // without extending the pre-existing window
        if (!(mergedWindows.contains(mergeResult) && mergedWindows.size() == 1)) {
            // 具体的合并方法
            mergeFunction.merge(
            mergeResult,
            mergedWindows,
            this.mapping.get(mergeResult),
            mergedStateWindows);
        }
    }
    // the new window created a new, self-contained window without merging
    if (mergeResults.isEmpty() || (resultWindow.equals(newWindow) && !mergedNewWindow)) {
    	this.mapping.put(resultWindow, resultWindow);
    }
    return resultWindow;
}



MergingWindowSet 来跟踪窗口合并的类
比如有A、B、C三个窗口需要合并,合并后的窗口为D窗口
这三个窗口在底层都有对应的状态集合,为了避免代价高昂的状态替换(创建新状态是很昂贵的),保持其中一个窗口作为原始的状态窗口,其他几个窗口的数据合并到该状态窗口中去
比如随机选择A作为状态窗口,那么B和C窗口中的数据需要合并到A窗口中去。
这样就没有新状态产生了,但是我们需要额外维护窗口与状态窗口之间的映射关系(D->A),这就是MergingWindowSet负责的工作。这个映射关系需要在失败重启后能够恢复,所以MergingWindowSet内部也是对该映射关系做了容错。

TimeWindow

该方法将所有待合并的窗口按照起始时间升序排序,遍历排序好的窗口,并调用intersects()方法判断它们是否相交。如果相交,则调用cover()方法合并返回一个覆盖两个窗口的窗口;如果不相交,则启动下一次合并过程。列表merged中存储的就是[合并结果, 原窗口集合]的二元组,如果原窗口集合的大小大于1,说明发生了合并,需要调用回调方法MergeCallback.merge()

public static void mergeWindows(
            Collection<TimeWindow> windows, MergingWindowAssigner.MergeCallback<TimeWindow> c) {

        // sort the windows by the start time and then merge overlapping windows

        List<TimeWindow> sortedWindows = new ArrayList<>(windows);

        Collections.sort(
                sortedWindows,
                new Comparator<TimeWindow>() {
                    @Override
                    public int compare(TimeWindow o1, TimeWindow o2) {
                        return Long.compare(o1.getStart(), o2.getStart());
                    }
                });

        List<Tuple2<TimeWindow, Set<TimeWindow>>> merged = new ArrayList<>();
        Tuple2<TimeWindow, Set<TimeWindow>> currentMerge = null;

        for (TimeWindow candidate : sortedWindows) {
            if (currentMerge == null) {
                currentMerge = new Tuple2<>();
                currentMerge.f0 = candidate;
                currentMerge.f1 = new HashSet<>();
                currentMerge.f1.add(candidate);
            } else if (currentMerge.f0.intersects(candidate)) {
                currentMerge.f0 = currentMerge.f0.cover(candidate);
                currentMerge.f1.add(candidate);
            } else {
                merged.add(currentMerge);
                currentMerge = new Tuple2<>();
                currentMerge.f0 = candidate;
                currentMerge.f1 = new HashSet<>();
                currentMerge.f1.add(candidate);
            }
        }

        if (currentMerge != null) {
            merged.add(currentMerge);
        }

        for (Tuple2<TimeWindow, Set<TimeWindow>> m : merged) {
            if (m.f1.size() > 1) {
                c.merge(m.f1, m.f0);
            }
        }
    }

MergingWindowSet

MergingWindowSet*addWindow(W newWindow, MergeFunction<W> mergeFunction) throws Exception {

        List<W> windows = new ArrayList<>();

        windows.addAll(this.mapping.keySet());
        windows.add(newWindow);

        //窗口的时域合并结果
        final Map<W, Collection<W>> mergeResults = new HashMap<>();
        //调用 TimeWindow 的 mergeWindows 方法 合并窗口
        windowAssigner.mergeWindows(
                windows,
                new MergingWindowAssigner.MergeCallback<W>() {
                    @Override
                    public void merge(Collection<W> toBeMerged, W mergeResult) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("Merging {} into {}", toBeMerged, mergeResult);
                        }
                        //将 toBeMerged 窗口集合 合并进 mergeResult
                        mergeResults.put(mergeResult, toBeMerged);
                    }
                });

        W resultWindow = newWindow;
        boolean mergedNewWindow = false;

        // perform the merge
        for (Map.Entry<W, Collection<W>> c : mergeResults.entrySet()) {
            W mergeResult = c.getKey();
            //需要合并的窗口的集合
            Collection<W> mergedWindows = c.getValue();

            // if our new window is in the merged windows make the merge result the
            // result window
            if (mergedWindows.remove(newWindow)) {
                mergedNewWindow = true;
                resultWindow = mergeResult;
            }

            // pick any of the merged windows and choose that window's state window
            // as the state window for the merge result
            W mergedStateWindow = this.mapping.get(mergedWindows.iterator().next());

            // figure out the state windows that we are merging
            List<W> mergedStateWindows = new ArrayList<>();
            for (W mergedWindow : mergedWindows) {
                W res = this.mapping.remove(mergedWindow);
                if (res != null) {
                    mergedStateWindows.add(res);
                }
            }

            this.mapping.put(mergeResult, mergedStateWindow);

            // don't put the target state window into the merged windows
            mergedStateWindows.remove(mergedStateWindow);

            // don't merge the new window itself, it never had any state associated with it
            // i.e. if we are only merging one pre-existing window into itself
            // without extending the pre-existing window
            if (!(mergedWindows.contains(mergeResult) && mergedWindows.size() == 1)) {
                // 具体的合并方法
                mergeFunction.merge(
                        mergeResult,
                        mergedWindows,
                        this.mapping.get(mergeResult),
                        mergedStateWindows);
            }
        }

        // the new window created a new, self-contained window without merging
        if (mergeResults.isEmpty() || (resultWindow.equals(newWindow) && !mergedNewWindow)) {
            this.mapping.put(resultWindow, resultWindow);
        }

        return resultWindow;
    }

WindowAssigner

Count Window Assigner

count window assigner 都是由 GlobalWindows Assigner 的分配器分配一个 GlobalWindow
将所有的数据都发往 GlobalWindow 中
	滚动计数窗口不带 evictor,只注册了一个带 purge 功能的 CountTrigger,
	当窗口中的元素数量达到了 size,trigger 就会返回 fire+purge 
	窗口就会执行计算并清空窗口中的所有元素,再接着储备新的元素
	从而实现了 tumbling 窗口之间无重叠

滑动计数窗口注册的trigger不带purge并且注册了一个evictor 
	窗口计算完后窗口中的数据会原封不动的保存下来
	evictor会剔除最先加入的元素,以保证窗口的最大元素个数都为size

Time Window Assigner

滚动Assigner
    TumblingProcessingTimeWindows
    	*assignWindows
    		1.获取程序时间
    		2.获取时间线偏移量 默认是 ALIGNED(对齐 0L)
    		3.获取窗口的开始时间 
    			TimeWindow.getWindowStartWithOffset(
    					系统时间, (globalOffset + staggerOffset) % size, size);
    		4.创建窗口放入集合中 
    			Collections.singletonList(new TimeWindow(start, start + size))
    	tigger
    		执行 ProcessingTimeTrigger 的 onElement 方法 为每条数据注册 timer
    			详细过程参考 timer 原理
    TumblingEventTimeWindows
		*assignWindows
			1.获取时间线偏移量 默认是 ALIGNED(对齐 0L)
			2.获取窗口的开始时间
				TimeWindow.getWindowStartWithOffset(
                            传入的事件时间, (globalOffset + staggerOffset) % size, size);
			3.创建窗口
		tigger
			执行 EventTimeTrigger 的 onElement 方法 注册timer
				//Timestamp ⼩于 Watermark 的数据,都已经到达了
                if (window.maxTimestamp() <= ctx.getCurrentWatermark()) {
                    // if the watermark is already past the window fire immediately
                    return TriggerResult.FIRE;
                } else {
                    ctx.registerEventTimeTimer(window.maxTimestamp());
                    return TriggerResult.CONTINUE;
                }

滑动Assigner
	SlidingProcessingTimeWindows
	
	SlidingEventTimeWindows
        对每个进入窗口的元素根据 event time 时间分配到(size / slide)个不同的窗口
        并在每个窗口上根据窗口结束时间注册一个定时器(相同窗口只会注册一份),
        当定时器超时时意味着该窗口完成了,这时会回调对应窗口的Trigger的onProcessingTime方法
		*assignWindows
			1.创建窗口集合List<TimeWindow> windows = new ArrayList<>((int) (size / slide))
			2.获取上一个的窗口开始时间 
				TimeWindow.getWindowStartWithOffset(timestamp, offset, slide)
			3.创建窗口
                for (long start = lastStart; start > timestamp - size; start -= slide) {
                    windows.add(new TimeWindow(start, start + size));
                }
         tigger
			执行 EventTimeTrigger 的 onElement 方法 注册timer

Session Window Assigner

session window 的创建非常的灵活 可以在必要时合并两个或多个窗口
为了实现其功能 flink api 拓展了 MergingWindowAssigner 窗口分配器 用来决定哪些窗口是可以合并的
并且在 Trigger 中添加了 onMerge 方法用来响应发生窗口合并之后对 trigge r的相关动作
MergingWindowAssigner
	// 需要合并的 windows 合并窗口的方法 callback
	*mergeWindows(Collection<W> windows, MergeCallback<W> callback)
	//合并窗口的方法 merge
	*interface MergeCallback{void merge(Collection<W> toBeMerged, W mergeResult)}
核心方法:WindowOperator*processElement
	1.为每一个进入的元素创建一个窗口 new TimeWindow(timestamp, timestamp + sessionTimeout)
	2.判断是不是 session 类型的窗口
		false 不做合并处理
		true 需要判断窗口是否需要合并 后续步奏
	3.获取 MergingWindowSet 	
		MergingWindowSet 中使用一个map保存窗口信息 k=当前新创建窗口 v=所有可见窗口
		具体判断是否合并的方法位于 TimeWindow 的 mergeWindows() 方法
		判断的依据为 v 是否减少了
	4.如果合并了则 
		根据新窗口的结束时间注册新的定时器  
		删除旧窗口注册的定时器 
		合并旧窗口中的状态到新窗口中
		...
		
		
		
processElement的代码,首先根据 window assigner 为新进入的元素分配窗口集合
对于 MergingWindowAssigner 分配的窗口,取出当前的 MergingWindowSet。
对于每个分配到的窗口,调用 MergingWindowSet 的 addWindow() 将其加入到 MergingWindowSet中,由MergingWindowSet 维护窗口与状态窗口之间的关系,并在需要窗口合并的时候,合并状态和trigger
然后根据映射关系,取出结果窗口对应的状态窗口,根据状态窗口取出对应的状态。将新进入的元素数据加入到该状态中。最后,根据trigger结果来对窗口数据进行处理,对于session window来说,这里都是不进行任何处理的。真正对窗口处理是由定时器超时后对完成的窗口调用processTriggerResult
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值