Flink basic concepts - Window (Continued)

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 WindowedStreaminstance, 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.triggera 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 evictorsetting 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 WindowOperatorits major expansion of the evictor properties and associated logic.

public class EvictingWindowOperator extends WindowOperator {
    private final Evictor evictor;
}

 In EvictingWindowOperator the emitWindowContentsrealized 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/

Guess you like

Origin www.cnblogs.com/lemos/p/12602863.html