FLINK基本的な概念 - ウィンドウ(続きます)

内蔵の方法

 

WindowedStream

KeyedStream 直接カウントウィンドウとタイムウィンドウを作成することができます。彼らは、最終的に基づいてwindow(WindowAssigner)ウィンドウでメソッドを作成し、作成するためのメソッドWindowedStreamオブジェクトと指定されたKeyedStream WindowAssignerの現在のパラメータを使用して、インスタンス。

DEFウィンドウ[W <:ウィンドウ(アサイナ:WindowAssigner [_>:T、W):WindowedStream [T、K、W] = { 
  新しいWindowedStream(新しいWindowedJavaStream [T、K、W](javaStream、アサイナ))
}

コンストラクタ

@PublicEvolving 
パブリックWindowedStreamを(KeyedStream <T、K>入力、
  WindowAssigner windowAssigner <スーパーT、W?>){ 
  this.input =入力。
  this.windowAssigner = windowAssigner。
  this.trigger = windowAssigner.getDefaultTrigger(input.getExecutionEnvironment())。
}

 

引き金

デフォルト初期化トリガ、また、別個のトリガーデフォルトのトリガーを被覆するための方法を提供します。FLINKカウントウィンドウが使用して構築されたwindowedStream.triggerメソッドのオーバーライドデフォルトのトリガーを。

公共WindowedStream <T、K、W>引き金(トリガー<?スーパーT、?スーパーW>トリガー){ 
	場合{(windowAssigner MergingWindowAssigner &&!trigger.canMergeは()のinstanceof)
		新しいUnsupportedOperationExceptionがスロー( "Aマージウィンドウ割当を使用することはできませんマージをサポートしていませんトリガに」)。
	} 

	(windowAssigner instanceofはBaseAlignedWindowAssigner)場合は、{ 
		新しいUnsupportedOperationExceptionが(。 "カスタム・トリガーで" + windowAssigner.getClass()getSimpleName()+ "を使用することはできません")投げます。
	} 

	this.trigger =トリガー。
	これを返します。
}

 

evictor

さらに、WindowedStreamより重要な特性があるevictorことによって、evictor設定方法。

@PublicEvolving 
公共WindowedStreamを<T、K、W>のEvictor(のEvictor <?スーパーT、?スーパーW>のEvictor){ 
	場合(windowAssigner instanceofはBaseAlignedWindowAssigner){ 
		新しいUnsupportedOperationExceptionがスロー(+ windowAssigner.getClass()。getSimpleName "を使用することはできません" ()+ "のEvictorを有します。")。
	} 
	this.evictor =エビクタ。
	これを返します。
}

エビクタプロパティに関連する実現ハンドラWindowedStreamでは、(空であるnull == evictor)を作成するかどうかを決定しますWindowOperator EvictingWindowOperatorEvictingWindowOperator 継承さWindowOperatorのEvictorの特性と関連するロジックの主要な展開。

パブリッククラスEvictingWindowOperatorはWindowOperator {延び
    プライベート最終のEvictorエビクタと、
}

 EvictingWindowOperatorでemitWindowContents実現した論理データは、プロセスを掃除します。

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/

おすすめ

転載: www.cnblogs.com/lemos/p/12602863.html