Built-in method
WindowedStream
By KeyedStream
can create Count Window and Time Window directly. They are ultimately based window(WindowAssigner)
method to create, create a method in the window WindowedStream
instance, using the current parameters of the object and the specified KeyedStream WindowAssigner.
def window[W <: Window](assigner: WindowAssigner[_ >: T, W]): WindowedStream[T, K, W] = {
new WindowedStream(new WindowedJavaStream[T, K, W](javaStream, assigner))
}
Construction method
@PublicEvolving
public WindowedStream(KeyedStream<T, K> input,
WindowAssigner<? super T, W> windowAssigner) {
this.input = input;
this.windowAssigner = windowAssigner;
this.trigger = windowAssigner.getDefaultTrigger(input.getExecutionEnvironment());
}
Trigger
Default initialization trigger, also provides a method for covering a separate trigger the default trigger. Flink count window is built using windowedStream.trigger
a method overrides the default trigger.
public WindowedStream<T, K, W> trigger(Trigger<? super T, ? super W> trigger) {
if (windowAssigner instanceof MergingWindowAssigner && !trigger.canMerge()) {
throw new UnsupportedOperationException("A merging window assigner cannot be used with a trigger that does not support merging.");
}
if (windowAssigner instanceof BaseAlignedWindowAssigner) {
throw new UnsupportedOperationException("Cannot use a " + windowAssigner.getClass().getSimpleName() + " with a custom trigger.");
}
this.trigger = trigger;
return this;
}
evictor
Further, WindowedStream there is a more important properties evictor
, by evictor
setting method.
@PublicEvolving
public WindowedStream<T, K, W> evictor(Evictor<? super T, ? super W> evictor) {
if (windowAssigner instanceof BaseAlignedWindowAssigner) {
throw new UnsupportedOperationException("Cannot use a " + windowAssigner.getClass().getSimpleName() + " with an Evictor.");
}
this.evictor = evictor;
return this;
}
In a related realization handler WindowedStream in accordance evictor property it is empty ( null == evictor
) decide whether to create WindowOperator
or EvictingWindowOperator
. EvictingWindowOperator
Inherited from WindowOperator
its major expansion of the evictor properties and associated logic.
public class EvictingWindowOperator extends WindowOperator {
private final Evictor evictor;
}
In EvictingWindowOperator the emitWindowContents
realized logic data cleaning process.
private void emitWindowContents(W window, Iterable<StreamRecord<IN>> contents, ListState<StreamRecord<IN>> windowState) throws Exception {
/** Window处理前数据清理 */
evictorContext.evictBefore(recordsWithTimestamp, Iterables.size(recordsWithTimestamp));
/** Window处理 */
userFunction.process(triggerContext.key, triggerContext.window, processContext, projectedContents, timestampedCollector);
/** Window处理后数据清理 */
evictorContext.evictAfter(recordsWithTimestamp, Iterables.size(recordsWithTimestamp));
}
源码剖析
一、Count Window API
1. Window API
下面代码片段是 KeyedStream
提供创建 Count Window的API。
// 滚动窗口
public WindowedStream<T, KEY, GlobalWindow> countWindow(long size) {
return window(GlobalWindows.create()).trigger(PurgingTrigger.of(CountTrigger.of(size)));
}
// 滑动窗口
public WindowedStream<T, KEY, GlobalWindow> countWindow(long size, long slide) {
return window(GlobalWindows.create())
.evictor(CountEvictor.of(size))
.trigger(CountTrigger.of(slide));
}
2. Assigner
通过方法window(GlobalWindows.create())
创建 WindowedStream实例,滚动计数窗口处理和滑动计数窗口处理都是基于 GlobalWindows
作为 WindowAssigner来创建窗口处理器。GlobalWindows
将所有数据都分配到同一个 GlobalWindow
中。
@PublicEvolving
public class GlobalWindows extends WindowAssigner<Object, GlobalWindow> {
private static final long serialVersionUID = 1L;
private GlobalWindows() {
}
@Override
public Collection<GlobalWindow> assignWindows(Object element, long timestamp, WindowAssignerContext context) {
return Collections.singletonList(GlobalWindow.get());
}
}
注意 GlobalWindows
是一个WindowAssigner,而GlobalWindow
是一个Window。GlobalWindow 继承了 Window,表示为一个窗口。对外提供 get()方法返回 GlobalWindow实例,并且是个全局单例。所以当使用 GlobalWindows作为 WindowAssigner时,所有数据将被分配到一个窗口中。
@PublicEvolving
public class GlobalWindow extends Window {
private static final org.apache.flink.streaming.api.windowing.windows.GlobalWindow INSTANCE = new org.apache.flink.streaming.api.windowing.windows.GlobalWindow();
private GlobalWindow() {
}
public static org.apache.flink.streaming.api.windowing.windows.GlobalWindow get() {
return INSTANCE;
}
}
3. Trigger & Evictor
翻滚计数窗口并不带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个元素,剔除旧元素。
计数窗口 | WindowAssigner | Evictor | Trigger | 说明 |
---|---|---|---|---|
滚动计数窗口 | GlobalWindows | - | PurgingTrigger | 窗口处理数据前后不清理数据,由Trigger返回值声明直接清理数据,清理数据依赖Trigger返回结果 |
滑动计数窗口 | GlobalWindows | CountEvictor | CountTrigger | Trigger返回结果不能清理数据(返回结果不带PURGE),在窗口处理完后数据会被保留下来,为下一个滑动窗口使用。因为使用了CountEvictor,会在窗口处理前清除不需要的数据 |
二、Time Window API
1. Window API
下面代码片段是 KeyedStream
提供创建 Time Window的API。
// 滚动窗口
public WindowedStream<T, KEY, TimeWindow> timeWindow(Time size) {
if (environment.getStreamTimeCharacteristic() == TimeCharacteristic.ProcessingTime) {
return window(TumblingProcessingTimeWindows.of(size));
} else {
return window(TumblingEventTimeWindows.of(size));
}
}
// 滑动窗口
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));
}
}
2. Assginer
下面的表格中展示了窗口类型和时间类型对应的 WindowAssigner 的实现类
时间窗口类型 | 时间类型 | WindowAssigner |
---|---|---|
滚动时间窗口 | ProcessingTime | TumblingProcessingTimeWindows |
滚动时间窗口 | IngestionTime | TumblingEventTimeWindows |
滚动时间窗口 | EventTime | TumblingEventTimeWindows |
滑动时间窗口 | ProcessingTime | SlidingProcessingTimeWindows |
滑动时间窗口 | IngestionTime | SlidingEventTimeWindows |
滑动时间窗口 | EventTime | SlidingEventTimeWindows |
① TumblingProcessingTimeWindows
初始化
构造参数 timestamp 与 offset 。比如按小时切割窗口,但每次都从一小时的第 15分钟开始。则可使用 of(Time.hours(1),Time.minutes(15)),你将会获得 0:15:00,1:15:00,2:15:00 ...区间的窗口。
public static TumblingProcessingTimeWindows of(Time size) {
return new TumblingProcessingTimeWindows(size.toMilliseconds(), 0);
}
public static TumblingProcessingTimeWindows of(Time size, Time offset) {
return new TumblingProcessingTimeWindows(size.toMilliseconds(), offset.toMilliseconds());
}
private TumblingProcessingTimeWindows(long size, long offset) {
if (offset < 0 || offset >= size) {
throw new IllegalArgumentException();
}
this.size = size;
this.offset = offset;
}
assignWindows
@Override
public Collection<TimeWindow> assignWindows(Object element, long timestamp, WindowAssignerContext context) {
final long now = context.getCurrentProcessingTime();
long start = TimeWindow.getWindowStartWithOffset(now, offset, size);
return Collections.singletonList(new TimeWindow(start, start + size));
}
该
方法每次都会返回一个新的窗口,也就是说窗口是不重叠的。但因为TimeWindow实现了equals
方法,所以通过计算后start, start + size相同的数据,在逻辑上是同一个窗口。
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TimeWindow window = (TimeWindow) o;
return end == window.end && start == window.start;
}
示例:
如果我们希望从第15秒开始,每过1分钟计算一次窗口数据,这种场景需要用到offset。基于处理时间的滚动窗口可以这样写
keyedStream.window(TumblingProcessingTimeWindows.of(Time.minutes(1), Time.seconds(15)))
我们假设数据从2019年1月1日 12:00:14到达,那么窗口以下面方式切割
Window[2019年1月1日 11:59:15, 2019年1月1日 12:00:15)
如果在2019年1月1日 12:00:16又一数据到达,那么窗口以下面方式切割
Window[2019年1月1日 12:00:15, 2019年1月1日 12:01:15)
② SlidingEventTimeWindows
初始化
public static SlidingEventTimeWindows of(Time size, Time slide) {
return new SlidingEventTimeWindows(size.toMilliseconds(), slide.toMilliseconds(), 0);
}
public static SlidingEventTimeWindows of(Time size, Time slide, Time offset) {
return new SlidingEventTimeWindows(size.toMilliseconds(), slide.toMilliseconds(),offset.toMilliseconds() % slide.toMilliseconds());
}
protected SlidingEventTimeWindows(long size, long slide, long offset) {
if (offset < 0 || offset >= slide || size <= 0) {
throw new IllegalArgumentException();
}
this.size = size;
this.slide = slide;
this.offset = offset;
}
assignWindows
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 {
throw new RuntimeException("Record has Long.MIN_VALUE timestamp (= no timestamp marker). " +
"Is the time characteristic set to 'ProcessingTime', or did you forget to call " +
"'DataStream.assignTimestampsAndWatermarks(...)'?");
}
}
首先根据事件时间、偏移量与滑动大小计算窗口的起始时间,再根据窗口大小切分成多个窗口。因为滑动窗口中一个数据可能落在多个窗口。
示例:
比如,希望每5秒滑动一次处理最近10秒窗口数据keyedStream.timeWindow(Time.seconds(10), Time.seconds(5))
。当数据源源不断流入Window Operator时,会按10秒切割一个时间窗,5秒滚动一次。
我们假设一条付费事件数据付费时间是2019年1月1日 17:11:24,那么这个付费数据将落到下面两个窗口中(请注意,窗口是左闭右开
)。
Window[2019年1月1日 17:11:20, 2019年1月1日 17:11:30)
Window[2019年1月1日 17:11:15, 2019年1月1日 17:11:25)
参考文章
https://tianshushi.github.io/2019/02/17/Flink-Window/