本文翻译自flink官网:https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/stream/operators/windows.html
Windows是处理无限流的核心。Windows将流分成有限大小的“存储桶”,我们可以在其上应用计算。本文档重点介绍如何在Flink中执行窗口,以及程序员如何从其提供的功能中获得最大收益。
窗口式Flink程序的一般结构如下所示。第一个段指的是键控流,第二个段指的是非键控流。正如我们所看到的,唯一的区别是keyBy(...) 的键控流
调用 window(...)
,而非键控流调用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"
在上面,方括号([…])中的命令是可选的。这表明Flink允许您以多种不同方式自定义窗口逻辑,从而使其最适合您的需求。
窗口生命周期
简单来说,一旦属于该窗口的第一个元素到达,就会创建一个窗口,并且当时间(事件或处理时间)超过其结束时间戳加上用户指定的时间(请参阅“ 允许延迟”)后,该窗口将被完全删除 )。Flink保证只删除基于时间的窗口,而不保证其他类型,例如:全局窗口(请参阅窗口分配器)。例如,采用基于事件时间的窗口化策略,该策略每5分钟创建一次不重叠(或翻滚)的窗口,并允许延迟1分钟,因此Flink将在 12:00 到
12:05
之间的第一个元素落入此间隔时,创建一个新窗口,当 watermark 达到 时间戳 12:06
时,将删除这个window。
此外,每个窗口将具有 Trigger
(参见触发器)和一个函数(ProcessWindowFunction
,ReduceFunction
, AggregateFunction
或FoldFunction
)(见窗口功能)连接到它。该函数将包含应用于窗口内容的计算,而 Trigger
指定了在什么条件下将 应用窗口函数(触发计算)。触发策略可能类似于“当窗口中的元素数大于4时”或“当 watermark 达到窗口末尾时”。触发器还可以决定在创建和删除窗口之间的任何时间清除窗口的内容。在这种情况下,清除仅是指窗口中的元素,而不是窗口元数据。这意味着仍可以将新数据添加到该窗口。
除上述内容外,您还可以指定一个Evictor
(请参阅Evictors),它将在触发器后以及应用此功能之前(或之后)从窗口中删除元素。
在下文中,我们将对上述每个组件进行更详细的介绍。我们先从上面的代码片段中的必需部分开始(请参见Keyed vs Non- Keyed Windows,Window Assigner和 Window Function),然后再转到可选部分。
键控与非键控Windows
要指定的第一件事是您的流是否应该设置key 。这必须在定义窗口之前完成。使用keyBy(...)
会将您的无限流分割成逻辑键流。如果keyBy(...)
未调用,则不会为您的流设置key。
在使用键控流的情况下,传入事件的任何属性都可以用作键(此处有更多详细信息)。拥有键控流将使您的窗口化计算可以由多个任务并行执行,因为每个逻辑键控流都可以独立于其余逻辑流进行处理。引用同一键的所有元素将被发送到同一并行任务。
对于非键控流,您的原始流将不会拆分为多个逻辑流,并且所有窗口逻辑将由单个任务执行,即并行度为1。
窗口分配器(Window Assigners)
指定流是否为键后,下一步是定义一个窗口分配器。窗口分配器定义了如何将元素分配给窗口。这是通过 WindowAssigner
在 window(...)
(针对键控流)或windowAll()
(针对非键控流)调用中指定您选择的选项来完成的。
WindowAssigner
负责将每个传入元素分配给一个或多个窗口。Flink 带有针对最常见用例的预定义窗口分配器,即滚动窗口, 滑动窗口,会话窗口和全局窗口。您还可以通过扩展WindowAssigner
类来实现自定义窗口分配器。所有内置窗口分配器(全局窗口除外)均基于时间将元素分配给窗口,时间可以是处理时间,也可以是事件时间。请查看事件时间部分,以了解处理时间和事件时间之间的差异以及时间戳和水印的生成方式。
基于时间的窗口具有开始时间戳(包括端点)和结束时间戳(包括端点),它们共同描述了窗口的大小。在代码中,Flink 在使用 TimeWindow
基于时间的窗口时使用,该方法具有查询开始和结束时间戳记的方法 maxTimestamp()
,还具有返回给定窗口允许的最大时间戳的附加方法。
在下面,我们展示Flink的预定义窗口分配器如何工作以及如何在 DataStream 程序中使用它们。下图显示了每个分配器的工作情况。紫色圆圈表示流的元素,这些元素由某个键(在这种情况下为用户1,用户2和用户3)划分。x轴显示时间进度。
翻滚窗户
翻滚窗口分配器的每个元素分配给指定的窗口的窗口大小。翻滚窗口具有固定的大小,并且不重叠。例如,如果您指定大小为5分钟的翻滚窗口,当前窗口将被evaluated (不知道怎么翻译,大概是被创建,并且分配元素),并且每五分钟将启动一个新窗口,如下图所示。
以下代码段显示了如何使用滚动窗口。
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-0 时区,例如,在中国,您必须指定的偏移量Time.hours(-8)
。(这里Flink 在某些版本有bug,JIRA : FLINK-11326)
滑动窗
滑动窗口分配器分配以固定长度的窗口。类似于滚动窗口分配器,窗口的大小由窗口大小参数配置。附加的窗口滑动参数控制滑动窗口启动的频率。因此,如果滑动参数小于窗口大小,则滑动窗口可能会重叠。在这种情况下,元素被分配给多个窗口。
例如,您可以将大小为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>)
时间间隔可以通过使用一个指定Time.milliseconds(x)
,Time.seconds(x)
, Time.minutes(x)
,等等。
如最后一个示例所示,滑动窗口分配器还采用一个可选offset
参数,该参数可用于更改窗口的对齐方式。例如,在没有偏移的情况下,每小时滑动30分钟的窗口将与整点对齐,即您将获得诸如 1:00:00.000 - 1:59:59.999
,1:30:00.000 - 2:29:59.999
依此类推的窗口。如果更改,可以提供一个偏移量。如15分钟的偏移量,你会拿到 1:15:00.000 - 2:14:59.999
,1:45:00.000 - 2:44:59.999
等的窗口,一个重要的用例的偏移是窗口调整到 UTC-0 时区,例如,在中国,您必须指定的偏移量Time.hours(-8)
。
会话窗口
在会话窗口中按活动会话分配器中的元素。与滚动窗口和滑动窗口相比,会话窗口不重叠且没有固定的开始和结束时间。相反,当会话窗口在一定时间段内未收到元素时(即发生不活动间隙时),它将关闭。会话窗口分配器可与静态配置会话间隙或与 会话间隙提取功能,其限定不活动周期有多长。当此时间段到期时,当前会话将关闭,随后的元素将分配给新的会话窗口。
以下代码段显示了如何使用会话窗口。
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
接口指定的。
注意:由于会话窗口没有固定的开始和结束,因此对它们的evaluated (决定窗口元素的归属)不同于滚动窗口和滑动窗口。在内部,会话窗口运算符会为每个到达的记录创建一个新窗口,如果窗口彼此之间的距离比已定义的间隔小,则将它们合并在一起。为了可合并的,会话窗口操作者需要一个合并触发器和一个合并 的窗函数,如ReduceFunction
,AggregateFunction
,或ProcessWindowFunction
(FoldFunction
不能合并。)
全局视窗
一个全局性的窗口分配器分配使用相同的密钥的所有元素到相同的单个全局窗口。仅当您还指定自定义触发器时,此窗口方案才有用。否则,将不会执行任何计算,因为全局窗口没有可以处理聚合元素的自然结束。
以下代码段显示了如何使用全局窗口。
val input: DataStream[T] = ... input .keyBy(<key selector>) .window(GlobalWindows.create()) .<windowed transformation>(<window function>)
窗口函数
定义窗口分配器后,我们需要指定要在每个窗口上执行的计算。这是窗口函数的职责,一旦系统确定某个窗口已准备好进行处理,就可以使用该窗口的函数来处理每个(可能是键控)窗口的元素(请参阅触发器,了解Flink如何确定什么时候窗口准备好)。
窗口函数可以是 ReduceFunction
,AggregateFunction
,FoldFunction
或 ProcessWindowFunction
。前两个可以更有效地执行(请参见“ 状态大小”部分),因为 Flink 可以在每个窗口元素到达时逐步地聚合它们。 ProcessWindowFunction
获取窗口中包含的所有元素的 Iterable ,以及有关元素所属窗口的其他元信息。
带 ProcessWindowFunction 的窗口转换
不能像其他情况一样有效地执行,因为Flink必须在调用函数之前在内部缓冲窗口的所有元素。这可以通过组合 ProcessWindowFunction
与 ReduceFunction
,AggregateFunction
或FoldFunction 来减轻(窗口状态大小),(
获得窗口元素的增量聚合,附加ReduceFunction
,AggregateFunction
或FoldFunction
)ProcessWindowFunction
接收 。我们将看看这些变体的每个示例。
Reduce功能
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) }
上面的示例汇总了窗口中所有元素的元组的第二个字段。
聚合函数
AggregateFunction
是一个一般化版本 ReduceFunction
,其具有三种类型:输入类型(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)
上面的示例计算窗口中元素的第二个字段的平均值。
Fold功能
FoldFunction指定如何将窗口的输入元素与输出类型的元素组合。 对于添加到窗口的每个元素和当前输出值,将递增调用FoldFunction。 第一个元素将与预定义的输出类型的初始值组合。
FoldFunction
可以定义像这样使用:
val input: DataStream[(String, Long)] = ... input .keyBy(<key selector>) .window(<window assigner>) .fold("") { (acc, v) => acc + v._2 }
上面的示例将所有输入Long
值附加到最初为空字符串中。
注意: fold()
不能与会话窗口或其他可合并窗口一起使用。
ProcessWindowFunction
ProcessWindowFunction 获取一个 Iterable,该Iterable包含窗口的所有元素,以及一个 Context 对象,该对象可以访问时间和状态信息,从而使其比其他窗口函数更具灵活性。这是以性能和资源消耗为代价的,因为无法增量聚合元素,而是需要在内部对其进行缓冲,直到将窗口视为已准备好进行处理为止。
ProcessWindowFunction
look 的定义如下:
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
参数是通过提取被指定的keyBy()
调用KeySelector 指定的
。如果是元组索引键或字符串字段引用,则始终使用此键类型,Tuple
并且必须手动将其强制转换为正确大小的元组以提取键字段。
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") } }
该示例显示了一个 ProcessWindowFunction
计算窗口中元素的数量。另外,窗口函数将有关窗口的信息添加到输出中。
注意:请注意 ProcessWindowFunction
用于简单的聚合(如count)效率很低。下一部分说明如何将 ReduceFunction
或 AggregateFunction
与 ProcessWindowFunction 结合使用,以同时获得增量聚合的 ProcessWindowFunction
。
具有增量聚合的ProcessWindowFunction
ProcessWindowFunction
可与 ReduceFunction、
AggregateFunction 或 FoldFunction组合
,为它们在窗口到达元素做增量聚会,当窗口关闭时ProcessWindowFunction
将提供汇总结果。这使得它可以递增地计算窗口,同时可以访问ProcessWindowFunction的其他窗口元信息。
注意:您也可以使用旧版 WindowFunction
而不是 ProcessWindowFunction
用于增量窗口聚合。
具有ReduceFunction的增量窗口聚合
以下示例显示了如何将增量 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的增量窗口聚合
以下示例显示了如何将增量 AggregateFunction
与 ProcessWindowFunction 组合
以计算平均值,并与平均值一起输出 key 和窗口。
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)) } }
具有FoldFunction的增量窗口聚合
以下示例显示了如何将增量 FoldFunction 与
ProcessWindowFunction 组合
以提取窗口中的事件数,并还返回窗口的 key 和结束时间。
val input: DataStream[SensorReading] = ... input .keyBy(<key selector>) .timeWindow(<duration>) .fold ( ("", 0L, 0), (acc: (String, Long, Int), r: SensorReading) => { ("", 0L, acc._3 + 1) }, ( key: String, window: TimeWindow, counts: Iterable[(String, Long, Int)], out: Collector[(String, Long, Int)] ) => { val count = counts.iterator.next() out.collect((key, window.getEnd, count._3)) } )
在 ProcessWindowFunction 中使用窗口状态
除了访问键控状态(如任何rich function 所允许的那样),ProcessWindowFunction
还可以使用键控状态,该键控状态的范围仅限于该函数当前正在处理的窗口。在这种情况下,重要的是要了解每个窗口状态所指的窗口是什么。涉及不同的“窗口”:
- 指定窗口操作时定义的窗口:这可能是1小时的翻滚窗口或2小时的滑动窗口滑动1小时。
- 给定键的已定义窗口的实际实例:对于用户ID xyz,这可能是从12:00到13:00的时间窗口。这是基于窗口定义的,并且根据作业当前正在处理的键的数量以及事件属于哪个时隙,会有很多窗口。
每个窗口的状态与这两个中的后者相关。这意味着,如果我们处理1000个不同键的事件,并且当前所有事件的事件都属于[12:00,13:00)时间窗口,那么将有1000个窗口实例,每个实例具有各自的每个窗口状态。
调用在Context
对象上有两种方法process()
可以访问两种状态:
globalState()
,它允许访问不在窗口范围内的键状态windowState()
,它允许访问也作用于窗口的键控状态
如果您预期同一窗口会多次触发,则此功能很有用,例如,对于迟到的数据有较早的触发,或者您有进行推测性较早触发的自定义触发器时,可能会发生这种情况。在这种情况下,您将存储有关先前触发或每个窗口状态中触发次数的信息。
使用窗口状态时,清除窗口时也要清除该状态,这一点很重要。这应该在clear()
方法中发生。
WindowFunction(旧版)
在某些 ProcessWindowFunction
可以使用的地方,您也可以使用WindowFunction
。这是旧版本 ProcessWindowFunction
,提供较少的上下文信息,并且没有某些高级功能,例如每个窗口的键控状态。该接口将在某个时候被弃用。
WindowFunction 的签名如下:
trait WindowFunction[IN, OUT, KEY, W <: Window] extends Function with Serializable { /** * Evaluates the window and outputs none or several elements. * * @param key The key for which this window is evaluated. * @param window The window that is being evaluated. * @param input 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 apply(key: KEY, window: W, input: Iterable[IN], out: Collector[OUT]) }
可以这样使用:
val input: DataStream[(String, Long)] = ... input .keyBy(<key selector>) .window(<window assigner>) .apply(new MyWindowFunction())
触发器
Trigger
确定窗口(由窗口分配器形成)何时调用窗口函数处理数据。每个 WindowAssigner
都有一个默认值 Trigger
。如果默认触发器不符合您的需求,则可以使用指定自定义触发器 trigger(...)
。
触发器接口具有五种方法,它们允许 Trigger
对不同事件做出反应:
onElement()
对于添加到窗口中的每个元素,都会调用该方法。onEventTime()
当注册的事件时间计时器触发时,将调用该方法。onProcessingTime()
当注册的处理时间计时器触发时,将调用该方法。- 该
onMerge()
方法与有状态触发器相关,并且在两个触发器的相应窗口合并时(例如在使用会话窗口时)合并两个触发器的状态。 - 最后,在
clear()
方法在删除相应的窗口后执行所需的任何操作。
关于上述方法,需要注意两点:
1)前三个通过返回来决定如何对调用事件采取行动 TriggerResult
。该动作可以是以下之一:
CONTINUE
: 没做什么FIRE
:触发计算PURGE
:清除窗口中的元素FIRE_AND_PURGE
:触发计算并随后清除窗口中的元素
2)这些方法中的任何一种都可以用于注册处理或事件时间计时器以用于将来的操作。
Fire与Purge
一旦触发器确定窗口已准备好进行处理,它将触发,即返回 FIRE
或 FIRE_AND_PURGE
。这是窗口 operator 发出当前窗口结果的信号。给定一个包含 ProcessWindowFunction
所有元素的窗口(可能在将它们传递到驱逐器之后)传递给 ProcessWindowFunction
。带有 ReduceFunction
,AggregateFunction
或 Windows ,FoldFunction
仅发出其预计算的汇总结果。
触发器触发时,可以是 FIRE
或 FIRE_AND_PURGE
。在FIRE 的同时
保留窗口内容,FIRE_AND_PURGE
删除其内容。默认情况下,预先实现的触发器仅在 FIRE,
不清除窗口状态的情况下触发计算。
注意 PURGE 将仅删除窗口的内容,并将保留有关该窗口的任何潜在元信息和任何触发状态。
WindowAssigners的默认触发器
默认 Trigger
的 WindowAssigner
是适用于许多使用情况。例如,所有事件时间窗口分配器都有 EventTimeTrigger
默认触发器。一旦水印通过窗口的末端,该触发器便会触发。
注意:默认触发器 GlobalWindow
是 NeverTrigger
永不触发的。因此,使用 GlobalWindow 时,您始终必须定义一个自定义触发器 。
注意:通过使用指定触发器 trigger()
您将覆盖的 WindowAssigner 默认触发器 。例如:如果您指定为 CountTrigger
,则 TumblingEventTimeWindows
您将不再基于时间进度(意味着窗口结束的时候是不会触发的,需要自己在触发器里面触发),而是仅通过计数获得窗口触发。现在,如果要基于时间和计数做出反应,则必须编写自己的自定义触发器。
内置和自定义触发器
Flink带有一些内置触发器。
- (已经提到)
EventTimeTrigger
根据事件时间(由水印测量)的进度触发。 - 在
ProcessingTimeTrigger fire
基于处理时间。 CountTrigger
一旦窗口中的元素数量超过给定的限制,就会触发。- PurgingTrigger 将另一个触发器作为参数,并将其转换为一个清除触发器。
如果需要实现自定义触发器,则应实现抽象类的 Trigger。请注意,API仍在不断发展,并可能在Flink的未来版本中更改。
驱逐器
Flink 的窗口模型允许 除了 WindowAssigner
和 Trigger
之外还指定一个可选的 Evictor
。可以使用 evictor(...)
方法完成(如本文档开头所示)驱逐器可以从窗口中删除元素,在触发器触发和 被施加的窗口函数执行之前(或之后)。为此,该Evictor
接口有两种方法:
/** * 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附带了三个预先实施的驱逐程序。这些是:
CountEvictor
:从窗口中保留用户指定数量的元素,并从窗口缓冲区的开头丢弃其余的元素。DeltaEvictor
:使用DeltaFunction
和threshold
,计算窗口缓冲区中最后一个元素与其余每个元素之间的差值,并删除差值大于或等于阈值的元素。TimeEvictor
:以interval
毫秒为单位作为参数,对于给定的窗口,它将max_ts
在其元素中找到最大时间戳,并删除所有时间戳小于的元素max_ts - interval
。
默认:默认情况下,所有预先实现的驱逐程序均在窗口函数之前应用其逻辑。
注意:指定 evictor 可防止任何预聚集,因为在应用计算之前必须将窗口的所有元素传递给 evictor 。
注意:Flink 不保证窗口内元素的顺序。这意味着,尽管 evictor 可以从窗口的开头删除元素,但不一定是最先到达的元素。
允许延迟
在使用事件时间窗口时,可能会发生元素迟到的情况,即 Flink 用于跟踪事件时间进度的水印已经超过了元素所属窗口的结束时间戳。请参阅 事件时间,尤其是迟到元素,以更全面地讨论 Flink 如何处理事件时间。
默认情况下,当水印超过窗口末端时,将删除晚期元素。但是,Flink允许为窗口运算符指定最大允许延迟。允许延迟指定元素删除之前可以延迟的时间,其默认值为0。在水印通过窗口末端之后但在通过窗口末端之前到达的元素加上允许延迟,仍添加到窗口中。根据使用的触发器,延迟但未掉落的元素可能会导致窗口再次触发。的情况就是这样EventTimeTrigger
。
为了使此工作正常进行,Flink 保持窗口的状态,直到允许的延迟过期为止。一旦发生这种情况,Flink 将删除该窗口并删除其状态,如“ 窗口生命周期”部分中所述。
默认:默认情况下,允许的延迟设置为 0
。也就是说,到达水印后的元素将被丢弃。
您可以这样指定允许的延迟:
val input: DataStream[T] = ... input .keyBy(<key selector>) .window(<window assigner>) .allowedLateness(<time>) .<windowed transformation>(<window function>)
注意:使用 GlobalWindows
窗口分配器时,永远不会考虑任何数据迟到,因为全局窗口的结束时间戳为Long.MAX_VALUE
。
获取迟到数据作为侧边输出
使用 Flink 的侧边输出功能,您可以获取最近被丢弃的数据流。
首先,您需要指定要在窗口流上使用获取迟到数据的 sideOutputLateData(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时,在水印通过窗口末尾之后,将保留窗口及其内容。在这些情况下,当延迟但未丢弃的元素到达时,可能会触发该窗口的另一次触发。这些触发称为late firings
,因为它们是由迟到的事件触发的,与之相反的main firing
是窗口的第一次触发。在会话窗口的情况下,后期触发会进一步导致窗口合并,因为它们可能“弥合”两个预先存在的未合并窗口之间的间隙。
注意:您应注意,迟到触发发射的元素应被视为先前计算(结果)的更新结果,即,您的数据流将包含同一计算的多个结果。根据您的应用程序,您需要考虑这些重复的结果或对它们进行删除重复数据。
窗口结果上做处理
窗口化操作的结果还是一个DataStream,结果操作元素中没有保留任何有关窗口化操作的信息,因此,如果要保留有关窗口的元信息,则必须在ProcessWindowFunction的结果元素中手动编码该信息。在结果元素上设置的唯一相关信息是元素时间戳。由于窗口结束时间戳是互斥的,因此将其设置为已处理窗口的最大允许时间戳,即结束时间戳-1。 请注意,对于事件时间窗口和处理时间窗口都是如此。 即在窗口操作元素之后始终具有时间戳,但这可以是事件时间时间戳或处理时间时间戳。 对于处理时间窗口,这没有特殊的含义,但是对于事件时间窗口,这连同水印与窗口的交互方式一起,可以以相同的窗口大小进行连续的窗口操作。 在查看水印如何与窗口交互之后我们将进行介绍。
水印和窗户的相互作用
在继续本节之前,您可能需要看一下有关 事件时间和水印的部分。
当水印到达窗口 operator 时,将触发两件事:
- 水印会触发所有最大时间戳(即end-timestamp-1)小于新水印的所有窗口的计算
- 水印被(按原样)转发到下游操作
直观地,一旦下游操作收到水印后,水印就会“flushes”所有在下游操作中被认为是后期的窗口。
连续窗口操作
如前所述,计算开窗结果的时间戳的方式以及水印与窗口的交互方式允许将连续的开窗操作组合在一起。当您要执行两个连续的窗口化操作时,如果您想使用不同的键(意思是有使用 keyby,不是说两个窗口使用不同的 keyby),但仍希望来自同一上游窗口的元素最终位于同一下游窗口中,此功能将非常有用。考虑以下示例:
val input: DataStream[Int] = ... val resultsPerKey = input .keyBy(<key selector>) .window(TumblingEventTimeWindows.of(Time.seconds(5))) .reduce(new Summer()) val globalResults = resultsPerKey .windowAll(TumblingEventTimeWindows.of(Time.seconds(5))) .process(new TopKWindowFunction())
在此示例中,在此示例中,第一个操作的时间窗口[0,5)的结果也将在随后的窗口操作中的时间窗口[0,5)中结束。这允许计算每个键的总和,然后在第二个操作中计算同一窗口内的前k个元素。
有效的 state 规模考虑
Windows 可以定义很长时间(例如几天,几周或几个月),因此会积累很大的状态。在估算窗口计算的存储需求时,需要牢记一些规则:
-
Flink 为每个元素所属的窗口创建一个副本。鉴于此,滚动窗口将保留每个元素的一个副本(一个元素恰好属于一个窗口,除非它被延迟放置)。相反,滑动窗口会为每个元素创建多个,如“ 窗口分配器”部分所述。因此,大小为1天的滑动窗口和滑动1秒的滑动窗口可能不是一个好主意。
-
ReduceFunction
,AggregateFunction
和FoldFunction
可以大大减少存储需求,因为它们提前地聚合元素并且每个窗口仅存储一个值。相反,使用ProcessWindowFunction
需要累积所有元素。 -
使用
Evictor
可以防止任何预聚合,因为在应用计算之前,必须将窗口的所有元素传递给 Evictor(请参阅Evictor)。
欢迎关注Flink菜鸟公众号,会不定期更新Flink(开发技术)相关的推文