Flink基础(八):流作业中的广播变量和BroadcastState

Broadcast State

支持将某一个流的数据广播到下游所有的 Task 中,数据都会存储在下游 Task 内存中,接收到广播的数据流后就可以在操作中利用这些数据,一般我们会将一些规则数据进行这样广播下去,然后其他的 Task 也都能根据这些规则数据做配置,更常见的就是规则动态的更新,然后下游还能够动态的感知。
Broadcast state 的特点是:

  • 使用 Map 类型的数据结构
  • 仅适用于同时具有广播流和非广播流作为数据输入的特定算子
  • 可以具有多个不同名称的 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() 方法执行处理逻辑,需要指定一个逻辑实现类作为参数,具体是哪种实现类取决于非广播流的类型:

  • 如果非广播流是 keyed stream,需要实现 KeyedBroadcastProcessFunction
  • 如果非广播流是 non-keyed stream,需要实现 BroadcastProcessFunction

那么该怎么获取这个 Broadcast state 呢,它需要通过上下文来获取:

ctx.getBroadcastState(ruleStateDescriptor)

BroadcastProcessFunction 和 KeyedBroadcastProcessFunction
这两个抽象函数有两个相同的需要实现的接口:

  • processBroadcastElement():处理广播流中接收的数据元
  • processElement():处理非广播流数据的方法

用于处理非广播流是 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:ctx.getBroadcastState(MapStateDescriptor stateDescriptor)
  • 查询数据元的时间戳:ctx.timestamp()
  • 获取当前水印:ctx.currentWatermark()
  • 获取当前处理时间:ctx.currentProcessingTime()
  • 向旁侧输出(side-outputs)发送数据:ctx.output(OutputTag outputTag, X value)

这两者不同之处在于对 Broadcast state 的访问限制:广播方对其具有读和写的权限(read-write),非广播方只有读的权限(read-only),为什么要这么设计呢,主要是为了保证 Broadcast state 在算子的所有并行实例中是相同的。由于 Flink 中没有跨任务的通信机制,在一个任务实例中的修改不能在并行任务间传递,而广播端在所有并行任务中都能看到相同的数据元,只对广播端提供可写的权限。同时要求在广播端的每个并行任务中,对接收数据的处理是相同的。如果忽略此规则会破坏 State 的一致性保证,从而导致不一致且难以诊断的结果。也就是说,processBroadcast() 的实现逻辑必须在所有并行实例中具有相同的确定性行为。

使用 Broadcast state 需要注意
  • 没有跨任务的通信,这就是为什么只有广播方可以修改 Broadcast state 的原因。
  • 用户必须确保所有任务以相同的方式为每个传入的数据元更新 Broadcast state,否则可能导致结果不一致。
  • 跨任务的 Broadcast state
    中的事件顺序可能不同,虽然广播的元素可以保证所有元素都将转到所有下游任务,但元素到达的顺序可能不一致。因此,Broadcast state
    更新不能依赖于传入事件的顺序。
  • 所有任务都会把 Broadcast state 存入 checkpoint,虽然 checkpoint 发生时所有任务都具有相同的
    Broadcast state。这是为了避免在恢复期间所有任务从同一文件中进行恢复(避免热点),然而代价是 state 在
    checkpoint 时的大小成倍数(并行度数量)增加。
  • Flink 确保在恢复或改变并行度时不会有重复数据,也不会丢失数据。在具有相同或改小并行度后恢复的情况下,每个任务读取其状态
    checkpoint。在并行度增大时,原先的每个任务都会读取自己的状态,新增的任务以循环方式读取前面任务的检查点。
  • 不支持 RocksDB state backend,Broadcast state 在运行时保存在内存中。

实战

需求:现在需要根据告警规则来对监控的流数据做判断,如果符合告警规则则告警,但是告警规则是可能随时调整的,调整的时候要求不能中断作业。

分析:更改之后就需要让监控的作业能够去感知到之前的规则发生了变动,所以就需要在作业中想个什么办法去获取到更改后的数据。有两种方式可以让作业知道规则的变更: push 和 pull 模式。

  • push 模式则需要在更新、删除、新增接口中不仅操作数据库,还需要额外的发送更新、删除、新增规则的事件到消息队列中,然后作业消费消息队列的数据再去做更新、删除、新增规则,这种及时性有保证,但是可能会有数据不统一的风险(如果消息队列的数据丢了,但是在接口中还是将规则的数据变更存储到数据库);
  • pull 模式下就需要作业定时去查找一遍所有的告警规则数据,然后存在作业内存中,这个时间可以设置的比较短,比如 1 分钟,这样就能既保证数据的一致性,时间延迟也是在容忍范围之内。

下面就演示如何用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));
        }
    }
}

这样就完成了整个需求,也能动态感知告警规则的变化…

你可能感兴趣的:(Flink学习之路)