文章目录
1.简介
Flink的窗口主要为4中类型:滚动、滑动、session和global。其中滚动和滑动是同一类,处理流程基本相同,session有特殊流程,global是全局只有一个窗口,不划分窗口。
在另一个层面上分,可以分为是否进行keyBy,windowAll是不进行keyby的全域窗口。
注意global和windowAll的差别。global是进行keyBy操作的,只是没有按时间再划分窗口;windowAll则可以按时间划分窗口,但是不做分区(keyBy)。这是两个层面上的东西,windowAll可以跟滚动、滑动、global任意结合(session好像没有结合)
1.1.基本结构
window接口会返回WindowedStream,可以设置一些基本属性,然后进行后续的聚合操作。
基本属性包括:触发器(trigger)、清除器(evictor)、允许延迟(allowedLateness)、侧边输出(sideOutput)
延迟设置有三项,注意差别。1、触发器设置的延迟,这个是跟Watermarks相关的,设置什么时候触发窗口;2、allowedLateness,这个是在窗口触发后,继续保留一定时间,有新延迟数据会再次触发;3、sideOutput,这个是在窗口数据已经清理之后,单独做处理的流程。
1.2.对象关系
1.2.1.WindowedStream
WindowedStream是window对应的stream类型,在调用window相关的算子时会返回WindowedStream。
1.2.2.WindowAssigner
WindowAssigner是核心,实际的窗口类都是实现的WindowAssigner接口。
1.2.3.WindowOperator
WindowOperator是对应的算子,在调用reduce等聚合算子后,会构建到WindowOperator。Operator是实际运行的算子,processElement接口对应算子处理过程,其中会调用到WindowAssigner的assignWindows确定窗口划分
@Override
public void processElement(StreamRecord<IN> element) throws Exception {
final Collection<W> elementWindows =
windowAssigner.assignWindows(
element.getValue(), element.getTimestamp(), windowAssignerContext);
1.3.调用链
数据流转流程调用链,待补充
2.assignWindows
确认数据落入的窗口,会返回一系列的窗口列表(只有滑动窗口才会落入多个窗口,所以对于步长和窗口长度差距很大的滑动窗口,可能存在隐患,一个数据在多个窗口,多个窗口状态都得缓存)
以SlidingEventTimeWindows为例,过程如下,可以看出,一个数据会落入size / slide个窗口当中。timestamp获取在watermark流程当中,另介绍
@Override
public Collection<TimeWindow> assignWindows(
Object element, long timestamp, WindowAssignerContext context) {
if (timestamp > Long.MIN_VALUE) {
List<TimeWindow> windows = new ArrayList<>((int) (size / 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
3.数据处理
assignWindows是processElement处理的第一步,后续会真正进行数据处理。根据窗口类型,分为不同的处理:1、MergingWindowAssigner,即Session窗口;2、其他窗口
session窗口有窗口合并机制,处理比较复杂,先看其他窗口,都是单纯的窗口计算
整体就是对assignWindows获得的窗口遍历进行操作
for (W window : elementWindows) {
3.1.超时
首先判断是否超时,就是已经过了窗口的结束时间,这里的超时是算上了allowedLateness的
protected boolean isWindowLate(W window) {
return (windowAssigner.isEventTime()
&& (cleanupTime(window) <= internalTimerService.currentWatermark()));
}
cleanupTime的计算如下,是包含allowedLateness的,计算的是窗口清理时间
private long cleanupTime(W window) {
if (windowAssigner.isEventTime()) {
long cleanupTime = window.maxTimestamp() + allowedLateness;
return cleanupTime >= window.maxTimestamp() ? cleanupTime : Long.MAX_VALUE;
} else {
return window.maxTimestamp();
}
}
3.2.窗口状态更新
窗口是一个聚合型的,新数据到达需要更新窗口的内容,可以认为是累加结果
State分很多类型,具体的后续再研究
windowState.setCurrentNamespace(window);
windowState.add(element.getValue());
3.3.窗口触发
就是根据窗口触发器,确认是否触发窗口计算。触发器可以自定义设置,SlidingEventTimeWindows默认的触发器是EventTimeTrigger,触发逻辑如下
触发逻辑简单来说就是数据Watermarks超过了窗口的最晚边界
public TriggerResult onElement(
Object element, long timestamp, TimeWindow window, TriggerContext ctx)
throws Exception {
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;
}
}
如果触发了,计算进行窗口计算
if (triggerResult.isFire()) {
ACC contents = windowState.get();
if (contents != null) {
emitWindowContents(triggerContext.window, contents);
}
}
窗口计算就是调用业务设置的function进行操作
private void emitWindowContents(W window, ACC contents) throws Exception {
timestampedCollector.setAbsoluteTimestamp(window.maxTimestamp());
processContext.window = window;
userFunction.process(
triggerContext.key, window, processContext, contents, timestampedCollector);
}
这个userFunction会根据算子设置不同初始化时设置,比如跟reduce,就会创建InternalSingleValueAllWindowFunction
3.4.清理窗口
完成窗口清理。Trigger执行后窗口有四种执行类型:CONTINUE、FIRE_AND_PURGE、FIRE、PURGE。其中CONTINUE不做操作,FIRE触发窗口,PURGE清理窗口
if (triggerResult.isPurge()) {
windowState.clear();
}
3.5.注册清理时间器
就是前面计算时间里的internalTimerService相关的内容,此处是将新的时间信息记录下来,以备下次使用
应该没有定时器的功能,需要继研究下
protected void registerCleanupTimer(W window) {
long cleanupTime = cleanupTime(window);
if (cleanupTime == Long.MAX_VALUE) {
// don't set a GC timer for "end of time"
return;
}
if (windowAssigner.isEventTime()) {
triggerContext.registerEventTimeTimer(cleanupTime);
} else {
triggerContext.registerProcessingTimeTimer(cleanupTime);
}
}
4.侧边输出
这一步是processElement在窗口处理之后的步骤,负责侧边输出
侧边输出是窗口已经清理后对数据的处理,如果没有设置侧边输出,则数据将被丢弃
if (isSkippedElement && isElementLate(element)) {
if (lateDataOutputTag != null) {
sideOutput(element);
} else {
this.numLateRecordsDropped.inc();
}
}
是在allowedLateness之后的,allowedLateness过了会清理窗口
protected boolean isElementLate(StreamRecord<IN> element) {
return (windowAssigner.isEventTime())
&& (element.getTimestamp() + allowedLateness
<= internalTimerService.currentWatermark());
}
5.onEventTime/onProcessingTime
以上2-4步骤是processElement的处理步骤,是有输出触发调用的,WindowOperator中还有onEventTime/onProcessingTime方法,是由Watermarks触发的
Watermarks在flink中也是像数据一样传输的,在接口AbstractStreamTaskNetworkInput(WindowOperator的上层调用链)的processElement方法中,根据传输的数据类型不同,有不同的处理
private void processElement(StreamElement recordOrMark, DataOutput<T> output) throws Exception {
if (recordOrMark.isRecord()) {
output.emitRecord(recordOrMark.asRecord());
} else if (recordOrMark.isWatermark()) {
statusWatermarkValve.inputWatermark(
recordOrMark.asWatermark(), flattenedChannelIndices.get(lastChannel), output);
} else if (recordOrMark.isLatencyMarker()) {
output.emitLatencyMarker(recordOrMark.asLatencyMarker());
} else if (recordOrMark.isWatermarkStatus()) {
statusWatermarkValve.inputWatermarkStatus(
recordOrMark.asWatermarkStatus(),
flattenedChannelIndices.get(lastChannel),
output);
} else {
throw new UnsupportedOperationException("Unknown type of StreamElement");
}
}
Watermarks的后续处理流程会触发onEventTime/onProcessingTime方法,由于是Watermarks触发的,所有没有针对数据的处理,只是针对窗口状态和触发的处理,整体流程基本就是processElement去除了数据处理的部分
6.session窗口
6.1.assignWindows
session窗口下,数据只会落入一个窗口。EventTimeSessionWindows的处理逻辑如下
@Override
public Collection<TimeWindow> assignWindows(
Object element, long timestamp, WindowAssignerContext context) {
return Collections.singletonList(new TimeWindow(timestamp, timestamp + sessionTimeout));
}
这个窗口并不是最终数据落入的窗口,因为session窗口要根据间隔时间合并,所以这个窗口是以数据时间为起点,超时时间为长度的窗口,这样,后续就可以根据两个边界进行窗口合并。
6.2.窗口合并
窗口合并操作在MergingWindowSet的addWindow的内,MergingWindowSet本身缓存了一系列的窗口,每次操作之后会更新这个列表
6.2.1.重叠窗口合并
首先是获取当前所有窗口(包括此次新增的窗口),进行合并操作
windows.addAll(this.mapping.keySet());
windows.add(newWindow);
final Map<W, Collection<W>> mergeResults = new HashMap<>();
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);
}
mergeResults.put(mergeResult, toBeMerged);
}
});
最后逻辑在TimeWindow的mergeWindows。其处理逻辑核心为两步:1、排序,按窗口的起始时间做排序;2、归并,遍历窗口列表,将有重叠的窗口进行合并
Collections.sort(
sortedWindows,
new Comparator<TimeWindow>() {
@Override
public int compare(TimeWindow o1, TimeWindow o2) {
return Long.compare(o1.getStart(), o2.getStart());
}
});
归并逻辑如下,分支一是空值状态的添加;分支二归并;分支三无归并项直接进入列表
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);
}
}
归并判断和具体操作如下,就是判断区域是否有重叠,有重叠的取最大上下限。由于是排序过的窗口,所以与下一个窗口没有重叠的话,本窗口肯定是独立的。需要确认的是窗口列表的来源,这个后文说明
public boolean intersects(TimeWindow other) {
return this.start <= other.end && this.end >= other.start;
}
/** Returns the minimal window covers both this window and the given window. */
public TimeWindow cover(TimeWindow other) {
return new TimeWindow(Math.min(start, other.start), Math.max(end, other.end));
}
6.2.2.窗口集合的增加
第一个窗口时,不会调用到windowAssigner.mergeWindows里的自定义merge函数,所以mergeResults为空,这种情况下,在返回前的最后一个操作中,会将窗口加入集合
实际这个方法起作用不止第一个窗口,产生一个独立窗口后都会加入
// the new window created a new, self-contained window without merging
if (mergeResults.isEmpty() || (resultWindow.equals(newWindow) && !mergedNewWindow)) {
this.mapping.put(resultWindow, resultWindow);
}
6.2.3.state合并
第一节中的合并是针对窗口范围进行的一个窗口合并,并没有实际对窗口数据进行合并。
注意第一节中的内容,列表中的currentMerge.f1的窗口跟原来列表里的窗口是一致不变的,而且在最后做了一个翻转。也就是说,实际结果就是一个老窗口-新窗口的一个对应关系,这样就可以基于此进行state合并了
for (Tuple2<TimeWindow, Set<TimeWindow>> m : merged) {
if (m.f1.size() > 1) {
c.merge(m.f1, m.f0);
}
}