1.Time&WaterMark
1.1时间分类
-
事件时间(event time): 事件产生的时间,记录的是设备生产(或者存储)事件的时间
-
摄取时间(ingestion time): Flink 读取事件时记录的时间
-
处理时间(processing time): Flink pipeline 中具体算子处理事件的时间
默认情况下,使用的是processing time;实际生产过程中,我们有些时候关注的是 event time
如果想要使用event time,需要额外给 Flink 提供一个时间戳提取器和 Watermark 生成器
可以通过如下指定时间语义
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
2.Water Mark
2.1思考
➢ 怎样避免乱序数据带来计算不正确?
➢ 遇到一个时间戳达到了窗口关闭时间,不应该立刻触发窗口计算,而是等待一段时间,等迟到的数据来了再关闭窗口
2.2 Water Mark
-
Watermark 是一种衡量 Event Time 进展的机制,可以设定延迟触发
-
Watermark 是用于处理乱序事件的,而正确的处理乱序事件,通常用Watermark 机制结合 window 来实现;
-
数据流中的 Watermark 用于表示 timestamp 小于 Watermark ( t’ <= t) 的数据,都已经到达了 ,因此,window 的执行也是由 Watermark 触发的。
-
watermark 用来让程序自己平衡延迟和结果正确性
2.3 watermark 的特点
-
watermark 是一条特殊的数据记录
-
watermark 必须单调递增,以确保任务的事件时间时钟在向前推进,而不是在后退
-
watermark 与数据的时间戳相关
2.4 watermark 的生成
TimestampAssigner 定义了抽取时间戳,以及生成 watermark 的方法,有两种类型
➢ AssignerWithPeriodicWatermarks
- 周期性的生成 watermark:系统会周期性的将 watermark 插入到流中
- 默认周期是200毫秒,可以使用ExecutionConfig.setAutoWatermarkInterval() 方法进行设置
- 升序和前面乱序的处理 BoundedOutOfOrdernessTimestampExtractor都是基于周期性 watermark 的。
➢ AssignerWithPunctuatedWatermarks
- 没有时间周期规律,可打断的生成 watermark。一般推荐使用 AssignerWithPeriodicWatermarks;该接口有AscendingTimestampExtractor、BoundedOutOfOrdernessTimestampExtractor两个抽象类,大家也可以继承此两个类
注意:笔者用的flink为1.10.0 。最新版本已经推荐弃用 这两个Api,新版api原理类似,大同小异
2.5 watermark 的传递
当一个算子输入有多个watermark时,该算子的waterMark取多个watermark的最小值,以 AbstractStreamOperator 抽象类为例,查看源码如下
public void processWatermark1(Watermark mark) throws Exception {
input1Watermark = mark.getTimestamp();
// 取最小值
long newMin = Math.min(input1Watermark, input2Watermark);
if (newMin > combinedWatermark) {
combinedWatermark = newMin;
processWatermark(new Watermark(combinedWatermark));
}
}
public void processWatermark2(Watermark mark) throws Exception {
input2Watermark = mark.getTimestamp();
// 取最小值
long newMin = Math.min(input1Watermark, input2Watermark);
// 如果最小值比上次合并值大,则生成新的watermark
if (newMin > combinedWatermark) {
combinedWatermark = newMin;
processWatermark(new Watermark(combinedWatermark));
}
}
WindowOperator、StreamMap、StreamSource、StreamSink都是直接或间接继承AbstractStreamOperator类
2.6处理空闲数据源
如果数据源中的某一个分区/分片在一段时间内未发送事件数据,则意味着 WatermarkGenerator
也不会获得任何新数据去生成 watermark。我们称这类数据源为空闲输入或空闲源。在这种情况下,当某些其他分区仍然发送事件数据的时候就会出现问题。由于下游算子 watermark 的计算方式是取所有不同的上游并行数据源 watermark 的最小值,则其 watermark 将不会发生变化。设置如下
WatermarkStrategy
.forBoundedOutOfOrderness[(Long, String)](Duration.ofSeconds(20))
.withIdleness(Duration.ofMinutes(1)) // 空闲时间
3.Window
3.1.window 概念
思考:一般真实的流都是无界的,怎样处理无界的数据?
- 可以把无限的数据流进行切分,得到有限的数据集进行处理 —— 也就是得到有界流
- 窗口(window)就是将无限流切割为有限流的一种方式,它会将流数据分发到有限大小的桶(bucket)中进行分析
3.2.window 类型
➢ 滚动窗口(Tumbling Windows)
- 将数据依据固定的窗口长度对数据进行切分
- 时间对齐,窗口长度固定,没有重叠
➢ 滑动窗口(Sliding Windows)
- 滑动窗口是固定窗口的更广义的一种形式,滑动窗口由固定的窗口 长度和滑动间隔组成
- 窗口长度固定,可以有重叠
➢ 全局窗口
- 当所有元素在一个没有结束时间的窗口
➢ session窗口
- 窗口大小不定,当上一个到达元素和当前到达元素时间间隔大于指定值时开一个新的窗口
可以这样理解:
窗口有窗口大小(size)和步长(step)两个概念;当size为定值且size=step为滚动窗口;当size为定值且size != step为滑动窗口;当size为无穷大时为global窗口;而session窗口的size不一定,需要根据元素到达间隔来判断
以上为窗口的类型,我们可以通过其它方式来进行分类(修饰)
3.3 window分类
3.3.1按照keyed分类
- Keyed Windows:keyed stream
- Non-Keyed Windows:non-keyed streams
通俗讲就是是否有keyby算子
3.3.2按time&count分类
以上提到的窗口size和step既可以指时间(time)也可以指数据数据(count)
例如:每5min统计一次(time);每100个元素统计一次(count)
以上分类可以修饰window类型;如每5分钟(time)统计各游戏(keyed)类目的收入(滚动窗口)
3.3.window API概览
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"
3.4window组成
3.4.1Window Assigners
1.使用
通过window方法可以传入WindowAssigner来指定窗口相关信息
def window[W <: Window](assigner: WindowAssigner[_ >: T, W]): WindowedStream[T, K, W]
系统已经给我们定义好了一些 WindowAssigner,继承关系如下:
因此您可以有如下用法:
input
.keyBy(<key selector>)
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5))) // 滑动窗口,窗口大小10分钟,步长为5分钟
.<windowed transformation>(<window function>)
input
.keyBy(<key selector>)
.window(TumblingProcessingTimeWindows.of(Time.seconds(5))) // 滚动窗口,窗口大小和步长均为5分钟
.<windowed transformation>(<window function>)
以上方法写起来比较长,系统还给我们提供了如下方法:
// count window 滚动窗口 窗口大小10个元素,步长10个元素
.countWindow(10)
// time window 窗口大小10分钟,步长5分钟
.timeWindow(Time.minutes(10),Time.minutes(5))
2.原理
如果自己编写 Window Assigners,需要实现以下方法(来自WindowAssigner)
/**
* Returns a {@code Collection} of windows that should be assigned to the element.
* 用来指定元素属于哪个窗口
* @param element The element to which windows should be assigned.
* @param timestamp The timestamp of the element.
* @param context The {@link WindowAssignerContext} in which the assigner operates.
*/
public abstract Collection<W> assignWindows(T element, long timestamp, WindowAssignerContext context);
/**
* Returns the default trigger associated with this {@code WindowAssigner}.
* 用于指定 Trigger ,请看下面的介绍
*/
public abstract Trigger<T, W> getDefaultTrigger(StreamExecutionEnvironment env);
/**
* Returns a {@link TypeSerializer} for serializing windows that are assigned by
* 序列化窗口
* this {@code WindowAssigner}.
*/
public abstract TypeSerializer<W> getWindowSerializer(ExecutionConfig executionConfig);
/**
* 是否event time
* Returns {@code true} if elements are assigned to windows based on event time,
* {@code false} otherwise.
*/
public abstract boolean isEventTime();
以上不够直观,我们可以打开TumblingEventTimeWindows源码看到如下代码:
// TumblingEventTimeWindows
@Override
public Collection<TimeWindow> assignWindows(Object element, long timestamp, WindowAssignerContext context) {
if (timestamp > Long.MIN_VALUE) {
// Long.MIN_VALUE is currently assigned when no timestamp is present
long start = TimeWindow.getWindowStartWithOffset(timestamp, offset, size);
return Collections.singletonList(new TimeWindow(start, start + size));
} 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(...)'?");
}
}
@Override
public Trigger<Object, TimeWindow> getDefaultTrigger(StreamExecutionEnvironment env) {
return EventTimeTrigger.create();
}
// TimeWindow 类
// offset为时区偏移量,windowSize为窗口大小
public static long getWindowStartWithOffset(long timestamp, long offset, long windowSize) {
return timestamp - (timestamp - offset + windowSize) % windowSize;
}
// 结束时间为end - 1
public long maxTimestamp() {
return end - 1;
}
可以得出如下结论:
- 当有元素流入的时候才会产生窗口,如果没有元素,则不存在窗口
- 一个元素可以属于多个窗口(请看SlidingEventTimeWindows类)
- 窗口的开始时间为starttime=timestamp - (timestamp - offset + windowSize) % windowSize,结束时间为starttime+windowSize -1。可以表示为[starttime,starttime+windowSize),左闭右开
3.4.2Window Functions
主要由三个Function如下
-
ReduceFunction
➢ 增量聚合函数,每条数据到来就进行计算(第一个元素不触发,因为只有一个值),保持一个简单的状态增量计算函数。可和全量计算(ProcessWindowFunction)组合使用 -
AggregateFunction
➢ 增量聚合函数,每条数据到来就进行计算,保持一个简单的状态增量计算函数。可和全量计算(ProcessWindowFunction)组合使用 -
ProcessWindowFunction
全量计算函数,输入为包含所有元素的迭代器,窗口到期后一次性计算
具体代码如下
ReduceFunction AggregateFunction ProcessFunction
3.4.3Triggers
官网说明如下:
A
Trigger
determines when a window (as formed by the window assigner) is ready to be processed by the window function. EachWindowAssigner
comes with a defaultTrigger
. If the default trigger does not fit your needs, you can specify a custom trigger usingtrigger(...)
.
- The
onElement()
method is called for each element that is added to a window. // 正确理解:每个元素到时调用一次,用来判断是否将计算结果向下游发送- The
onEventTime()
method is called when a registered event-time timer fires. // 正确理解:如果是基于EventTime且窗口到期了,调用一次,否则不调用。用来判断是否向下游发送数据- The
onProcessingTime()
method is called when a registered processing-time timer fires. // 正确理解:如果是基于ProcessingTime
且窗口到期了,调用一次,否则不调用,用来判断是否向下游发送数据- The
onMerge()
method is relevant for stateful triggers and merges the states of two triggers when their corresponding windows merge, e.g. when using session windows.- Finally the
clear()
method performs any action needed upon removal of the corresponding window // 窗口到期时调用TriggerResult,枚举值
CONTINUE
: do nothingFIRE
: trigger the computationPURGE
: clear the elements in the window, andFIRE_AND_PURGE
: trigger the computation and clear the elements in the window afterwards.
笔者测试发现,官网文档描述有误,个人理解如下:
- 每当元素到来时均触发窗口函数进行计算(此处仅讨论增量计算)
- Triggers用来表示计算的结果是否向下游发送,
CONTINUE为不向下游发送结果,FIRE为向下游发送结果
- 默认情况下窗口函数的计算在Triggers之前进行(加上Evictors后执行顺序不一样,有时间再研究)。当窗口函数计算完成后才决定是否向下游发送结果
查看EventTimeTrigger(EventTime默认的触发器)源码如下
@Override
public TriggerResult onElement(Object element, long timestamp, TimeWindow window, TriggerContext ctx) throws Exception {
// 如果当前水印时间大于 窗口结束日期,则每来一个元素触发一次计算
if (window.maxTimestamp() <= ctx.getCurrentWatermark()) {
// if the watermark is already past the window fire immediately
return TriggerResult.FIRE;
} else {
// 注册 Timer在指定时间触发
ctx.registerEventTimeTimer(window.maxTimestamp());
return TriggerResult.CONTINUE;
}
}
@Override
public TriggerResult onEventTime(long time, TimeWindow window, TriggerContext ctx) { // 如果触发时间和窗口最大时间相等,则触发计算
return time == window.maxTimestamp() ?
TriggerResult.FIRE :
TriggerResult.CONTINUE;
}
@Override
public void clear(TimeWindow window, TriggerContext ctx) throws Exception {
ctx.deleteEventTimeTimer(window.maxTimestamp());
}
不难理解,其发射逻辑为:
* 当到窗口的截止时间时向下游发送数据
* 如果还有其它元素到来(窗口结束时间以后),每到来一次向下游发送一次结果
3.4.4Evictors
The evictor has the ability to remove elements from a window after the trigger fires and before and/or after the window function is applied.
有如下两个方法
/**
* 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);
3.4.5 迟到数据处理
-
.allowedLateness() —— 允许处理迟到的数据
-
.sideOutputLateData() —— 将迟到的数据放入侧输出流
-
.getSideOutput() —— 获取侧输出流
注意:
- 窗口的生命周期为第一个元素到来时产生,当时间为starttime + size + allowedLateness() 结束
- 正常情况下(窗口使用默认trigger) 窗口的首次触发时间为starttime + size - 1ms(向下游发送数据)
- starttime + size ~ starttime + size + allowedLateness() 时间内每来一条数据,则向下游发送一条数据
- 结束后flink将会移除窗口且清除相关状态
- 当窗口被清除后该窗口期还有数据到来(即starttime + size + allowedLateness()时间后),则输出到侧输出流中
可以看到flink通过watermark、allowedLateness来保证数据正确性,通过sideOutputLateData来存储未处理的数据
具体例子 参考 时间不够,后续再对Evictors、allowedLateness等做详细研究吧
以上 仅为个人理解