Table of Contents
概述
Windows是处理无限流的核心。是一种切割无限数据为有限块进行处理的手段。Windows将流分成有限大小的“buckets”,我们可以基于每个块的数据进行计算。
Flink的窗口程序一般结构如下所示。第一个代码段指的是键控流窗口,第二个代码段指的是非键控流窗口。两者唯一的区别是keyBy(...)之后调用window(...)的称为成为键控流;DataStream直接调用windowAll(...)的称为非键控流。
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"
- trigger() —— 触发器,定义 window 什么时候关闭,触发计算并输出结果
- evitor() —— 移除器(剔除器),定义移除某些数据的逻辑
- allowedLateness() —— 允许处理迟到的数据
- sideOutputLateData() —— 将迟到的数据放入侧输出流
- getSideOutput() —— 获取侧输出流
方括号([…])中的命令是可选的。这表明Flink允许以多种不同方式自定义窗口逻辑,从而达到最适合的操作。
两者的区别:键控流按key将流在逻辑上分为多个流,不同的逻辑流之间都可以独立于其他逻辑流进行处理,即并行度有多个。而非键控流不会拆分为多个逻辑流,所以窗口逻辑将由单个任务执行,即并行度为1。
窗口的种类
滚动窗口(Tumbling Windows)
滚动窗口分配器将每个元素分配到一个指定窗口大小的窗口中,滚动窗口有一个固定的大小,并且不会出现重叠。例如:如果你指定了一个5分钟大小的滚动窗口,窗口的创建如下图所示:
代码实现:
val input: DataStream[T] = ...
// tumbling event-time windows
input
.keyBy(<key selector>)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.<windowed transformation>(<window function>)
// tumbling processing-time windows
input
.keyBy(<key selector>)
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.<windowed transformation>(<window function>)
// daily tumbling event-time windows offset by -8 hours.
input
.keyBy(<key selector>)
.window(TumblingEventTimeWindows.of(Time.days(1), Time.hours(-8)))
.<windowed transformation>(<window function>)
时间间隔可以通过Time.milliseconds(x)
,Time.seconds(x)
, Time.minutes(x)
等指定。
如最后一个示例所示,滚动窗口还有一个可选参数offset,该参数可用于更改窗口的对齐方式。例如,如果没有偏移,则每小时滚动窗口与自然时间对齐,即1:00:00.000 - 1:59:59.999,2:00:00.000 - 2:59:59.999依此类推。如果要更改,可以提供一个偏移量。如设置15分钟的偏移量,则窗口时间为 1:15:00.000 - 2:14:59.999,2:15:00.000 - 3:14:59.999。偏移量最常见的用法是修正UTC时区等。例如,在中国则可以指定偏移量Time.hours(-8)。
滑动窗口(Sliding Windows)
滑动窗口将每个元素分配给固定长度的窗口。与滚动窗口类似,窗口大小由窗口大小参数window size配置。另一个窗口滑动参数控制滑动窗口的滑动步长。因此,如果滑动步长小于窗口大小,则滑动窗口可能重叠。在这种情况下,同一个元素会被分配给多个窗口。
例如,有10分钟大小的窗口,滑动5分钟。这样,每5分钟会得到一个窗口,其中包含在下图所示的最后10分钟内到达的事件。
代码示例:
val input: DataStream[T] = ...
// sliding event-time windows
input
.keyBy(<key selector>)
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.<windowed transformation>(<window function>)
// sliding processing-time windows
input
.keyBy(<key selector>)
.window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.<windowed transformation>(<window function>)
// sliding processing-time windows offset by -8 hours
input
.keyBy(<key selector>)
.window(SlidingProcessingTimeWindows.of(Time.hours(12), Time.hours(1), Time.hours(-8)))
.<windowed transformation>(<window function>)
滑动窗口与滚动窗口一样可以设置对其方式。
会话窗口(Session Windows)
会话窗口按活动的会话对元素进行分组。与滚动窗口和滑动窗口相比,会话窗口不重叠,也没有固定的开始和结束时间。相反,当会话窗口在一段时间内没有接收元素时,它将关闭,即当一段时间没有活动时,就会生成一个窗口。session窗口通过session间隔来配置,这个session间隔定义了非活跃周期的长度,当这个间隔内没有收到新元素,那么当前的session将关闭并且后续的元素将被分配到新的session窗口中去。
代码示例:
val input: DataStream[T] = ...
// event-time session windows with static gap
input
.keyBy(<key selector>)
.window(EventTimeSessionWindows.withGap(Time.minutes(10)))
.<windowed transformation>(<window function>)
// event-time session windows with dynamic gap
input
.keyBy(<key selector>)
.window(EventTimeSessionWindows.withDynamicGap(new SessionWindowTimeGapExtractor[String] {
override def extract(element: String): Long = {
// determine and return session gap
}
}))
.<windowed transformation>(<window function>)
// processing-time session windows with static gap
input
.keyBy(<key selector>)
.window(ProcessingTimeSessionWindows.withGap(Time.minutes(10)))
.<windowed transformation>(<window function>)
// processing-time session windows with dynamic gap
input
.keyBy(<key selector>)
.window(DynamicProcessingTimeSessionWindows.withDynamicGap(new SessionWindowTimeGapExtractor[String] {
override def extract(element: String): Long = {
// determine and return session gap
}
}))
.<windowed transformation>(<window function>)
静态间隔可以通过使用中的一个来指定Time.milliseconds(x)
,Time.seconds(x)
, Time.minutes(x)
,等。
动态间隔是通过实现SessionWindowTimeGapExtractor
接口指定。
注意:由于会话窗口没有固定的开始和结束,因此它们的计算方式与滚动和滑动窗口不同。会话窗口为每个到达的记录创建一个新窗口,如果窗口之间的距离比定义的间隔更小,则将它们合并在一起。为了能够合并,会话窗口操作符需要一个合并触发器和一个合并窗口函数,例如ReduceFunction、AggregateFunction或ProcessWindowFunction(FoldFunction不能合并)
全局窗口(Global Windows)
将具有相同键的所有元素分配给同一个全局窗口。只有在指定了自定义触发器时,此窗口方案才有用。否则,将不会执行任何计算,因为全局窗口是没有终止的。
全局窗口的应用场景几乎是没有的。
代码示例:
val input: DataStream[T] = ...
input
.keyBy(<key selector>)
.window(GlobalWindows.create())
.<windowed transformation>(<window function>)
窗口函数
ReduceFunction
ReduceFunction指定如何组合输入中的两个元素以生成同一类型的输出元素。Flink使用ReduceFunction递增聚合窗口元素。
ReduceFunction的定义和使用方法如下:
val input: DataStream[(String, Long)] = ...
input
.keyBy(<key selector>)
.window(<window assigner>)
.reduce { (v1, v2) => (v1._1, v1._2 + v2._2) }
上面的例子聚合了窗口中所有元素元组的第二个字段。其中v1代表之前做过的所有操作得出的元素,v2代表当前元素。
AggregateFunction
AggregateFunction比ReduceFunction的应用范围更广(但是一般不会用到,reduce就足够了),它有三个参数:输入类型(IN)、累加器类型(ACC)和输出类型(OUT)。输入类型是输入流中元素的类型,AggregateFunction有一个方法将一个输入元素添加到累加器。该接口还具有创建初始累加器、将两个累加器合并为一个累加器以及从累加器中提取输出(OUT类型)的方法。我们将在下面的示例中看到这是如何工作的。
与ReduceFunction相同,Flink将在窗口的输入元素到达时递增地聚合它们。
AggregateFunction的定义和使用方法如下:
/**
* The accumulator is used to keep a running sum and a count. The [getResult] method
* computes the average.
*/
class AverageAggregate extends AggregateFunction[(String, Long), (Long, Long), Double] {
override def createAccumulator() = (0L, 0L)
override def add(value: (String, Long), accumulator: (Long, Long)) =
(accumulator._1 + value._2, accumulator._2 + 1L)
override def getResult(accumulator: (Long, Long)) = accumulator._1 / accumulator._2
override def merge(a: (Long, Long), b: (Long, Long)) =
(a._1 + b._1, a._2 + b._2)
}
val input: DataStream[(String, Long)] = ...
input
.keyBy(<key selector>)
.window(<window assigner>)
.aggregate(new AverageAggregate)
上述代码计算窗口中元素第二个字段的平均值。
FoldFunction
FoldFunction指定如何将窗口的输入元素与输出类型的元素组合。对添加到窗口的每个元素和当前输出值增量调用FoldFunction。与ReduceFunction类似。区别在于FoldFunction需要预设一个初始值,reduce不用。
val input: DataStream[(String, Long)] = ...
input
.keyBy(<key selector>)
.window(<window assigner>)
.fold("") { (acc, v) => acc + v._2 }
上面的示例将所有输入的值追加到最初为空的String上。
注意:fold()不能与会话窗口或其他可合并窗口一起使用。
ProcessWindowFunction
ProcessWindowFunction是一个底层函数,获取一个包含窗口所有元素的Iterable,以及一个可以访问时间和状态信息的上下文对象,这使它能够比其他窗口函数更加灵活。但这是以性能和资源消耗为代价的,因为元素不能增量聚合,而是需要在内部缓冲,直到窗口被认为可以处理为止。
ProcessWindowFunction的结构如下:
abstract class ProcessWindowFunction[IN, OUT, KEY, W <: Window] extends Function {
/**
* Evaluates the window and outputs none or several elements.
*
* @param key The key for which this window is evaluated.
* @param context The context in which the window is being evaluated.
* @param elements The elements in the window being evaluated.
* @param out A collector for emitting elements.
* @throws Exception The function may throw exceptions to fail the program and trigger recovery.
*/
def process(
key: KEY,
context: Context,
elements: Iterable[IN],
out: Collector[OUT])
/**
* The context holding window metadata
*/
abstract class Context {
/**
* Returns the window that is being evaluated.
*/
def window: W
/**
* Returns the current processing time.
*/
def currentProcessingTime: Long
/**
* Returns the current event-time watermark.
*/
def currentWatermark: Long
/**
* State accessor for per-key and per-window state.
*/
def windowState: KeyedStateStore
/**
* State accessor for per-key global state.
*/
def globalState: KeyedStateStore
}
}
注意:如果key是元组的key,或者是一个字符串的某个字段,那么这个key的类型总是二元组,需要手动设置其元组类型,将其转换为大小正确的元组。
ProcessWindowFunction的定义和使用方法如下:
val input: DataStream[(String, Long)] = ...
input
.keyBy(_._1)
.timeWindow(Time.minutes(5))
.process(new MyProcessWindowFunction())
/* ... */
class MyProcessWindowFunction extends ProcessWindowFunction[(String, Long), String, String, TimeWindow] {
def process(key: String, context: Context, input: Iterable[(String, Long)], out: Collector[String]) = {
var count = 0L
for (in <- input) {
count = count + 1
}
out.collect(s"Window ${context.window} count: $count")
}
}
该示例展示了使用一个processWindow函数,该函数统计了窗口中元素的个数。并输出有关窗口的信息。
注意,对简单聚合(如count)使用ProcessWindowFunction是非常低效的。下面介绍如何将ReduceFunction或AggregateFunction与ProcessWindowFunction组合以获取ProcessWindowFunction的增量聚合和添加的信息。
具有增量聚合功能的底层窗口函数
ProcessWindowFunction可以与ReduceFunction、AggregateFunction或FoldFunction组合,在元素到达窗口时增量聚合元素。当窗口关闭时,ProcessWindowFunction将返回聚合结果。这使得它可以在访问窗口的上下文信息的同时进行增量计算。
提示:一般进行增量聚合计算的情况,都是直接使用增量聚合函数,很少会和ProcessWindowFunction合起来用。
下面的示例演示如何将增量ReduceFunction与ProcessWindowFunction组合,以返回窗口中的最小事件以及窗口的开始时间。
val input: DataStream[SensorReading] = ...
input
.keyBy(<key selector>)
.timeWindow(<duration>)
.reduce(
(r1: SensorReading, r2: SensorReading) => { if (r1.value > r2.value) r2 else r1 },
( key: String,
context: ProcessWindowFunction[_, _, _, TimeWindow]#Context,
minReadings: Iterable[SensorReading],
out: Collector[(Long, SensorReading)] ) =>
{
val min = minReadings.iterator.next()
out.collect((context.window.getStart, min))
}
)
下面的示例演示了如何将AggregateFunction与ProcessWindowFunction组合起来计算平均值,并同时输出键和窗口以及平均值。
val input: DataStream[(String, Long)] = ...
input
.keyBy(<key selector>)
.timeWindow(<duration>)
.aggregate(new AverageAggregate(), new MyProcessWindowFunction())
// Function definitions
/**
* The accumulator is used to keep a running sum and a count. The [getResult] method
* computes the average.
*/
class AverageAggregate extends AggregateFunction[(String, Long), (Long, Long), Double] {
override def createAccumulator() = (0L, 0L)
override def add(value: (String, Long), accumulator: (Long, Long)) =
(accumulator._1 + value._2, accumulator._2 + 1L)
override def getResult(accumulator: (Long, Long)) = accumulator._1 / accumulator._2
override def merge(a: (Long, Long), b: (Long, Long)) =
(a._1 + b._1, a._2 + b._2)
}
class MyProcessWindowFunction extends ProcessWindowFunction[Double, (String, Double), String, TimeWindow] {
def process(key: String, context: Context, averages: Iterable[Double], out: Collector[(String, Double)]) = {
val average = averages.iterator.next()
out.collect((key, average))
}
}
在ProcessWindowFunction中使用窗口状态
除了访问键控状态(任何富函数都可以)之外,ProcessWindowFunction还可以使用键控状态,与富函数不同的是,ProcessWindowFunction的状态称为Per-window State,即其作用域仅为函数当前正在处理的窗口内部。
Per-window State在ProcessWindowFunction中分为两类:
- globeState:全局状态,窗口中的keyed state数据不限定在某个窗口中。
- windowState:窗口状态,窗口中的keyed state数据限定在固定的窗口中。
如果同一窗口会多次触发(如event-time触发器加上允许最大延迟时间,则有肯触发多次计算),则此功能很有用。例如可以存储每次窗口触发的次数以及最新一次触发的信息,为下一次窗口触发提供逻辑处理信息。使用Per-window State数据时要及时清理状态数据,可以覆写,调用ProcessWindowFunction的clear()完成状态数据的清理。
触发器
触发器决定窗口何时将数据交给窗口函数处理。每个窗口都有一个默认触发器。如果默认触发器不符合业务需要,也可以使用自定义的触发器。
触发器接口有五种方法:
onElement()
:每个元素被添加到窗口时,都会触发此方法。onEventTime():
当注册一个event-time 定时器时会被调用。onProcessingTime():
当注册一个processing-time 定时器时被调用。onMerge():
与状态性触发器相关,两个窗口合并时,合并两个触发器的状态,如使用session window时,窗口会进行合并,此时调用该方法。clear()
:窗口关闭时触发,用于做一些清理工作。
关于上述方法,需要注意两件事:
1) 前三个函数通过返回TriggerResult来决定如何处理它们的调用事件。操作可以是以下操作之一:
- CONTINUE:什么都不做
- FIRE:触发计算
- PURGE:清除窗口中的元素
- FIRE_AND_PURGE:触发计算,然后清除窗口中的元素。
2) 这些方法中的任何一个都可以用于为将来的操作注册processing-time 定时器或event-time定时器。
触发与清除
一旦一个触发器认为一个窗口已经可以进行处理,它将触发并返回FIRE或者FIRE_AND_PURGE。这意味着当前窗口马上就要触发计算,并将元素发送给计算方法。如一个带有ProcessWindowFunction的窗口,当触发器触发fire后,所有元素都被传递给ProcessWindowFunction(如果有剔除器,则先经过剔除器)。
如果窗口的计算函数时ReduceFunction、AggregateFunction或FoldFunction,则只发出它们聚合的结果,因为在窗口内部已经由这些预聚合方法进行ji's。
触发器触发的方式有两种:FIRE或者FIRE_AND_PURGE。如果是FIRE的话,将保留window中的内容,FIRE_AND_PURGE则会清除window的内容。默认情况下,触发器使用的是FIRE。
注意:清除操作仅清除window的内容,但会保留窗口的元数据信息和触发器状态。
默认触发器和自定义触发器
WindowAssigner的默认触发器适用于各种用例。例如,所有事件时间窗口分配程序都有一个event time trigger作为默认触发器。一旦watermark 大于窗口的endtime,那么这个触发器就会触发。
注意:全局窗口的默认触发器是一个永不触发的触发器。因此,使用GlobalWindow时,如果需要用到触发器,就必须自定义触发器。
注意:通过使用trigger()指定触发器后,将覆盖WindowAssigner的默认触发器。例如,如果为TumblingEventTimeWindows指定CountTrigger,则不再根据时间进度而仅按count触发窗口,两者不同时生效。所以,如果想同时基于时间和计数触发窗口,就必须编写自定义触发器。
flink的默认触发器有四种:
EventTimeTrigger:时间时间触发器,根据watermark触发。
ProcessingTimeTrigger:处理时间触发器,根据元素在被处理的那台机器的系统时间触发。
CountTrigger:计数触发器,根据元素的个数触发。
PurgingTrigger :清除触发器,将另一个触发器作为参数,将其转换为清除触发器,即在原有触发器的基础上,添加清除窗口内容功能。
如果需要自定义触发器的话,则需要实现Trigger抽象类 。但是该API仍在开发中,在未来的flink版本中可能会有变化。
剔除器
Flink的窗口还可以使用剔除器,用于从窗口中移除元素。使用exictor(…)方法调用(如本文开头所示)。剔除器作用在触发器触发之后,且窗口函数执行之前或之后。逐出器接口有两个方法:
/**
* Optionally evicts elements. Called before windowing function.
*
* @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<TimestampedValue<T>> elements, int size, W window, EvictorContext evictorContext);
/**
* Optionally evicts elements. Called after windowing function.
*
* @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<TimestampedValue<T>> elements, int size, W window, EvictorContext evictorContext);
evictBefore():用于在窗口函数执行之前剔除元素。
evictAfter():用于在窗口函数执行之后剔除元素。
flink有三个预设的Evictors:
CountEvictor:当窗口内元素到达自定义的个数后,则不再向窗口中添加元素。
DeltaEvictor:需要传入一个DeltaFunction和一个threshold,使用DeltaFunction计算窗口缓冲区中最后一个元素与缓冲区中其余所有元素的delta值,并移除delta值大于或等于threshold的元素。
TimeEvictor:使用窗口的时间间隔作为参数,在窗口元素中查找最大时间戳max-ts,并删除时间戳小于max-ts减去interval的所有元素。即剔除过期数据。
默认情况下,预设的剔除器都在窗口函数之前执行。
注意:如果指定了剔除器,则预聚合不生效,因为在进行计算之前,每一条元素都会经过剔除器才会进入窗口。
注意:Flink不能保证窗口中元素的顺序。这意味着尽管剔除器即使从窗口的开头移除元素,但这些元素不一定是最先到达或最后到达的元素。
如何处理迟到的事件元素?
使用事件时间(event-time)窗口时,可能会发生元素到达晚的情况,即本应该在上一个窗口处理的元素,由于延迟到达flink,而watermark已经超过窗口的endtim而启动计算,导致这个元素没有在上一个窗口中处理。
关于watermark,在我的另一篇文章中有介绍:https://blog.csdn.net/x950913/article/details/106246807
默认情况下,当watermark超过窗口的endtime时,将删除延迟到达的元素。但是,Flink可以为窗口指定一个最大延迟时间。Allowed lateness指定watermark超过endtime多少时间后,再收到事件时间在该窗口内的元素时,会再次触发窗口的计算,其默认值为0。watermark超过endtime后,会触发一次计算,在允许延迟的时间范围内到达的元素,仍然会添加到窗口中,且再次触发计算。所以如果使用的是EventTimeTrigger触发器,延迟但未丢弃的元素可能会导致窗口再次触发。
默认情况下,允许的延迟设置为0。也就是说,在endtime后到达的元素将被删除。
指定允许延迟时间,如下所示:
val input: DataStream[T] = ...
input
.keyBy(<key selector>)
.window(<window assigner>)
.allowedLateness(<time>)
.<windowed transformation>(<window function>)
另外,当使用GlobalWindows窗口时,任何数据都不会被认为是延迟的,因为全局窗口的结束时间戳是Long的最大值.
将迟到元素从侧输出流输出
可以将延迟到达的元素从侧输出流中输出。
首先需要创建一个OutputTag用于接收延迟数据。然后,指定将窗口中的延迟数据发送到OutputTag中:
val lateOutputTag = OutputTag[T]("late-data")
val input: DataStream[T] = ...
val result = input
.keyBy(<key selector>)
.window(<window assigner>)
.allowedLateness(<time>)
.sideOutputLateData(lateOutputTag)
.<windowed transformation>(<window function>)
val lateStream = result.getSideOutput(lateOutputTag)
迟到元素处理注意事项
当指定允许的延迟大于0时,在watermark超过窗口的endtime后,仍会保留窗口及其内容。在这些情况下,当一个延迟但未被丢弃的元素到达时,它可能会再次触发这个窗口的触发器。此时这些触发器的Fire被称为late firings,因为它们是由迟到元素触发的,而主Fire是窗口的第一次Fire。在会话窗口(session windows)的情况下,延迟触发可能进一步导致窗口合并,因为它们可能“桥接”两个预先存在的未合并窗口。
注意:延迟到达的元素所触发的计算应被视为对先前计算的结果的更新,即watermark到达endtime后窗口会进行一次计算,之后延迟到达的元素会触发新的计算来更新计算结果,所以数据流将包含同一计算的多个结果。根据应用场景不同,需要考虑是否需要消除重复数据。
窗口计算后还可以做什么?
使用窗口计算后得出的结果仍是一个DataStream,该DataStream中的元素不会保留有关窗口的信息。所以,如果需要保存窗口的元数据信息,就必须编写代码,在ProcessWindowFunction中将窗口的元数据信息与元素进行整合。输出元素中的时间戳是唯一与窗口相关的信息,可以将其设置为窗口的最大允许延迟时间,即endtime-1,因为endtime之前的元素才属于这个窗口,大于等于endtime的则属于下一个窗口(event-time windows 和processing-time windows都是这样的)。
在经过窗口函数处理后的元素总会包含一个时间戳,可以是event-time时间戳也可以是 processing-time时间戳。
对于 processing-time 窗口可能没什么作用,但对于event-time窗口,配合watermark机制,可以将event-time属于同一窗口的元素放在另一个窗口中处理,即连续的窗口操作,在下面有介绍。
watermarks对窗口的作用
在此稍微提及一点关于watermark和窗口的作用。
触发器基于watermark有两种处理方式:
- 当watermark大于等于窗口的endtime时,触发窗口计算。
- 当watermark小于于窗口的endtime时,则将watermark转发给下游操作(维护watermark为窗口中所有元素中的最大时间戳)
连续的窗口操作
如上所述,经过窗口函数计算后的结果仍然带有时间戳,且与watermark搭配使用,就可以连续使用多个窗口操作。如在上游窗口计算之后,仍可以对得出的结果以不同的key、不同的function进行计算。如:
DataStream<Integer> input = ...;
DataStream<Integer> resultsPerKey = input
.keyBy(<key selector>)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.reduce(new Summer());
DataStream<Integer> globalResults = resultsPerKey
.windowAll(TumblingEventTimeWindows.of(Time.seconds(5)))
.process(new TopKWindowFunction());
上述的例子中,事件时间戳在0~5秒的元素(包含0秒,不含5秒),经过第一个窗口计算后,产生的结果传入第二个窗口时也属于0~5秒的窗口,即同窗口的元素在下游窗口中仍属于同一窗口。如第一个窗口计算0~5秒内每个key的和,再在第二个窗口中取出0~5秒内key的和的TopK。
如何估计窗口存储大小?
定义窗口的时间范围时,可以定义一个很长的时间,甚至是几天、几周或几月。因此窗口可以累积相当多的数据。在估计窗口计算的存储量时,需要记住以下几个规则:
- Flink会为每个窗口的所有元素都创建一个副本,因此,滚动窗口对每个元素仅保留一个副本,因为一个元素恰好仅属于一个窗口,除非它被搁置(dropped late)。相反,滑动窗口会为元素保存多个副本,因为一个元素可能属于多个窗口,每个窗口都会保存一次。所以,如果使用滑动窗口,那么应该尽量避免窗口大小大,而滑动步长小的情况,如窗口大小为1天,滑动步长为1秒。
- ReduceFunction,AggregateFunction和FoldFunction可以大大减少存储需求,因为他们会尽量早地对元素进行聚合,且每个窗口仅存储一个值,而不是所有元素。相反,ProcessWindowFunction需要存储每个元素。
- 使用剔除器Evictor会阻止所有预聚合操作,因为在算之前,必须将窗口的所有元素传递到剔除器。