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