Flink supports different notions of time in streaming programs.
对于Flink里面的三种时间:
事件时间
Event time: Event time is the time that each individual event occurred on its producing device. This time is typically embedded within the records before they enter Flink, and that event timestamp can be extracted from each record. In event time, the progress of time depends on the data, not on any wall clocks. Event time programs must specify how to generate Event Time Watermarks, which is the mechanism that signals progress in event time. This watermarking mechanism is described in a later section, below.
**事件时间:**事件时间是指每个事件在其生产设备上发生的时间。 通常在记录进入Flink之前将其嵌入到记录中,并且可以从每个记录中提取事件时间戳。 在事件时间中,时间的进度取决于数据,而不取决于任何挂钟。 事件时间程序必须指定如何生成事件时间水印”,这是信号事件时间进展的机制。 此水印机制将在后面的部分以下中进行描述。
In a perfect world, event time processing would yield completely consistent and deterministic results, regardless of when events arrive, or their ordering. However, unless the events are known to arrive in-order (by timestamp), event time processing incurs some latency while waiting for out-of-order events. As it is only possible to wait for a finite period of time, this places a limit on how deterministic event time applications can be.
在理想情况下,事件时间处理将产生完全一致且确定的结果,而不管事件何时到达或它们的顺序如何。 但是,除非已知事件是按时间戳(按时间戳)到达的,否则事件时间处理会在等待无序事件时产生一些延迟。 由于只能等待有限的时间,因此这限制了确定性事件时间应用程序的可用性。
Assuming all of the data has arrived, event time operations will behave as expected, and produce correct and consistent results even when working with out-of-order or late events, or when reprocessing historic data. For example, an hourly event time window will contain all records that carry an event timestamp that falls into that hour, regardless of the order in which they arrive, or when they are processed. (See the section on late events for more information.)
假设所有数据都已到达,事件时间操作将按预期方式运行,即使在处理无序或迟到事件或重新处理历史数据时,也会产生正确且一致的结果。 例如,每小时事件时间窗口将包含所有带有落入该小时事件时间戳的记录,无论它们到达的顺序或处理的时间。 (有关更多信息,请参见晚期事件部分。)
Note that sometimes when event time programs are processing live data in real-time, they will use some processing time operations in order to guarantee that they are progressing in a timely fashion.
请注意,有时当事件时间程序实时处理实时数据时,它们将使用一些“处理时间”操作,以确保它们及时进行。
摄取时间
Ingestion time: Ingestion time is the time that events enter Flink. At the source operator each record gets the source’s current time as a timestamp, and time-based operations (like time windows) refer to that timestamp.
**摄取时间:**摄取时间是事件进入Flink的时间。 在源操作员处,每条记录都将源的当前时间作为时间戳记,并且基于时间的操作(例如时间窗口)引用该时间戳记。
Ingestion time sits conceptually in between event time and processing time. Compared to processing time, it is slightly more expensive, but gives more predictable results. Because ingestion time uses stable timestamps (assigned once at the source), different window operations over the records will refer to the same timestamp, whereas in processing time each window operator may assign the record to a different window (based on the local system clock and any transport delay).
摄取时间在概念上位于事件时间和处理时间之间。 与处理时间相比,它稍微贵一点,但结果却更可预测。 由于摄取时间使用稳定的时间戳(在源处分配了一次),因此对记录的不同窗口操作将引用相同的时间戳,而在处理时间中,每个窗口操作员都可以将记录分配给不同的窗口(基于 本地系统时钟和任何传输延迟)。
Compared to event time, ingestion time programs cannot handle any out-of-order events or late data, but the programs don’t have to specify how to generate watermarks.
与“事件时间”相比,“摄入时间”程序不能处理任何乱序事件或迟到的数据,但是程序不必指定如何生成“水印”。
Internally, ingestion time is treated much like event time, but with automatic timestamp assignment and automatic watermark generation.
在内部,“摄取时间”与“事件时间”非常相似,但是具有自动时间戳分配和自动水印生成功能。
处理时间
Processing time: Processing time refers to the system time of the machine that is executing the respective operation.
**处理时间:**处理时间是指执行相应操作的机器的系统时间。
When a streaming program runs on processing time, all time-based operations (like time windows) will use the system clock of the machines that run the respective operator. An hourly processing time window will include all records that arrived at a specific operator between the times when the system clock indicated the full hour. For example, if an application begins running at 9:15am, the first hourly processing time window will include events processed between 9:15am and 10:00am, the next window will include events processed between 10:00am and 11:00am, and so on.
当流式程序按处理时间运行时,所有基于时间的操作(如时间窗口)都将使用运行该操作的计算机系统时钟。 每小时处理时间窗口将包括系统时钟指示整小时的时间之间到达特定操作的所有记录。 例如,如果应用程序在9:15 am开始运行,则第一个每小时处理时间窗口将包括在9:15 am和10:00 am之间处理的事件,下一个窗口将包括在10:00 am和11:00 am之间处理的事件,依此类推。
Processing time is the simplest notion of time and requires no coordination between streams and machines. It provides the best performance and the lowest latency. However, in distributed and asynchronous environments processing time does not provide determinism, because it is susceptible to the speed at which records arrive in the system (for example from the message queue), to the speed at which the records flow between operators inside the system, and to outages (scheduled, or otherwise).
处理时间是最简单的时间概念,不需要流和机器之间的协调。 它提供了最佳的性能和最低的延迟。 但是,在分布式和异步环境中,处理时间不能提供确定性,因为它容易受到记录到达系统的速度(例如,从消息队列中来),记录在系统内部的操作员之间流动的速度以及中断(计划的或其他方式)的影响。
The first part of a Flink DataStream program usually sets the base time characteristic. That setting defines how data stream sources behave (for example, whether they will assign timestamps), and what notion of time should be used by window operations like KeyedStream.timeWindow(Time.seconds(30))
.
The following example shows a Flink program that aggregates events in hourly time windows. The behavior of the windows adapts with the time characteristic.
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);
// alternatively:
// env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime);
// env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
DataStream<MyEvent> stream = env.addSource(new FlinkKafkaConsumer09<MyEvent>(topic, schema, props));
stream
.keyBy( (event) -> event.getUser() )
.timeWindow(Time.hours(1))
.reduce( (a, b) -> a.add(b) )
.addSink(...);
Windows are at the heart of processing infinite streams. Windows split the stream into “buckets” of finite size, over which we can apply computations. This document focuses on how windowing is performed in Flink and how the programmer can benefit to the maximum from its offered functionality.
The general structure of a windowed Flink program is presented below. The first snippet refers to keyed streams, while the second to non-keyed ones. As one can see, the only difference is the keyBy(...)
call for the keyed streams and the window(...)
which becomes windowAll(...)
for non-keyed streams. This is also going to serve as a roadmap for the rest of the page.
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"
In the above, the commands in square brackets ([…]) are optional. This reveals that Flink allows you to customize your windowing logic in many different ways so that it best fits your needs.
In a nutshell, a window is created as soon as the first element that should belong to this window arrives, and the window is completely removed when the time (event or processing time) passes its end timestamp plus the user-specified allowed lateness
(see Allowed Lateness). Flink guarantees removal only for time-based windows and not for other types, e.g. global windows (see Window Assigners). For example, with an event-time-based windowing strategy that creates non-overlapping (or tumbling) windows every 5 minutes and has an allowed lateness of 1 min, Flink will create a new window for the interval between 12:00
and 12:05
when the first element with a timestamp that falls into this interval arrives, and it will remove it when the watermark passes the 12:06
timestamp.
In addition, each window will have a Trigger
(see Triggers) and a function (ProcessWindowFunction
, ReduceFunction
, AggregateFunction
or FoldFunction
) (see Window Functions) attached to it. The function will contain the computation to be applied to the contents of the window, while the Trigger
specifies the conditions under which the window is considered ready for the function to be applied. A triggering policy might be something like “when the number of elements in the window is more than 4”, or “when the watermark passes the end of the window”. A trigger can also decide to purge a window’s contents any time between its creation and removal. Purging in this case only refers to the elements in the window, and not the window metadata. This means that new data can still be added to that window.
Apart from the above, you can specify an Evictor
(see Evictors) which will be able to remove elements from the window after the trigger fires and before and/or after the function is applied.
In the following we go into more detail for each of the components above. We start with the required parts in the above snippet (see Keyed vs Non-Keyed Windows, Window Assigner, and Window Function) before moving to the optional ones.
The first thing to specify is whether your stream should be keyed or not. This has to be done before defining the window. Using the keyBy(...)
will split your infinite stream into logical keyed streams. If keyBy(...)
is not called, your stream is not keyed.
In the case of keyed streams, any attribute of your incoming events can be used as a key (more details here). Having a keyed stream will allow your windowed computation to be performed in parallel by multiple tasks, as each logical keyed stream can be processed independently from the rest. All elements referring to the same key will be sent to the same parallel task.
In case of non-keyed streams, your original stream will not be split into multiple logical streams and all the windowing logic will be performed by a single task, i.e. with parallelism of 1.
After specifying whether your stream is keyed or not, the next step is to define a window assigner. The window assigner defines how elements are assigned to windows. This is done by specifying the WindowAssigner
of your choice in the window(...)
(for keyed streams) or the windowAll()
(for non-keyed streams) call.
A WindowAssigner
is responsible for assigning each incoming element to one or more windows.
每个传入的数据分配给一个或者多个窗口
Flink comes with pre-defined window assigners for the most common use cases, namely tumbling windows, sliding windows, session windows and global windows. You can also implement a custom window assigner by extending the WindowAssigner
class. All built-in window assigners (except the global windows) assign elements to windows based on time, which can either be processing time or event time. Please take a look at our section on event time to learn about the difference between processing time and event time and how timestamps and watermarks are generated.
Flink带有针对最常见用例的预定义窗口分配器,即滚动窗口,滑动窗口,会话窗口和全局窗口。 您还可以通过扩展WindowAssigner类来实现自定义窗口分配器。 所有内置窗口分配器(全局窗口除外)均基于时间将元素分配给窗口,时间可以是处理时间,也可以是事件时间。
Time-based windows have a start timestamp (inclusive) and an end timestamp (exclusive) that together describe the size of the window. In code, Flink uses TimeWindow
when working with time-based windows which has methods for querying the start- and end-timestamp and also an additional method maxTimestamp()
that returns the largest allowed timestamp for a given windows.
基于时间的窗口具有一个“开始时间戳”(包括)和“结束时间戳”(不包括),它们共同描述了窗口的大小。 在代码中,Flink在使用基于时间的窗口时使用“ TimeWindow”,它具有查询开始时间戳和结束时间戳的方法,还有一个用于返回给定窗口允许的最大时间戳的附加方法“ maxTimestamp()”。
In the following, we show how Flink’s pre-defined window assigners work and how they are used in a DataStream program. The following figures visualize the workings of each assigner. The purple circles represent elements of the stream, which are partitioned by some key (in this case user 1, user 2 and user 3). The x-axis shows the progress of time.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g3wUogno-1598358985586)(…/images/image-20200708213826035.png)]
A tumbling windows assigner assigns each element to a window of a specified window size. Tumbling windows have a fixed size and do not overlap. For example, if you specify a tumbling window with a size of 5 minutes, the current window will be evaluated and a new window will be started every five minutes as illustrated by the following figure.
固定大小,窗口之间不会重叠
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iwdeYcSN-1598358985588)(https://ci.apache.org/projects/flink/flink-docs-release-1.11/fig/tumbling-windows.svg)]
The sliding windows assigner assigns elements to windows of fixed length. Similar to a tumbling windows assigner, the size of the windows is configured by the window size parameter. An additional window slide parameter controls how frequently a sliding window is started. Hence, sliding windows can be overlapping if the slide is smaller than the window size. In this case elements are assigned to multiple windows.
固定大小,可能重叠
滑动窗口分配器将元素分配给固定长度的窗口。 与滚动窗口分配器类似,窗口的大小由window size参数配置。 附加的 window slide参数控制滑动窗口的启动频率。 因此,如果幻灯片小于窗口大小,则滑动窗口可能会重叠。 在这种情况下,元素被分配给多个窗口。
For example, you could have windows of size 10 minutes that slides by 5 minutes. With this you get every 5 minutes a window that contains the events that arrived during the last 10 minutes as depicted by the following figure.
例如,您可以将大小为10分钟的窗口滑动5分钟。 这样,您每隔5分钟就会得到一个窗口,其中包含最近10分钟内到达的事件,如下图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kc8uip0P-1598358985589)(https://ci.apache.org/projects/flink/flink-docs-release-1.11/fig/sliding-windows.svg)]
After defining the window assigner, we need to specify the computation that we want to perform on each of these windows. This is the responsibility of the window function, which is used to process the elements of each (possibly keyed) window once the system determines that a window is ready for processing (see triggers for how Flink determines when a window is ready).
定义窗口分配器后,我们需要指定要在每个窗口上执行的计算。 这是“窗口功能”的职责,一旦系统确定某个窗口已准备好进行处理,就可以使用该窗口功能来处理每个(可能是键控)窗口的元素(请参阅[triggers](https://ci.apache。 org / projects / flink / flink-docs-release-1.11 / zh / dev / stream / operators / windows.html#triggers),以了解Flink如何确定何时准备好窗口。
The window function can be one of ReduceFunction
, AggregateFunction
, FoldFunction
or ProcessWindowFunction
. The first two can be executed more efficiently (see [State Size](https://ci.apache.org/projects/flink/flink-docs-release-1.11/zh/dev/stream/operators/windows.html#state size) section) because Flink can incrementally aggregate the elements for each window as they arrive. A ProcessWindowFunction
gets an Iterable
for all the elements contained in a window and additional meta information about the window to which the elements belong.
A windowed transformation with a ProcessWindowFunction
cannot be executed as efficiently as the other cases because Flink has to buffer all elements for a window internally before invoking the function. This can be mitigated by combining a ProcessWindowFunction
with a ReduceFunction
, AggregateFunction
, or FoldFunction
to get both incremental aggregation of window elements and the additional window metadata that the ProcessWindowFunction
receives. We will look at examples for each of these variants.
A ReduceFunction
specifies how two elements from the input are combined to produce an output element of the same type. Flink uses a ReduceFunction
to incrementally aggregate the elements of a window.
DataStream<Tuple2<String, Long>> input = ...;
input
.keyBy(<key selector>)
.window(<window assigner>)
.reduce(new ReduceFunction<Tuple2<String, Long>> {
public Tuple2<String, Long> reduce(Tuple2<String, Long> v1, Tuple2<String, Long> v2) {
return new Tuple2<>(v1.f0, v1.f1 + v2.f1);
}
});
An AggregateFunction
is a generalized version of a ReduceFunction
that has three types: an input type (IN
), accumulator type (ACC
), and an output type (OUT
). The input type is the type of elements in the input stream and the AggregateFunction
has a method for adding one input element to an accumulator. The interface also has methods for creating an initial accumulator, for merging two accumulators into one accumulator and for extracting an output (of type OUT
) from an accumulator. We will see how this works in the example below.
Same as with ReduceFunction
, Flink will incrementally aggregate input elements of a window as they arrive.
/**
* 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<String, Long>, Tuple2<Long, Long>, Double> {
@Override
public Tuple2<Long, Long> createAccumulator() {
return new Tuple2<>(0L, 0L);
}
@Override
public Tuple2<Long, Long> add(Tuple2<String, Long> value, Tuple2<Long, Long> accumulator) {
return new Tuple2<>(accumulator.f0 + value.f1, accumulator.f1 + 1L);
}
@Override
public Double getResult(Tuple2<Long, Long> accumulator) {
return ((double) accumulator.f0) / accumulator.f1;
}
@Override
public Tuple2<Long, Long> merge(Tuple2<Long, Long> a, Tuple2<Long, Long> b) {
return new Tuple2<>(a.f0 + b.f0, a.f1 + b.f1);
}
}
DataStream<Tuple2<String, Long>> input = ...;
input
.keyBy(<key selector>)
.window(<window assigner>)
.aggregate(new AverageAggregate());
A FoldFunction
specifies how an input element of the window is combined with an element of the output type. The FoldFunction
is incrementally called for each element that is added to the window and the current output value. The first element is combined with a pre-defined initial value of the output type.
A FoldFunction
can be defined and used like this:
DataStream<Tuple2<String, Long>> input = ...;
input
.keyBy(<key selector>)
.window(<window assigner>)
.fold("", new FoldFunction<Tuple2<String, Long>, String>> {
public String fold(String acc, Tuple2<String, Long> value) {
return acc + value.f1;
}
});
A ProcessWindowFunction gets an Iterable containing all the elements of the window, and a Context object with access to time and state information, which enables it to provide more flexibility than other window functions. This comes at the cost of performance and resource consumption, because elements cannot be incrementally aggregated but instead need to be buffered internally until the window is considered ready for processing.
ProcessWindowFunction获取一个Iterable,该Iterable包含窗口的所有元素,以及一个Context对象,该对象可以访问时间和状态信息,从而使其比其他窗口函数更具灵活性。 这是以性能和资源消耗为代价的,因为无法增量聚合元素,而是需要在内部对其进行缓冲,直到将窗口视为已准备好进行处理为止。
The signature of ProcessWindowFunction
looks as follows: