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

这里就会有这样一个问题:FLink 是怎么基于事件时间和Watermark处理迟到数据的呢

在回答这个问题之前,建议大家可以看下下面的Google 的三篇论文,关于流处理的模型:

https://www.vldb.org/pvldb/vol8/p1792-Akidau.pdf 《The Dataflow Model: A Practical Approach to Balancing Correctness, Latency, and Cost in Massive-Scale, Unbounded, Out-of-Order Data Processing》

high-level的现代数据处理概念指引:

https://www.oreilly.com/ideas/the-world-beyond-batch-streaming-101

https://www.oreilly.com/ideas/the-world-beyond-batch-streaming-102

---------------------------进入正题--------------------------------

现在进入正题:FLink 是怎么基于事件时间和Watermark处理迟到数据的呢?

这个问题可以分成两个部分:

  1.   1. 基于事件时间创建Timestamp Watermark(后面会详细介绍)
  2.  
  3.   2. 处理迟到数据

1. 基于事件时间创建Timestamp 和Watermark

为了方便查看,这里使用 assignTimestampsAndWatermarks(assigner: AssignerWithPeriodicWatermarks[T]): DataStream[T]  重载方法基于每个事件生成水印代码如下:

  1. val input = env.addSource(source)
  2. .map(json => {
  3. // json : {"id" : 0, "createTime" : "2019-08-24 11:13:14.942", "amt" : "9.8"}
  4. val id = json.get("id").asText()
  5. val createTime = json.get("createTime").asText()
  6. val amt = json.get("amt").asText()
  7. LateDataEvent("key", id, createTime, amt)
  8. })
  9. // assign watermarks every event
  10. .assignTimestampsAndWatermarks(new AssignerWithPunctuatedWatermarks[LateDataEvent]() {
  11. // check extractTimestamp emitted watermark is non-null and large than previously
  12. override def checkAndGetNextWatermark(lastElement: LateDataEvent, extractedTimestamp: Long): Watermark = {
  13. new Watermark(extractedTimestamp)
  14. }
  15. // generate next watermark
  16. override def extractTimestamp(element: LateDataEvent, previousElementTimestamp: Long): Long = {
  17. val eventTime = sdf.parse(element.createTime).getTime
  18. eventTime
  19. }
  20. })

```

扩展:数据在算子中是以StreamRecord 对象作为流转抽象结构如下:

  1. public final class StreamRecord<T> extends StreamElement {
  2.  
  3. /** The actual value held by this record. 具体数据*/
  4. private T value;
  5.  
  6. /** The timestamp of the record. 该数据对应的时间戳 */
  7. private long timestamp;
  8.  
  9. }

StreamElement 也是 Watermark 和 StreamStatus的父类,简单来说就是Flink 承载消息的基类(这里可以指定,Watermark 是和事件一个级别的抽象,而Timestamp 是Watermark和事件的成员变量,代表Watermark和事件的时间)

```

assignTimestampsAndWatermarks 是基于事件的数据(extractTimestamp 方法中返回的Timestamp),替换StreamRecord 对象中的Timestamp和发出新的Watermark(如果当前事件的Timestamp 生成的Watermark大于上一次的Watermark)

下面我们来debug这部分源码:

首先在extractTimestamp  方法中添加断点查看Timestamp 和Watermark的生成:

  1. TimestampsAndPunctuatedWatermarksOperator.processElement(使用的类取决于assignTimestampsAndWatermarks 方法的参数) 中处理事件的Timestamp和对应的Watermark
  2.  
  3. StreamRecord对象的创建在 StreamSourceContexts.processAndCollectWithTimestamp 中,使用的Timestamp 是数据在kafka的时间,在KafkaFetcher.emitRecord方法中从consumerRecord中获取:

KafkaFetcher.emitRecord   发出从kafka中消费到的数据:

  1. protected void emitRecord(
  2. T record,
  3. KafkaTopicPartitionState<TopicPartition> partition,
  4. long offset,
  5. ConsumerRecord<?, ?> consumerRecord) throws Exception {
  6.  
  7. emitRecordWithTimestamp(record, partition, offset, consumerRecord.timestamp());
  8. }

StreamSourceContexts.processAndCollectWithTimestamp 创建StreamRecord 对象

  1. protected void processAndCollectWithTimestamp(T element, long timestamp) {
  2. output.collect(reuse.replace(element, timestamp)); // 放入真正的事件时间戳
  3. }

下面我们来看 TimestampsAndPunctuatedWatermarksOperator.processElement 的源码

  1. @Override
  2. public void processElement(StreamRecord<T> element) throws Exception {
  3. // 获取这条数据
  4. final T value = element.getValue();
  5. // userFunction 就是代码里面创建的匿名类 AssignerWithPunctuatedWatermarks
  6. // 调用extractTimestamp,获取新的Timestamp
  7. // element.hasTimestamp 有的话就用,没有就给默认值long类型 的最小值
  8. final long newTimestamp = userFunction.extractTimestamp(value,
  9. element.hasTimestamp() ? element.getTimestamp() : Long.MIN_VALUE);
  10. // 使用新的Timestamp 替换StreamRecord 旧的Timestamp
  11. output.collect(element.replace(element.getValue(), newTimestamp));
  12. // 获取下一个Watermark,调用实现的 checkAndGetNextWatermark 方法
  13. final Watermark nextWatermark = userFunction.checkAndGetNextWatermark(value, newTimestamp);
  14. // 如果新的Watermark 大于上一个Watermark 就发出新的
  15. if (nextWatermark != null && nextWatermark.getTimestamp() > currentWatermark) {
  16. currentWatermark = nextWatermark.getTimestamp();
  17. output.emitWatermark(nextWatermark);
  18. }
  19. }

至此Timestamp和Watermark的创建(或者说生成)就好了

2. Flink 处理迟到数据

  为了演示这个功能,在上面的程序中添加了window算子和迟到数据侧边输出的方法 sideOutputLateData,为了方便查看,这里再添加一次全部代码

  1. val source = new FlinkKafkaConsumer[ObjectNode]("late_data", new JsonNodeDeserializationSchema(), Common.getProp)
  2. // 侧边输出的tag
  3. val late = new OutputTag[LateDataEvent]("late")
  4.  
  5. val input = env.addSource(source)
  6. .map(json => {
  7. // json : {"id" : 0, "createTime" : "2019-08-24 11:13:14.942", "amt" : "9.8"}
  8. val id = json.get("id").asText()
  9. val createTime = json.get("createTime").asText()
  10. val amt = json.get("amt").asText()
  11. LateDataEvent("key", id, createTime, amt)
  12. })
  13. // assign watermarks every event
  14. .assignTimestampsAndWatermarks(new AssignerWithPunctuatedWatermarks[LateDataEvent]() {
  15. // check extractTimestamp emitted watermark is non-null and large than previously
  16. override def checkAndGetNextWatermark(lastElement: LateDataEvent, extractedTimestamp: Long): Watermark = {
  17. new Watermark(extractedTimestamp)
  18. }
  19. // generate next watermark
  20. override def extractTimestamp(element: LateDataEvent, previousElementTimestamp: Long): Long = {
  21. val eventTime = sdf.parse(element.createTime).getTime
  22. eventTime
  23. }
  24. })
  25. // after keyBy will have window number of different key
  26. .keyBy("key")
  27. .window(TumblingEventTimeWindows.of(Time.minutes(1)))
  28. // get lateData
  29. .sideOutputLateData(late)
  30. .process(new ProcessWindowFunction[LateDataEvent, LateDataEvent, Tuple, TimeWindow] {
  31. // just for debug window process late data
  32. override def process(key: Tuple, context: Context, elements: Iterable[LateDataEvent], out: Collector[LateDataEvent]): Unit = {
  33. // print window start timestamp & end timestamp & current watermark time
  34. println("window:" + context.window.getStart + "-" + context.window.getEnd + ", currentWatermark : " + context.currentWatermark)
  35. val it = elements.toIterator
  36. while (it.hasNext) {
  37. val current = it.next()
  38. out.collect(current)
  39. }
  40. }
  41. })
  42. // print late data
  43. input.getSideOutput(late).print("late:")
  44. input.print("apply:")
  45. env.execute("LateDataProcess")

代码逻辑很简单,主要是为了加入window算子,process算子是为了方便debug到window算子中

下面开始debug源码:

在process 方法中添加断点:

这次直接从window算子接收上游发过来的数据开始看起:

StreamInputProcessor.processInput方法负责将接收到的事件(数据、Watermark、StreamStatus、LatencyMarker),反序列化为 StreamElement(上文已经说得了,是事件抽象的基类),判断具体是那种消息,分别进行处理

  1. public boolean processInput() throws Exception {
  2.  
  3. while (true) {
  4. if (currentRecordDeserializer != null) {
  5. DeserializationResult result = currentRecordDeserializer.getNextRecord(deserializationDelegate);
  6.  
  7. if (result.isBufferConsumed()) {
  8. currentRecordDeserializer.getCurrentBuffer().recycleBuffer();
  9. currentRecordDeserializer = null;
  10. }
  11.  
  12. if (result.isFullRecord()) {
  13. StreamElement recordOrMark = deserializationDelegate.getInstance();
  14.  
  15. if (recordOrMark.isWatermark()) {
  16. // handle watermark
  17. statusWatermarkValve.inputWatermark(recordOrMark.asWatermark(), currentChannel);
  18. continue;
  19. } else if (recordOrMark.isStreamStatus()) {
  20. // handle stream status
  21. statusWatermarkValve.inputStreamStatus(recordOrMark.asStreamStatus(), currentChannel);
  22. continue;
  23. } else if (recordOrMark.isLatencyMarker()) {
  24. // handle latency marker
  25. synchronized (lock) {
  26. streamOperator.processLatencyMarker(recordOrMark.asLatencyMarker());
  27. }
  28. continue;
  29. } else {
  30. // now we can do the actual processing
  31. StreamRecord<IN> record = recordOrMark.asRecord();
  32. synchronized (lock) {
  33. numRecordsIn.inc();
  34. streamOperator.setKeyContextElement1(record);
  35. streamOperator.processElement(record);
  36. }
  37. return true;
  38. }
  39. }
  40. }
  41.  
  42. }

注:代码比较长,挑选了跟这次主题相关的部分

Watermark:

数据:

这里我们主要看数据的处理逻辑:

  1. // now we can do the actual processing
  2. StreamRecord<IN> record = recordOrMark.asRecord();
  3. synchronized (lock) {
  4. // metric 的Counter,统计有多少条数据进来
  5. numRecordsIn.inc();
  6. // 选择当前的key(类似与数据分区,每个key一个,里面存储自己的states)
  7. streamOperator.setKeyContextElement1(record);
  8. // 真正在进到WindowOperator 中处理数据了
  9. streamOperator.processElement(record);
  10. }
  1. 就到了 WindowOperator.processElement 方法(主要判断逻辑都在这里)
  1. // 判断windowAssigner 是不是MergingWindowAssigner
  2. if (windowAssigner instanceof MergingWindowAssigner)

区分开会话窗口和滑动、跳动窗口的处理逻辑,会话窗口的各个key的窗口是不对齐的

直接到 else部分:

  1. } else {
  2. for (W window: elementWindows) {
  3.  
  4. // drop if the window is already late 判断窗口数据是否迟到
  5. // 是,就直接跳过这条数据,重新处理下一条数据
  6. if (isWindowLate(window)) {
  7. continue;
  8. }

PS: 写了这么久,终于到迟到数据处理的地方了 -_-

下面看下 isWindowLate 部分的处理逻辑:

  1. /**
  2. * Returns {@code true} if the watermark is after the end timestamp plus the allowed lateness
  3. * of the given window.
  4. */
  5. protected boolean isWindowLate(W window) {
  6. // 只有事件时间下,并且 窗口元素的最大时间 + 允许迟到时间 <= 当前Watermark 的时候为true(即当前窗口元素迟到了)
  7. return (windowAssigner.isEventTime() && (cleanupTime(window) <= internalTimerService.currentWatermark()));
  8. }
  9.  
  10. /**
  11. * Returns the cleanup time for a window, which is
  12. * {@code window.maxTimestamp + allowedLateness}. In
  13. * case this leads to a value greater than {@link Long#MAX_VALUE}
  14. * then a cleanup time of {@link Long#MAX_VALUE} is
  15. * returned.
  16. * 返回窗口的cleanup 时间, 窗口的最大时间 + 允许延迟的时间
  17. * @param window the window whose cleanup time we are computing.
  18. */
  19. private long cleanupTime(W window) {
  20. if (windowAssigner.isEventTime()) {
  21. long cleanupTime = window.maxTimestamp() + allowedLateness;
  22. return cleanupTime >= window.maxTimestamp() ? cleanupTime : Long.MAX_VALUE;
  23. } else {
  24. return window.maxTimestamp();
  25. }
  26. }

看一条正常到达的数据

  1. {"id" : 891, "createTime" : "2019-08-24 17:51:44.152", "amt" : "5.6"}

891 这条数据的事件时间是:2019-08-24 17:51:44.152 ,1 分钟的整分窗口,这条数据对应的窗口就是: [2019-08-24 17:51:00.000, 2019-08-24 17:52:000) ,对应的时间戳是 : [1566640260000, 1566640320000) ,当前的Watermark 是 : 1566640294102,窗口数据的最大时间戳大于 当前的Watermark, 不是迟到数据,不跳过。

现在在来看一条迟到的数据

  1. {"id" : 892, "createTime" : "2019-08-24 17:51:54.152", "amt" : "3.6"}

892 这条数据的事件时间是:2019-08-24 17:51:54.152 ,1 分钟的整分窗口,这条数据对应的窗口就是: [2019-08-24 17:51:00.000, 2019-08-24 17:52:000) ,对应的时间戳是 : [1566640260000, 1566640320000) ,当前的Watermark 是 : 1566652224102 ,窗口数据的最大时间戳小于 当前的Watermark, 数据是迟到数据,跳过。

上面就是窗口对迟到数据的处理源码dubug了,到这里就已经讲完Flink 处理迟到数据的两个部分:

  1.   1. 基于事件时间创建Timestamp Watermark(后面会详细介绍)
  2.  
  3.   2. 窗口处理迟到数据

注: 这里加上“窗口”,明确是window 算子做的这些事情

下面在来看下窗口迟到输出的SideOutput ,源码在:WindowOperator.processElement 方法的最后一段:

  1. // side output input event if 事件时间
  2. // element not handled by any window 没有window处理过这条数据,上面isSkippedElement 默认值为true,如果上面判断为迟到数据,isSkippedElement就会为false
  3. // late arriving tag has been set
  4. // windowAssigner is event time and current timestamp + allowed lateness no less than element timestamp
  5. if (isSkippedElement && isElementLate(element)) {
  6. // 设置了 lateDataOutputTag 即window 算子后面的 .sideOutputLateData(late)
  7. if (lateDataOutputTag != null){
  8. sideOutput(element);
  9. } else {
  10. this.numLateRecordsDropped.inc();
  11. }
  12. }
  13.  
  14. /**
  15. * Decide if a record is currently late, based on current watermark and allowed lateness.
  16. * 事件时间,并且 元素的时间戳 + 允许延迟的时间 <= 当前watermark 是为true
  17. * @param element The element to check
  18. * @return The element for which should be considered when sideoutputs
  19. */
  20. protected boolean isElementLate(StreamRecord<IN> element){
  21. return (windowAssigner.isEventTime()) &&
  22. (element.getTimestamp() + allowedLateness <= internalTimerService.currentWatermark());
  23. }
  24.  
  25. /**
  26. * Write skipped late arriving element to SideOutput.
  27. *
  28. * @param element skipped late arriving element to side output
  29. */
  30. protected void sideOutput(StreamRecord<IN> element){
  31. output.collect(lateDataOutputTag, element);
  32. }

搞定

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

【源码解析】Flink 是如何处理迟到数据的更多相关文章

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

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

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

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

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

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

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

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

  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. Hadoop源码解析之: TextInputFormat如何处理跨split的行

    我们知道hadoop将数据给到map进行处理前会使用InputFormat对数据进行两方面的预处理: 对输入数据进行切分,生成一组split,一个split会分发给一个mapper进行处理. 针对每个 ...

  8. [源码解析] GroupReduce,GroupCombine 和 Flink SQL group by

    [源码解析] GroupReduce,GroupCombine和Flink SQL group by 目录 [源码解析] GroupReduce,GroupCombine和Flink SQL grou ...

  9. Flink 源码解析 —— 源码编译运行

    更新一篇知识星球里面的源码分析文章,去年写的,周末自己录了个视频,大家看下效果好吗?如果好的话,后面补录发在知识星球里面的其他源码解析文章. 前言 之前自己本地 clone 了 Flink 的源码,编 ...

随机推荐

  1. Java - MyBites 逆向工程

    逆向工程是什么呢? 说白了就是 mybatis 中提供了一个可以让你从 已经创建好的 数据库中,去通过表名,生成对应类,类属性和XML文件(sql语句). 源码:mybatis_AutoGenerat ...

  2. 使用fastjson 进行jsonObject转实体类对象

    使用fastjson 进行jsonObject转实体类对象   1 <dependency> 2 <groupId>com.alibaba</groupId> 3 ...

  3. JDK、JRE、JVM之间的关系及JDK安装

    JRE (Java Runtime Environment) :是Java程序的运行时环境,包含 JVM 和运行时所需要的 核心类库 .JDK (Java Development Kit):是Java ...

  4. 第4课.vi编辑器

    1.vi编辑器的配置 cd /etc/vim cp vimrc ~/.vimrc cd ~ gedit .vimrc 在.vimrc中加入如下内容: "关闭兼容功能 set nocompat ...

  5. js通过html的url获取参数值

    function getUrlParameter(name){ name = name.replace(/[]/,"\[").replace(/[]/,"\[" ...

  6. 谈MongoDB的应用场景

    转载自:http://blog.csdn.net/adparking/article/details/38727911 MongoDB的应用场景在网上搜索了下,很少介绍关于传统的信息化应用中如何使用M ...

  7. Web API系列(四) 使用ActionFilterAttribute 记录 WebApi Action 请求和返回结果记录

    转自:https://www.cnblogs.com/hnsongbiao/p/7039666.html 需要demo在github中下载: https://github.com/shan333cha ...

  8. mongoDB线上数据库连接报错记录

    报错信息提示:无法在第一次连接时连接到服务器 别的一切正常,经过查询得知,是因为如果电脑没设定固定IP,并且重启情况下可能会导致IP地址更改. 解决方法: 将本机ip地址添加到cluster的白名单即 ...

  9. Cogs 1708. 斐波那契平方和(矩阵乘法)

    斐波那契平方和 ★★☆ 输入文件:fibsqr.in 输出文件:fibsqr.out 简单对比 时间限制:0.5 s 内存限制:128 MB [题目描述] ,对 1000000007 取模.F0=0, ...

  10. Error instantiating class cn.edu.zju.springmvc.pojo.Items with invalid types () or values (). 报错解决方法

    org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.Reflecti ...