支持将某一个流的数据广播到下游所有的 Task 中,数据都会存储在下游 Task 内存中,接收到广播的数据流后就可以在操作中利用这些数据,一般我们会将一些规则数据进行这样广播下去,然后其他的 Task 也都能根据这些规则数据做配置,更常见的就是规则动态的更新,然后下游还能够动态的感知。
Broadcast state 的特点是:
那么我们该如何使用 Broadcast State 呢?下面通过一个例子来讲解一下,在这个例子中,我要广播的数据是监控告警的通知策略规则,然后下游拿到我这个告警通知策略去判断哪种类型的告警发到哪里去,该使用哪种方式来发,静默时间多长等。
第一个数据流是要处理的数据源,流中的对象具有告警或者恢复的事件,其中用一个 type 字段来标识哪个事件是告警,哪个事件是恢复,然后还有其他的字段标明是哪个集群的或者哪个项目的,简单代码如下:
DataStreamSource alertData = env.addSource(new FlinkKafkaConsumer011<>("alert",
new AlertEventSchema(),
parameterTool.getProperties()));
然后第二个数据流是要广播的数据流,它是告警通知策略数据(定时从 MySQL 中读取的规则表),简单代码如下:
DataStreamSource alarmdata = env.addSource(new GetAlarmNotifyData());
// MapState 中保存 (RuleName, Rule) ,在描述类中指定 State name
MapStateDescriptor ruleStateDescriptor = new MapStateDescriptor<>(
"RulesBroadcastState",
BasicTypeInfo.STRING_TYPE_INFO,
TypeInformation.of(new TypeHint() {}));
// alarmdata 使用 MapStateDescriptor 作为参数广播,得到广播流
BroadcastStream ruleBroadcastStream = alarmdata.broadcast(ruleStateDescriptor);
然后你要做的是将两个数据流进行连接,连接后再根据告警规则数据流的规则数据进行处理
alertData.connect(ruleBroadcastStream)
.process(
new KeyedBroadcastProcessFunction() {
//根据告警规则的数据进行处理告警事件
}
)
alertData.connect(ruleBroadcastStream)
该 connect 方法将两个流连接起来后返回一个 BroadcastConnectedStream 对象,BroadcastConnectedStream 调用 process() 方法执行处理逻辑,需要指定一个逻辑实现类作为参数,具体是哪种实现类取决于非广播流的类型:
那么该怎么获取这个 Broadcast state 呢,它需要通过上下文来获取:
ctx.getBroadcastState(ruleStateDescriptor)
BroadcastProcessFunction 和 KeyedBroadcastProcessFunction
这两个抽象函数有两个相同的需要实现的接口:
用于处理非广播流是 non-keyed stream 的情况:
public abstract class BroadcastProcessFunction extends BaseBroadcastProcessFunction {
public abstract void processElement(IN1 value, ReadOnlyContext ctx, Collector out) throws Exception;
public abstract void processBroadcastElement(IN2 value, Context ctx, Collector out) throws Exception;
}
用于处理非广播流是 keyed stream 的情况:
public abstract class KeyedBroadcastProcessFunction {
public abstract void processElement(IN1 value, ReadOnlyContext ctx, Collector out) throws Exception;
public abstract void processBroadcastElement(IN2 value, Context ctx, Collector out) throws Exception;
public void onTimer(long timestamp, OnTimerContext ctx, Collector out) throws Exception;
}
可以看到这两个接口提供的上下文对象有所不同。非广播方(processElement)使用 ReadOnlyContext,而广播方(processBroadcastElement)使用 Context。这两个上下文对象(简称 ctx)通用的方法接口有:
这两者不同之处在于对 Broadcast state 的访问限制:广播方对其具有读和写的权限(read-write),非广播方只有读的权限(read-only),为什么要这么设计呢,主要是为了保证 Broadcast state 在算子的所有并行实例中是相同的。由于 Flink 中没有跨任务的通信机制,在一个任务实例中的修改不能在并行任务间传递,而广播端在所有并行任务中都能看到相同的数据元,只对广播端提供可写的权限。同时要求在广播端的每个并行任务中,对接收数据的处理是相同的。如果忽略此规则会破坏 State 的一致性保证,从而导致不一致且难以诊断的结果。也就是说,processBroadcast() 的实现逻辑必须在所有并行实例中具有相同的确定性行为。
需求:现在需要根据告警规则来对监控的流数据做判断,如果符合告警规则则告警,但是告警规则是可能随时调整的,调整的时候要求不能中断作业。
分析:更改之后就需要让监控的作业能够去感知到之前的规则发生了变动,所以就需要在作业中想个什么办法去获取到更改后的数据。有两种方式可以让作业知道规则的变更: push 和 pull 模式。
下面就演示如何用pull模式来演示如何实现这个需求
首先自定义 Source 以一个并行度去读取 MySQL 中的告警规则数据,代码如下:
//定时从数据库中查出告警规则数据
DataStreamSource> alarmDataStream = env.addSource(new GetAlertRuleSourceFunction()).setParallelism(1);
public class GetAlertRuleSourceFunction extends RichSourceFunction> {
PreparedStatement ps;
private Connection connection;
private volatile boolean isRunning = true;
private ParameterTool parameterTool;
@Override
public void open(Configuration parameters) throws Exception {
parameterTool = (ParameterTool) getRuntimeContext().getExecutionConfig().getGlobalJobParameters();
connection = getConnection();
String sql = "select * from alert_rule;";
if (connection != null) {
ps = this.connection.prepareStatement(sql);
}
}
@Override
public void run(SourceContext> ctx) throws Exception {
List list = new ArrayList<>();
while (isRunning) {
ResultSet resultSet = ps.executeQuery();
while (resultSet.next()) {
AlertRule alertRule = new AlertRule().builder()
.id(resultSet.getInt("id"))
.name(resultSet.getString("name"))
.measurement(resultSet.getString("measurement"))
.thresholds(resultSet.getString("thresholds"))
.build();
list.add(alertRule);
}
log.info("=======select alarm notify from mysql, size = {}, map = {}", list.size(), list);
ctx.collect(list);
list.clear();
Thread.sleep(1000 * 60);
}
}
@Override
public void cancel() {
try {
super.close();
if (connection != null) {
connection.close();
}
if (ps != null) {
ps.close();
}
} catch (Exception e) {
log.error("runException:{}", e);
}
isRunning = false;
}
private static Connection getConnection() {
//获取数据库连接
}
}
这就是每隔一分钟就去数据库读取一次告警规则
按照上面讲的,首先需要一个MapStateDescriptor
final static MapStateDescriptor ALERT_RULE = new MapStateDescriptor<>(
"alert_rule",
BasicTypeInfo.STRING_TYPE_INFO,
TypeInformation.of(AlertRule.class));
定义好了之后开始将监控数据与告警规则数据通过 connect 算子进行连接。
SingleOutputStreamOperator machineData = env.addSource(consumer)
.assignTimestampsAndWatermarks(new MetricWatermark());
DataStreamSource> alarmDataStream = env.addSource(new GetAlertRuleSourceFunction()).setParallelism(1);//定时从数据库中查出告警规则数据
machineData.connect(alarmDataStream.broadcast(ALERT_RULE)).process(...)
其中连接的时候需要使用 broadcast 算子将告警规则数据广播,接着在 process 中的 processElement 方法中处理监控数据并与广播数据进行关联,在 processBroadcastElement 方法中处理广播数据,代码如下:
new BroadcastProcessFunction, MetricEvent>() {
@Override
public void processElement(MetricEvent value, ReadOnlyContext ctx, Collector out) throws Exception {
ReadOnlyBroadcastState broadcastState = ctx.getBroadcastState(ALERT_RULE);
if (broadcastState.contains(value.getName())) {
AlertRule alertRule = broadcastState.get(value.getName());
double used = (double) value.getFields().get(alertRule.getMeasurement());
if (used > Double.valueOf(alertRule.getThresholds())) {
log.info("AlertRule = {}, MetricEvent = {}", alertRule, value);
out.collect(value);
}
}
}
@Override
public void processBroadcastElement(List value, Context ctx, Collector out) throws Exception {
if (value == null || value.size() == 0) {
return;
}
BroadcastState alertRuleBroadcastState = ctx.getBroadcastState(ALERT_RULE);
for (int i = 0; i < value.size(); i++) {
alertRuleBroadcastState.put(value.get(i).getName(), value.get(i));
}
}
}
这样就完成了整个需求,也能动态感知告警规则的变化…