flink watermark原理总结
flink如何处理乱序?
通过窗口对input按照eventTime进行聚合,使得大体按照event time 发生的顺序去处理数据,同时利用watermark来触发窗口。(watermark + window机制)
watermark原理及简介?
Watermark是Flink为了处理EventTime时间类型(其他时间类型不考虑乱序问题)的窗口计算提出的一种机制,本质上也是一种时间戳。Watermark是用于处理乱序事件的,而正确的处理乱序事件,通常用watermark机制结合window来实现。
当operator通过基于Event Time的时间窗口来处理数据时,它必须在确定所有属于该时间窗口的消息全部流入此操作符后,才能开始处理数据。但是由于消息可能是乱序的,所以operator无法直接确认何时所有属于该时间窗口的消息全部流入此操作符。WaterMark包含一个时间戳,Flink使用WaterMark标记所有小于该时间戳的消息都已流入,Flink的数据源在确认所有小于某个时间戳的消息都已输出到Flink流处理系统后,会生成一个包含该时间戳的WaterMark,插入到消息流中输出到Flink流处理系统中,Flink operator算子按照时间窗口缓存所有流入的消息,当操作符处理到WaterMark时,它对所有小于该WaterMark时间戳的时间窗口的数据进行处理并发送到下一个操作符节点,然后也将WaterMark发送到下一个操作符节点。
watermark有什么用?
流处理的过程中,从事件产生,到流经source,operator,中间是有一个过程和时间的,虽然大部分情况下,流到operator的数据都是按照事件产生的时间顺序来的,但是由于网络,背压等原因,导致乱序的产生。(out-of-order或者说late element)
对于late element,我们不能无限期的等下去,必须有一个机制保证在特定的时间后,必须触发window去计算,这个特别的机制就是watermark。
watermark如何产生?
通常,在接收到source的数据后,会立刻生成watermark;但是,也可以在source之后,应用简单的map、filter再生成watermark。
生成watermark的方式有两种:
1.Periodic Watermarks
Periodic Watermarks,周期性的产生watermark,即每隔一定时间间隔或者达到一定的记录条数,产生一个watermark。
而在实际的生产中,periodic方式必须结合时间和记录数两个维度,否则,在极端情况下容易产生很大的延时。
2.Punctuated Watermarks
Punctuated Watermarks,数据流中每一个递增的event time 都会产生一个watermark。
在实际的生产中,punctuated 方式在TPS很高的场景下会产生大量的watermark,
在一定程度上对下游算子造成压力,所以只有在实时性要求非常高的场景才会选择punctuated方式。
代码实现:
注意:为什么Watermark=currentMaxTimestamp - maxLateTime?
假设不考虑延迟,watermark=currentMaxTimeStamp,随着水位线的上升,当水位线(即当前最大时间)超过endtime时,所有的数据已经全部进入该窗口了。
继续考虑存在延迟的情况,为了使得延时了maxLateTime的数据全部进入窗口,预先让水位线下降maxLateTime,在这种情况下,当水位线依然超过endtime时,表明在允许延迟的情况下,所有数据全部进入该窗口了。
如果顺序,只需要最新的event time >=windowEndTime,窗口就会触发。
如果乱序,由于要等 maxLateTime,所以最新的event time - maxLateTime >=windowEndTime时,窗口触发。这里取其次,只要currentMaxTimestamp- maxLateTime>=windowEndTime,窗口就会触发。
/**
*flink 1.7
hello,2019-09-17 11:34:05.890
hello,2019-09-17 11:34:07.890
hello,2019-09-17 11:34:13.890
hello,2019-09-17 11:34:08.890
hello,2019-09-17 11:34:16.890
hello,2019-09-17 11:34:19.890
hello,2019-09-17 11:34:21.890
*/
public class WaterMarkTest {
public static void main(String[] args) throws ParseException {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
env.setParallelism(1);
//设置多久查看一下当前的水位线... 默认200ms
// env.getConfig().setAutoWatermarkInterval(10000);
// System.err.println("interval : " + env.getConfig().getAutoWatermarkInterval());
DataStreamSource<String> streamSource = env.socketTextStream("hdp-01", 9999);
//
DataStream<String> dataStream = streamSource.assignTimestampsAndWatermarks(new MyWaterMark());
dataStream.map(new MapFunction<String, Tuple2<String, Integer>>() {
@Override
public Tuple2<String, Integer> map(String value) throws Exception {
String[] split = value.split(",");
String key = split[0];
return new Tuple2<>(key, 1);
}
}).keyBy(0)
.timeWindow(Time.seconds(10))
// .sum(1)
//自定义的一个计算规则...
.apply(new MyWindowFunction())
.printToErr();
try {
env.execute();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*
*数据进来,先extract时间,同时更新max值,再生成watermark
*/
class MyWaterMark implements AssignerWithPeriodicWatermarks<String> {
//目前系统里所有数据的最大事件时间
long currentMaxTimestamp = 0;
long maxLateTime = 5000;//允许数据延迟5s
Watermark wm= null;
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
@Nullable
@Override
//周期性的获取目前的水位线时间,默认200ms
public Watermark getCurrentWatermark() {
//未处理数据的延迟/乱序问题
// wm = new Watermark(currentMaxTimestamp);
//处理数据的延迟/乱序问题
wm = new Watermark(currentMaxTimestamp - maxLateTime);
System.out.println(format.format(System.currentTimeMillis()) + " 获取当前水位线: " + wm + ","+ format.format(wm.getTimestamp()));
return wm;
}
/**
*
* @param element 流中的数据 形如:"hello,2019-09-17 10:24:50.958"
* @param previousElementTimestamp 上条数据的时间戳
* @return 新的时间戳
*/
@Override
public long extractTimestamp(String element, long previousElementTimestamp) {
String[] split = element.split(",");
String key = split[0];
long timestamp = 0 ;
try {
//将2019-09-17 10:24:50.958 格式时间转成时间戳
timestamp = format.parse(split[1]).getTime();
} catch (ParseException e) {
e.printStackTrace();
}
//对比新数据的时间戳和目前最大的时间戳,取大的值作为新的时间戳
currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp);
System.err.println(key +", 本条数据的时间戳: "+ timestamp + "," +format.format(timestamp)
+ "|目前数据中的最大时间戳: "+ currentMaxTimestamp + ","+ format.format(currentMaxTimestamp)
+ "|水位线时间戳: "+ wm + ","+ format.format(wm.getTimestamp()));
return timestamp;
}
}
class MyWindowFunction implements WindowFunction<Tuple2<String,Integer>, String, Tuple, TimeWindow> {
@Override
public void apply(Tuple tuple, TimeWindow window, Iterable<Tuple2<String, Integer>> input, Collector<String> out) throws Exception {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
int sum = 0;
for(Tuple2<String,Integer> tuple2 : input){
sum +=tuple2.f1;
}
long start = window.getStart();
long end = window.getEnd();
out.collect("key:" + tuple.getField(0) + " value: " + sum + "| window_start :"
+ format.format(start) + " window_end :" + format.format(end)
);
}
}