flink watermark原理总结

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)
        );

    }
}

发布了4 篇原创文章 · 获赞 0 · 访问量 513

猜你喜欢

转载自blog.csdn.net/The_Inertia/article/details/104089489