Flink-Watermark

354 阅读2分钟

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

什么是Watermark?

Watermaker就是给数据再额外的加的一个时间列,也就是Watermaker是个时间戳!

如何计算Watermaker?

Watermaker = 数据的事件时间 - 最大允许的延迟时间或乱序时间

注意:后面通过源码会发现,准确来说:

Watermaker = 当前窗口的最大的事件时间 - 最大允许的延迟时间或乱序时间

这样可以保证Watermaker水位线会一直上升(变大),不会下降

Watermark有什么用?

窗口如果按照系统时间来触发计算的,如: [10:00:00 ~ 10:00:10) 的窗口,

一但系统时间到了10:00:10就会触发计算,那么可能会导致延迟到达的数据丢失!

那么现在有了Watermaker,窗口就可以按照Watermaker来触发计算!

也就是说Watermaker是用来触发窗口计算的!

Watermaker如何触发窗口计算的?

窗口计算的触发条件为:

1.窗口中有数据

2.Watermaker >= 窗口的结束时间

3.Watermaker = 当前窗口的最大的事件时间 - 最大允许的延迟时间或乱序时间

也就是说只要不断有数据来,就可以保证Watermaker水位线是会一直上升/变大的,不会下降/减小的

所以最终一定是会触发窗口计算的

注意:上面的触发公式进行如下变形:

1、Watermaker >= 窗口的结束时间

2、Watermaker = 当前窗口的最大的事件时间 - 最大允许的延迟时间或乱序时间

3、当前窗口的最大的事件时间 - 最大允许的延迟时间或乱序时间 >= 窗口的结束时间

4、当前窗口的最大的事件时间 >= 窗口的结束时间 + 最大允许的延迟时间或乱序时间

图解Watermark

image.png

代码演示

env

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

source

DataStreamSource<Order> orderDS = env.addSource(new SourceFunction<Order>() {
    private boolean flag = true;

    @Override
    public void run(SourceContext<Order> sourceContext) throws Exception {
        Random random = new Random();
        while (flag) {
            String orderId = UUID.randomUUID().toString();
            int userId = random.nextInt(2);
            int money = random.nextInt(101);
            long eventTime = System.currentTimeMillis() - random.nextInt(5) * 1000;
            System.out.println("发送的数据为:" + userId + " : " + df.format(eventTime));
            sourceContext.collect(new Order(orderId, userId, money, eventTime));
            TimeUnit.SECONDS.sleep(1);
        }
    }

    @Override
    public void cancel() {
        flag = false;
    }
}).setParallelism(1);

逻辑处理

正常处理

SingleOutputStreamOperator<Order> orderDSWithWM = orderDS.assignTimestampsAndWatermarks(WatermarkStrategy.<Order>forBoundedOutOfOrderness(Duration.ofSeconds(5))
        .withTimestampAssigner((order, l) -> order.getEventTime()));

SingleOutputStreamOperator<Order> result = orderDSWithWM.keyBy(order -> order.getUserId()).window(TumblingEventTimeWindows.of(Time.seconds(5))).sum("money");

重写方法增加打印观察数据

SingleOutputStreamOperator<Order> waterDS = orderDS.assignTimestampsAndWatermarks(new WatermarkStrategy<Order>() {
    @Override
    public WatermarkGenerator<Order> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
        return new WatermarkGenerator<Order>() {
            private int userId = 0;
            private long eventTime = 0L;
            private final long outOfOrdernessMillis = 3000L;
            private long maxTimestamp = Long.MIN_VALUE + outOfOrdernessMillis + 1;

            @Override
            public void onEvent(Order order, long eventTimestamp, WatermarkOutput watermarkOutput) {
                userId = order.getUserId();
                eventTime = order.getEventTime();
                maxTimestamp = Math.max(maxTimestamp, eventTimestamp);
            }

            @Override
            public void onPeriodicEmit(WatermarkOutput watermarkOutput) {
                Watermark watermark = new Watermark(maxTimestamp - outOfOrdernessMillis - 1);
                System.out.println("key:" + userId + ",系统时间:" + df.format(System.currentTimeMillis()) + ",事件事件:" + df.format(eventTime) + ",水印时间:" + df.format(watermark.getTimestamp()));
                watermarkOutput.emitWatermark(watermark);
            }
        };
    }
}.withTimestampAssigner(((order, l) -> order.getEventTime())));

SingleOutputStreamOperator<String> result = waterDS.keyBy(Order::getUserId)
        .window(TumblingEventTimeWindows.of(Time.seconds(5)))
        .apply(new WindowFunction<Order, String, Integer, TimeWindow>() {
            @Override
            public void apply(Integer key, TimeWindow timeWindow, Iterable<Order> iterable, Collector<String> collector) throws Exception {
                List<String> eventTimeList=new ArrayList<>();
                for (Order order:iterable){
                    Long eventTime= order.getEventTime();
                    eventTimeList.add(df.format(eventTime));
                }
                String outStr=String.format("key:%s,窗口开始结束:[%s~%s),属于该窗口的事件时间:%s",key.toString(),df.format(timeWindow.getStart()),df.format(timeWindow.getEnd()),eventTimeList.toString());
                collector.collect(outStr);
            }
        });

sink

result.print();