Flink之广播状态模式

一.提供的API

为了展示所提供的API,我们将以一个示例开始,然后介绍其完整功能。作为正在运行的示例,我们将使用这样的情况,其中有一系列不同颜色和形状的对象,并且我们希望找到遵循某种模式的相同颜色的对象对,例如矩形后跟三角形。我们假设这组有趣的模式会随着时间而演变。

在此示例中,第一个流将包含Item带有Color和Shape属性的type元素。另一个流将包含Rules。

从流开始Items,我们只需要键入它的Color,因为我们要对相同颜色的。这将确保相同颜色的元素最终出现在同一台物理计算机上。

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

移至Rules,应将包含它们的流广播到所有下游任务,并且这些任务应将它们存储在本地,以便它们可以针对所有传入的Items进行评估。下面的代码段将使用广播规则流,并且使用提供的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);

最后,根据Rules中的Item流中的传入元素评估,我们需要:

  1. 连接两个流。
  2. 指定匹配检测逻辑。

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

  1. 如果输入了密码,则该函数为KeyedBroadcastProcessFunction。
  2. 如果它是非键,则该函数为BroadcastProcessFunction。

鉴于我们的非广播流已加密,以下代码段包含上述调用:

注意:应该在非广播流上调用connect,并以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和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 stateDescriptor)
  2. 允许查询元素的时间戳:ctx.timestamp()
  3. 获取当前的水印: ctx.currentWatermark()
  4. 获得当前处理时间:ctx.currentProcessingTime()
  5. 将元素发射到侧面输出:ctx.output(OutputTag outputTag, X value)。

不同之处在于每个人对广播状态的访问类型。广播方对此具有读写访问权限,而非广播方具有只读访问权限(因此具有名称)。原因是在Flink中没有跨任务通信。因此,为确保我们操作的所有并行实例中,广播状态中的内容相同,我们仅向广播端提供读写访问权限,广播端在所有任务中看到的元素相同,因此我们需要对每个任务进行计算该端的传入元素在所有任务中都相同。忽略此规则将破坏状态的一致性保证,从而导致结果不一致,并且常常难以调试结果。

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

最后,由于KeyedBroadcastProcessFunction事实是在键控流上运行,因此它公开了某些功能,这些功能不适用于BroadcastProcessFunction。那是:

  1. 所述ReadOnlyContext的processElement()方法可以访问Flink的底层定时器服务,其允许注册事件和处理时间的定时器。当计时器触发时,onTimer()会使用调用, OnTimerContext公开了与ReadOnlyContextplus 相同的功能。询问触发的计时器是事件还是处理时间的能力,并且
    查询与计时器关联的键。
  2. 所述Context的processBroadcastElement()方法包含方法 applyToKeyedState(StateDescriptor stateDescriptor, KeyedStateFunction 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之后,本节重点介绍使用广播状态时要记住的重要事项。这些是:

  • 没有跨任务通信:如前所述,这就是为什么仅广播方 KeyedBroadcastProcessFunction/BroadcastProcessFunction可以修改广播状态的内容的原因。此外,用户必须确保所有任务对于每个传入元素都以相同的方式修改广播状态的内容。否则,不同的任务可能具有不同的内容,从而导致结果不一致。
  • 广播状态中事件的顺序在各个任务之间可能有所不同:尽管广播流的元素保证了所有元素(最终)将进入所有下游任务,但是元素对于每个任务的到达顺序可能不同。因此,每个传入元素的状态更新必须不取决于传入事件的顺序。
  • 所有任务都会检查其广播状态:尽管发生检查点时,所有任务在其广播状态中具有相同的元素(检查点屏障不会越过元素),但所有任务都将指向其广播状态,而不仅仅是其中一个。这是一项设计决策,要避免在还原过程中从同一文件读取所有任务(从而避免出现热点),尽管这样做的代价是将检查点状态的大小增加了p倍(并行度)。Flink保证在还原/缩放后不会重复,也不会丢失数据。在使用相同或更小的并行度进行恢复的情况下,每个任务都会读取其检查点状态。扩展后,每个任务都会读取自己的状态,其余任务(p_new-p_old)以循环方式读取先前任务的检查点。
  • 没有RocksDB状态后端:在运行时将广播状态保留在内存中,并且应该相应地进行内存配置。这适用于所有操作状态。

你可能感兴趣的:(大数据,流计算,Flink,flink)