Flink Broadcast State 状态广播

本文开头附:Flink 学习路线系列 ^ _ ^

1.前言

       在 Apache Flink 1.5.0 中引入了广播状态(Broadcast State)。广播状态模式指的一种流应用程序,其中低吞吐量的事件流(例如,包含一组规则数据/字典数据)被广播到某个 operator 的所有并发实例中,然后针对来自另一条原始数据流中的数据,进行关联操作。此处附:Flink Broadcast State 官网英文介绍

       我们接下来要介绍的 Broadcast State, 就类似于 Map 结构,我们可以把 Broadcast State 就理解为是一个大 Map,它也可以执行 put、get、putAll、remove 等操作。

2.Broadcast State 使用示例

       现在有两个流:一个是数据流(A),一个是广播流(B)。这两个流的来源我们都可以自定义,这里我们就用平常最常用的 Kafka 来作为输入源。

1.场景:

        我们在操作SQL 时,经常会遇到关联维表数据的操作。接下来我们就在实时系统中使用关联维表数据这一场景,来介绍 Broadcast State 的使用。

2.以前的解决方式:

        Ⅰ.之前解决这问题,我们会对流中的数据进行 Transformation 算子操作,通过 new RichMapFuntion() 方式,重写其中的 open() 、close()、map() 方法。具体步骤如下:

  1. 在 open() 方法中连接数据库;
  2. 在 map() 方法中查询数据库中的维表信息,并将流中的数据与数据库查询的数据进行关联,从而达到关联维表的目的;
  3. 在 close() 方法中关闭SQL相关流。

       上述方式,因为一条一查数据库,数据库如果不及时返回数据,就很容易造成阻塞。

       Ⅱ.此时我们还有一种方式,那就是异步I/O查询。请跳转链接查看:Flink使用异步I/O访问外部数据。异步I/O查询也仅能缓解阻塞问题,它仍然需要不停的向数据库发送请求。

3.以上两种方式的弊端:

       以上两种方式,都需要通过网络的方式,去向数据库发起请求,还有可能造成阻塞情况,这种方式下效率显然不尽人意。

4.使用Broadcast State方式:

       我们将 A 流设置为读取 Kafka 中某个 Topic 的消息数据。

       MySQL 中的维表数据,我们可以使用 Canal(阿里巴巴 MySQL binlog 增量订阅&消费组件) ,将 MySQL 中的维表数据,直接发送消息到 Kafka 中某个Topic。然后我们通过读取 该Topic 下的数据,这就是B流

       目前,我们可以将B流通过 Broadcast State 的方式广播到 Flink 中执行任务的每一个 subTask,这样子便能够解决请求数据库这个问题。我们只需要通过 Canal 这个中间件,就能够实时获取维表中的数据。

       但是这些数据在 Kafka 中,我们消费一次就没有了,万一维表数据发生变化怎么办?比如来了个新业务,需要维表中加入一条新数据呢。此时我们怎么才能再次通过 Kafka ,获取到最新的维表中的数据呢?答案:我们可以设置读取Kafka的 groupId 为 UUID,便可以解决这个问题。每次有变动时,B流都会获取到最新的数据了。

       Canal中间件的使用,可以将MySQL数据以及增量数据,实时发送消息到 Kafka,此部分需要配置,可以自行网络了解。

数据部分,使用 Canal 经 Kafka 返回数据,格式如下:
在这里插入图片描述

{“data”:[{“id”:“A1”,“name”:“普通会员”,“create_time”:“2020-02-25 15:07:23”,“update_time”:“2020-02-25 15:07:23”}],“database”:“db_user”,“es”:157855220300,“id”:2,“isDdl”:false,“mysqlType”:{“id”:“varchar(20)”,“name”:“varchar(20)”,“create_time”:“datetime”,“update_time”:“datetime”},“old”:null,“pkNames”:null,“sql”:"",“sqlType”:{“id”:12,“name”:12,“create_time”:93,“update_time”:93},“table”:“user_type_dic”,“ts”:1578552558892,“type”:“INSERT”}

5.数据格式:

A流:(用户ID,用户类型ID,登录时间)
     (100001,A1,2020-02-12)
     (100002,A2,2020-02-05)
     (100003,A3,2020-01-09)
 
B流:(用户类型ID,类型名称 )
     (A1,普通会员)
     (A2,金牌会员)
     (A3,钻石会员)

6.代码:

FlinkUtils.java

/**
 * TODO FlinkUtils工具类(持续更新编写)
 *
 * @author liuzebiao
 * @Date 2020-2-18 9:11
 */
public class FlinkUtils {

    private static StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

    /**
     * 返回 Flink流式环境
     * @return
     */
    public static StreamExecutionEnvironment getEnv(){
        return env;
    }

    /**
     * Flink 从 Kafka 中读取数据(满足Exactly-Once)
     * @param parameters
     * @param clazz
     * @param 
     * @return
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    public static <T> DataStream<T> createKafkaStream(ParameterTool parameters,String topics,String groupId, Class<? extends DeserializationSchema> clazz) throws IllegalAccessException, InstantiationException {

        //设置全局参数
        env.getConfig().setGlobalJobParameters(parameters);
        //1.只有开启了CheckPointing,才会有重启策略
        //设置Checkpoint模式(与Kafka整合,一定要设置Checkpoint模式为Exactly_Once)
        env.enableCheckpointing(parameters.getLong("checkpoint.interval",5000L),CheckpointingMode.EXACTLY_ONCE);
        //2.默认的重启策略是:固定延迟无限重启
        //此处设置重启策略为:出现异常重启3次,隔5秒一次(你也可以在flink-conf.yaml配置文件中写死。此处配置会覆盖配置文件中的)
        env.getConfig().setRestartStrategy(RestartStrategies.fixedDelayRestart(10, Time.seconds(20)));
        //系统异常退出或人为 Cancel 掉,不删除checkpoint数据
        env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);

        /**2.Source:读取 Kafka 中的消息**/
        //Kafka props
        Properties properties = new Properties();
        //指定Kafka的Broker地址
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, parameters.getRequired("bootstrap.server"));
        //指定组ID
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
        //如果没有记录偏移量,第一次从最开始消费
        properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, parameters.get("auto.offset.reset","earliest"));
        //Kafka的消费者,不自动提交偏移量
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, parameters.get("enable.auto.commit","false"));

        List<String> topicList = Arrays.asList(topics.split(","));

        FlinkKafkaConsumer<T> kafkaConsumer = new FlinkKafkaConsumer(topicList, clazz.newInstance(), properties);

        return env.addSource(kafkaConsumer);
    }
}

配置文件 config.properties

bootstrap.server=192.168.204.210:9092,192.168.204.211:9092,192.168.204.212:9092
auto.offset.reset=earliest
enable.auto.commit=false

BroadcastStateDemo.java

/**
 * TODO Broadcast State 广播状态 Demo
 *
 * @author liuzebiao
 * @Date 2020-2-25 13:45
 */
public class BroadcastStateDemo {

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

        //使用ParameterTool,读取配置文件信息
        ParameterTool parameters = ParameterTool.fromPropertiesFile("配置文件路径");

        /********************************** 获取 A流过程 start *************************************/
        //读取 Kafka 中要处理的数据
        //(100001,A1,2020-02-12)
        DataStream<String> kafkaStream = FlinkUtils.createKafkaStream(parameters, "topicA", "groupA", SimpleStringSchema.class);

        //对数据进行处理后,得到 A 流
        SingleOutputStreamOperator<Tuple3<String, String, String>> streamA = kafkaStream.map(new MapFunction<String, Tuple3<String, String, String>>() {
            @Override
            public Tuple3<String, String, String> map(String line) throws Exception {
                String[] fields = line.split(",");
                return Tuple3.of(fields[0], fields[1], fields[2]);
            }
        });
        /********************************** 获取 A流过程 end *************************************/


        /********************************** 获取 B流过程 start *************************************/
        //读取 经 Canal 发送到 Kafka 指定 topic 中的数据
        //(A1,普通会员)
        DataStream<String> dicDataStream = FlinkUtils.createKafkaStream(parameters, "user_type_dic", UUID.randomUUID().toString(), SimpleStringSchema.class);

        SingleOutputStreamOperator<Tuple3<String, String, String>> typeStream = dicDataStream.process(new ProcessFunction<String, Tuple3<String, String, String>>() {
            @Override
            public void processElement(String jsonStr, Context context, Collector<Tuple3<String, String, String>> out) throws Exception {
                //Canal 发送到 Kafka 的消息为 JSON 格式,接下来完成对 Json 格式的拆分
                JSONObject jsonObject = JSON.parseObject(jsonStr);
                JSONArray jsonArray = jsonObject.getJSONArray("data");

                String type = jsonObject.getString("type");
                if ("INSERT".equals(type) || "UPDATE".equals(type) || "DELETE".equals(type)) {
                    for (int i = 0; i < jsonArray.size(); i++) {
                        JSONObject obj = jsonArray.getJSONObject(i);
                        String id = obj.getString("id");
                        String name = obj.getString("name");
                        out.collect(Tuple3.of(id, name, type));
                    }
                }
            }
        });
        /********************************** 获取 B流过程 end *************************************/

        /********************************** 将 B 流进行广播 start *************************************/
        /**定义一个广播的状态描述器**/
        MapStateDescriptor<String, String> mapStateDescriptor = new MapStateDescriptor<String, String>(
                "dic-state",
                Types.STRING,
                Types.STRING
        );
        /**将 B 流进行广播,得到 BroadcastStream 类型的广播数据流**/
        BroadcastStream<Tuple3<String, String, String>> broadcastStateStreamB = typeStream.broadcast(mapStateDescriptor);
        /********************************** 将 B 流进行广播 end *************************************/


        /********************************** 将 A 流 与广播出去的 B流 进行关联 start *************************************/
        /**
         * 将要处理的流 A 与 已广播的流 B 进行关联操作
         */
        SingleOutputStreamOperator<Tuple4<String, String, String, String>> broadcastStream = streamA.connect(broadcastStateStreamB).process(new BroadcastProcessFunction<Tuple3<String, String, String>, Tuple3<String, String, String>, Tuple4<String, String, String, String>>() {
            /**
             * 处理要计算的活动数据
             * @param tuple A流中的数据
             * @param readOnlyContext 只读上下文(只负责读,不负责写)
             * @param out 返回的数据
             * @throws Exception
             */
            @Override
            public void processElement(Tuple3<String, String, String> tuple, ReadOnlyContext readOnlyContext, Collector<Tuple4<String, String, String, String>> out) throws Exception {
                ReadOnlyBroadcastState<String, String> mapState = readOnlyContext.getBroadcastState(mapStateDescriptor);

                String id = tuple.f0;
                String type = tuple.f1;
                String time = tuple.f2;

                //根据用户类型ID,到广播的stateMap中关联对应的数据
                String typeName = mapState.get(type);

                //将关联后的数据,进行输出
                out.collect(Tuple4.of(id, type, typeName, time));
            }

            /**
             * 处理规则数据
             * @param tuple 广播流中的数据
             * @param context 上下文(此处负责写)
             * @param out
             * @throws Exception
             */
            @Override
            public void processBroadcastElement(Tuple3<String, String, String> tuple, Context context, Collector<Tuple4<String, String, String, String>> out) throws Exception {

                //id
                String id = tuple.f0;
                String name = tuple.f1;
                String type = tuple.f2;

                //新来一条规则数据,就将规则数据添加到内存
                BroadcastState<String, String> mapState = context.getBroadcastState(mapStateDescriptor);

                if ("DELETE".equals(type)) {
                    mapState.remove(id);
                } else {
                    mapState.put(id, name);
                }

            }
        });
        /********************************** 将 A 流 与广播出去的 B流 进行关联 end *************************************/

        //输出最终返回的流
        //(100001,A1,普通会员,2020-02-12)
        //(100002,A2,金牌会员,2020-02-05)
        //(100003,A3,钻石会员,2020-01-09)
        broadcastStream.print();

        FlinkUtils.getEnv().execute("BroadcastStateDemo");

    }
}

7.代码返回结果

返回如下结果,已经满足与维表关联的效果。

(100001,A1,普通会员,2020-02-12)
(100002,A2,金牌会员,2020-02-05)
(100003,A3,钻石会员,2020-01-09)

       如果此时,我们维护的维表有新数据要添加或者修改时,那么它会通过 Canal 采集,实时将增量数据发送到 Kafka 中指定的 Topic,实时系统便会读取到 Kafka 发来的数据,实现灵活关联操作。

3.Broadcast State 优缺点

       优点: 可以解决实时系统数据关联维表,每次都需要通过网络查询 SQL 的弊端;

       缺点: 不适用于数据量特别大的维表/规则数据。

       针对数据量特别大的维表或者规则数据,还是得建议你使用异步I/O来解决了。大量的数据就不能使用 Broadcast State了。因为它会在每一个 subtask 中都存储一份,太占用内存资源了。Broadcast State 只适用于少量的一些规则数据、字典表数据等。


博主写作不易,来个关注呗

求关注、求点赞,加个关注不迷路 ヾ(◍°∇°◍)ノ゙

博主不能保证写的所有知识点都正确,但是能保证纯手敲,错误也请指出,望轻喷 Thanks♪(・ω・)ノ

你可能感兴趣的:(Flink)