Flink如何处理乱序数据?

本章主要针对Flink Time中的Event Time、Ingestion Time、Processing Time以及Watermark进行详细讲解。

1 Time

Stream数据中的Time(时间)分为以下3种。

  • Event Time:事件产生的时间,它通常由事件中的时间戳描述。
  • Ingestion Time:事件进入Flink的时间。
  • Processing Time:事件被处理时当前系统的时间。

这几种时间的对应关系如图1所示。

图1 Flink中的3种Time之间的关系

假设原始日志如下。

2019-01-10 10:00:01,134 INFO executor.Executor: Finished task in state 0.0

这条数据进入Flink的时间是2019-01-10 20:00:00,102。

到达Window处理的时间为2019-01-10 20:00:01,100。

如果我们想要统计每分钟内接口调用失败的错误日志个数,使用哪个时间才有意义?因为数据有可能出现延迟,所以使用数据进入Flink的时间或者Window处理的时间,其实是没有意义的,此时使用原始日志中的时间才是有意义的,那才是数据产生的时间。

我们在Flink的Stream程序中处理数据时,默认使用的是哪个时间呢?如何修改呢?默认情况下,Flink在Stream程序中处理数据使用的时间是ProcessingTime,想要修改使用时间可以使用setStreamTimeCharacteristic(),代码如图2所示。

在使用EventTime处理Stream数据的时候会遇到数据乱序的问题,流处理从Event(事件)产生,流经Source,再到Operator,这中间需要一定的时间。虽然大部分情况下,传输到Operator的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络延迟等原因而导致乱序的产生,特别是使用Kafka的时候,多个分区之间的数据无法保证有序。因此,在进行Window计算的时候,不能无限期地等下去,必须要有个机制来保证在特定的时间后,必须触发Window进行计算,这个特别的机制就是Watermark。Watermark是用于处理乱序事件的。

2.1 Watermark

Watermark可以翻译为水位线,有3种应用场景。

  • 有序的Stream中的Watermark,如图3所示。

 

  • 多并行度Stream中的Watermark,如图5所示。

 

2.2 Watermark的生成方式

通常情况下,在接收到Source的数据后,应该立刻生成Watermark,但是也可以在应用简单的Map或者Filter操作后再生成Watermark。

注意:如果指定多次Watermark,后面指定的值会覆盖前面的值。

Watermark的生成方式有两种。

1.With Periodic Watermarks

  • 周期性地触发Watermark的生成和发送,默认是100ms。
  • 每隔N秒自动向流里注入一个Watermark,时间间隔由ExecutionConfig.setAutoWatermarkInterval决定。每次调用getCurrentWatermark方法,如果得到的Watermark不为空并且比之前的大,就注入流中。
  • 可以定义一个最大允许乱序的时间,这种比较常用。
  • 实现AssignerWithPeriodicWatermarks接口。

2.With Punctuated Watermarks

  • 基于某些事件触发Watermark的生成和发送。
  • 基于事件向流里注入一个Watermark,每一个元素都有机会判断是否生成一个Watermark。如果得到的Watermark不为空并且比之前的大,就注入流中。
  • 实现AssignerWithPunctuatedWatermarks接口。

第1种方式比较常用,所以在这里我们使用第1种方式进行分析。

参考官网文档中With Periodic Watermarks的使用方法,如图6所示。

图6 With Periodic Watermarks的使用

图6所示代码中的extractTimestamp方法是从数据本身中提取EventTime。getCurrentWatermar方法是获取当前水位线,利用currentMaxTimestamp - maxOutOfOrderness。maxOutOfOrderness表示是允许数据的最大乱序时间。

在这里也需要实现接口AssignerWithPeriodicWatermarks,参考代码如图7所示。

3 EventTime+Watermark解决乱序数据的案例详解

3.1 实现Watermark的相关代码

1.程序说明

首先通过Socket模拟接收数据,然后使用map函数进行处理,接着调用assignTimestampsAnd-Watermarks方法抽取timestamp并生成Watermark,最后调用Window打印信息来验证Window被触发的时机。

2.代码实现

 
  1. package xuwei.tech.streaming.streamApiDemo;
  2.  
  3. import org.apache.Flink.api.common.functions.MapFunction;
  4. import org.apache.Flink.api.java.tuple.Tuple;
  5. import org.apache.Flink.api.java.tuple.Tuple2;
  6. import org.apache.Flink.streaming.api.TimeCharacteristic;
  7. import org.apache.Flink.streaming.api.DataStream.DataStream;
  8. import org.apache.Flink.streaming.api.environment.StreamExecutionEnvironment;
  9. import org.apache.Flink.streaming.api.functions.AssignerWithPeriodicWatermarks;
  10. import org.apache.Flink.streaming.api.functions.windowing.WindowFunction;
  11. import org.apache.Flink.streaming.api.watermark.Watermark;
  12. import org.apache.Flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
  13. import org.apache.Flink.streaming.api.windowing.time.Time;
  14. import org.apache.Flink.streaming.api.windowing.windows.TimeWindow;
  15. import org.apache.Flink.util.Collector;
  16.  
  17. import javax.annotation.Nullable;
  18. import java.text.SimpleDateFormat;
  19. import java.util.ArrayList;
  20. import java.util.Collections;
  21. import java.util.Iterator;
  22. import java.util.List;
  23.  
  24. /**
  25. * Watermark案例
  26. * Created by xuwei.tech
  27. */
  28. public class StreamingWindowWatermark {
  29.  
  30. public static void main(String[] args) throws Exception {
  31. //定义Socket的端口号
  32. int port = 9000;
  33. //获取运行环境
  34. StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
  35. //设置使用EventTime,默认使用processtime
  36. env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
  37. //设置并行度为1,默认并行度是当前机器的CPU数量
  38. env.setParallelism(1);
  39.  
  40. //连接Socket获取输入的数据
  41. DataStream<String> text = env.socketTextStream("hadoop100", port, "\n");
  42.  
  43. //解析输入的数据
  44. DataStream<Tuple2<String, Long>> inputMap = text.map(new MapFunction<String, Tuple2<String, Long>>() {
  45. @Override
  46. public Tuple2<String, Long> map(String value) throws Exception {
  47. String[] arr = value.split(",");
  48. return new Tuple2<>(arr[0], Long.parseLong(arr[1]));
  49. }
  50. });
  51.  
  52. //抽取timestamp和生成Watermark
  53. DataStream<Tuple2<String, Long>> waterMarkStream = inputMap.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks<Tuple2<String, Long>>() {
  54.  
  55. Long currentMaxTimestamp = 0L;
  56. final Long maxOutOfOrderness = 10000L;// 最大允许的乱序时间是10s
  57.  
  58. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
  59. /**
  60. * 定义生成Watermark的逻辑
  61. * 默认100ms被调用一次
  62. */
  63. @Nullable
  64. @Override
  65. public Watermark getCurrentWatermark() {
  66. return new Watermark(currentMaxTimestamp - maxOutOfOrderness);
  67. }
  68.  
  69. //定义如何提取timestamp
  70. @Override
  71. public long extractTimestamp(Tuple2<String, Long> element, long previousElementTimestamp) {
  72. long timestamp = element.f1;
  73. currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp);
  74. System.out.println("key:"+element.f0+",eventtime:["+element.f1+"|"+sdf.format(element.f1)+"],currentMaxTimestamp:["+currentMaxTimestamp+"|"+
  75. sdf.format(currentMaxTimestamp)+"],watermark:["+getCurrentWatermark().getTimestamp()+"|"+sdf.format(getCurrentWatermark().getTimestamp())+"]");
  76. return timestamp;
  77. }
  78. });
  79.  
  80. //分组,聚合
  81. DataStream<String> window = waterMarkStream.keyBy(0)
  82. .window(TumblingEventTimeWindows.of(Time.seconds(3)))//按照消息的EventTime分配窗口,和调用Time Window效果一样
  83. .apply(new WindowFunction<Tuple2<String, Long>, String, Tuple, TimeWindow>() {
  84. /**
  85. * 对Window内的数据进行排序,保证数据的顺序
  86. * @param tuple
  87. * @param window
  88. * @param input
  89. * @param out
  90. * @throws Exception
  91. */
  92. @Override
  93. public void apply(Tuple tuple, TimeWindow window, Iterable<Tuple2
  94. <String, Long>> input, Collector<String> out) throws Exception {
  95. String key = tuple.toString();
  96. List<Long> arrarList = new ArrayList<Long>();
  97. Iterator<Tuple2<String, Long>> it = input.iterator();
  98. while (it.hasNext()) {
  99. Tuple2<String, Long> next = it.next();
  100. arrarList.add(next.f1);
  101. }
  102. Collections.sort(arrarList);
  103. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
  104. String result = key + "," + arrarList.size() + "," + sdf.format(arrarList.get(0)) + "," + sdf.format(arrarList.get(arrarList.size() - 1))
  105. + "," + sdf.format(window.getStart()) + "," + sdf.format(window.getEnd());
  106. out.collect(result);
  107. }
  108. });
  109. //测试,把结果打印到控制台即可
  110. window.print();
  111.  
  112. //注意:因为Flink是懒加载的,所以必须调用execute方法,这样上面的代码才会执行
  113. env.execute("eventtime-watermark");
  114.  
  115. }

3.程序详解

(1)接收Socket数据。

(2)将每行数据按照逗号分隔,每行数据调用Map转换成Tuple<String,Long>类型。其中Tuple中的第1个元素代表具体的数据,第2个元素代表数据的EventTime。

(3)抽取Timestamp,生成Watermark,允许的最大乱序时间是10s,并打印(Key,EventTime,CurrentMaxTimestamp,Watermark)等信息。

(4)分组聚合,Window窗口大小为3s,输出(Key,窗口内元素个数,窗口内最初元素进入的时间,窗口内最后元素进入的时间,窗口自身开始时间,窗口自身结束时间)。

3.2 通过数据跟踪Watermark的时间

在这里重点查看Watermark和Timestamp的时间,通过数据的输出来确定Window的触发时机。

首先开启Socket,输入第一条数据。

  1. [root@hadoop100 soft]# nc -l 9000
  2. 0001,1538359882000

输出的结果如图8(略)所示。

为了查看方便,我们把输入内

容汇总到表格中,如表1所示。

表1 Watermark输出的结果

此时,Watermark的时间已经落后于CurrentMaxTimeStamp10s了,我们继续输入。

  1. [root@hadoop100 soft]# nc -l 9000
  2. 0001,1538359882000
  3. 0001,1538359886000

此时,输出的结果如图9(略)所示。

 
  1. [root@hadoop100 soft]# nc -l 9000
  2. 0001,1538359882000
  3. 0001,1538359886000
  4. 0001,1538359892000

输出的结果内容如图10(略)所示。

 

本文截选自《Flink入门与实战》

徐葳 著

  • 深入浅出展现Flink技术精髓
  • 力求详细而完整地描述Flink大数据项目实战
  • 从零开始快速掌握Flink的基本原理和核心功能,51CTO学院网课配套教材

本书旨在帮助读者从零开始快速掌握Flink的基本原理与核心功能。本书首先介绍了Flink的基本原理和安装部署,并对Flink中的一些核心API进行了详细分析。然后配套对应的案例分析,分别使用Java代码和Scala代码实现案例。最后通过两个项目演示了Flink在实际工作中的一些应用场景,帮助读者快速掌握Flink开发。
学习本书需要大家具备一些大数据的基础知识,比如Hadoop、Kafka、Redis、Elasticsearch等框架的基本安装和使用。本书也适合对大数据实时计算感兴趣的读者阅读。

学习本书需要大家具备一些大数据的基础知识,例如Hadoop、Kafka、Redis、Elasticsearch等框架的基本安装和使用。本书也适合对大数据实时计算感兴趣的爱好者阅读。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值