Flink-水位
摘要
本文主要讲解Flink的水位相关基础理论知识,并会辅以源码讲解。
可参考:
1 水位产生背景
前面Time和窗口章节提到过Processing Time和Event Time两种时间度量方式,后者可以尽可能让Event划分窗口准确。比如
可以看到由于网络延迟,造成C到流式引擎的时间稍有延迟,那么分别按两种Time进行处理的情况如下:
可以看到,如果基于Processing Time则Event C会被错误划分到第二个窗口,而基于Event Time划分正确。
但由于系统中各种因素如网络延迟等,数据仍可能会迟到,窗口不可能一直开着无限等待所有数据。MillWheel论文中提出了lower watermark机制目的就是规范Window尽可能等待这些会迟到的数据的机制。
具体来说,支持EventTime的流处理程序需要一种能丈量EventTIme进度的方式,比如一小时宽度的Window算子需要在EventTime流逝了一小时后被通知关闭该活跃窗口。
2 水位基本概念
2.1 水位是什么
上一小节中提到的丈量EventTIme进度的方式,在Flink中就是水位。
Watermarker是嵌入在EventTime轴上的,用来判断EventTime窗口内的所有数据均已到达流式执行引擎的一种时间推理工具,是一种既可以在流处理引擎Source侧嵌入,也可以在MQ侧嵌入的时间戳。(大白话:Watermarker标记水位时间戳以下的数据已经到齐了)。
具体来说,小于水位时间戳的Event不会再出现,也可以将水位称为事件推进器。
水位迟到早到影响:
- 水位如果迟到,会让窗口存活时间变长,增加系统运行负担
- 水位如果早到,会让窗口提前关闭,导致数据处理结果不准确
以上问题可用触发器解决。
2.2 Low Watermark低水位
Low Watermark即低水位,本质上是一个时间戳,用以标记该比时间戳以前的Event都已到达,更晚的事件没有到达。
每个计算节点上分别维护了一个Low Watermark,计算公式如下:
3 流式系统中的两种Timer概念
Timer窗口开始计算数据的触发器,让窗口不要一直等待。Timer主要有:
- Wall Time Timer
挂钟时间,就是现实世界时间作为触发条件 - Low Watermark Timer
低水位时间作为触发条件
4 ProcessFunction
注册的Timer会放入一个Flink内部的优先级队列,在org.apache.flink.streaming.api.operators.InternalTimerServiceImpl
:
private final KeyGroupedInternalPriorityQueue<TimerHeapInternalTimer<K, N>> eventTimeTimersQueue;
@Override
public void registerEventTimeTimer(N namespace, long time) {
eventTimeTimersQueue.add(new TimerHeapInternalTimer<>(time, (K) keyContext.getCurrentKey(), namespace));
}
public void advanceWatermark(long time) throws Exception {
currentWatermark = time;
InternalTimer<K, N> timer;
// 这就是那个Timer优先级队列
while ((timer = eventTimeTimersQueue.peek()) != null && timer.getTimestamp() <= time) {
eventTimeTimersQueue.poll();
keyContext.setCurrentKey(timer.getKey());
triggerTarget.onEventTime(timer);
}
}
5 Flink水位的基本概念
可参考Generating Timestamps / Watermarks
5.1 重要概念
5.1.1 概述
Flink中的水位也作为数据流的一部分在各Flink组件、算子之间流动,带有时间戳。水位由数据源嵌入或在没嵌入时由Flink应用程序来自定义生成,这样才能使得基于Event Time的窗口正常运行。
比如watermark(t)
表示数据流中的EventTime已经到达时间t
,意味着不会再有时间戳t'<=t
的事件再输入。
5.1.2 水位与Event顺序性
- 顺序流
其中,Event顺序到达时,水位表示小于等于其时间戳的Event都已经到达,而大于水位的Event还没有被观测到:
- 乱序流
而在Event乱序时,水位至关重要。此时水位意味着比特定的时间戳小或相等的所有event都已到达。但需要注意在乱序时,比水位时间戳大的Event也可能到达,即事件迟到:
5.1.3 只有部分Event带有水位
考虑系统开销,所以只有部分Event附带了水位,即Flink中的水位是离散的。只是为了便于分析,通常连续绘制水印曲线。
5.1.4 水位单调递增
单调递增、作为Event Time的推进器。
Flink接收到的数据就相当于浮在一个密封且不定时注水的水罐的水面的物体,该水位线的高度只会随着注水升高而不会降低。每当一个新数据进来时,会重新计算水位线时间戳。如果计算结果小于当前水位线,不会更新现有的水位线。
5.1.5 水位与EventTime窗口计算触发
当水位线持续上升到达EventTime窗口触发时间时才会触发EventTime窗口的计算。
5.1.6 水位的意义
Watermark的意义在于数据无序传递的时候有一定容错率,如果晚来的数据在容错范围之内,会当做正常传递来处理。超过则丢弃。
5.2 并行流中的水位
5.2.1 Source实例独立水位
水位是在Source
中或紧随其后生成的。一般来说,每个并行的Source子任务实例都有独立的水位,用来标记各自实例的EventTime上升情况。
5.2.2 水位提升
水位在数据流中传输,到达算子时会提升该处的EventTime,然后计算后生成新的水位发送到下游算子。
5.2.3 消费多输入流的水位
一些算子消费多个输入流,比如union
、跟随在keyBy
或partition
后的算子,这类算子的EventTime是他的所有输入流中的EventTime中的最小值,并保持更新。
下图展示了并行流场景中EvnetTime和水位的情况:
重点如下:
-
灰色圆圈代表算子名,其下方小括号内是算子实例序号;
-
每个算子右上黄色方框中的数字表该算子当前EventTime。
-
灰色箭头代表数据流向
-
数据流箭头上的白色框内左侧为Event,右侧为其Timestamp。
如上方左数第一个I|35
代表I事件的Timestmap为35。 -
标明了
Watermark
的虚线为当前发送的Watermark,发送到下游算子更新。 -
右侧的
window
算子都是各自接收来自shuffle stream的两个map算子的部分数据流,所以他们EventTime=Min(EventTime(map(1)),EventTime(map(2)))=Min(29,14)=14
。因为之前map(2)
这趟流的EventTime是14,所以两个window算子的当前水位都是14。随着
map(2)
的EventTime发出了新水位W(17)
,后续两个window算子的EventTime将会上升到17。
当使用Kafka作为Source时,每个Kafka Partition也许会是递增时间戳或有界乱序的简易EventTime模式。
5.3 完美水位(perfect watermark)和启发式水位(heuristic watermark)
5.3.1 完美水印
5.3.1.1 概述
完美水印暗含早于水印标记的EventTime的所有事件均已到达。
比如在有序的无界数据集中每个最近的事件的EventTime就是完美水印,如Kafka每个分区的最近记录的EventTime就是绝对有序的。
5.3.1.2 小结
完美水印定义比较困难。而且如果完美水印迟到,会造成后续窗口的操作全部受迟到的窗口的影响而推迟,拉长了窗口生存期,增加处理的资源开销。
5.3.2 启发式水印
5.3.2.1 概述
尽可能确定时间戳的一种估计方法,可能出现Event晚于水印到达,而被抛弃,导致计算精确度下降。
5.3.2.2 小结
启发式水印代价较低。
6 Flink关于迟到事件处理
6.1 迟到事件概念
可参考:
- Late Elements
- Allowed Lateness
水位含义是早于它的事件不应再出现,但由于各种原因导致接收到水位时间戳以前的的消息是不可避免的,这就是所谓的迟到事件。
比如水位已经上升到Watermark(t)
,却发现后来输入的元素的时间戳t'<t
即迟到事件,比如在Flink用于跟踪EventTime进度的水印已经超过了输入元素所属Window的结束时间戳。在真实世界中,可能元素延迟时间无法预测,所以没办法指定一个准确预测所有元素都已到达的水位。而且就算延迟有界,如果水位宽度设的过大也往往不是我们想要的效果,因为会造成过大计算延迟和开销。
6.2 迟到事件处理方法
6.2.1 概述
实际上迟到事件是乱序事件的特例,和一般乱序事件不同的是它们的乱序程度超出了水位线的预计,导致窗口在它们到达之前已经关闭并完成了计算,Flink处理迟到事件的方法有3种。
6.2.2 Drop - 将迟到事件视为错误消息并丢弃
Flink默认处理方式。
6.2.3 EventTimeTrigger - 重新激活已经关闭的窗口,并重新计算以修正结果
即使用Allowed Lateness
机制,允许用户定义允许的最大迟到时长,默认为0(即在水印后到达的小于水印的时间戳元素全部被抛弃)。Flink 会据此在窗口关闭后一直保存窗口的状态直至超时。在此期间的接收到的迟到事件不会被丢弃,而是会触发窗口的重新计算。这就是EventTimeTrigger
。
Flink会一直维护窗口状态,直到窗口右边界+迟到生存期时间过期后才会移除该窗口、删除状态。
示例代码:
val input: DataStream[T] = ...
input
.keyBy(<key selector>)
.window(<window assigner>)
.allowedLateness(<time>)
.<windowed transformation>(<window function>)
注意:
- 因为保存窗口状态需要额外内存,并且如果窗口计算使用了
ProcessWindowFunction API
还可能使得每个迟到事件触发一次代价较大的窗口全量计算。所以允许迟到时长不宜设得太长,迟到Event数也不宜过多,否则应该考虑降低水位线提高的速度或者调整算法。 - 对于SessionWinow可,能由于“弥合”两个预先存在的未合并窗口之间的间隙导致进一步的窗口合并。
- 需要去重
由于迟到生存期导致的窗口late fire不会覆盖而只是append到结果流中。我们应该根据具体情况决定是否要覆盖或去重!
6.2.4 SideOuput - 将迟到事件收集起来另外处理
使用Side Output
机制,可以将迟到事件单独放入一个数据流分支,这会作为窗口计算结果的副产品产出,以便用户获取后对其进行专门处理。
示例代码:
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)
7 空闲Source的水印生成
一段时间内没有元素流入时可能导致窗口不会被触发从而无法产出数据,此时可使用周期性水位生成器,他不仅仅依赖于输入元素的时间戳来生成水印。比如可以在一段时间内没有观察到新的事件输入时使用当前ProcessingTime来作为事件基准计算水印。
8 Flink生成水位的方式
8.1 生成水位时机
8.1.1 概述
要使用EventTime时间戳,就要求数据流的每个元素中都分配EventTimestamp,比如从元素中某个字段按一定规则取值。
目前有两种方式分配时间戳、生成水位的方式:
8.1.2 直接在Source生成
直接在Source算子内为元素分配时间戳,还会发送水位线,推荐使用。相当于把整个的 timestamp 分配和 watermark 生成的逻辑放在流处理应用的源头。
该选项可以使得Source更好的将shard、分区、split信息应用在水位生成,可在更好的层次上追踪水位,生产水位整体来说更精确。
这个方式都不再需要timestamp assigner
了(如果提供,会覆盖source中的时间戳和水位)。
例子如下:
override def run(ctx: SourceContext[MyType]): Unit = {
while (/* condition */) {
val next: MyType = getNext()
// 收集timestamp,为元素分配时间戳
ctx.collectWithTimestamp(next, next.eventTimestamp)
if (next.hasWatermarkTime) {
// 发送水位
ctx.emitWatermark(new Watermark(next.getWatermarkTime))
}
}
}
8.1.3 通过TimestampAssigner / WatermarkGenerator
只有在无法直接给Source指定水位策略时使用。
接收流为输入,生产出的流中元素都带有时间戳和水位。如果原始流已经有定义timestamp/watermark,会被当前覆盖。
一般我们会在source之后定义TimestampAssigner
来从流元素的字段里提取EventTime,但不是必须,只要在在首个跟event time
相关的算子之前定义即可。(Kafka Connector除外,参考Kafka Connector documentation)
Flink 1.11提供了一个WatermarkStrategy
接口实现了这两个功能:
public interface WatermarkStrategy<T> extends TimestampAssignerSupplier<T>, WatermarkGeneratorSupplier<T>{
/**
* Instantiates a {@link TimestampAssigner} for assigning timestamps according to this
* strategy.
*/
@Override
TimestampAssigner<T> createTimestampAssigner(TimestampAssignerSupplier.Context context);
/**
* Instantiates a WatermarkGenerator that generates watermarks according to this strategy.
*/
@Override
WatermarkGenerator<T> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context);
}
Flink已经自带常用实现,但也可以自己实现接口来自定义。一个使用自带的、处理有界乱序的水位用法如下:
WatermarkStrategy
.forBoundedOutOfOrderness[(Long, String)](Duration.ofSeconds(20))
.withTimestampAssigner(new SerializableTimestampAssigner[(Long, String)] {
override def extractTimestamp(element: (Long, String), recordTimestamp: Long): Long = element._1
})
8.2 自定义水位
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
val stream: DataStream[MyEvent] = env.readFile(
myFormat, myFilePath, FileProcessingMode.PROCESS_CONTINUOUSLY, 100,
FilePathFilter.createDefaultFilter())
val withTimestampsAndWatermarks: DataStream[MyEvent] = stream
.filter( _.severity == WARNING )
.assignTimestampsAndWatermarks(<watermark strategy>)
withTimestampsAndWatermarks
.keyBy( _.getGroup )
.timeWindow(Time.seconds(10))
.reduce( (a, b) => a.add(b) )
.addSink(...)
8.3 周期性水位(Periodic Watermark)
8.3.1 概述
根据Event Time或Processing Time,按照用户自定义的固定时间间隔(默认是200ms,可通过env.getConfig.setAutoWatermarkInterval
设置)周期性地触发水印生成器,尝试生成和发送新的水位线,不论是否有新的消息抵达。但如果不符合要求,则不会发送水印。
在两次水位线提升的时间间隔内会有一批可能带有时间戳消息流入,用户可以根据这部分数据来计算出新的水位线。
比如,最简单的水位计算方法就是取目前为止最大的Event Time,然而这种方式比较暴力,对乱序事件的容忍程度比较低,容易出现大量的迟到事件。比如一个时间间隔内来了 1 3 7 15四个事件,将水位更新为15。结果后面又来了事件10,就认为是迟到事件,默认被抛弃。
8.3.2 TimestampsAndPeriodicWatermarksOperator 源码
-
关键源码1:
StreamExecutionEnvironment#setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
我们通过这个方法设置TimeCharacteristic
为EventTime时,会自动将autoWatermarkInterval
设为200ms:public void setStreamTimeCharacteristic(TimeCharacteristic characteristic) { this.timeCharacteristic = Preconditions.checkNotNull(characteristic); if (characteristic == TimeCharacteristic.ProcessingTime) { getConfig().setAutoWatermarkInterval(0); } else { getConfig().setAutoWatermarkInterval(200); } }
-
关键源码2:
TimestampsAndPeriodicWatermarksOperator
和AbstractUdfStreamOperatorr
构造方法DataStream#assignTimestampsAndWatermarks
方法会利用我们定义的AssignerWithPeriodicWatermarks
来构建TimestampsAndPeriodicWatermarksOperator
。这里贴出了
TimestampsAndPeriodicWatermarksOperato
类定义,他继承了AbstractUdfStreamOperator
,在构造方法里调用了父类userFunction
参数的构造方法,将自定义水位生成器赋值给userFunction
:public class TimestampsAndPeriodicWatermarksOperator<T> extends AbstractUdfStreamOperator<T, AssignerWithPeriodicWatermarks<T>> implements OneInputStreamOperator<T, T>, ProcessingTimeCallback { public TimestampsAndPeriodicWatermarksOperator(AssignerWithPeriodicWatermarks<T> assigner) { super(assigner); this.chainingStrategy = ChainingStrategy.ALWAYS; } } public AbstractUdfStreamOperator(F userFunction) { this.userFunction = requireNonNull(userFunction); checkUdfCheckpointingPreconditions(); }
-
关键源码3:
TimestampsAndPeriodicWatermarksOperator#open
会在初始化算子时调用该方法,这里初始化了当前水位、发送waterMark的时间间隔、向ProcessingTimeService
注册定时发送水位的任务:public void open() throws Exception { super.open(); currentWatermark = Long.MIN_VALUE; watermarkInterval = getExecutionConfig().getAutoWatermarkInterval(); if (watermarkInterval > 0) { long now = getProcessingTimeService().getCurrentProcessingTime(); getProcessingTimeService().registerTimer(now + watermarkInterval, this); } }
-
关键源码4:
TimestampsAndPeriodicWatermarksOperator#processElement
每条记录都会触发调用此方法来更新本记录时间戳,然后将记录发送到下游:@Override public void processElement(StreamRecord<T> element) throws Exception { final long newTimestamp = userFunction.extractTimestamp(element.getValue(), element.hasTimestamp() ? element.getTimestamp() : Long.MIN_VALUE); output.collect(element.replace(element.getValue(), newTimestamp)); }
这里的userFunction就是我们自己实现的
AssignerWithPeriodicWatermarks
。举个例子,比如我们想让周期性水位每次将期间时间戳最大值作为水位发送,可以在userFunction.extractTimestamp
中记录本算子实例收到的EventTime最大值,具体可以看 8.3.3 例子。 -
关键源码5:
TimestampsAndPeriodicWatermarksOperator#onProcessingTime
这是前面提到的向ProcessingTimeService
注册的定时发送水位的任务,调用周期就是autoWatermarkInterval
。会去自定义的AssignerWithPeriodicWatermarks
取当前水位,然后判断是否该水位大于本类保存的currentWatermark
。如果满足,则更新currentWatermark,并向下游发送新水位;如果不满足,则不发送水位,仅注册下个周期触发水位发送逻辑的定时任务。这就印证了水位是不断上升不能下降的概念:
@Override public void onProcessingTime(long timestamp) throws Exception { // register next timer Watermark newWatermark = userFunction.getCurrentWatermark(); if (newWatermark != null && newWatermark.getTimestamp() > currentWatermark) { currentWatermark = newWatermark.getTimestamp(); // emit watermark output.emitWatermark(newWatermark); } long now = getProcessingTimeService().getCurrentProcessingTime(); getProcessingTimeService().registerTimer(now + watermarkInterval, this); }
8.3.3 AssignerWithPeriodicWatermarks例子
注意,AssignerWithPeriodicWatermarks已经在1.11中 deprecated!,因为新的API有了全新的抽象
- 适用于乱序且有相对固定的最大延迟时间的官方例子1,完整的可参考Assigners allowing a fixed amount of lateness和
org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
。简单例子如下:
// 每个窗口都会有该类的一个实例,因此可以利用实例的成员变量保存状态,比如该例中的当前最大时间戳。
class BoundedOutOfOrdernessGenerator extends AssignerWithPeriodicWatermarks[MyEvent] {
// 相当于是允许的最大延迟时间,应对元素乱序
val maxOutOfOrderness = 3500L; // 3.5 seconds
var currentMaxTimestamp: Long;
// Flink会对每个元素调用extractTimestamp方法获取数据的eventTime时间戳
// 所以在两次调用`getCurrentWatermark`的间隔内会更新currentMaxTimestamp为
// 这段autoWatermarkInterval时间内输入元素的最大Timestamp值
override def extractTimestamp(element: MyEvent, previousElementTimestamp: Long): Long = {
val timestamp = element.getCreationTime()
currentMaxTimestamp = max(timestamp, currentMaxTimestamp)
timestamp;
}
// 周期性调用getCurrentWatermark方法获取水位时间戳(默认每200ms被调用一次)
// 用于生成新的水位线,新的水位线只有大于当前水位线才是有效
// 如果返回的水印非空并且大于先前的水印,则将发出新的水印。
override def getCurrentWatermark(): Watermark = {
// return the watermark as current highest timestamp minus the out-of-orderness bound
new Watermark(currentMaxTimestamp - maxOutOfOrderness);
}
}
- 另一个例子
/**
* 生成水位,比ProcessingTime之后一定时间。
* 同时假设元素在一定界限的时延内到达flink
*/
class TimeLagWatermarkGenerator extends AssignerWithPeriodicWatermarks[MyEvent] {
val maxTimeLag = 5000L // 5 seconds
override def extractTimestamp(element: MyEvent, previousElementTimestamp: Long): Long = {
element.getCreationTime
}
override def getCurrentWatermark(): Watermark = {
// return the watermark as current time minus the maximum time lag
new Watermark(System.currentTimeMillis() - maxTimeLag)
}
}
8.3.4 Blink SQL使用的WatermarkAssignerOperator 源码
WatermarkAssignerOperator也属于周期性水位,这里简单分析下源码。
-
关键源码1:
StreamExecutionEnvironment#setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
这和前面提到的相同,会自动将autoWatermarkInterval
设为200ms: -
关键源码2:
WatermarkAssignerOperatorFactory#createStreamOperator
这里会用现编译、加载、生成一个水印分配器WatermarkGenerator的实现对象,然后用来生成WatermarkAssignerOperator
public StreamOperator createStreamOperator(StreamTask containingTask, StreamConfig config, Output output) { WatermarkGenerator watermarkGenerator = generatedWatermarkGenerator.newInstance(containingTask.getUserCodeClassLoader()); WatermarkAssignerOperator operator = new WatermarkAssignerOperator(rowtimeFieldIndex, watermarkGenerator, idleTimeout); operator.setup(containingTask, config, output); return operator; }
-
关键源码3:WatermarkGenerator的实现对象
WatermarkGenerator$2
的代码
这个是动态生成的,会根据我们定义的DDL中EventTime字段顺序、watermark_strategy_expression
不同而略有不同。主要功能是计算DDL中定义的event_time
字段的水位时间。以下是设置
WATERMARK FOR ts AS ts - INTERVAL '0.001' SECOND
时生成的代码:public final class WatermarkGenerator$2 extends org.apache.flink.table.runtime.generated.WatermarkGenerator { public WatermarkGenerator$2(Object[] references) throws Exception {} @Override public void open(org.apache.flink.configuration.Configuration parameters) throws Exception {} @Override public Long currentWatermark(org.apache.flink.table.dataformat.BaseRow row) throws Exception { org.apache.flink.table.dataformat.SqlTimestamp field$3; // 用来判断event_time字段是否为空 boolean isNull$3; boolean isNull$4; org.apache.flink.table.dataformat.SqlTimestamp result$5; // 因为我们测试例子中event_time字段为第3个字段(字段序号从0开始),所以判断第三个是否空 isNull$3 = row.isNullAt(3); field$3 = null; if (!isNull$3) { // event_time字段不为空,就获取该字段的Timestamp(3)类型数据 field$3 = row.getTimestamp(3, 3); } isNull$4 = isNull$3 || false; result$5 = null; if (!isNull$4) { // event_time字段不为空,就将刚才获取到的Timestamp(3)类型数据转为毫秒数后减1,然后构成SqlTimestamp // 精确到纳秒 result$5 = org.apache.flink.table.dataformat.SqlTimestamp.fromEpochMillis(field$3.getMillisecond() - ((long) 1L), field$3.getNanoOfMillisecond()); } if (isNull$4) { return null; } else { // 返回转换后的SqlTimestamp的毫秒数,即计算得到的水位时间 return result$5.getMillisecond(); } } @Override public void close() throws Exception {} }
当设置
WATERMARK FOR ts AS ts
时生成代码略有不同,因为水位直接就用该字段的时间戳转换即可:public Long currentWatermark(org.apache.flink.table.dataformat.BaseRow row) throws Exception { org.apache.flink.table.dataformat.SqlTimestamp field$3; boolean isNull$3; isNull$3 = row.isNullAt(3); field$3 = null; if (!isNull$3) { field$3 = row.getTimestamp(3, 3); } if (isNull$3) { return null; } else { return field$3.getMillisecond(); } }
-
关键源码4:
WatermarkAssignerOperator
构造方法
public WatermarkAssignerOperator(int rowtimeFieldIndex, WatermarkGenerator watermarkGenerator, long idleTimeout) {
this.rowtimeFieldIndex = rowtimeFieldIndex;
this.watermarkGenerator = watermarkGenerator;
// 0
this.idleTimeout = idleTimeout;
// 该算子可以被连接到其他算子尾部和头部
this.chainingStrategy = ChainingStrategy.ALWAYS;
}
- 关键源码5:
WatermarkAssignerOperator#open
会在初始化算子时调用该方法,这里初始化了当前水位currentWatermark
、发送waterMark的时间间隔watermarkInterval
、向ProcessingTimeService
注册定时发送水位的任务:public void open() throws Exception { super.open(); // watermark and timestamp should start from 0 this.currentWatermark = 0; this.watermarkInterval = getExecutionConfig().getAutoWatermarkInterval(); this.lastRecordTime = getProcessingTimeService().getCurrentProcessingTime(); this.streamStatusMaintainer = getContainingTask().getStreamStatusMaintainer(); if (watermarkInterval > 0) { // 水位检测间隔默认为0, // 但只要我们设置大于0或(TimeCharacteristic.EventTime)则会注册`ProcessingTimeService` long now = getProcessingTimeService().getCurrentProcessingTime(); getProcessingTimeService().registerTimer(now + watermarkInterval, this); } // 将StreamingRuntimeContext放入watermarkGenerator FunctionUtils.setFunctionRuntimeContext(watermarkGenerator, getRuntimeContext()); FunctionUtils.openFunction(watermarkGenerator, new Configuration()); }
- 关键源码6:
WatermarkAssignerOperator#advanceWatermark
判断当前水位是否大于之前的水位,大于就更新lastWatermark并发送当前水位到下游
private void advanceWatermark() {
if (currentWatermark > lastWatermark) {
lastWatermark = currentWatermark;
// emit watermark
output.emitWatermark(new Watermark(currentWatermark));
}
}
-
关键源码7:
WatermarkAssignerOperator#processElement
每条记录都会触发调用此方法:- 尝试更新
currentWatermark
(如果本记录时间戳更大), - 将记录发送到下游。
- 如果当前记录水位减去当前系统最后更新水位的差大于水位监测间隔时间,则还会尝试提升水位。
这里为了代码复用调用advanceWatermark
方法内又判断了一次if (currentWatermark > lastWatermark)
,但其实是重复判断?!因为我理解watermarkInterval
始终应该是非负的,也就是说已经判断currentWatermark > lastWatermark
:
public void processElement(StreamRecord<BaseRow> element) throws Exception { if (idleTimeout > 0) { // mark the channel active streamStatusMaintainer.toggleStreamStatus(StreamStatus.ACTIVE); lastRecordTime = getProcessingTimeService().getCurrentProcessingTime(); } // 该条记录 BaseRow row = element.getValue(); if (row.isNullAt(rowtimeFieldIndex)) { throw new RuntimeException("RowTime field should not be null," + " please convert it to a non-null long value."); } // 根据该条记录的event_time字段计算当前水位 Long watermark = watermarkGenerator.currentWatermark(row); if (watermark != null) { // 当前记录水位不为空且大于当前高水位就更新高水位 currentWatermark = Math.max(currentWatermark, watermark); } // 发送该条记录到算子链下游 output.collect(element); // 如果当前计算出的水位减去之前最后更新的水位大于水位监测间隔时间就尝试提升水位 // 在这里也做提升水位的尝试是为了避免在系统负载高(如CPU负载太高)时,不能及时周期性调用onProcessingTime if (currentWatermark - lastWatermark > watermarkInterval) { advanceWatermark(); } }
- 尝试更新
-
关键源码8:
WatermarkAssignerOperator#onProcessingTime
这是前面提到的向ProcessingTimeService
注册的定时发送水位的任务。- 本方法执行是周期性的,调用周期就是
autoWatermarkInterval
。 - 这里会先尝试提升水位。
- 如果设置了
idleTimeout
且检测到系统时间减去处理的最后一条记录时间已经超过阈值idleTimeout,则会将本channel标记空闲,来忽略本channel发送的水印。 - 最后再注册下个周期触发水位发送逻辑的定时任务。这就印证了水位是不断上升不能下降的概念:
public void onProcessingTime(long timestamp) throws Exception { advanceWatermark(); if (idleTimeout > 0) { final long currentTime = getProcessingTimeService().getCurrentProcessingTime(); if (currentTime - lastRecordTime > idleTimeout) { // mark the channel as idle to ignore watermarks from this channel streamStatusMaintainer.toggleStreamStatus(StreamStatus.IDLE); } } // register next timer long now = getProcessingTimeService().getCurrentProcessingTime(); getProcessingTimeService().registerTimer(now + watermarkInterval, this); }
- 本方法执行是周期性的,调用周期就是
-
WatermarkAssignerOperator小结
其实每条记录都会通过设置的event_time字段计算出水位,如果比当前系统记录的currentWatermark
更大就会更新currentWatermark
为计算的值。而有个周期性任务(周期由
autoWatermarkInterval
决定),会判断currentWatermark
是否大于lastWatermark
,大于就更新lastWatermark并发送水位到下游。
8.4 间歇性水位(Punctuated Watermark)
8.4.1 概述
间歇性水位场景下,每个Event都携带EventTime,且某些Event携带特殊标志(如Session结束标志)则可通过Event流中某些特殊标记来决定是否生成、发送新水位。
这种方式下窗口的触发与时间无关,而是决定于何时收到特定标记的Event。
注意:
由于可能每条记录都产生水位,但因为每个水位都会导致下游计算开销,所以过多的水印会降低性能!
8.4.2 源码
-
关键源码1:
TimestampsAndPunctuatedWatermarksOperator#processElement
每条记录都会触发调用此方法来更新本记录时间戳,然后将记录发送到下游。这一步和TimestampsAndPeriodicWatermarksOperator
相同。但不同的是,在发送记录后,还会立刻调用
userFunction.checkAndGetNextWatermark
,按需生成水位。如果生成了水位且水位时间戳大于本对象持有的currentWatermark
,就立刻更新currentWatermark并发送水位给下游。因为
TimestampsAndPunctuatedWatermarksOperator
继承了ProcessingTimeCallback
,通过实现onProcessingTime
方法注册定时水位任务;而本类没有继承ProcessingTimeCallback类,而是通过每条记录调用userFunction.checkAndGetNextWatermark
按需发送水位。public void processElement(StreamRecord<T> element) throws Exception { final T value = element.getValue(); final long newTimestamp = userFunction.extractTimestamp(value, element.hasTimestamp() ? element.getTimestamp() : Long.MIN_VALUE); output.collect(element.replace(element.getValue(), newTimestamp)); final Watermark nextWatermark = userFunction.checkAndGetNextWatermark(value, newTimestamp); if (nextWatermark != null && nextWatermark.getTimestamp() > currentWatermark) { currentWatermark = nextWatermark.getTimestamp(); output.emitWatermark(nextWatermark); } }
8.4.3 AssignerWithPunctuatedWatermarks例子
注意,AssignerWithPunctuatedWatermarks已经在1.11中 deprecated!,因为新的API有了全新的抽象
- 官方例子:
class PunctuatedAssigner extends AssignerWithPunctuatedWatermarks[MyEvent] { override def extractTimestamp(element: MyEvent, previousElementTimestamp: Long): Long = { element.getCreationTime } override def checkAndGetNextWatermark(lastElement: MyEvent, extractedTimestamp: Long): Watermark = { // 遇到拥有水印的event,才发送水印 if (lastElement.hasWatermarkMarker()) new Watermark(extractedTimestamp) else null } }
8.5 递增式水位(Assigner with acending timestamp)
递增式水位其实也是AssignerWithPeriodicWatermarks
周期性水位的一种,只不过Source产生的元素的时间戳单调递增。此时,每个当前时间戳都可以作为水位,表示没有更小的时间戳事件还会再到来。
请注意,这里说的递增,是指只需要每个并行Source任务的时间戳内部递增。 例如,一个并行Source实例读取一个Kafka分区时,则只需要在每个Kafka分区内将时间戳记递增即可。 此时,每当对并行流进行shuffle、union、连接或merge时,Flink的水印合并机制将对用户透明地生成正确的水印!
递增式水位可用以生成完美水印,用于顺序、无界Event流。完美水印的含义就是小于水印时间戳的Event都已到达,大于的还未观测到。
应用有Kafka多分区时有序处理。此时将在Kafka使用者内部针对每个Kafka分区生成水印,并且按与合并水印在Stream Shuffle上相同的方式合并每个分区的水印。如果事件时间戳严格按照每个Kafka分区递增,则使用该递增时间戳水位生成器来为每个分区生成的水印将产生整体的完美水印。
递增式水位可参考类org.apache.flink.streaming.api.functions.timestamps.AscendingTimestampExtractor
,
- 1.10示例代码如下:
val kafkaSource = new FlinkKafkaConsumer09[MyType]("myTopic", schema, props)
kafkaSource.assignTimestampsAndWatermarks(new AscendingTimestampExtractor[MyType] {
def extractAscendingTimestamp(element: MyType): Long = element.eventTimestamp
})
val stream: DataStream[MyType] = env.addSource(kafkaSource)
- 1.11代码如下:
val kafkaSource = new FlinkKafkaConsumer[MyType]("myTopic", schema, props)
kafkaSource.assignTimestampsAndWatermarks(
WatermarkStrategy
.forBoundedOutOfOrderness(Duration.ofSeconds(20)))
val stream: DataStream[MyType] = env.addSource(kafkaSource)
8.6 Flink 1.11新的API抽象
8.6.1 概述
Flink 1.11的API提供了全新的WatermarkStrategy, TimestampAssigner, WatermarkGenerator抽象,统一了周期性和间隙性水位API,更清晰。
8.6.2 一个使用自带的、处理有界乱序的水位用法如下
WatermarkStrategy
.forBoundedOutOfOrderness[(Long, String)](Duration.ofSeconds(20))
.withTimestampAssigner(new SerializableTimestampAssigner[(Long, String)] {
override def extractTimestamp(element: (Long, String), recordTimestamp: Long): Long = element._1
})
8.6.3 处理idle Source
比如某个并行task持续一段时间没有新的输入时,水位无法更新。同时这会导致整个并行水位也无法更新,因为计算时取的并行水位最小值。处理方法:
WatermarkStrategy
.forBoundedOutOfOrderness[(Long, String)](Duration.ofSeconds(20))
// 如果设置了`idleTimeout`且检测到系统时间减去处理的最后一条记录时间已经超过阈值idleTimeout,
// 则会将本channel标记空闲,来忽略本channel发送的水印。
.withIdleness(Duration.ofMinutes(1))
8.6.4 WatermarkGenerator
统一AssignerWithPunctuatedWatermarks和AssignerWithPeriodicWatermarks:
/**
* The {@code WatermarkGenerator} generates watermarks either based on events or
* periodically (in a fixed interval).
*
* <p><b>Note:</b> This WatermarkGenerator subsumes the previous distinction between the
* {@code AssignerWithPunctuatedWatermarks} and the {@code AssignerWithPeriodicWatermarks}.
*/
@Public
public interface WatermarkGenerator<T> {
/**
* Called for every event, allows the watermark generator to examine and remember the
* event timestamps, or to emit a watermark based on the event itself.
*/
void onEvent(T event, long eventTimestamp, WatermarkOutput output);
/**
* Called periodically, and might emit a new watermark, or not.
*
* <p>The interval in which this method is called and Watermarks are generated
* depends on {@link ExecutionConfig#getAutoWatermarkInterval()}.
*/
void onPeriodicEmit(WatermarkOutput output);
}
8.6.5 周期性水位
周期性水位一般用onEvent
方法记录事件EventTime和更新水位,通过onPeriodicEmit
方法周期性(ExecutionConfig.setAutoWatermarkInterval,变量为autoWatermarkInterval,默认200ms)调用发送水位。官方例子如下(Flink还自带了一个类似的BoundedOutOfOrdernessWatermarks
):
- 基于EventTime例子
/**
*
* This generator generates watermarks assuming that elements arrive out of order,
* but only to a certain degree. The latest elements for a certain timestamp t will arrive
* at most n milliseconds after the earliest elements for timestamp t.
*/
class BoundedOutOfOrdernessGenerator extends AssignerWithPeriodicWatermarks[MyEvent] {
val maxOutOfOrderness = 3500L // 3.5 seconds
var currentMaxTimestamp: Long = _
// 每条记录都尝试更新最大时间戳
override def onEvent(element: MyEvent, eventTimestamp: Long): Unit = {
currentMaxTimestamp = max(eventTimestamp, currentMaxTimestamp)
}
// 周期性发送水位,水位为当前最大时间戳减去允许乱序事件
override def onPeriodicEmit(): Unit = {
// emit the watermark as current highest timestamp minus the out-of-orderness bound
output.emitWatermark(new Watermark(currentMaxTimestamp - maxOutOfOrderness - 1));
}
}
- 基于ProcessingTime例子
/**
*
* This generator generates watermarks that are lagging behind processing time by a fixed amount.
* It assumes that elements arrive in Flink after a bounded delay.
*/
class TimeLagWatermarkGenerator extends AssignerWithPeriodicWatermarks[MyEvent] {
val maxTimeLag = 5000L // 5 seconds
override def onEvent(element: MyEvent, eventTimestamp: Long): Unit = {
// don't need to do anything because we work on processing time
}
override def onPeriodicEmit(): Unit = {
output.emitWatermark(new Watermark(System.currentTimeMillis() - maxTimeLag));
}
}
8.6.6 间歇性水位
间歇性水位通过onEvent
方法检测包含水位信息的特定标记的事件或符号,一旦发现就立刻发送水位,一般不会使用onPeriodicEmit
发送水位。
需要注意的是,虽然可以将每个事件都赋予水位,但水位会造成一定开销,所以过多的水位反而会降低整体性能表现。示例如下:
class PunctuatedAssigner extends AssignerWithPunctuatedWatermarks[MyEvent] {
override def onEvent(element: MyEvent, eventTimestamp: Long): Unit = {
if (event.hasWatermarkMarker()) {
output.emitWatermark(new Watermark(event.getWatermarkTimestamp()))
}
}
override def onPeriodicEmit(): Unit = {
// don't need to do anything because we emit in reaction to events above
}
}
8.6.7 自带单调递增WatermarkGenerator
WatermarkStrategy.forMonotonousTimestamps()
8.6.8 自带容忍固定迟到时间的WatermarkGenerator
WatermarkStrategy.forMonotonousTimestamps()
8.7 更多好文
9 算子对Watermark处理
收到水位后,算子必须先将由水位导致的计算全部做完并产生输出,最后再计算、发送水位到下游。
比如
WindowOperator
会先评估所有会被触发的窗口,在计算后将结果下发,最后再发送水位到下游。TwoInputStreamOperator
也差不多,不同的是将所有输入中的最小水位作为当前水位发送。
下图存疑,看了源码其实很多算子实现方式不太相同。
上图中所属timer队列就是前面提到的Flink内部的优先级队列。
相关代码可以参考org.apache.flink.streaming.api.operators.AbstractStreamOperator
,实现类如WindowOperator
,以下方法被算子用来处理收到收到来自上游发送的水位。
public void processWatermark(Watermark mark) throws Exception {
if (timeServiceManager != null) {
// 1. 提升当前算子水位
// 2. 遍历已注册的Timer的优先级队列,调用triggerTarget.onEventTime(timer)
// 3. 比如WindowOperator就是尝试触发window计算
timeServiceManager.advanceWatermark(mark);
}
output.emitWatermark(mark);
}
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);
if (newMin > combinedWatermark) {
combinedWatermark = newMin;
processWatermark(new Watermark(combinedWatermark));
}
}
10 水位传播
11 水位监控
12 时空穿梭
处于调试或审计目的将事件时间调回到过去某个时间点,并重新开始数据处理任务。
参考和转载文档
- Apache Flink 进阶教程(二):Time 深度解析
- 作者:崔星灿
- 出处:Ververica