Flink 自定义水位线

一般来说,Flink内置的水位线生成器就可以满足应用需求了。不过有时由于业务逻辑可能非常复杂,这时对水位线生成的逻辑也有更高的要求,开发人员就必须自定义实现水位线策略WatermarkStrategy了。在WatermarkStrategy中,时间戳分配器TimestampAssigner都是大同小异的,指定字段提取时间戳就可以了;而不同策略的关键就在于WatermarkGenerator的实现。整体说来,Flink有两种不同的生成水位线的方式:一种是周期性的(Periodic),另一种是断点式的(Punctuated)。就是WatermarkGenerator接口中的两个方法——onEvent()和onPeriodicEmit(),前者是在每个事件到来时调用,而后者由框架周期性调用。周期性调用的方法中发出水位线,自然就是周期性生成水位线;而在事件触发的方法中发出水位线,自然就是断点式生成了。两种方式的不同就集中体现在这两个方法的实现上;

1、周期性水位线生成器(Periodic Generator)

周期性生成器一般是通过onEvent()观察判断输入的事件,而在onPeriodicEmit()里发出水位线

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        //生成周期性水位线
        env.addSource(new ClickSource())
                .assignTimestampsAndWatermarks(new MyWatermarkStrategy())
                .print();

        env.execute();
    }

    //自定义周期性生成水位线
    public static class MyWatermarkStrategy implements WatermarkStrategy<Event> {


        @Override
        public TimestampAssigner<Event> createTimestampAssigner(TimestampAssignerSupplier.Context context) {
            return new SerializableTimestampAssigner<Event>() {
                @Override
                public long extractTimestamp(Event element, long recordTimestamp) {
                    return element.timestamp * 1000L;
                }
            };
        }

        @Override
        public WatermarkGenerator<Event> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
            //周期生成水位线
            return new MyPeriodicGenerator();
            
        }
    }

    //周期性生成水位线
    public static class MyPeriodicGenerator implements WatermarkGenerator<Event> {
        //延迟时间
        private long delayTime = 5000L;
        //观察到最大时间cuo
        private long maxTs = Long.MIN_VALUE + delayTime + 1L;

        //每来一条数据调研一次
        @Override
        public void onEvent(Event event, long eventTimestamp, WatermarkOutput output) {
            //更新最大时间cuo
            maxTs = Math.max(event.timestamp, maxTs);
        }

        @Override
        public void onPeriodicEmit(WatermarkOutput output) {
            //发射水位线,默认200 ms 调用一次
            output.emitWatermark(new Watermark(maxTs - delayTime - 1L));
        }
    }

在onPeriodicEmit()里调用output.emitWatermark(),就可以发出水位线了;这个方法由系统框架周期性地调用,默认200ms一次。所以水位线的时间戳是依赖当前已有数据的最大时间戳的(这里的实现与内置生成器类似,也是减去延迟时间再减1),但具体什么时候生成与数据无关。

2、断点式水位线生成器(PunctuatedGenerator)

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        //生成周期性水位线
        env.addSource(new ClickSource())
                .assignTimestampsAndWatermarks(new MyWatermarkStrategy())
                .print();

        env.execute();
    }

    //自定义周期性生成水位线
    public static class MyWatermarkStrategy implements WatermarkStrategy<Event> {


        @Override
        public TimestampAssigner<Event> createTimestampAssigner(TimestampAssignerSupplier.Context context) {
            return new SerializableTimestampAssigner<Event>() {
                @Override
                public long extractTimestamp(Event element, long recordTimestamp) {
                    return element.timestamp * 1000L;
                }
            };
        }

        @Override
        public WatermarkGenerator<Event> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
            //周期生成水位线
            // return new MyPeriodicGenerator(); 
            //断点生成水位线
            return new MyPunctuatedGenerator();
        }
    }

    //断点生成水位线
    public static class MyPunctuatedGenerator implements WatermarkGenerator<Event> {

        @Override
        public void onEvent(Event event, long eventTimestamp, WatermarkOutput output) {
            //只有遇到特定数据时,才发送水位线
            if (event.user.equals("依琳")) {
                output.emitWatermark(new Watermark(event.timestamp - 1L));
            }
        }

        @Override
        public void onPeriodicEmit(WatermarkOutput output) {
            //onEvent 已经发送了水位线,onPeriodicEmit不做处理即可
        }
    }

在onEvent()中判断当前事件的user字段,只有遇到“依琳”这个特殊的值时,才调用output.emitWatermark()发出水位线。这个过程是完全依靠事件来触发的,所以水位线的生成一定在某个数据到来之后。

3、在自定义数据源中发送水位线

也可以在自定义的数据源中抽取事件时间,然后发送水位线。这里要注意的是,在自定义数据源中发送了水位线以后,就不能再在程序中使用assignTimestampsAndWatermarks方法 来 生 成 水 位 线 了 。 在 自 定 义 数 据 源 中 生 成 水 位 线 和 在 程 序 中 使 用assignTimestampsAndWatermarks方法生成水位线二者只能取其一。示例程序如下:

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.addSource(new ClickSourceWithWatermark()).print();
        env.execute();

    }
ClickSourceWithWatermark 逻辑
public class ClickSourceWithWatermark implements ParallelSourceFunction<Event> {
    // 声明标志位
    private Boolean running = true;

    @Override
    public void run(SourceContext<Event> ctx) throws Exception {
        //随机生成数据
        Random random = new Random();
        //随机范围
        String[] users = {"令狐冲", "依琳", "任盈盈", "莫大", "风清扬"};

        String[] urls = {"./home", "./cat", "./pay", "./info"};
        //循环生成数据
        while (running) {
            //生成数据
            String user = users[random.nextInt(users.length)];
            String url = urls[random.nextInt(urls.length)];
            Event event = new Event(user, url, Calendar.getInstance().getTimeInMillis());

            //发送数据
            ctx.collect(event);

            //发送水位线
            ctx.emitWatermark(new Watermark(event.timestamp - 1L));
        }
    }

    @Override
    public void cancel() {
        running = false;
    }
}

在自定义水位线中生成水位线相比assignTimestampsAndWatermarks方法更加灵活,可以任意的产生周期性的、非周期性的水位线,以及水位线的大小也完全由我们自定义。所以非常适合用来编写Flink的测试程序,测试Flink的各种各样的特性。

posted @ 2022-06-28 05:52  晓枫的春天  阅读(883)  评论(1编辑  收藏  举报