文章转载自:https://blog.csdn.net/xu47043...
作者:bigdata1024
侵删
watermarks的生成方式有两种:
1:With Periodic Watermarks:周期性的触发watermark的生成和发送。
2:With Punctuated Watermarks:基于某些事件触发watermark的生成和发送。
第一种方式比较常用,所以在这里我们使用第一种方式进行分析。
参考官网文档中With Periodic Watermarks的使用方法
代码中的extractTimestamp方法是从数据本身中提取EventTime。
getCurrentWatermar方法是获取当前水位线,利用currentMaxTimestamp - maxOutOfOrderness,这里的maxOutOfOrderness表示是允许数据的最大乱序时间。
所以在这里我们使用的话也实现接口AssignerWithPeriodicWatermarks。
1、实现watermark相关代码
1.1、程序说明
从socket模拟接收数据,然后使用map进行处理,后面再调用assignTimestampsAndWatermarks方法抽取timestamp并生成watermark。最后再调用window打印信息来验证window被触发的时机。
完整代码如下:
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
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.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.watermark.Watermark;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import javax.annotation.Nullable;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
/**
*
* Watermark 案例
*
* Created by xuwei.tech.
*/
public class StreamingWindowWatermark1 {
public static void main(String[] args) throws Exception {
//定义socket的端口号
int port = 9000;
//获取运行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//设置使用eventtime,默认是使用processtime
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
//设置并行度为1,默认并行度是当前机器的cpu数量
env.setParallelism(1);
//连接socket获取输入的数据
DataStream text = env.socketTextStream("192.168.59.133", port, "\n");
//解析输入的数据
DataStream> inputMap = text.map(new MapFunction>() {
@Override
public Tuple2 map(String value) throws Exception {
String[] arr = value.split(",");
return new Tuple2<>(arr[0], Long.parseLong(arr[1]));
}
});
//抽取timestamp和生成watermark
DataStream> waterMarkStream = inputMap.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks>() {
Long currentMaxTimestamp = 0L;
final Long maxOutOfOrderness = 10000L;// 最大允许的乱序时间是10s
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
/**
* 定义生成watermark的逻辑
* 默认100ms被调用一次
*/
@Nullable
@Override
public Watermark getCurrentWatermark() {
return new Watermark(currentMaxTimestamp - maxOutOfOrderness);
}
//定义如何提取timestamp
@Override
public long extractTimestamp(Tuple2 element, long previousElementTimestamp) {
long timestamp = element.f1;
currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp);
System.out.println("key:"+element.f0+",eventtime:["+element.f1+"|"+sdf.format(element.f1)+"],currentMaxTimestamp:["+currentMaxTimestamp+"|"+
sdf.format(currentMaxTimestamp)+"],watermark:["+getCurrentWatermark().getTimestamp()+"|"+sdf.format(getCurrentWatermark().getTimestamp())+"]");
return timestamp;
}
});
//分组,聚合
DataStream window = waterMarkStream.keyBy(0)
.window(TumblingEventTimeWindows.of(Time.seconds(3)))//按照消息的EventTime分配窗口,和调用TimeWindow效果一样
.apply(new WindowFunction, String, Tuple, TimeWindow>() {
/**
* 对window内的数据进行排序,保证数据的顺序
* @param tuple
* @param window
* @param input
* @param out
* @throws Exception
*/
@Override
public void apply(Tuple tuple, TimeWindow window, Iterable> input, Collector out) throws Exception {
String key = tuple.toString();
List arrarList = new ArrayList();
Iterator> it = input.iterator();
while (it.hasNext()) {
Tuple2 next = it.next();
arrarList.add(next.f1);
}
Collections.sort(arrarList);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
String result = key + "," + arrarList.size() + "," + sdf.format(arrarList.get(0)) + "," + sdf.format(arrarList.get(arrarList.size() - 1))
+ "," + sdf.format(window.getStart()) + "," + sdf.format(window.getEnd());
out.collect(result);
}
});
//测试-把结果打印到控制台即可
window.print();
//注意:因为flink是懒加载的,所以必须调用execute方法,上面的代码才会执行
env.execute("eventtime-watermark");
}
}
1.2、程序详解
- 接收socket数据。
- 将每行数据按照逗号分隔,每行数据调用map转换成tuple
类型。其中tuple中的第一个元素代表具体的数据,第二行代表数据的eventtime。 - 抽取timestamp,生成watermar,允许的最大乱序时间是10s,并打印(key,eventtime,currentMaxTimestamp,watermark)等信息
- 分组聚合,window窗口大小为3秒,输出(key,窗口内元素个数,窗口内最早元素的时间,窗口内最晚元素的时间,窗口自身开始时间,窗口自身结束时间)。
2、通过数据跟踪watermark的时间
在这里重点查看watermark和timestamp的时间,通过数据的输出来确定window的触发时机。
首先我们开启socker,输入第一条数据
0001,1538359882000
输出的内容如下:
此时,wartermark的时间,已经落后于currentMaxTimestamp10秒了。我们继续输入
0001,1538359886000
此时,输入内容如下:
继续输入:
0001,1538359892000
到这里,window仍然没有被触发,此时watermark的时间已经等于了第一条数据的Event Time了。那么window到底什么时候被触发呢?
我们再次输入:
0001,1538359893000
window仍然没有触发,此时,我们的数据已经发到2018-10-01 10:11:33.000了,根据eventtime来算,最早的数据已经过去了11秒了,window还没有开始计算,那到底什么时候会触发window呢?
我们再次增加1秒,输入:
0001,1538359894000
到这里,我们做一个说明:
window的触发机制,是先按照自然时间将window划分,如果window大小是3秒,那么1分钟内会把window划分为如下的形式【左闭右开】:
[00:00:00,00:00:03)
[00:00:03,00:00:06)
[00:00:06,00:00:09)
[00:00:09,00:00:12)
[00:00:12,00:00:15)
[00:00:15,00:00:18)
[00:00:18,00:00:21)
[00:00:21,00:00:24)
[00:00:24,00:00:27)
[00:00:27,00:00:30)
[00:00:30,00:00:33)
[00:00:33,00:00:36)
[00:00:36,00:00:39)
[00:00:39,00:00:42)
[00:00:42,00:00:45)
[00:00:45,00:00:48)
[00:00:48,00:00:51)
[00:00:51,00:00:54)
[00:00:54,00:00:57)
[00:00:57,00:01:00)
window的设定无关数据本身,而是系统定义好了的。
输入的数据中,根据自身的Event Time,将数据划分到不同的window中,如果window中有数据,则当watermark时间>=Event Time时,就符合了window触发的条件了,最终决定window触发,还是由数据本身的Event Time所属的window中的window_end_time决定。
上面的测试中,最后一条数据到达后,其水位线已经升至10:11:24秒,正好是最早的一条记录所在window的window_end_time,所以window就被触发了。
为了验证window的触发机制,我们继续输入数据:
0001,1538359896000
汇总如下:
此时,watermark时间虽然已经达到了第二条数据的时间,但是由于其没有达到第二条数据所在window的结束时间,所以window并没有被触发。那么,第二条数据所在的window时间是:
[00:00:24,00:00:27)
也就是说,我们必须输入一个10:11:27秒的数据,第二条数据所在的window才会被触发。我们继续输入:
此时,我们已经看到,window的触发要符合以下几个条件:
**1、watermark时间 >= window_end_time
2、在[window_start_time,window_end_time)区间中有数据存在,注意是左闭右开的区间**
同时满足了以上2个条件,window才会触发。
3、watermark+window处理乱序数据
我们上面的测试,数据都是按照时间顺序递增的,现在,我们输入一些乱序的(late)数据,看看watermark结合window机制,是如何处理乱序的。
输入两行数据
0001,1538359899000
0001,1538359891000
可以看到,虽然我们输入了一个10:11:31的数据,但是currentMaxTimestamp和watermark都没变。此时,按照我们上面提到的公式:
1、watermark时间 >= window_end_time
2、在[window_start_time,window_end_time)中有数据存在
watermark时间(10:11:29) < window_end_time(10:11:33),因此不能触发window。
那如果我们再次输入一条10:11:43的数据,此时watermark时间会升高到10:11:33,这时的window一定就会触发了,我们试一试:
输入:
0001,1538359903000
这里,我么看到,窗口中有2个数据,10:11:31和10:11:32,但是没有10:11:33的数据,原因是窗口是一个前闭后开的区间,10:11:33的数据是属于[10:11:33,10:11:36)的窗口的。
Flink应该如何设置最大乱序时间 ?
这个要结合自己的业务以及数据情况去设置。如果maxOutOfOrderness设置的太小,而自身数据发送时由于网络等原因导致乱序或者late太多,那么最终的结果就是会有很多单条的数据在window中被触发,数据的正确性影响太大。
对于严重乱序的数据,需要严格统计数据最大延迟时间,才能保证计算的数据准确,延时设置太小会影响数据准确性,延时设置太大不仅影响数据的实时性,更加会加重Flink作业的负担,不是对eventTime要求特别严格的数据,尽量不要采用eventTime方式来处理,会有丢数据的风险。
上边的结果,已经表明,对于out-of-order的数据,Flink可以通过watermark机制结合window的操作,来处理一定范围内的乱序数据。那么对于“迟到(late element)”太多的数据,Flink是怎么处理的呢?
4:late element(延迟数据)的处理
延迟数据的三种处理方案
4.1:丢弃(默认)
输入:
0001,1538359890000
0001,1538359903000
输出:
注意:此时watermark是2018-10-01 10:11:33.000
下面我们再输入几个eventtime小于watermark的时间
输入:【输入了三行内容】
0001,1538359890000
0001,1538359891000
0001,1538359892000
输出:
注意:此时并没有触发window。因为输入的数据所在的窗口已经执行过了,flink默认对这些迟到的数据的处理方案就是丢弃。
4.2:allowedLateness 指定允许数据延迟的时间
在某些情况下,我们希望对迟到的数据再提供一个宽容的时间。
Flink提供了allowedLateness方法可以实现对迟到的数据设置一个延迟时间,在指定延迟时间内到达的数据还是可以触发window执行的。
修改代码:
输入:【输入两行内容】
0001,1538359890000
0001,1538359903000
输出: 输入:【输入三行内容】 在这里看到每条数据都触发了window执行。 汇总: 汇总如下: 输出: 发现输入的三行数据都触发了window的执行。 我们再输入一条数据,把water调整到10:11:35 输入: 输出: 输入: 发现这几条数据都没有触发window。 分析: 总结: 解释: 4.3:sideOutputLateData 收集迟到的数据 通过sideOutputLateData 可以把迟到的数据统一收集,统计存储,方便后期排查问题。 我们来输入一些数据验证一下 此时,window被触发执行了,此时watermark是10:11:33 输出: 此时,针对这几条迟到的数据,都通过sideOutputLateData保存到了outputTag中。
此时watermark是2018-10-01 10:11:33.000
那么现在我们输入几条eventtime
0001,1538359890000
0001,1538359891000
0001,1538359892000
我们再输入一条数据,把water调整到10:11:34
此时,把water上升到了10:11:34,我们再输入几条eventtime
0001,1538359890000
0001,1538359891000
0001,1538359892000
0001,1538359905000
此时,watermark上升到了10:11:35
我们再输入几条eventtime
0001,1538359890000
0001,1538359891000
0001,1538359892000
当watemark等于10:11:33的时候,正好是window_end_time,所以会触发[10:11:30~10:11:33) 的window执行。
当窗口执行过后,我们输入[10:11:30~10:11:33) window内的数据会发现window是可以被触发的。
当watemark提升到10:11:34的时候,我们输入[10:11:30~10:11:33)window内的数据会发现window也是可以被触发的。
当watemark提升到10:11:35的时候,我们输入[10:11:30~10:11:33)window内的数据会发现window不会被触发了。
由于我们在前面设置了allowedLateness(Time.seconds(2)),可以允许延迟在2s内的数据继续触发window执行。
所以当watermark是10:11:34的时候可以触发window,但是10:11:35的时候就不行了。
对于此窗口而言,允许2秒的迟到数据,即第一次触发是在watermark >=window_end_time时
第二次(或多次)触发的条件是watermark < window_end_time + allowedLateness时间内,这个窗口有late数据到达时。
当watermark等于10:11:34的时候,我们输入eventtime为10:11:30、10:11:31、10:11:32的数据的时候,是可以触发的,因为这些数据的window_end_time都是10:11:33,也就是10:11:34<10:11:33+2 为true。
但是当watermark等于10:11:35的时候,我们再输入eventtime为10:11:30、10:11:31、10:11:32的数据的时候,这些数据的window_end_time都是10:11:33,此时,10:11:35<10:11:33+2 为false了。所以最终这些数据迟到的时间太久了,就不会再触发window执行了。
需要先调整代码:
输入:
0001,1538359890000
0001,1538359903000
下面我们再输入几个eventtime小于watermark的数据测试一下
输入:
0001,1538359890000
0001,1538359891000
0001,1538359892000