文章目录
时间语义
在流处理中一分钟代表什么?
在处理无限的事件流(包含了连续到达的事件),时间成为流处理程序的核心方面。假设我们想要连续的计算结果,可能每分钟就要计算一次。在我们的流处理程序上下文中,一分钟的意思是什么?
考虑一个程序需要分析一款移动端的在线游戏的用户所产生的事件流。游戏中的用户分了组,而应用程序将收集每个小组的活动数据,基于小组中的成员多快达到了游戏设定的目标,然后在游戏中提供奖励。例如额外的生命和用户升级。例如,如果一个小组中的所有用户在一分钟之内都弹出了 500 个泡泡,他们将升一级。Alice 是一个勤奋的玩家,她在每天早晨的通勤时间玩游戏。问题在于 Alice 住在柏林,并且乘地铁去上班。而柏林的地铁手机信号很差。我们设想一个这样的场景,Alice 当她的手机连上网时,开始弹泡泡,然后游戏会将数据发送到我们编写的应用程序中,这时地铁突然进入了隧道,她的手机也断网了。Alice 还在玩这个游戏,而产生的事件将会缓存在手机中。当地铁离开隧道,Alice 的手机又在线了,而手机中缓存的游戏事件将发送到应用程序。我们的应用程序应该如何处理这些数据?在这个场景中一分钟的意思是什么?这个一分钟应该包含 Alice 离线的那段时间吗?下图展示了这个问题。
在线手游是一个简单的场景,展示了应用程序的运算应该取决于事件实际发生的时间,而不是应用程序收到事件的时间。如果我们按照应用程序收到事件的时间来进行处理的话,最糟糕的后果就是,Alice 和她的朋友们再也不玩这个游戏了。但是还有很多时间语义非常关键的应用程序,我们需要保证时间语义的正确性。如果我们只考虑我们在一分钟之内收到了多少数据,我们的结果会变化,因为结果取决于网络连接的速度或处理的速度。相反,定义一分钟之内的事件数量,这个一分钟应该是数据本身的时间。
在 Alice 的这个例子中,流处理程序可能会碰到两个不同的时间概念:处理时间和事件时间。
处理时间
处理时间是处理流的应用程序的机器的本地时钟的时间(墙上时钟)。处理时间的窗口包含了一个时间段内来到机器的所有事件。这个时间段指的是机器的墙上时钟。如下图所示,在 Alice 的这个例子中,处理时间窗口在 Alice 的手机离线的情况下,时间将会继续行走。但这个处理时间窗口将不会收集 Alice 的手机离线时产生的事件。
事件时间
事件时间是流中的事件实际发生的时间。事件时间基于流中的事件所包含的时间戳。通常情况下,在事件进入流处理程序前,事件数据就已经包含了时间戳。下图展示了事件时间窗口将会正确的将事件分发到窗口中去。可以如实反应事情是怎么发生的。即使事件可能存在延迟。
事件时间使得计算结果的过程不需要依赖处理数据的速度。基于事件时间的操作是可以预测的,而计算结果也是确定的。无论流处理程序处理流数据的速度快或是慢,无论事件到达流处理程序的速度快或是慢,事件时间窗口的计算结果都是一样的。
可以处理迟到的事件只是我们使用事件时间所克服的一个挑战而已。普遍存在的事件乱序问题可以使用事件时间得到解决。考虑和 Alice 玩同样游戏的 Bob,他恰好和 Alice 在同一趟地铁上。Alice 和 Bob 虽然玩的游戏一样,但他们的手机信号是不同的运营商提供的。当 Alice 的手机没信号时,Bob 的手机依然有信号,游戏数据可以正常发送出去。
如果使用事件时间,即使碰到了事件乱序到达的情况,我们也可以保证结果的正确性。还有,当我们在处理可以重播的流数据时,由于时间戳的确定性,我们可以快进过去。也就是说,我们可以重播一条流,然后分析历史数据,就好像流中的事件是实时发生一样。另外,我们可以快进历史数据来使我们的应用程序追上现在的事件,然后应用程序仍然是一个实时处理程序,而且业务逻辑不需要改变。
水位线
在对事件时间窗口的讨论中,我们忽略了一个很重要的方面:我们应该怎样去决定何时触发事件时间窗口的计算?也就是说,在我们可以确定一个时间点之前的所有事件都已经到达之前,我们需要等待多久?我们如何知道事件是迟到的?在分布式系统无法准确预测行为的现实条件下,以及外部组件所引发的事件的延迟,以上问题并没有准确的答案。
水位线是全局进度的度量标准。系统可以确信在一个时间点之后,不会有早于这个时间点发生的事件到来了。本质上,水位线提供了一个逻辑时钟,这个逻辑时钟告诉系统当前的事件时间。当一个运算符接收到含有时间 T 的水位线时,这个运算符会认为早于时间 T 的发生的事件已经全部都到达了。对于事件时间窗口和乱序事件的处理,水位线非常重要。运算符一旦接收到水位线,运算符会认为一段时间内发生的所有事件都已经观察到,可以触发针对这段时间内所有事件的计算了。
水位线提供了一种结果可信度和延时之间的妥协。激进的水位线设置可以保证低延迟,但结果的准确性不够。在这种情况下,迟到的事件有可能晚于水位线到达,我们需要编写一些代码来处理迟到事件。另一方面,如果水位线设置的过于宽松,计算的结果准确性会很高,但可能会增加流处理程序不必要的延时。
在很多真实世界的场景里面,系统无法获得足够的知识来完美的确定水位线。在手游这个场景中,我们无法得知一个用户离线时间会有多长,他们可能正在穿越一条隧道,可能正在乘飞机,可能永远不会再玩儿了。水位线无论是用户自定义的或者是自动生成的,在一个分布式系统中追踪全局的时间进度都不是很容易。所以仅仅依靠水位线可能并不是一个很好的主意。流处理系统还需要提供一些机制来处理迟到的元素(在水位线之后到达的事件)。根据应用场景,我们可能需要把迟到事件丢弃掉,或者写到日志里,或者使用迟到事件来更新之前已经计算好的结果。
Flink 的时间属性
Flink 支持三种时间属性,分别是:Event Time、Ingestion Time、Processing Time
EventTime:事件创建的时间
IngestionTime:事件进入 Flink 的时间
ProcessingTime:执行算子的本地系统时间,与机器相关
事件时间演示
Demo 1
package com.at.watermark;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import java.sql.Timestamp;
import java.time.Duration;
import java.util.Iterator;
/**
* @create 2023-02-19
*/
public class EventTimeLaterDataDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// Flink 1.12 默认使用的是 事件时间,具体的操作通过水位线抽取定义
// env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
// 通过 socket 获取数据源
DataStreamSource<String> socketTextStream = env.socketTextStream("hadoop102", 9099);
// 将事件转为 Tuple2 ,f0:words,f1:时间戳 ms
SingleOutputStreamOperator<Tuple2<String, Long>> mapStream = socketTextStream.map(new MapFunction<String, Tuple2<String, Long>>() {
@Override
public Tuple2<String, Long> map(String value) throws Exception {
String[] elems = value.split(" ");
return Tuple2.of(elems[0], Long.parseLong(elems[1]) * 1000L);
}
});
// 抽取 watermark 时间×
SingleOutputStreamOperator<Tuple2<String, Long>> withWatermarkStream = mapStream.assignTimestampsAndWatermarks(
WatermarkStrategy
// 最大迟到时间 1 s
.<Tuple2<String, Long>>forBoundedOutOfOrderness(Duration.ofSeconds(1))
.withTimestampAssigner(
new SerializableTimestampAssigner<Tuple2<String, Long>>() {
@Override
public long extractTimestamp(Tuple2<String, Long> element, long recordTimestamp) {
// 抽取事件中第二个元素为时间戳字段,必须为毫秒
return element.f1;
}
}
)
);
// 按第一个字段分组
KeyedStream<Tuple2<String, Long>, String> keyedStream = withWatermarkStream.keyBy(e -> e.f0);
// 为分组后的事件划分一个 5 s 的滚动事件时间窗口
WindowedStream<Tuple2<String, Long>, String, TimeWindow> windowedStream = keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(5)));
// 统计窗口中元素的个数
SingleOutputStreamOperator<String> countWinProcessStream = windowedStream.process(new ProcessWindowFunction<Tuple2<String, Long>, String, String, TimeWindow>() {
@Override
public void process(String key, Context context, Iterable<Tuple2<String, Long>> elements, Collector<String> out) throws Exception {
System.out.println("当前水位线:" + context.currentWatermark());
Timestamp winStart = new Timestamp(context.window().getStart());
Timestamp winEnd = new Timestamp(context.window().getEnd());
int count = 0;
Iterator<Tuple2<String, Long>> iterator = elements.iterator();
while (iterator.hasNext()) {
iterator.next();
count++;
}
out.collect("key = " + key + "\t window [ " + winStart + " - " + winEnd + " ) 有 " + count + " 条元素");
}
});
// sink
countWinProcessStream.print();
// 执行
env.execute();
}
}
[zero@hadoop102 ~]$ nc -lk 9099
a 1
a 2
a 5
a 4
a 6
> 当前水位线:4999
> key = a window [ 1970-01-01 08:00:00.0 - 1970-01-01 08:00:05.0 ) 有 3 条元素
a 7
a 8
a 10
a 11
> 当前水位线:9999
> key = a window [ 1970-01-01 08:00:05.0 - 1970-01-01 08:00:10.0 ) 有 4 条元素
通过演示我们可以发现:
1.窗口是一个左闭右开的区间
2.迟到的事件默认处理策略是丢弃
Demo 2
package com.at.watermark;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import java.sql.Timestamp;
import java.time.Duration;
import java.util.Iterator;
/**
* @create 2023-02-19
*/
public class EventTimeLaterDataDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// Flink 1.12 默认使用的是 事件时间,具体的操作通过水位线抽取定义
// env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
// 通过 socket 获取数据源
DataStreamSource<String> socketTextStream = env.socketTextStream("hadoop102", 9099);
// 将事件转为 Tuple2 ,f0:words,f1:时间戳 ms
SingleOutputStreamOperator<Tuple2<String, Long>> mapStream = socketTextStream.map(new MapFunction<String, Tuple2<String, Long>>() {
@Override
public Tuple2<String, Long> map(String value) throws Exception {
String[] elems = value.split(" ");
return Tuple2.of(elems[0], Long.parseLong(elems[1]) * 1000L);
}
});
// 抽取 watermark 时间×
SingleOutputStreamOperator<Tuple2<String, Long>> withWatermarkStream = mapStream.assignTimestampsAndWatermarks(
WatermarkStrategy
// 最大迟到时间 1 s
.<Tuple2<String, Long>>forBoundedOutOfOrderness(Duration.ofSeconds(1))
.withTimestampAssigner(
new SerializableTimestampAssigner<Tuple2<String, Long>>() {
@Override
public long extractTimestamp(Tuple2<String, Long> element, long recordTimestamp) {
// 抽取事件中第二个元素为时间戳字段,必须为毫秒
return element.f1;
}
}
)
);
// 按第一个字段分组
KeyedStream<Tuple2<String, Long>, String> keyedStream = withWatermarkStream.keyBy(e -> e.f0);
// 为分组后的事件划分一个 5 s 的滚动事件时间窗口
WindowedStream<Tuple2<String, Long>, String, TimeWindow> windowedStream = keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(5)));
// 统计窗口中元素的个数
SingleOutputStreamOperator<String> countWinProcessStream = windowedStream.process(new ProcessWindowFunction<Tuple2<String, Long>, String, String, TimeWindow>() {
@Override
public void process(String key, Context context, Iterable<Tuple2<String, Long>> elements, Collector<String> out) throws Exception {
System.out.println("当前水位线:" + context.currentWatermark());
Timestamp winStart = new Timestamp(context.window().getStart());
Timestamp winEnd = new Timestamp(context.window().getEnd());
int count = 0;
Iterator<Tuple2<String, Long>> iterator = elements.iterator();
while (iterator.hasNext()) {
iterator.next();
count++;
}
out.collect("key = " + key + "\t window [ " + winStart + " - " + winEnd + " ) 有 " + count + " 条元素");
}
});
// sink
countWinProcessStream.print();
// 执行
env.execute();
}
}
[zero@hadoop102 ~]$ nc -lk 9099
a 1
a 2
a 5
a 4
a 6
> 当前水位线:4999
> key = a window [ 1970-01-01 08:00:00.0 - 1970-01-01 08:00:05.0 ) 有 3 条元素
a 7
a 8
a 10
a 11
> 当前水位线:9999
> key = a window [ 1970-01-01 08:00:05.0 - 1970-01-01 08:00:10.0 ) 有 4 条元素
通过示例可知:
1.水位线 = 当前窗口观察到的最大时间 - 最大延迟时间 - 1 ms
2.设置迟到时间可以更精确的处理事件,但是会增加看到数据的延时
Demo 3
public class AssignWatermarkConsumer {
public static void main(String[] args) throws Exception{
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
env
.addSource(new ClickSource())
.filter(event -> "./home".equals(event.url))
.assignTimestampsAndWatermarks(new WatermarkStrategy<Event>() {
// 自定义 watermark 生成
@Override
public TimestampAssigner<Event> createTimestampAssigner(TimestampAssignerSupplier.Context context) {
return new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event element, long recordTimestamp) {
// 指定 程序数据源里的时间戳是哪一个字段
return element.timestamp;
}
};
}
@Override
public WatermarkGenerator<Event> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
return new WatermarkGenerator<Event>() {
// max delay time 5s
private Long delayTime = 5000L;
// 系统观察的的最大时间戳 防止越界
private Long maxTs = - Long.MAX_VALUE + delayTime + 1L;
@Override
public void onEvent(Event event, long eventTimestamp, WatermarkOutput output) {
// 每来一条数据 更新最大时间戳
maxTs = Math.max(event.timestamp,maxTs);
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// 发送 watermark
// 默认 200ms 调用一次
output.emitWatermark(new Watermark(maxTs - delayTime - 1L));
}
};
}
})
.print();
env.execute();
}
}
Watermark 源码
watermark 也是一个事件,它用来标识应该何时触发窗口计算,在 Watermark 类中有用的信息就一个 timestamp
@PublicEvolving
public final class Watermark extends StreamElement {
public static final Watermark MAX_WATERMARK = new Watermark(Long.MAX_VALUE);
public static final Watermark UNINITIALIZED = new Watermark(Long.MIN_VALUE);
// ------------------------------------------------------------------------
/** The timestamp of the watermark in milliseconds. */
private final long timestamp;
/** Creates a new watermark with the given timestamp in milliseconds. */
public Watermark(long timestamp) {
this.timestamp = timestamp;
}
/** Returns the timestamp associated with this {@link Watermark} in milliseconds. */
public long getTimestamp() {
return timestamp;
}
}
assignTimestampsAndWatermarks( ) 会产生一个新的 DataStream,同时会创建一个TimestampsAndWatermarksTransformation
/**
* Assigns timestamps to the elements in the data stream and generates watermarks to signal
* event time progress. The given {@link WatermarkStrategy} is used to create a {@link
* TimestampAssigner} and {@link WatermarkGenerator}.
*
* <p>For each event in the data stream, the {@link TimestampAssigner#extractTimestamp(Object,
* long)} method is called to assign an event timestamp.
*
* <p>For each event in the data stream, the {@link WatermarkGenerator#onEvent(Object, long,
* WatermarkOutput)} will be called.
*
* <p>Periodically (defined by the {@link ExecutionConfig#getAutoWatermarkInterval()}), the
* {@link WatermarkGenerator#onPeriodicEmit(WatermarkOutput)} method will be called.
*
* <p>Common watermark generation patterns can be found as static methods in the {@link
* org.apache.flink.api.common.eventtime.WatermarkStrategy} class.
*
* @param watermarkStrategy The strategy to generate watermarks based on event timestamps.
* @return The stream after the transformation, with assigned timestamps and watermarks.
*/
public SingleOutputStreamOperator<T> assignTimestampsAndWatermarks(
WatermarkStrategy<T> watermarkStrategy) {
final WatermarkStrategy<T> cleanedStrategy = clean(watermarkStrategy);
// match parallelism to input, to have a 1:1 source -> timestamps/watermarks relationship
// and chain
final int inputParallelism = getTransformation().getParallelism();
final TimestampsAndWatermarksTransformation<T> transformation =
new TimestampsAndWatermarksTransformation<>(
"Timestamps/Watermarks",
inputParallelism,
getTransformation(),
cleanedStrategy);
getExecutionEnvironment().addOperator(transformation);
return new SingleOutputStreamOperator<>(getExecutionEnvironment(), transformation);
}
通过示例可知,程序中通过 assignTimestampsAndWatermarks 定义水位线的抽取,通过 WatermarkStrategy 获取 watermark 具体的抽取策略
Watermark 的抽取策略
watermark 的抽取策略主要有两种
AssignerWithPeriodicWatermarksAdapter:基于周期性的,默认 200 ms 插入一次 watermark
AssignerWithPunctuatedWatermarksAdapter:非周期性的
public class AssignAscendingWatermark {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 每隔1分钟插入一次水位线 默认200ms
env.getConfig().setAutoWatermarkInterval(60 * 1000L);
env
.addSource(new ClickSource())
// 分配升序时间戳
.assignTimestampsAndWatermarks(
WatermarkStrategy.forMonotonousTimestamps()
)
.print();
env.execute();
}
}
public class AssignWatermarkConsumerForPunctuated {
public static void main(String[] args) throws Exception {
// 自定义 非周期性 watermark
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
env
.addSource(new ClickSource())
.assignTimestampsAndWatermarks(
new WatermarkStrategy<Event>() {
@Override
public TimestampAssigner<Event> createTimestampAssigner(TimestampAssignerSupplier.Context context) {
return new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event element, long recordTimestamp) {
return element.timestamp;
}
};
}
@Override
public WatermarkGenerator<Event> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
return new WatermarkGenerator<Event>() {
@Override
public void onEvent(Event event, long eventTimestamp, WatermarkOutput output) {
// Alice click event emit watermark
if ("Alice".equals(event.user)) {
output.emitWatermark(new Watermark(event.timestamp - 1L));
}
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// 非周期性发送 watermark 该方法无需任何操作
}
};
}
}
)
.print();
env.execute();
}
}
发送 watermark
watermark 的抽取逻辑同样会被定义为一个算子,在 org.apache.flink.streaming.api.datastream.DataStream#assignTimestampsAndWatermarks(org.apache.flink.api.common.eventtime.WatermarkStrategy) 方法中创建后会被放入 StreamExecutionEnvironment 的 Transformation 集合中(在后续的 Flink 设计实现与原理中将详细讲解),其算子包装具体由 TimestampsAndWatermarksOperator 类实现
在 TimestampsAndWatermarksOperator#open() 方法中定义一个定时器并将其放入队列中,交由线程池定时调度,基于周期性的 watermark 通常由 BoundedOutOfOrdernessWatermarks 实现,通过 onPeriodicEmit 方法向下游发送水位线,最后将 watermark 发送到下游所有算子
# org.apache.flink.streaming.runtime.operators.TimestampsAndWatermarksOperator#open
public void open() throws Exception {
super.open();
timestampAssigner = watermarkStrategy.createTimestampAssigner(this::getMetricGroup);
watermarkGenerator =
emitProgressiveWatermarks
? watermarkStrategy.createWatermarkGenerator(this::getMetricGroup)
: new NoWatermarksGenerator<>();
wmOutput = new WatermarkEmitter(output);
watermarkInterval = getExecutionConfig().getAutoWatermarkInterval();
if (watermarkInterval > 0 && emitProgressiveWatermarks) {
// 获取系统当前时间
final long now = getProcessingTimeService().getCurrentProcessingTime();
// 注册一个定时器
// org.apache.flink.api.common.ExecutionConfig#autoWatermarkInterval = 200
getProcessingTimeService().registerTimer(now + watermarkInterval, this);
}
}
@Override
public void onProcessingTime(long timestamp) throws Exception {
watermarkGenerator.onPeriodicEmit(wmOutput);
final long now = getProcessingTimeService().getCurrentProcessingTime();
getProcessingTimeService().registerTimer(now + watermarkInterval, this);
}
# org.apache.flink.streaming.runtime.tasks.SystemProcessingTimeService#registerTimer
/**
* Registers a task to be executed no sooner than time {@code timestamp}, but without strong
* guarantees of order.
*
* @param timestamp Time when the task is to be enabled (in processing time)
* @param callback The task to be executed
* @return The future that represents the scheduled task. This always returns some future, even
* if the timer was shut down
*/
@Override
public ScheduledFuture<?> registerTimer(long timestamp, ProcessingTimeCallback callback) {
long delay =
ProcessingTimeServiceUtil.getProcessingTimeDelay(
timestamp, getCurrentProcessingTime());
// we directly try to register the timer and only react to the status on exception
// that way we save unnecessary volatile accesses for each timer
try {
return timerService.schedule(
wrapOnTimerCallback(callback, timestamp), delay, TimeUnit.MILLISECONDS);
} catch (RejectedExecutionException e) {
final int status = this.status.get();
if (status == STATUS_QUIESCED) {
return new NeverCompleteFuture(delay);
} else if (status == STATUS_SHUTDOWN) {
throw new IllegalStateException("Timer service is shut down");
} else {
// something else happened, so propagate the exception
throw e;
}
}
}
# java.util.concurrent.ScheduledThreadPoolExecutor#delayedExecute
private void delayedExecute(RunnableScheduledFuture<?> task) {
if (isShutdown())
reject(task);
else {
super.getQueue().add(task);
if (!canRunInCurrentRunState(task) && remove(task))
task.cancel(false);
else
ensurePrestart();
}
}
# org.apache.flink.api.common.eventtime.BoundedOutOfOrdernessWatermarks#BoundedOutOfOrdernessWatermarks
/**
* Creates a new watermark generator with the given out-of-orderness bound.
*
* @param maxOutOfOrderness The bound for the out-of-orderness of the event timestamps.
*/
public BoundedOutOfOrdernessWatermarks(Duration maxOutOfOrderness) {
checkNotNull(maxOutOfOrderness, "maxOutOfOrderness");
checkArgument(!maxOutOfOrderness.isNegative(), "maxOutOfOrderness cannot be negative");
this.outOfOrdernessMillis = maxOutOfOrderness.toMillis();
// start so that our lowest watermark would be Long.MIN_VALUE.
// 初始化
this.maxTimestamp = Long.MIN_VALUE + outOfOrdernessMillis + 1;
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// 发送 watermark
output.emitWatermark(new Watermark(maxTimestamp - outOfOrdernessMillis - 1));
}
# org.apache.flink.streaming.runtime.operators.TimestampsAndWatermarksOperator.WatermarkEmitter#emitWatermark
public void emitWatermark(org.apache.flink.api.common.eventtime.Watermark watermark) {
long ts = watermark.getTimestamp();
if (ts > this.currentWatermark) {
this.currentWatermark = ts;
this.markActive();
this.output.emitWatermark(new Watermark(ts));
}
}
# org.apache.flink.streaming.runtime.io.RecordWriterOutput#emitWatermark
public void emitWatermark(Watermark mark) {
if (announcedStatus.isIdle()) {
return;
}
watermarkGauge.setCurrentWatermark(mark.getTimestamp());
serializationDelegate.setInstance(mark);
try {
// 将 watermark 发送到下游所有算子
recordWriter.broadcastEmit(serializationDelegate);
} catch (IOException e) {
throw new UncheckedIOException(e.getMessage(), e);
}
}
下游接收 watermark
当上游一个 task 将 watermark 广播到下游的所有 channel(可以理解成下游所有task)之后,下游的 task 会更新对上游 inputChannel 记录状态信息中的 watermark 值,下游每个 task 都记录这上游所有 task 的状态值。然后下游 task 再从所有上游 inputChannel(即上游所有task)中选出一个最小值的 watermark,如果这个watermark 大于最近已经发送的 watermark,那么就调用 outputHandler 对新 watermark 进行处理。
# org.apache.flink.streaming.runtime.io.StreamOneInputProcessor#processInput
public DataInputStatus processInput() throws Exception {
DataInputStatus status = input.emitNext(output);
if (status == DataInputStatus.END_OF_DATA) {
endOfInputAware.endInput(input.getInputIndex() + 1);
output = new FinishedDataOutput<>();
} else if (status == DataInputStatus.END_OF_RECOVERY) {
if (input instanceof RecoverableStreamTaskInput) {
input = ((RecoverableStreamTaskInput<IN>) input).finishRecovery();
}
return DataInputStatus.MORE_AVAILABLE;
}
return status;
}
# org.apache.flink.streaming.runtime.io.AbstractStreamTaskNetworkInput#processElement
// watermark 对齐操作
private void processElement(StreamElement recordOrMark, DataOutput<T> output) throws Exception {
if (recordOrMark.isRecord()) {
output.emitRecord(recordOrMark.asRecord());
} else if (recordOrMark.isWatermark()) {
statusWatermarkValve.inputWatermark(
recordOrMark.asWatermark(), flattenedChannelIndices.get(lastChannel), output);
} else if (recordOrMark.isLatencyMarker()) {
output.emitLatencyMarker(recordOrMark.asLatencyMarker());
} else if (recordOrMark.isWatermarkStatus()) {
statusWatermarkValve.inputWatermarkStatus(
recordOrMark.asWatermarkStatus(),
flattenedChannelIndices.get(lastChannel),
output);
} else {
throw new UnsupportedOperationException("Unknown type of StreamElement");
}
}
总结
Watermark 是一种衡量 Event Time 进展的机制,可以设定延迟触
Watermark 是用于处理乱序事件的,而正确的处理乱序事件,通常用 Watermark 机制结合 window 来实现
数据流中的 Watermark 用于表示 timestamp 小于 Watermark 的数据,都已经到达了,因此,window 的执行也是由 Watermark 触发的
watermark = 窗口观察到的最大时间 - 最大延迟时间 - 1ms
Watermark 用来让程序自己平衡延迟和结果正确性
Watermark 是一条特殊的数据记录
Watermark 必须单调递增,以确保任务的事件时间时钟在向前推进,而不是在后退
考资料
https://nightlies.apache.org/flink/flink-docs-release-1.16/docs/learn-flink/streaming_analytics/
https://nightlies.apache.org/flink/flink-docs-release-1.16/docs/dev/datastream/event-time/generating_watermarks/
https://github.com/confucianzuoyuan