在flink中的时间窗口中有个重要概念,就是watermark,也就是我们经常谈论的水印,这里我们不对水印的概念和使用方式进行介绍,这里从源码的角度来看,如何不断的生成水印。
在flink中,有两种水印TimestampsAndPunctuatedWatermarksOperator
TimestampsAndPeriodicWatermarksOperator
我们编写原因水印的代码如下:
//抽取timestamp和生成watermarkDataStream> waterMarkStream =
inputMap.assignTimestampsAndWatermarks(
new AssignerWithPeriodicWatermarks>() {
Long currentMaxTimestamp = 0L;
final Long maxOutOfOrderness = 10000L; // 最大允许的乱序时间是10s
@Nullable
@Override
public Watermark getCurrentWatermark() {
return new Watermark(currentMaxTimestamp - maxOutOfOrderness);
}
//定义如何提取timestamp@Override
public long extractTimestamp(Tuple2 element, long previousElementTimestamp) {
long timestamp = element.f1;
return timestamp;
}
});
TimestampsAndPunctuatedWatermarksOperator
是一个流运算符,生成水印是根据输入的元素,没输出一个元素,就会输出一个水印,如果不想输出水印,那么就输出一个null,核心代码
public void processElement(StreamRecord element) throws Exception {
final T value = element.getValue();
// 通过用户的代码获取到事件时间,注入到element里面就直接往下个opeartor发送final long newTimestamp = userFunction.extractTimestamp(value,
element.hasTimestamp() ? element.getTimestamp() : Long.MIN_VALUE);
output.collect(element.replace(element.getValue(), newTimestamp));
//通过用户代码获取水印,这里会判断水印是否为null//不为null的就直接往下游emit 了final Watermark nextWatermark = userFunction.checkAndGetNextWatermark(value, newTimestamp);
if (nextWatermark != null && nextWatermark.getTimestamp() > currentWatermark) {
currentWatermark = nextWatermark.getTimestamp();
output.emitWatermark(nextWatermark);
}
}
上面的方法中,我们每一个元素的处理,都会调用processElement 方法,参数就是处理的灭一个元素,方便内部主要做下面几件事:
1、从用户的代码中获取事件时间,然后注入到element中,然后发送到下一个operator中
2、通过用户的代码获取定义的水印,如果水印不为null,那么就emit到下游
根据上面的分析,我们可知,如果存在水印,那么每一个元素后就会输出一个水印。
TimestampsAndPeriodicWatermarksOperator
这也是一个流操作,定义水印的生成方式,从类名字中的periodic,我们可以猜测这是一个周期性生成水印的操作,我们从类中看核心代码:
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);
}
}
open 方法时这个类的初始化方法,我们可以从上面的代码中看到,在open方法中,先从我们的环境配置中获取周期生成水印的时间间隔watermarkInterval ,如果时间间隔大于0,那么就获取当前的时间,然后注册一个process定时器,下次触发的时间是now+watermarkInterval ,从这里我们可以看到,这个类生成水印是需要借助processTime服务的。
// 到了一定的间隔时间 会触发onProcessingTime 这个方法里面的内容@Override
public void onProcessingTime(long timestamp) throws Exception {
// register next timerWatermark 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);
}
onProcessingTime 这个方法时到了定时器触发的时候,会调用这个方法。这个方法主要作用如下:
1、从用户的代码中获取watermark,如果存在watermark,并且时间大于currentWatermark,那么就emit一个水印到下游。
2、获取当前时间now,然后用now+watermarkInterval 继续注册一个process定时器。
public void processElement(StreamRecord element) throws Exception {
// 获取事件时间,然后发送出去final long newTimestamp = userFunction.extractTimestamp(element.getValue(),
element.hasTimestamp() ? element.getTimestamp() : Long.MIN_VALUE);
output.collect(element.replace(element.getValue(), newTimestamp));
}
上面的processElement 方法,就是从用户代码中获取时间,然后注册到element中,输出到下游。
上面我们就分析了flink中的两种水印。