生成Timestamp和Watermark 的三个重载方法介绍可参见上一篇博客: Flink assignAscendingTimestamps 生成水印的三个重载方法

之前想研究下Flink是怎么处理乱序的数据,看了相关的源码,加上测试,发现得到了与预期完全不相同的结果。

预期是:乱序到达的数据,flink可以基于数据的事件时间,自动整理数据,依次计算输出

结果是:在assignTimestampsAndWatermarks(assigner: AssignerWithPeriodicWatermarks[T]): DataStream[T]指派timestamp和watermark的情况下,乱序到达的数据:迟到的数据直接从侧边输出了,超前的数据直接结束当前的窗口,开启超前数据对应的窗口,后面到达的正常数据,直接作为迟到数据处理了

在得到上面的结果的过程中,仔细的研究了一下生产Timestamp和Watermark相关的源码。

Flink DataStream API 目前只能通过 assignTimestampsAndWatermarks方法创建时间戳和水印有两种生成模式:

  1、基于事件时间创建每个事件的Timestamp 和 基于事件时间周期性的创建Watermark(默认周期为200ms)

  2、基于事件时间创建每个事件的Timestamp 和 基于事件时间每个事件都创建一个Watermark(如果新的Watermark大于当前的Watermark,才会发出)

事件时间下,事件的Timestamp的创建都是直接依赖于事件携带的事件时间,而Watermark则是基于事件时间生成Watermark,所以有周期性创建Watermark和标记的Watermark(With Punctuated Watermarks)的区分(官网中基于Kafka 的分区时间作为Watermark 也是周期性的生成Watermark,只不过传入的事件时间改为事件在kafka中的timestamp了)

1、周期性的创建Watermark

周期性的创建Watermark的有两种方法(kafka的分区时间的忽略):  

assignTimestamps(extractor: TimestampExtractor[T]): DataStream[T]
assignTimestampsAndWatermarks(assigner: AssignerWithPeriodicWatermarks[T]): DataStream[T]

1.1  assignTimestamps(extractor: TimestampExtractor[T]): DataStream[T] 对应源码

调用方法如下:

.assignAscendingTimestamps(element => {
// 方便打断点debug
println("xxxxxx : " + element.createTime)
sdf.parse(element.createTime).getTime
})

周期性的创建Watermark 是在 TimestampsAndPeriodicWatermarksOperator 中生成、发出,对应的时间来源是调用不同的生成timestamp 和 Watermark 的实现类

TimestampsAndPeriodicWatermarksOperator  相应代码如下:

  /*
处理事件元素: 获取对应的事件时间的时间戳,替换事件默认的时间戳(如果数据源是kafka,时间戳就是数据在kafka中的时间戳)
*/
@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));
}
/*
处理时间(Watermark) : 获取当前时间对应的上一次的事件时间,生成新的watermark,新的watermark的时间戳大于当前的watermark,就发出新的watermark
*/
@Override
public void onProcessingTime(long timestamp) throws Exception {
// 从这里可以看到,每200ms 打印一次
System.out.println("timestamp : " + timestamp + ", system.current : " + System.currentTimeMillis());
// 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();
// 注册timer ,周期性的调用,下面会展开
getProcessingTimeService().registerTimer(now + watermarkInterval, this);
}

在这种生成timestamp 和 Watermark 的情况下,userFunction  对应的类是:AscendingTimestampExtractor

对应源码如下:

@Override
public final long extractTimestamp(T element, long elementPrevTimestamp) {
// 调用 assignAscendingTimestamps 的参数函数
final long newTimestamp = extractAscendingTimestamp(element);
if (newTimestamp >= this.currentTimestamp) {
// 这是为了下面生成Watermark的方法,总能得到 大于等于 当前Watermark的 时间戳
this.currentTimestamp = newTimestamp;
return newTimestamp;
} else {
violationHandler.handleViolation(newTimestamp, this.currentTimestamp);
return newTimestamp;
}
} @Override
public final Watermark getCurrentWatermark() {
return new Watermark(currentTimestamp == Long.MIN_VALUE ? Long.MIN_VALUE : currentTimestamp - 1);
}

timestamp的生成: TimestampsAndPeriodicWatermarksOperator#processElement  方法,调用AscendingTimestampExtractor#extractTimestamp 再调用 用户代码中具体生成timestamp 的方法,最终生成事件对应的timestamp,替换原有的timestamp

Watermark的生成:TimestampsAndPeriodicWatermarksOperator#onProcessingTime 方法,调用 AscendingTimestampExtractor#getCurrentWatermark, 返回生成timestamp 时的 currentTimestamp -1 ,生成  Watermark,如果生成的Watermark的timestamp 大于当前的  Watermark的timestamp 就发出新的Watermark

1.2 assignTimestampsAndWatermarks(assigner: AssignerWithPeriodicWatermarks[T]): DataStream[T]

调用方法如下:

.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[LateDataEvent](Time.milliseconds(50)) {
override def extractTimestamp(element: LateDataEvent): Long = {
println("current timestamp : " + sdf.parse(element.createTime).getTime)
sdf.parse(element.createTime).getTime
}
})

在这种生成timestamp 和 Watermark 的情况下,userFunction  对应的类是:BoundedOutOfOrdernessTimestampExtractor

对应源码

    @Override
public final Watermark getCurrentWatermark() {
// this guarantees that the watermark never goes backwards.
long potentialWM = currentMaxTimestamp - maxOutOfOrderness;
if (potentialWM >= lastEmittedWatermark) {
lastEmittedWatermark = potentialWM;
}
return new Watermark(lastEmittedWatermark);
} @Override
public final long extractTimestamp(T element, long previousElementTimestamp) {
long timestamp = extractTimestamp(element);
if (timestamp > currentMaxTimestamp) {
// 这是为了上面上次Watermark的方法总能获取到 大于等于 当前Watermark的时间戳
currentMaxTimestamp = timestamp;
}
return timestamp;
}

基本上与上面相同,只是这种情况下,生成Watermark会 减去相应的 maxOutOfOrderness (允许延迟时间,就是代码中BoundedOutOfOrdernessTimestampExtractor对应的参数)

之所以说是周期性的,是因为生成Watermark的方法是周期性调用的:

// 注册timer 定期执行
getProcessingTimeService().registerTimer(now + watermarkInterval, this); // 对应 watermarkInterval 来自与系统配置
watermarkInterval = getExecutionConfig().getAutoWatermarkInterval(); // 对应配置, 默认 200ms
env.getConfig.setAutoWatermarkInterval(400)

看代码可知,生成timestamp和Watermark是两条线,timestamp 是每个事件消息都会生成,而Watermark 是周期的

2、标记的Watermark(With Punctuated Watermarks)

这种Watermark的生成只有一种,对应代码如下:

.assignTimestampsAndWatermarks(new AssignerWithPunctuatedWatermarks[LateDataEvent]() {
// check extractTimestamp emitted watermark is non-null and large than previously 生成当前事件的Watermark
override def checkAndGetNextWatermark(lastElement: LateDataEvent, extractedTimestamp: Long): Watermark = {
new Watermark(extractedTimestamp)
} // generate next watermark 生成当前事件的timestamp
override def extractTimestamp(element: LateDataEvent, previousElementTimestamp: Long): Long = {
val eventTime = sdf.parse(element.createTime).getTime
eventTime
}
})

对应上面生成添加时间戳到事件中和发出Watermark  在 TimestampsAndPunctuatedWatermarksOperator中具体如下:

@Override
public void processElement(StreamRecord<T> element) throws Exception {
final T value = element.getValue();
// extractTimestamp 方法就是assignTimestampsAndWatermarks 中的 extractTimestamp 生成事件的时间戳
final long newTimestamp = userFunction.extractTimestamp(value,
element.hasTimestamp() ? element.getTimestamp() : Long.MIN_VALUE); output.collect(element.replace(element.getValue(), newTimestamp));
// checkAndGetNextWatermark 方法就是assignTimestampsAndWatermarks 中的 checkAndGetNextWatermark,检查Watermark
final Watermark nextWatermark = userFunction.checkAndGetNextWatermark(value, newTimestamp);
// 新的Watermark大于当前的Watermark才会发出
if (nextWatermark != null && nextWatermark.getTimestamp() > currentWatermark) {
currentWatermark = nextWatermark.getTimestamp();
output.emitWatermark(nextWatermark);
}
}

这里可以看出,每条数据都会生成 tiemstamp 和 Watermark(不一定会发出,如果数据都是正常的,Watermark的消息会和事件的消息一样多,所以会影响性能)

搞定。

欢迎关注Flink菜鸟公众号,会不定期更新Flink(开发技术)相关的推文

【源码解析】Flink 是如何基于事件时间生成Timestamp和Watermark的更多相关文章

  1. Flink 源码解析 —— Flink JobManager 有什么作用?

    JobManager 的作用 https://t.zsxq.com/2VRrbuf 博客 1.Flink 从0到1学习 -- Apache Flink 介绍 2.Flink 从0到1学习 -- Mac ...

  2. Flink 源码解析 —— Flink TaskManager 有什么作用?

    TaskManager 有什么作用 https://t.zsxq.com/RZbu7yN 博客 1.Flink 从0到1学习 -- Apache Flink 介绍 2.Flink 从0到1学习 -- ...

  3. [源码解析] Flink的groupBy和reduce究竟做了什么

    [源码解析] Flink的groupBy和reduce究竟做了什么 目录 [源码解析] Flink的groupBy和reduce究竟做了什么 0x00 摘要 0x01 问题和概括 1.1 问题 1.2 ...

  4. [源码解析] Flink UDAF 背后做了什么

    [源码解析] Flink UDAF 背后做了什么 目录 [源码解析] Flink UDAF 背后做了什么 0x00 摘要 0x01 概念 1.1 概念 1.2 疑问 1.3 UDAF示例代码 0x02 ...

  5. [源码解析] Flink的Slot究竟是什么?(1)

    [源码解析] Flink的Slot究竟是什么?(1) 目录 [源码解析] Flink的Slot究竟是什么?(1) 0x00 摘要 0x01 概述 & 问题 1.1 Fllink工作原理 1.2 ...

  6. [源码解析] Flink的Slot究竟是什么?(2)

    [源码解析] Flink 的slot究竟是什么?(2) 目录 [源码解析] Flink 的slot究竟是什么?(2) 0x00 摘要 0x01 前文回顾 0x02 注册/更新Slot 2.1 Task ...

  7. 谷歌BERT预训练源码解析(一):训练数据生成

    目录预训练源码结构简介输入输出源码解析参数主函数创建训练实例下一句预测&实例生成随机遮蔽输出结果一览预训练源码结构简介关于BERT,简单来说,它是一个基于Transformer架构,结合遮蔽词 ...

  8. 源码解析-Abp vNext丨分布式事件总线DistributedEventBus

    前言 上一节咱们讲了LocalEventBus,本节来讲本地事件总线(DistributedEventBus),采用的RabbitMQ进行实现. Volo.Abp.EventBus.RabbitMQ模 ...

  9. 【源码解析】Flink 是如何处理迟到数据

    相信会看到这篇文章的都对Flink的时间类型(事件时间.处理时间.摄入时间)和Watermark有些了解,当然不了解可以先看下官网的介绍:https://ci.apache.org/projects/ ...

随机推荐

  1. PHP项目部署 Linux 服务器

    一.运行环境 Centos7 x64 lnmp (Linux , Nginx , Mysql , PHP/Python) 二.安装依赖和修改配置 安装Lnmp环境集成包:https://lnmp.or ...

  2. Java switch语句使用注意事项

    1.多个case后面的值不可以重复 2.switch后面小括号的数据类型(case 后面的数值类型)只能是以下类型 基本数据类型: byte / short / char / int 引用数据类型:S ...

  3. 使用Optional优雅处理null

    先假设一个场景.如下所示 public class Person { private String name; public Person() { } public Person(String nam ...

  4. Mysql 碎片整理与统计信息收集

    ======重新收集统计信息======= 1.分析和存储表的关键字分布 analyze table table_name; analyze 用于收集优化器的统计信息.和tuning相关:对 myis ...

  5. Linux学习建议[转]

    端正学习态度学linux不会为了当黑客或者骇客,如果你为了当黑客或骇客而学习Linux,那么你离进监狱不远了,只是时间早晚而已.很多小白都知道“黑客攻击工具”很多来源与Linux平台上的,我也曾指导过 ...

  6. 100: cf 878C set+并查集+链表

    $des$Berland要举行 $n$ 次锦标赛,第一次只有一个人,之后每一次会新加入一个人.锦标赛中有 $k$ 种运动项目,每个人在这 $k$ 种项目上都有一个能力值,每次会选择任意两个还未被淘汰的 ...

  7. 洛谷 P1950 长方形_NOI导刊2009提高(2) 题解

    P1950 长方形_NOI导刊2009提高(2) 题目描述 小明今天突发奇想,想从一张用过的纸中剪出一个长方形. 为了简化问题,小明做出如下规定: (1)这张纸的长宽分别为n,m.小明讲这张纸看成是由 ...

  8. 洛谷 P2058 海港 题解

    P2058 海港 题目描述 小K是一个海港的海关工作人员,每天都有许多船只到达海港,船上通常有很多来自不同国家的乘客. 小K对这些到达海港的船只非常感兴趣,他按照时间记录下了到达海港的每一艘船只情况: ...

  9. (28)打鸡儿教你Vue.js

    单件商品金额计算和单选全选功能 new Vue({ el: '#app', data: { totalMoney: 0, productList: [] }, filters: { formatMon ...

  10. lp3676 小清新数据结构题

    传送门 Description 有一棵\(n\)个点的树,每个点有一个点权. 现在有\(q\)次操作,每次操作是修改一个点的点权或指定一个点,询问以这个点为根时每棵子树点权和的平方和. Solutio ...