一、Watermark简介
Watermark是一种衡量Event Time进展的机制,它是数据本身的一个隐藏属性。通常基于Event Time的数据,自身都包含一个timestamp.watermark是用于处理乱序事件的,而正确的处理乱序事件,通常用watermark机制结合window来实现。简单来说,我们可以把他理解为一个水位线,这个Watermarks在不断的变化,一旦Watermarks大于了某个window的end_time,就会触发此window的计算,Watermarks就是用来触发window计算的
在实际的生产中,由于业务系统背压或网络延迟导致事件的创建时间和处理时间不一致,导致流处理的结果跟实际结果有较大的差异。但是对于延迟数据,我们又不能无限期的等下去,必须要有个机制来保证一个特定的时间后,必须触发window去进行计算了。这个特别的机制,就是watermark。其作用定义一个最大乱序时间,举个例子,比如某条消息记录时间为2020-04-06 10:00:10,如果乱序最大允许时间为10s,那么就认为2020-04-06 10:00:00之前产生的所有消息记录都到齐了,可以进行计算。
二、Watermark生成方式
一般情况下,Flink在接收到Source数据后,应该立即生成Watermark,但是我们可以在经过了简单的operator后再生成Watermark,需要注意的是,如果多次生成了Watermark,后面的会覆盖前面的;生成watermark的方式主要有2大类:
1).Periodic - 一定时间间隔或者达到一定的记录条数会产生一个watermark,通常这种用的比较多。
2).Punctuated – 基于event time通过一定的逻辑产生watermark,比如收到一个数据就产生一个WaterMark,时间是event time + 10秒。
以上两种产生方式,都有机制来保证产生的watermark是单调递增的。即使有了watermark,如果现实中,数据没有满足watermark所保证的条件怎么办?比如Flink处理了08:01的watermark,但是之后遇到了event time是07:00~08:00之间的数据怎么办?首先如果这种事情出现的概率非常小,不影响所要求的准确度,可以直接把数据丢弃;如果这种事情出现的概率比较大,就要调整产生water mark的机制了。
除了把违反watermark机制的数据丢弃,也有不丢弃的处理方法,比如通过一些机制来更新之前统计的结果,这种方式会有一定的性能开销。
三、Watermark代码实例
下面我们通过一个实例来演示下Periodic Watermark,我们从socket接收数据,然后讲过简单的operator之后,立刻抽取timetamp并生成Watermark,之后应用window来看看watermark和event time如何变化,才导致window被触发计算的。
先把主函数代码贴一下,仔细看代码中的注释:
package com.hadoop.ljs.flink110.window;import org.apache.flink.api.common.functions.FilterFunction;import org.apache.flink.api.common.functions.MapFunction;import org.apache.flink.api.java.tuple.Tuple3;import org.apache.flink.streaming.api.TimeCharacteristic;import org.apache.flink.streaming.api.datastream.DataStream;import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks;import org.apache.flink.streaming.api.watermark.Watermark;import org.apache.flink.streaming.api.windowing.time.Time;import javax.annotation.Nullable;/** * @author: Created By lujisen * @company ChinaUnicom Software JiNan * @date: 2020-04-06 15:54 * @version: v1.0 * @description: com.hadoop.ljs.flink110.window * 在实现滚动窗口 设置EventTime为时间处理标准,统计每个窗口单词出现次数 窗口时间是30秒,消息的最大延迟时间是5秒 */public class TumblingWindowWatermarkWordCount { public static void main(String[] args) throws Exception { StreamExecutionEnvironment senv = StreamExecutionEnvironment.getExecutionEnvironment(); /*设置使用EventTime作为Flink的时间处理标准,不指定默认是ProcessTime*/ senv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime); //这里为了便于理解,设置并行度为1,默认并行度是当前机器的cpu数量 senv.setParallelism(1); /*指定数据源 从socket的9000端口接收数据,先进行了不合法数据的过滤*/ DataStream sourceDS = senv.socketTextStream("localhost", 9000) .filter(new FilterFunction() { @Override public boolean filter(String line) throws Exception { if(null==line||"".equals(line)) { return false; } String[] lines = line.split(","); if(lines.length!=2){ return false; } return true; } }); /*做了一个简单的map转换,将数据转换成Tuple2格式,第一个字段代表是时间 第二个字段代表的是单词,第三个字段固定值出现了1次*/ DataStream> wordDS = sourceDS.map(new MapFunction>() { @Override public Tuple3map(String line) throws Exception { String[] lines = line.split(","); return new Tuple3(Long.valueOf(lines[0]), lines[1],1); } }); /*设置Watermark的生成方式为Periodic Watermark,并实现他的两个函数getCurrentWatermark和extractTimestamp*/ DataStream> wordCount = wordDS.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks>() { private Long currentMaxTimestamp = 0L; /*最大允许的消息延迟是5秒*/ private final Long maxOutOfOrderness = 5000L; @Nullable @Override public Watermark getCurrentWatermark() { return new Watermark(currentMaxTimestamp - maxOutOfOrderness); } @Override public long extractTimestamp(Tuple3 element, long previousElementTimestamp) { long timestamp = element.f0; currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp); return timestamp; } /*这里根据第二个元素 单词进行统计 时间窗口是30秒 最大延时是5秒,统计每个窗口单词出现的次数*/ }).keyBy(1) /*时间窗口是30秒*/ .timeWindow(Time.seconds(30)) .sum(2); wordCount.print("\n单词统计:"); senv.execute("Window WordCount"); }}
下面我简单演示下计算步骤:
1.这里时间窗口是30秒,延迟时间是5秒,我这里先后输入一下数据,第一个字段代表是EventTime,第二个是出现的单词,
10000,a20000,b29999,c30000,a34998,a
这里时间窗口是30秒,Flink的时间窗口是左闭右开的[0,30000),如果这里我们设置消息延迟是0秒,输入29999,c 就应该触发,窗口计算, 可是这里我们设计的最大延迟是5秒,那什么时候触发第一次窗口计算呢?应该是29999+5000=34999,如下图所示,输入34998,a没有触发计算,跟我们的推测是一致的,如下图所示:
2.接下来我们继续输入,34999,b应该会第一次触发窗口计算进行数据的输出,至此我们应该输入了6行数据,那输出的计算结果是什么呢?这里应该是输出[0,30000)时间段之间的数据,如下图所示:
3.接下来我们还要验证两件事,一是第二次窗口触发时间,按照这样计算,那下次触发计算的的时间应该是30000+29999+5000=64999,另一个是:第一次窗口触发计算完成后,又来了一条25000,a数据,该如何处理呢,由于触发了计算之后watermark应该更新成了30000,比他小的数据会被丢弃,我们验证下:
输入了25000,a和64998,c没有触发计算,如图所示:
继续输入64999,b,触发第二次窗口计算,在[3000,59999]窗口,截图写错了,应该都是闭区间,这里最30000,a和34998,a两次出现a进行了单词求和结果为2,对于b只在34999,b出现了一次,所以求和为1,而在触发第一次窗口计算之后,出现了数据25000,a,这里由于超过了最大的延迟5秒,Flink直接丢弃数据不做处理,如图所示:
至此,Watermark机制相关知识讲解完毕,大家可通过我的代码实例,自己去演示一遍,不然不太好理解。感谢关注!!
如果觉得我的文章能帮到您,请关注微信公众号“大数据开发运维架构”,并转发朋友圈,谢谢支持!!!