窗口是另一类的算子,是DataStream的逻辑边界,在第一个元素到达后创建,在生命周期结束后被销毁。
窗口分为两大类:
Keyed Windows
stream
.keyBy(...) <- keyed versus non-keyed windows
.window(...) <- required: "assigner"
[.trigger(...)] <- optional: "trigger" (else default trigger)
[.evictor(...)] <- optional: "evictor" (else no evictor)
[.allowedLateness(...)] <- optional: "lateness" (else zero)
[.sideOutputLateData(...)] <- optional: "output tag" (else no side output for late data)
.reduce/aggregate/fold/apply() <- required: "function"
[.getSideOutput(...)] <- optional: "output tag"
Non-Keyed Windows
stream
.windowAll(...) <- required: "assigner"
[.trigger(...)] <- optional: "trigger" (else default trigger)
[.evictor(...)] <- optional: "evictor" (else no evictor)
[.allowedLateness(...)] <- optional: "lateness" (else zero)
[.sideOutputLateData(...)] <- optional: "output tag" (else no side output for late data)
.reduce/aggregate/fold/apply() <- required: "function"
[.getSideOutput(...)] <- optional: "output tag"
在上面,方括号([…])中的命令是可选的。这表明Flink允许您以多种不同方式自定义窗口逻辑,以便最适合您的需求。
当属于该窗口的第一个元素到达时,就会创建一个窗口,当时间(事件或处理时间)通过它的结束时间戳加上用户指定的允许延迟时,该窗口将被完全删除。Flink指只保证基于时间的窗口删除。
此外,每个窗口都具有一个触发器和一个函数(ProcessWindowFunction、ReduceFunction、AggregateFunction或FoldFunction)。函数包含窗口内容的计算,触发器为已做好准备运行函数的条件。
在定义窗口前,应先确定流是否使用KeyBy(…)进行逻辑分组。
在KeyByStream的情况下,将允许多个任务窗口并行执行运算,因为每个逻辑KeyByStream都可以独立于其他逻辑KeyByStream进行处理。所有引用相同键的元素将被发送到相同的并行任务。
在non-keyedStreams的情况下,原始流不会被分割成多个逻辑流,所有的窗口逻辑将由一个任务执行,即并行度为1。
窗口分配器定义将一个元素分配给一个或多个窗口。通过调用window(…)(keyByStream)或windowAll()(Non-KeyedStream)来完成。
将时间拆分成具有固定长度,不重叠的时间片段。例如,如果滚动窗口将windows Size设置为5分钟,则每五分钟将启动一个新窗口,如下图所示。
时间间隔设置
DataStream input = ...;
// 滚动窗口-事件时间
input
.keyBy()
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.();
// 滚动窗口-处理时间
input
.keyBy()
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.();
// 滚动窗口-事件时间并设置8小时的偏移量(用于抵消时差)
input
.keyBy()
// 偏移量15分钟:1:00:00.000 - 1:59:59.999 -> 1:15:00.000 - 2:14:59.999
// 在中国,必须指定偏移量Time.hours(-8) (中国是UTC +8 ,flink默认使用UTC-0)
.window(TumblingEventTimeWindows.of(Time.days(1), Time.hours(-8)))
.();
由windows Size设置一个固定大小的窗口。并附加一个window slide来设置窗口移动的距离。因此如果window slide小于window size将会有同一元素被分配到多个窗口的情况发生。
DataStream input = ...;
// 滑动窗口-事件时间
input
.keyBy()
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.();
// 滑动窗口-处理时间
input
.keyBy()
.window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.();
// 滑动窗口-事件时间并设置8小时的偏移量(用于抵消时差)
input
.keyBy()
.window(SlidingProcessingTimeWindows.of(Time.hours(12), Time.hours(1), Time.hours(-8)))
.();
按活动会话分配元素。会话窗口不重叠,没有固定的开始和结束时间,与滚动窗口和滑动窗口相反。当会话窗口在一段时间内没有接收到元素时,即当发生不活动的间隙时会关闭会话窗口。会话窗口可以设置静态会话间隙或动态会话间隙函数(通过实现SessionWindowTimeGapExtractor接口),该功能定义不活动时间段的长度。当到达此期限使,将关闭当前会话,后续元素将分配给新的会话窗口。
DataStream input = ...;
// 会话窗口 - 事件时间 - 静态会话间隙
input
.keyBy()
.window(EventTimeSessionWindows.withGap(Time.minutes(10)))
.();
// 会话窗口 - 事件时间 - 动态会话间隙函数
input
.keyBy()
.window(EventTimeSessionWindows.withDynamicGap((element) -> {
// determine and return session gap
}))
.();
// 会话窗口 -处理时间 - 静态会话间隙
input
.keyBy()
.window(ProcessingTimeSessionWindows.withGap(Time.minutes(10)))
.();
// 会话窗口 - 处理时间 - 动态会话间隙函数
input
.keyBy()
.window(ProcessingTimeSessionWindows.withDynamicGap((element) -> {
// determine and return session gap
}))
.();
将具有相同键的所有元素分配给同一个全局窗口。只有在指定自定义触发器时,此窗口模式才有用。否则,将不执行任何计算,因为全局窗口没有一个自然的结束,我们不能在结束处处理聚合的元素。
DataStream input = ...;
input
.keyBy()
.window(GlobalWindows.create())
.();
定义窗口分配器后,需要指定需要在每个窗口上执行的计算。这是窗口函数的职责,窗口函数在系统确定窗口准备好进行处理后用于处理每个窗口的元素。
窗口函数可以是ReduceFunction,AggregateFunction,FoldFunction或ProcessWindowFunction。前两个可以更有效率地执行,因为Flink可以在每个窗口进行增量聚合。ProcessWindowFunction获取Iterable窗口中包含的所有元素以及有关元素所属窗口的其他元信息。
具有a的窗口转换ProcessWindowFunction不能像其他情况一样有效地执行,因为Flink必须在调用函数之前在内部缓冲窗口的所有元素。这可以通过组合a ProcessWindowFunction,a ReduceFunction,AggregateFunction或者FoldFunction获得窗口元素的增量聚合和ProcessWindowFunction接收的附加窗口元数据 来减轻。我们将查看每个变体的示例。
ReduceFunction将输入的两个元素进行组合以生成相同类型的新元素。Flink使用ReduceFunction对窗口元素进行增量聚合。
ReduceFunction的定义和使用:
DataStream> input = ...;
input
.keyBy()
.window()
.reduce(new ReduceFunction> {
public Tuple2 reduce(Tuple2 v1, Tuple2 v2) {
return new Tuple2<>(v1.f0, v1.f1 + v2.f1);
}
});
AggregateFunction是一个通用版本,ReduceFunction它有三种类型:输入类型(IN),累加器类型(ACC)和输出类型(OUT)。输入类型是输入流中元素的类型,并且AggregateFunction具有将一个输入元素添加到累加器的方法。该接口还具有用于创建初始累加器的方法,用于将两个累加器合并到一个累加器中以及用于OUT从累加器提取输出(类型)。我们将在下面的示例中看到它的工作原理。
与之相同ReduceFunction,Flink将在窗口到达时递增地聚合窗口的输入元素。
AggregateFunction的定义和使用:
/**
* The accumulator is used to keep a running sum and a count. The {@code getResult} method
* computes the average.
*/
private static class AverageAggregate
implements AggregateFunction, Tuple2, Double> {
@Override
public Tuple2 createAccumulator() {
return new Tuple2<>(0L, 0L);
}
@Override
public Tuple2 add(Tuple2 value, Tuple2 accumulator) {
return new Tuple2<>(accumulator.f0 + value.f1, accumulator.f1 + 1L);
}
@Override
public Double getResult(Tuple2 accumulator) {
return ((double) accumulator.f0) / accumulator.f1;
}
@Override
public Tuple2 merge(Tuple2 a, Tuple2 b) {
return new Tuple2<>(a.f0 + b.f0, a.f1 + b.f1);
}
}
DataStream> input = ...;
input
.keyBy()
.window()
.aggregate(new AverageAggregate());
FoldFunction指定窗口的输入元素如何与输出类型的元素组合。对于添加到窗口的每个元素,都会递增地调用FoldFunction。第一个元素与输出类型的预定义初值相结合。
FoldFunction的定义和使用:
DataStream> input = ...;
input
.keyBy()
.window()
.fold("", new FoldFunction, String>> {
public String fold(String acc, Tuple2 value) {
return acc + value.f1;
}
});
ProcessWindowFunction获取一个包含窗口所有元素(全量计算)的迭代器,以及一个具有时间和状态信息访问权的上下文对象,这使它能够比其他窗口函数提供更大的灵活性。这是以性能和资源消耗为代价的,因为元素不能增量地聚合,而是需要在内部进行缓冲,直到认为窗口已经做好进行处理的准备。
ProcessWindowFunction的定义和使用:
DataStream> input = ...;
input
.keyBy(t -> t.f0)
.timeWindow(Time.minutes(5))
.process(new MyProcessWindowFunction());
/* ... */
public class MyProcessWindowFunction
extends ProcessWindowFunction, String, String, TimeWindow> {
@Override
public void process(String key, Context context, Iterable> input, Collector out) {
long count = 0;
for (Tuple2 in: input) {
count++;
}
out.collect("Window: " + context.window() + "count: " + count);
}
}
注意:使用ProcessWindowFunction做简单的聚合(例如count)效率非常低。
ProcessWindowFunction可以与ReduceFunction、AggregateFunction或FoldFunction组合使用,以便在元素到达窗口时增量地聚合它们。当窗口关闭时,ProcessWindowFunction将提供聚合的结果。这允许它在访问ProcessWindowFunction的附加窗口元信息的同时递增地计算窗口。
使用ProcessWindowFunction + ReduceFunction基于增量聚合
DataStream input = ...;
input
.keyBy()
.timeWindow()
.reduce(new MyReduceFunction(), new MyProcessWindowFunction());
// Function definitions
private static class MyReduceFunction implements ReduceFunction {
public SensorReading reduce(SensorReading r1, SensorReading r2) {
return r1.value() > r2.value() ? r2 : r1;
}
}
private static class MyProcessWindowFunction
extends ProcessWindowFunction, String, TimeWindow> {
public void process(String key,
Context context,
Iterable minReadings,
Collector> out) {
SensorReading min = minReadings.iterator().next();
out.collect(new Tuple2(context.window().getStart(), min));
}
}
使用ProcessWindowFunction + AggregateFunction基于增量聚合
DataStream> input = ...;
input
.keyBy()
.timeWindow()
.aggregate(new AverageAggregate(), new MyProcessWindowFunction());
// Function definitions
/**
* The accumulator is used to keep a running sum and a count. The {@code getResult} method
* computes the average.
*/
private static class AverageAggregate
implements AggregateFunction, Tuple2, Double> {
@Override
public Tuple2 createAccumulator() {
return new Tuple2<>(0L, 0L);
}
@Override
public Tuple2 add(Tuple2 value, Tuple2 accumulator) {
return new Tuple2<>(accumulator.f0 + value.f1, accumulator.f1 + 1L);
}
@Override
public Double getResult(Tuple2 accumulator) {
return ((double) accumulator.f0) / accumulator.f1;
}
@Override
public Tuple2 merge(Tuple2 a, Tuple2 b) {
return new Tuple2<>(a.f0 + b.f0, a.f1 + b.f1);
}
}
private static class MyProcessWindowFunction
extends ProcessWindowFunction, String, TimeWindow> {
public void process(String key,
Context context,
Iterable averages,
Collector> out) {
Double average = averages.iterator().next();
out.collect(new Tuple2<>(key, average));
}
}
使用ProcessWindowFunction + FoldFunction基于增量聚合
DataStream input = ...;
input
.keyBy()
.timeWindow()
.fold(new Tuple3("",0L, 0), new MyFoldFunction(), new MyProcessWindowFunction())
// Function definitions
private static class MyFoldFunction
implements FoldFunction > {
public Tuple3 fold(Tuple3 acc, SensorReading s) {
Integer cur = acc.getField(2);
acc.setField(cur + 1, 2);
return acc;
}
}
private static class MyProcessWindowFunction
extends ProcessWindowFunction, Tuple3, String, TimeWindow> {
public void process(String key,
Context context,
Iterable> counts,
Collector> out) {
Integer count = counts.iterator().next().getField(2);
out.collect(new Tuple3(key, context.window().getEnd(),count));
}
}
除了访问键控状态(如任何丰富的函数可以),a ProcessWindowFunction还可以使用键控状态,该键控状态的作用域是函数当前正在处理的窗口。在这种情况下,了解每个窗口状态所指的窗口是很重要的。涉及不同的“窗口”:
指定窗口操作时定义的窗口:这可能是1小时的翻滚窗口或滑动1小时的2小时滑动窗口。
给定键的已定义窗口的实际实例:对于user-id xyz,这可能是从12:00到13:00的时间窗口。这基于窗口定义,并且将基于作业当前正在处理的键的数量以及基于事件落入的时隙而存在许多窗口。
每窗口状态与后两者相关联。这意味着如果我们处理1000个不同键的事件,并且所有这些事件的事件当前都落入[12:00,13:00]时间窗口,那么将有1000个窗口实例,每个窗口实例都有自己的键控每窗口状态。
调用接收的Context对象有两种方法process()允许访问两种类型的状态:
globalState(),允许访问没有作用于窗口的键控状态
windowState(),允许访问也限定在窗口范围内的键控状态
如果您预计同一窗口会发生多次触发,则此功能非常有用,如果您迟到的数据或者您有自定义触发器进行投机性早期触发时可能会发生这种情况。在这种情况下,您将存储有关先前点火的信息或每个窗口状态的点火次数。
使用窗口状态时,清除窗口时清除该状态也很重要。这应该在clear()方法中发生。
触发器负责决定在窗口的什么时间点启动应用程序定义的数据处理任务。水印迟到会拉长窗口生存周期,水印早到会导致数据处理结果不准确,触发器就是为解决这两个问题而引入的。每个都有一个默认值。如果默认触发器不符合需要,可以使用指定自定义触发器(WindowAssignerTriggertrigger(…))
触发器接口有五种方法可以Trigger对不同的事件做出反应:
关于上述方法需要注意两点:
1)前三类触发机制的结果(TriggerResult)分为以下四种情况:
2)这些方法中的任何一种都可用于为将来的操作注册处理或事件时间计时器。
一旦触发器确定窗口已经做好处理准备,它就会触发,即它返回FIRE或FIRE_AND_PURGE。这是窗口操作符发出当前窗口结果的信号。给定一个带有ProcessWindowFunction的窗口,所有元素都被传递给ProcessWindowFunction(可能在将它们传递给一个回收器之后)。带有ReduceFunction、AggregateFunction或FoldFunction的窗口只会发出它们急切聚合的结果。
当触发器触发时,它可以触发FIRE或FIRE_AND_PURGE。当触发FIRE保留窗口的内容时,触发FIRE_AND_PURGE时删除它的内容。默认情况下,预实现的触发器只触发FIRE而不清除窗口状态。
注意:清除将简单地删除窗口的内容,并将保留有关窗口和任何触发状态的任何潜在元信息。
WindowAssigner的默认触发器适用于许多用例。例如,所有事件时间窗口分配程序都有一个EventTimeTrigger作为默认触发器。一旦水印经过窗口的末端,这个触发器就会触发。
注意:
Flink附带了一些内置触发器。
如果需要实现自定义触发器,则应该检查抽象 Trigger类。请注意,API仍在不断发展,可能会在Flink的未来版本中发生变化。
清除器在触发器触发后,窗口函数执行前或窗口函数执行后清除窗口内元素。
有以下两个方法:
/**
* 触发器触发后,窗口函数执行前
*
* @param elements The elements currently in the pane.
* @param size The current number of elements in the pane.
* @param window The {@link Window}
* @param evictorContext The context for the Evictor
*/
void evictBefore(Iterable> elements, int size, W window, EvictorContext evictorContext);
/**
* 触发器触发后,窗口函数执行后
*
* @param elements The elements currently in the pane.
* @param size The current number of elements in the pane.
* @param window The {@link Window}
* @param evictorContext The context for the Evictor
*/
void evictAfter(Iterable> elements, int size, W window, EvictorContext evictorContext);
Flink附带三个预实现的evictors。这些都是:
默认情况下,所有预先实现的evictors在窗口函数之前应用它们的逻辑。
注意:指定逐出器会阻止任何预聚合,因为在应用计算之前,必须将窗口的所有元素传递给逐出器。同时Flink不保证窗口内元素的顺序。这意味着尽管逐出器可以从窗口的开头移除元素,但这些元素不一定是首先到达或最后到达的元素。
当使用事件时间窗口时,可能会出现元素延迟到达的情况,即Flink用来跟踪事件时间进程的水印已经超过了元素所属窗口的结束时间戳。
默认情况下,当水印经过窗口末尾时,将删除较晚的元素。然而,Flink允许为窗口操作符指定允许的最大延迟。允许延迟指定元素在被删除之前可以延迟多少时间,其默认值为0。在水印经过窗口末尾之后到达的元素,但是在水印经过窗口末尾之前到达的元素,加上允许的延迟,仍然被添加到窗口中。根据使用的触发器的不同,延迟但未删除的元素可能会导致窗口再次触发。EventTimeTrigger就是这种情况。
为了使这个工作,Flink保持窗口的状态,直到它们允许的延迟过期为止。一旦发生这种情况,Flink就会删除窗口并删除它的状态,正如窗口生命周期部分中描述的那样。
情况下,允许的延迟设置为 0。也就是说,到达水印后面的元素将被丢弃。
可以允许指定的延迟,如下所示:
DataStream input = ...;
input
.keyBy()
.window()
.allowedLateness(