Flink DataStream WaterMark

Flink DataStream WaterMark

为了处理事件时间,Flink需要知道事件的时间戳,这意味着流中的每个元素都需要分配其事件时间戳。这通常通过从元素中的某个字段访问/提取时间戳来完成。

时间戳分配与生成水印密切相关,水印告诉系统事件时间的进展。

1.生成Watermark的两种方式

1.1 在数据源(source)中生成WaterMark

public class WaterMarkStream {

    public static void main(String[] args) throws Exception {

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        env.setParallelism(1);

        Properties p = new Properties();
        p.setProperty("bootstrap.servers", "localhost:9092");
        p.setProperty("group.id", "test");
        String type = "{\"type\":\"object\",\"properties\":{\"Msg\":{\"type\":\"object\",\"properties\":{\"MemberId\":{\"type\":\"string\"},\"LogActionName__Special__\":{\"type\":\"string\"},\"LogControllerName__Special__\":{\"type\":\"string\"},\"LogTime__Special__\":{\"type\":\"string\"}}}}}";
        
        FlinkKafkaConsumer010 consumer010 = new FlinkKafkaConsumer010<>("app_scene_roominfo", new JsonRowDeserializationSchema(type), p);

        consumer010.assignTimestampsAndWatermarks(new AscendingTimestampExtractor() {
            @Override
            public long extractAscendingTimestamp(Row element) {
                // 因为定义的mapping json是属于object的嵌套格式,element对象是最外一层的数据结构,需要自己通过getField取出,再获取里面的元素
                Row field = (Row) element.getField(0);
                int arity = field.getArity();
                String time = (String) field.getField(arity - 1);

                Date date = null;
                try {
                    date = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss").parse(time);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
                return date == null ? 0 : date.getTime();
            }
        });
        DataStreamSource source = env.addSource(consumer010);

        source.print();

        env.execute("WaterMarkStream");
    }
}

1.2 通过时间戳分配器生成水印

public class WatermarkStream {

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

        Properties p = new Properties();
        p.setProperty("bootstrap.servers", "localhost:9092");
        p.setProperty("group.id", "test");

        FlinkKafkaConsumer010 consumer010 = new FlinkKafkaConsumer010<>("app_scene_roominfo", new SimpleStringSchema(), p);
        DataStreamSource ds = env.addSource(consumer010);

        SingleOutputStreamOperator operator = ds
                .map(new RichMapFunction() {
                    @Override
                    public Value map(String value) throws Exception {
                        return new Gson().fromJson(value, Value.class);
                    }
                })
                .assignTimestampsAndWatermarks(new AscendingTimestampExtractor() {
                    @Override
                    public long extractAscendingTimestamp(Value element) {
                        long time = 0;
                        try {
                            Date date = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss").parse(element.getMsg().getLogTime());
                            time = date.getTime();
                        } catch (ParseException e) {
                            e.printStackTrace();
                        }
                        return time;
                    }
                });

        operator.print();

        env.execute("WatermarkStream");
    }
}

2. WaterMark的种类

Flink提供了2种水印,一种是周期性水印(AssignerWithPeriodicWatermarks),一种是间断性水印(AssignerWithPeriodicWatermarks)。

2.1 周期性水印(AssignerWithPeriodicWatermarks)

默认周期性水印是200ms触发一次。可以通过env对象进行修改,如:

# 改为1s触发一次水印
env.getConfig().setAutoWatermarkInterval(1000);

周期性水印的2种常用的实现:

  • AscendingTimestampExtractor:具有递增时间戳的分发者

    这种是把Stream想成理想状态,事件是以升序的时间过来,并生成水印。在这种情况下,当前时间戳始终可以充当水印,因为没有更早的时间戳会到达。

    如果数据真的有延迟,怎么办,AscendingTimestampExtractor的提供3种策略:

    • LoggingHandler:WARN log记录延迟数据,默认实现
    • IgnoreHandler:忽略延迟数据
    • FailingHandler:处理失败
    .assignTimestampsAndWatermarks(new AscendingTimestampExtractor() {
        @Override
        public long extractAscendingTimestamp(Value element) {
            long time = 0;
            try {
                Date date = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss").parse(element.getMsg().getLogTime());
                time = date.getTime();
            } catch (ParseException e) {
                e.printStackTrace();
            }
            return time;
        }
    });
    
    

    如果需要修改策略,可以调用AscendingTimestampExtractorwithViolationHandler(newAscendingTimestampExtractor.IgnoringHandler())方法。

  • BoundedOutOfOrdernessTimestampExtractor:允许固定数量的迟到分配者

    这个分配器可以设置最大延迟时间,来挽回一些迟到的数据进入窗口。

    .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor(Time.seconds(30)) {
        @Override
        public long extractTimestamp(Value element) {
            long time = 0;
            try {
                Date date = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss").parse(element.getMsg().getLogTime());
                time = date.getTime();
            } catch (ParseException e) {
                e.printStackTrace();
            }
            return time;
        }
    });
    

2.2 间断性水印(AssignerWithPunctuatedWatermarks)

public class PunctuatedWatermark {
    public static void main(String[] args) throws Exception {

        StreamExecutionEnvironment sEnv = StreamExecutionEnvironment.getExecutionEnvironment();
        sEnv.setParallelism(1);


        Properties p = new Properties();
        p.setProperty("bootstrap.servers", "localhost:9092");

        SingleOutputStreamOperator op = sEnv
                .addSource(new FlinkKafkaConsumer010("app_scene_roominfo", new SimpleStringSchema(), p))
                .map(new MapFunction() {
                    @Override
                    public Value map(String value) throws Exception {
                        return new Gson().fromJson(value, Value.class);
                    }
                })
                .assignTimestampsAndWatermarks(new AssignerWithPunctuatedWatermarks() {

                    /**
                     * 只有当返回的水印不为空且其时间戳大于以前发送的水印时,才会发出返回的水印(以保留递增水印的契约)。如果返回空值,或者返回的水印的时间戳小于上次发出的水印的时间戳,则不会生成新的水印。
                     * @param lastElement 上一条记录
                     * @param extractedTimestamp extractTimestamp()返回的时间戳
                     * @return 水印对象
                     */
                    @Override
                    public Watermark checkAndGetNextWatermark(Value lastElement, long extractedTimestamp) {
                        // 2.创建水印
                        return new Watermark(extractedTimestamp);
                    }

                    @Override
                    public long extractTimestamp(Value element, long previousElementTimestamp) {
                        // 1.生成水印时间戳
                        Date date = null;
                        try {
                            date = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss").parse(element.getMsg().getLogTime());
                        } catch (ParseException e) {
                            e.printStackTrace();
                        }
                        return date == null ? 0 : date.getTime();
                    }
                });

        op.print();

        sEnv.execute("PunctuatedWatermark");
    }
}

间断性水印会给每个事件都生成一个水印,会导致下游计算开销变大,降低程序性能。一般使用较少。

3. 每个卡夫卡分区的时间戳

当使用Apache Kafka作为数据源时,每个Kafka分区可能具有简单的事件时间模式(升序时间戳或有界无序)。但是,当从Kafka消费流时,多个分区通常并行消耗,交错来自分区的事件并破坏每个分区模式(这是Kafka的消费者客户端工作的固有方式)。

在这种情况下,您可以使用Flink的Kafka分区感知水印生成。使用该功能,根据Kafka分区在Kafka使用者内部生成水印,并且每个分区水印的合并方式与在流shuffle上合并水印的方式相同。

例如,如果事件时间戳严格按每个Kafka分区升序,则使用升序时间戳水印生成器生成每分区水印 将产生完美的整体水印。

下图显示了如何使用per-Kafka分区水印生成,以及在这种情况下水印如何通过流数据流传播。

你可能感兴趣的:(大数据)