flink DataStream API(四)状态和容错-使用状态

使用State

在本节中,您将了解 Flink 提供的用于编写有状态程序的 API。请查看 Stateful Stream Processing以了解有状态流处理背后的概念。

Keyed DataStream

如果要使用Keyed state,首先需要在 DataStream 上指定一个key,该key用于对状态进行分区。你可以在DataStream上使用Java/Scala API中的keyBy(KeySelector)Python API中的key_by(KeySelector)指定一个key。这将产生一个KeyedStream,然后允许使用keyed的操作。

key选择器函数将单个记录作为输入并返回该记录的keykey可以是任何类型,并且必须是从确定性计算中派生出来的。

Flink 的数据模型不是基于键值对的。因此不需要将数据集包装成键和值。key是虚拟的,他们作为函数被定义,用来指导运算符进行数据分组。

以下示例显示了一个key选择器函数,该函数仅返回对象的字段:

// some ordinary POJO
public class WC {
  public String word;
  public int count;

  public String getWord() { return word; }
}
DataStream<WC> words = // [...]
KeyedStream<WC> keyed = words
  .keyBy(WC::getWord);

使用keyed state

keyed state接口提供了对不同类型状态的访问,这些状态的范围都限定在当前输入元素的键内。这意味着这种类型的状态只能在 KeyedStream 上使用,它可以通过 Java/Scala API 中的 stream.keyBy(…)Python API 中的 stream.key_by(…) 创建.

现在,我们将首先查看不同类型的可用状态,然后我们将看到如何在程序中使用它们。以下是不同类型的可用状态:

  • ValueState<T>:该状态只能保存一个可以更新和检索的值(如上所述,范围限定为输入元素的键内,所以此操作在每个键内可能都有一个值)。

  • ListState<T>:改状态是一个列表,用于保存多个元素。您可以在向改状态中追加元素,并且可以获取所有元素的Iterable。使用 add(T)addAll(List<T>) 添加元素,可以使用 Iterable<T> get() 获取 Iterable。您还可以使用 update(List<T>) 覆盖现有列表。

  • ReducingState<T>:改状态仅保存一个值,该值等于向该状态中添加的所有元素的聚合值。该接口类似于ListState,但是使用add(T)增加的元素会通过使用指定的ReduceFunction进行聚合。该状态保存的就是聚合后的值。

  • AggregatingState<IN, OUT>:改状态仅保存一个值,该值表示添加到该状态中的所有值的聚合结果。与 ReducingState 相反,聚合类型可能与添加到状态中的元素类型不同。该接口与 ListState 相同,但使用 add(IN) 添加到该状态中的元素将使用指定的 AggregateFunction 进行聚合。

  • MapState<UK, UV>:用于保存一个映射列表。您可以将键值对放入该状态并检索当前存储的所有映射的 Iterable。使用 put(UK, UV)putAll(Map<UK, UV>) 添加映射。可以使用 get(UK) 检索值。可以分别使用 entry()、keys()values() 检索映射、键和值。您还可以使用 isEmpty() 来检查此映射是否为空。

所有类型的状态也有一个 clear() 方法,用于清除当前key的状态。

重要的是要记住,这些状态对象仅用于与状态相联系。状态不一定存储在内部,可能存储在磁盘或其他地方。要记住的第二件事是,您从状态中获得的值取决于输入元素的键。因此,如果涉及的键不同,返回的值有可能不同。

要获得状态句柄,您必须创建一个StateDescriptorStateDescriptor包含状态的名称(正如我们稍后将看到的,您可以创建多个状态,并且它们必须具有唯一的名称以便您可以引用它们)、状态所保存值的类型,以及用户可能指定的函数,例如 ReduceFunction。根据您要检索的状态类型,您可以创建 ValueStateDescriptor、ListStateDescriptor、AggregatingStateDescriptor、ReducingStateDescriptorMapStateDescriptor

状态是使用 RuntimeContext 访问的。 RuntimeContext 具有以下访问状态的方法:

  • ValueState<T> getState(ValueStateDescriptor<T>)
  • ReducingState<T> getReducingState(ReducingStateDescriptor<T>)
  • ListState<T> getListState(ListStateDescriptor<T>)
  • AggregatingState<IN, OUT> getAggregatingState(AggregatingStateDescriptor<IN, ACC, OUT>)
  • MapState<UK, UV> getMapState(MapStateDescriptor<UK, UV>)
public class CountWindowAverage extends RichFlatMapFunction<Tuple2<Long, Long>, Tuple2<Long, Long>> {

    /**
     * The ValueState handle. The first field is the count, the second field a running sum.
     */
    private transient ValueState<Tuple2<Long, Long>> sum;

    @Override
    public void flatMap(Tuple2<Long, Long> input, Collector<Tuple2<Long, Long>> out) throws Exception {

        // access the state value
        Tuple2<Long, Long> currentSum = sum.value();

        // update the count
        currentSum.f0 += 1;

        // add the second field of the input value
        currentSum.f1 += input.f1;

        // update the state
        sum.update(currentSum);

        // if the count reaches 2, emit the average and clear the state
        if (currentSum.f0 >= 2) {
            out.collect(new Tuple2<>(input.f0, currentSum.f1 / currentSum.f0));
            sum.clear();
        }
    }

    @Override
    public void open(Configuration config) {
        ValueStateDescriptor<Tuple2<Long, Long>> descriptor =
                new ValueStateDescriptor<>(
                        "average", // the state name
                        TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {}), // type information
                        Tuple2.of(0L, 0L)); // default value of the state, if nothing was set
        sum = getRuntimeContext().getState(descriptor);
    }
}

// this can be used in a streaming program like this (assuming we have a StreamExecutionEnvironment env)
env.fromElements(Tuple2.of(1L, 3L), Tuple2.of(1L, 5L), Tuple2.of(1L, 7L), Tuple2.of(1L, 4L), Tuple2.of(1L, 2L))
        .keyBy(value -> value.f0)
        .flatMap(new CountWindowAverage())
        .print();

// the printed output will be (1,4) and (1,5)

State 生存时间 (TTL)

可以将生存时间(TTL)分配给任何类型的Keyed state。如果配置了一个TTL,并且一个状态值已经过期,则存储的值将会尽可能地清除,下面将详细讨论这个问题。

所有集合类型的状态都支持对每个条目进行ttl设置。这意味着列表元素和映射项独立过期。

为了使用状态 TTL,必须首先构建一个 StateTtlConfig 配置对象。通过配置,TTL功能可以在任何状态描述符中启用:

import org.apache.flink.api.common.state.StateTtlConfig;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.time.Time;

StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1))
    .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
    .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
    .build();
    
ValueStateDescriptor<String> stateDescriptor = new ValueStateDescriptor<>("text state", String.class);
stateDescriptor.enableTimeToLive(ttlConfig);

该配置有几个选项需要注意:

newBuilder 方法的第一个参数是必需的,它是生存时间值。

状态TTL的刷新通过更新的类型来决定(默认情况下为OnCreateAndWrite):

  • StateTtlConfig.UpdateType.OnCreateAndWrite : 仅在创建和写入访问的时候刷新TTL
  • StateTtlConfig.UpdateType.OnReadAndWrite -:仅在读和写入的时候刷新TTL。

状态可见性配置决定是否在读取时返回过期值(如果尚未清除)(默认为 NeverReturnExpired):

  • StateTtlConfig.StateVisibility.NeverReturnExpired :过期值从不会被返回
  • StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp :如果仍然可用则返回

ReturnExpiredIfNotCleanedUp 允许在清除之前返回过期状态。

清除过期状态

默认情况下,过期值会在读取时显式删除,例如 ValueState#value,如果配置的状态后端支持,则在后台定期垃圾清理。可以在 StateTtlConfig 中禁用后台清理:

import org.apache.flink.api.common.state.StateTtlConfig;
StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1))
    .disableCleanupInBackground()
    .build();

要对后台的某些特殊清理进行更细粒度的控制,您可以按如下所述单独配置它。目前,堆状态后端依赖于增量清理,RocksDB后端使用压缩过滤器进行后台清理。

完整快照清理

此外,您可以在获取完整状态快照时激活清理,这将减少快照的大小。在当前实现下,本地状态不会被清除,但在从上一个快照恢复的情况下,它将不包括已删除的过期状态。可以在StateTtlConfig中配置:

import org.apache.flink.api.common.state.StateTtlConfig;
import org.apache.flink.api.common.time.Time;

StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1))
    .cleanupFullSnapshot()
    .build();

此选项不适用于 RocksDB 状态后端的增量检查点。

增量清理

另一个选项是以增量方式触发某些状态项的清理。触发器可以是来自每个状态访或者处理每个记录的回调,如果此清理策略在特定状态下处于活动状态,则存储后端会在其所有条目上为此状态保留一个惰性全局迭代器,每次触发增量清理时,遍历的状态里的条目将被检查,过期的条目将被清除。

这个特性可以在 StateTtlConfig 中配置:

import org.apache.flink.api.common.state.StateTtlConfig;
 StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1))
    .cleanupIncrementally(10, true)
    .build();

这个策略有两个参数。第一个是每次清除触发时检查的状态条目的数量。每访问状态时都会触发。第二个参数定义是否在每个记录处理时额外触发清理。

提示:

  • 如果没有访问该状态或没有处理任何记录,则过期状态将持续存在。
  • 增量清理所花费的时间会增加记录处理延迟。
  • 目前仅对堆状态后端设置增量清理。为 RocksDB 设置它没有任何效果。
  • 对于现有作业,可以随时在 StateTtlConfig 中激活或停用此清理策略,例如从保存点重新启动后。

RocksDB 压缩期间的清理

如果使用了RocksDB状态后端,则会调用一个Flink特定的压缩过滤器来进行后台清理。RocksDB定期运行异步压缩来合并状态从而更新状态并减少存储。Flink压缩过滤器检查带有TTL的状态项的过期时间戳,并清理过期值。

这个特性可以在 StateTtlConfig 中配置:

import org.apache.flink.api.common.state.StateTtlConfig;

StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.seconds(1))
    .cleanupInRocksdbCompactFilter(1000)
    .build();

RocksDB 压缩过滤器会在每次处理一定数量的状态条目后从 Flink 查询当前时间戳,用于检查过期时间。您可以更改它并将自定义值传递给 StateTtlConfig.newBuilder(...).cleanupInRocksdbCompactFilter(long queryTimeAfterNumEntries) 方法。更频繁地更新时间戳可以提高清理速度,但会降低压缩性能,因为它使用来自本机代码的 JNI 调用。 RocksDB 后端的默认后台清理每处理 1000 个条目就查询当前时间戳。

通过激活FlinkCompactionFilter的调试级别,您可以从RocksDB过滤器的本地代码中激活调试日志:

log4j.logger.org.rocksdb.FlinkCompactionFilter=DEBUG

提示:

  • 在压缩过程中调用TTL滤波器会减慢它的速度。TTL过滤器必须解析最后一次访问的时间戳,并为每个正在被压缩的键的存储状态条目检查其过期时间。对于集合状态类型(列表或映射),也会对每个存储的元素调用检查。
  • If this feature is used with a list state which has elements with non-fixed byte length, the native TTL filter has to call additionally a Flink java type serializer of the element over JNI per each state entry where at least the first element has expired to determine the offset of the next unexpired element.
  • 对于现有作业,可以随时在 StateTtlConfig 中激活或停用此清理策略,例如从保存点重新启动后。

算子 State

算子状态(或非键态)是绑定到一个并行算子实例的状态。Kafka连接器是在Flink中使用算子 State的一个很好的例子。Kafka消费者的每个并行实例都维护一个主题分区和偏移量的映射,作为它的算子状态。

算子状态接口支持在并行度改变时在并行算子实例之间重新分配状态。这种再分配有不同的方案。

在典型的有状态 Flink 应用程序中,您不需要算子状态。它主要是一种特殊类型的状态。

注意: Python DataStream API 仍然不支持 算子 状态。

Broadcast State

广播状态是一种特殊的算子状态。引入它是为了支持需要将一个流的记录广播到所有下游任务中,这些记录用于在所有子任务之间维护相同的状态。然后可以在处理第二个流的记录时访问此状态。举个例子,我们可以想象一个低吞吐量的流包含一组规则,我们想要利用这组规则对来自另一个流的所有元素进行评估。考虑到上述类型的用例,广播状态与其他算子状态的不同之处在于:

  • 他有一个映射格式
  • 它仅适用于具有广播流和非广播流作为输入的特定算子,以及
  • 这样的算子可以有多个不同名称的广播状态。

注意:Python DataStream API 仍然不支持广播状态。

使用算子 State

要使用算子状态,有状态函数可以实现CheckpointedFunction接口。

CheckpointedFunction接口通过不同的重新分配方案提供对非键状态的访问。它需要实施两种方法:

void snapshotState(FunctionSnapshotContext context) throws Exception;

void initializeState(FunctionInitializationContext context) throws Exception;

每当需要执行检查点时,就会调用snapshotState()。对应的initializeState()在每次初始化用户定义函数时调用,无论是在函数首次初始化时,还是在函数实际上从较早的检查点恢复时。因此,initializeState()不仅是初始化不同类型状态的地方,而且也是包含状态恢复逻辑的地方。

当前只支持列表类型的算子状态。状态应该是可序列化对象的列表,彼此独立。换句话说,这些对象是可以重新分配非键控状态的最佳粒度。根据状态访问方法,定义了以下重新分配方案:

  • 平分重分发:每个算子返回一个状态元素列表。整个状态在逻辑上是所有列表的串联。在恢复/重分发时,列表被均匀地分割成与并行算子相同数量的子列表。每个算子都获得一个子列表,子列表可以为空,也可以包含一个或多个元素。例如,如果并行度为1,算子的检查点状态包含元素element1和element2,那么当并行度增加到2时,element1可能会在算子实例0中,而element2将进入算子实例1。
  • 联合再分配:每个算子返回一个状态元素列表。整个状态在逻辑上是所有列表的串联。在恢复/重新分配时,每个算子都会获得完整的状态元素列表。如果您的列表可能具有高基数,请不要使用此功能。检查点元数据将存储每个列表条目的偏移量,这可能导致 RPC 帧大小或内存不足错误。

下面是一个有状态SinkFunction的例子,它在将元素发送到外部之前使用CheckpointedFunction来缓冲元素。它演示了基本的均分重分发列表状态:

public class BufferingSink
        implements SinkFunction<Tuple2<String, Integer>>,
                   CheckpointedFunction {

    private final int threshold;

    private transient ListState<Tuple2<String, Integer>> checkpointedState;

    private List<Tuple2<String, Integer>> bufferedElements;

    public BufferingSink(int threshold) {
        this.threshold = threshold;
        this.bufferedElements = new ArrayList<>();
    }

    @Override
    public void invoke(Tuple2<String, Integer> value, Context contex) throws Exception {
        bufferedElements.add(value);
        if (bufferedElements.size() == threshold) {
            for (Tuple2<String, Integer> element: bufferedElements) {
                // send it to the sink
            }
            bufferedElements.clear();
        }
    }

    @Override
    public void snapshotState(FunctionSnapshotContext context) throws Exception {
        checkpointedState.clear();
        for (Tuple2<String, Integer> element : bufferedElements) {
            checkpointedState.add(element);
        }
    }

    @Override
    public void initializeState(FunctionInitializationContext context) throws Exception {
        ListStateDescriptor<Tuple2<String, Integer>> descriptor =
            new ListStateDescriptor<>(
                "buffered-elements",
                TypeInformation.of(new TypeHint<Tuple2<String, Integer>>() {}));

        checkpointedState = context.get算子StateStore().getListState(descriptor);

        if (context.isRestored()) {
            for (Tuple2<String, Integer> element : checkpointedState.get()) {
                bufferedElements.add(element);
            }
        }
    }
}

initializeState方法接受FunctionInitializationContext作为参数。他是初始化非监控状态的容器。这些是ListState的容器。其中非键控状态对象将在checkpoint的时后被存储。

请注意状态是如何初始化的,类似于键控状态,其 StateDescriptor 包含状态名称和有关状态的其他信息。

ListStateDescriptor<Tuple2<String, Integer>> descriptor =
    new ListStateDescriptor<>(
        "buffered-elements",
        TypeInformation.of(new TypeHint<Tuple2<String, Integer>>() {}));

checkpointedState = context.getOperatorStateStore().getListState(descriptor);

状态访问方法的命名约定包含了它的再分配模式和状态结构。例如,要在恢复时使用联合重新分配方案的列表状态,可以使用getUnionListState(descriptor)访问状态。如果方法名不包含重新分配模式,例如getListState(descriptor),它仅仅意味着将使用基本的均匀分割重新分配模式。

初始化容器后,我们使用上下文的 isRestored() 方法来检查我们是否在失败后正在恢复。如果这是真的,即我们正在恢复,则应用恢复逻辑。

如修改后的BufferingSink的代码所示,在状态初始化期间恢复的这个ListState保存在一个类变量中,以便将来在snapshotState()中使用。在那里,ListState将清除前一个检查点包含的所有对象,然后填充我们想要checkpoint的新对象。

顺便说明一下,监控状态也可以在initializeState()方法中初始化。这可以使用提供的FunctionInitializationContext来完成。

有状态的源函数

与其他操作符相比,有状态源需要更多的关注。为了使状态和输出集合的更新是原子的(对于故障/恢复时只需要一次语义),用户需要从源的上下文中获得一个锁。

public static class CounterSource
        extends RichParallelSourceFunction<Long>
        implements CheckpointedFunction {

    /**  current offset for exactly once semantics */
    private Long offset = 0L;

    /** flag for job cancellation */
    private volatile boolean isRunning = true;
    
    /** Our state object. */
    private ListState<Long> state;

    @Override
    public void run(SourceContext<Long> ctx) {
        final Object lock = ctx.getCheckpointLock();

        while (isRunning) {
            // output and state update are atomic
            synchronized (lock) {
                ctx.collect(offset);
                offset += 1;
            }
        }
    }

    @Override
    public void cancel() {
        isRunning = false;
    }

    @Override
    public void initializeState(FunctionInitializationContext context) throws Exception {
        state = context.getOperatorStateStore().getListState(new ListStateDescriptor<>(
                "state",
                LongSerializer.INSTANCE));
                
        // restore any state that we might already have to our fields, initialize state
        // is also called in case of restore.
        for (Long l : state.get()) {
            offset = l;
        }
    }

    @Override
    public void snapshotState(FunctionSnapshotContext context) throws Exception {
        state.clear();
        state.add(offset);
    }
}

当Flink完全确认某个检查点时,某些算子可能需要该信息,以便与外部世界进行通信。在本例中,请参见org.apache.flink.api.common.state.CheckpointListener接口。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值