本文开头附:Flink 学习路线系列 ^ _ ^
在 Apache Flink 1.5.0 中引入了广播状态(Broadcast State)。广播状态模式指的一种流应用程序,其中低吞吐量的事件流(例如,包含一组规则数据/字典数据)被广播到某个 operator 的所有并发实例中,然后针对来自另一条原始数据流中的数据,进行关联操作。此处附:Flink Broadcast State 官网英文介绍
我们接下来要介绍的 Broadcast State, 就类似于 Map 结构,我们可以把 Broadcast State 就理解为是一个大 Map
,它也可以执行 put、get、putAll、remove 等操作。
现在有两个流:一个是数据流(A)
,一个是广播流(B)
。这两个流的来源我们都可以自定义,这里我们就用平常最常用的 Kafka 来作为输入源。
1.场景:
我们在操作SQL 时,经常会遇到关联维表数据的操作。接下来我们就在实时系统中使用关联维表数据这一场景,来介绍 Broadcast State 的使用。
2.以前的解决方式:
Ⅰ.
之前解决这问题,我们会对流中的数据进行 Transformation 算子操作,通过 new RichMapFuntion() 方式,重写其中的 open() 、close()、map() 方法。具体步骤如下:
- 在 open() 方法中连接数据库;
- 在 map() 方法中查询数据库中的维表信息,并将流中的数据与数据库查询的数据进行关联,从而达到关联维表的目的;
- 在 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 发来的数据,实现灵活关联操作。
优点: 可以解决实时系统数据关联维表,每次都需要通过网络查询 SQL 的弊端;
缺点: 不适用于数据量特别大的维表/规则数据。
针对数据量特别大的维表或者规则数据,还是得建议你使用异步I/O来解决了。大量的数据就不能使用 Broadcast State了。因为它会在每一个 subtask 中都存储一份,太占用内存资源了。Broadcast State 只适用于少量的一些规则数据、字典表数据等。
博主写作不易,来个关注呗
求关注、求点赞,加个关注不迷路 ヾ(◍°∇°◍)ノ゙
博主不能保证写的所有知识点都正确,但是能保证纯手敲,错误也请指出,望轻喷 Thanks♪(・ω・)ノ