天天看点

3.3、Flink流处理(Stream API)- State & Fault Tolerance(状态和容错)之 The Broadcast State Pattern(广播状态模式)提供的APIs重要内容

目录

提供的APIs

BroadcastProcessFunction and KeyedBroadcastProcessFunction

重要内容

使用状态描述算子状态,该状态在恢复时均匀地分布在算子的并行任务中,或者统一使用整个状态来初始化恢复后的并行任务。

第三种受支持的操作符状态是广播状态。广播状态被引入以支持这样的用例:来自一个流的一些数据需要广播到所有下游任务,在那里它被本地存储,并用于处理另一个流上的所有传入元素。例如,广播状态可以自然地出现,可以想象一个低吞吐量流包含一组规则,我们希望根据来自另一个流的所有元素对这些规则进行评估。

考虑到上述类型的用例,broadcast state 与其他算子状态的区别在于:

  1. map 格式
  2. 它只对特定的算子可用,这些算子有一个广播流和一个非广播流作为输入,并且
  3. 这样的算子可以具有多个具有不同名称的广播状态。

提供的APIs

为了展示所提供的api,我们将从一个示例开始,然后介绍它们的全部功能。作为我们的运行示例,我们将使用这样一种情况:我们有一个具有不同颜色和形状的对象流,并且我们希望找到遵循特定模式的相同颜色的对象对,例如,一个矩形后面跟着一个三角形。我们假设一组有趣的模式会随着时间的推移而发展。

在本例中,第一个流将包含带有颜色和形状属性的Item类型的元素。另一个流将包含规则。

从项目流开始,我们只需要按颜色对它进行 key by,因为我们想要相同颜色的对。这将确保相同颜色的元素最终出现在相同的物理机器上。

// key the shapes by color
KeyedStream<Item, Color> colorPartitionedStream = shapeStream
                        .keyBy(new KeySelector<Shape, Color>(){...});
           

继续讨论规则,包含它们的流应该广播到所有下游任务,这些任务应该将它们存储在本地,以便它们可以根据所有传入的 item 对它们进行评估。下面的代码片段将:

  1. 传播规则流
  2. 使用提供的MapStateDescriptor,它将创建规则存储的传播状态
// a map descriptor to store the name of the rule (string) and the rule itself.
MapStateDescriptor<String, Rule> ruleStateDescriptor = new MapStateDescriptor<>(
			"RulesBroadcastState",
			BasicTypeInfo.STRING_TYPE_INFO,
			TypeInformation.of(new TypeHint<Rule>() {}));
		
// broadcast the rules and create the broadcast state
BroadcastStream<Rule> ruleBroadcastStream = ruleStream
                        .broadcast(ruleStateDescriptor);
           

 最后,为了针对来自 item 流的输入元素评估规则,我们需要:

  1. 连接两个流,并且
  2. 指定我们的匹配检测逻辑。

可以通过调用非广播流上的 connect()来连接流(键控或非键控),参数为 BroadcastStream。这将返回 BroadcastConnectedStream,我们可以使用特殊类型的 CoProcessFunction 在其上调用 process()。该函数将包含我们的匹配逻辑。函数的确切类型取决于非广播流的类型:

  • 如果它是键控的,那么这个函数就是一个 KeyedBroadcastProcessFunction。
  • 如果它是非键控的,则该函数是BroadcastProcessFunction。

鉴于我们的非广播流是 keyed,以下片段包括上述调用:

注意:连接应该在非广播流上调用,以 BroadcastStream 作为参数。
DataStream<String> output = colorPartitionedStream
                 .connect(ruleBroadcastStream)
                 .process(
                     
                     // type arguments in our KeyedBroadcastProcessFunction represent: 
                     //   1. the key of the keyed stream
                     //   2. the type of elements in the non-broadcast side
                     //   3. the type of elements in the broadcast side
                     //   4. the type of the result, here a string
                     
                     new KeyedBroadcastProcessFunction<Color, Item, Rule, String>() {
                         // my matching logic
                     }
                 );
           

BroadcastProcessFunction and KeyedBroadcastProcessFunction

对于 CoProcessFunction,这些函数有两个要实现的过程方法;processBroadcastElement()负责处理广播流中的传入元素,而 processElement()用于非广播流。方法的完整签名如下:

public abstract class BroadcastProcessFunction<IN1, IN2, OUT> extends BaseBroadcastProcessFunction {

    public abstract void processElement(IN1 value, ReadOnlyContext ctx, Collector<OUT> out) throws Exception;

    public abstract void processBroadcastElement(IN2 value, Context ctx, Collector<OUT> out) throws Exception;
}
           
public abstract class KeyedBroadcastProcessFunction<KS, IN1, IN2, OUT> {

    public abstract void processElement(IN1 value, ReadOnlyContext ctx, Collector<OUT> out) throws Exception;

    public abstract void processBroadcastElement(IN2 value, Context ctx, Collector<OUT> out) throws Exception;

    public void onTimer(long timestamp, OnTimerContext ctx, Collector<OUT> out) throws Exception;
}
           

首先需要注意的是,这两个函数都需要 processBroadcastElement()方法的实现来处理广播端中的元素,而 processElement()方法需要实现来处理非广播端中的元素。

这两种方法在它们所提供的上下文中是不同的。非广播方有一个 ReadOnlyContext,而广播方有一个 Context。

这两个上下文(以下列举中的 ctx ):

  1. 允许访问广播状态:

    ctx.getBroadcastState(MapStateDescriptor<K, V> stateDescriptor)

  2. 允许查询元素的时间戳:

    ctx.timestamp()

  3. 获取当前水印:

    ctx.timestamp()

  4. 获取当前处理时间:

    ctx.currentProcessingTime()

  5. 发出元素:

    ctx.output(OutputTag<X> outputTag, X value)

getBroadcastState() 中的

stateDescriptor 

应该与上面的 .broadcast(ruleStateDescriptor) 中的

stateDescriptor 

相同。

区别在于每个广播状态的访问类型。广播方对其具有读写访问权,而非广播方具有只读访问权(即名称)。原因是在Flink中没有跨任务通信。为了保证广播状态下的内容在算子的所有并行实例中是相同的,我们只允许广播方进行读写访问,在所有任务中看到相同的元素,我们要求这一侧的每个输入元素的计算在所有任务中都是相同的。忽略这条规则会破坏状态的一致性保证,导致不一致和 debug 结果的困难。

注意:在“processBroadcast()”中实现的逻辑必须在所有并行实例中具有相同的确定性行为!

最后,由于KeyedBroadcastProcessFunction是在键控流上操作的,所以它公开了BroadcastProcessFunction不可用的一些功能。那就是:

  1. processElement()方法中的ReadOnlyContext允许访问Flink的底层计时器服务,该服务允许注册事件和/或处理时间计时器。当计时器触发时,使用OnTimerContext调用onTimer()(如上所示),OnTimerContext公开与ReadOnlyContext plus相同的功能。能够询问触发的计时器是事件还是处理时间;查询与计时器关联的 key。
  2. processBroadcastElement() 方法中的 context 包含 applyToKeyedState(StateDescriptor<S, VS> stateDescriptor, KeyedStateFunction<KS, S> function) 方法。

    这允许将 KeyedStateFunction 注册到与所提供的

    stateDescriptor 

    相关联的所有键的所有状态。
注意:只有在' KeyedBroadcastProcessFunction '的' processElement() '处才能注册计时器,而且只能在那里注册。这在“processBroadcastElement()”方法中是不可能的,因为没有与广播元素相关联的键。

回到我们最初的例子,我们的KeyedBroadcastProcessFunction看起来可能如下:

new KeyedBroadcastProcessFunction<Color, Item, Rule, String>() {

    // store partial matches, i.e. first elements of the pair waiting for their second element
    // we keep a list as we may have many first elements waiting
    private final MapStateDescriptor<String, List<Item>> mapStateDesc =
        new MapStateDescriptor<>(
            "items",
            BasicTypeInfo.STRING_TYPE_INFO,
            new ListTypeInfo<>(Item.class));

    // identical to our ruleStateDescriptor above
    private final MapStateDescriptor<String, Rule> ruleStateDescriptor = 
        new MapStateDescriptor<>(
            "RulesBroadcastState",
            BasicTypeInfo.STRING_TYPE_INFO,
            TypeInformation.of(new TypeHint<Rule>() {}));

    @Override
    public void processBroadcastElement(Rule value,
                                        Context ctx,
                                        Collector<String> out) throws Exception {
        ctx.getBroadcastState(ruleStateDescriptor).put(value.name, value);
    }

    @Override
    public void processElement(Item value,
                               ReadOnlyContext ctx,
                               Collector<String> out) throws Exception {

        final MapState<String, List<Item>> state = getRuntimeContext().getMapState(mapStateDesc);
        final Shape shape = value.getShape();
    
        for (Map.Entry<String, Rule> entry :
                ctx.getBroadcastState(ruleStateDescriptor).immutableEntries()) {
            final String ruleName = entry.getKey();
            final Rule rule = entry.getValue();
    
            List<Item> stored = state.get(ruleName);
            if (stored == null) {
                stored = new ArrayList<>();
            }
    
            if (shape == rule.second && !stored.isEmpty()) {
                for (Item i : stored) {
                    out.collect("MATCH: " + i + " - " + value);
                }
                stored.clear();
            }
    
            // there is no else{} to cover if rule.first == rule.second
            if (shape.equals(rule.first)) {
                stored.add(value);
            }
    
            if (stored.isEmpty()) {
                state.remove(ruleName);
            } else {
                state.put(ruleName, stored);
            }
        }
    }
}
           

重要内容

在描述了所提供的api之后,本节将重点介绍在使用广播状态时需要记住的重要事项。这些都是:

  • 没有跨任务通信:如前所述,这就是为什么只有(键入)broadcastprocessfunction的广播端可以修改广播状态的内容的原因。此外,用户必须确保所有任务以相同的方式修改每个传入元素的广播状态的内容。否则,不同的任务可能有不同的内容,导致结果不一致。
  • 广播状态中事件的顺序可能因任务而异:尽管广播流的元素可以保证所有元素(最终)都将转到所有下游任务,但是元素到达每个任务的顺序可能不同。因此,每个传入元素的状态更新不能依赖于传入事件的顺序。
  • 所有任务检查它们的广播状态:虽然发生检查点时,所有任务的广播状态都具有相同的元素(检查点屏障不会跳过元素),但是所有任务都将检查它们的广播状态,而不是其中一个。这个设计避免在恢复期间从同一文件读取所有任务(从而避免热点),尽管这是以将检查点状态的大小增加p(=并行度)为代价的。Flink 保证在恢复/重新缩放时不会有重复和丢失数据。在并行度相同或更小的情况下进行恢复,每个任务读取其检查点状态。在扩展时,每个任务读取自己的状态,其余的任务(p_new-p_old)以循环方式读取以前任务的检查点。
  • 没有 RocksDB 状态后端:广播状态在运行时保存在内存中,应该相应地执行内存供应。这适用于所有的操作符状态。