flink的状态和容错

1、flink的状态和容错

keyed stream。如果想使用keyed state,首先要为dataStream指定key。主键用于状态分区,相同key的数据发送到一起。

对于key 的选择:当为Tuple类型的时候,可以使用lambda表达值 x->x.f0 指定key是那个字段,当然也可以实现keySelect函数,或者调用相应对象字段的get方法。

2、状态类型

ValueState:

保存一个可以检索和更新的值,可以调用update(T)进行更新,value() 进行检索

ListState:

保存元素列表,可以调用add(T)、addAll(List)添加元素,get方法可以获取整个列表。可以通过update更新列表。

RedcuingState:

保存一个单值,表示添加到状态的所有值的聚合。使用add(T)添加元素,使用提供的ReduceFunction进行聚合。

AggregatingState<IN,OUT>:

保留一个单值,表示添加到状态的所有值的聚合。与RedcuingState不同的是,聚合类型可能与添加到状态的元素类型不同。接口与ListState类似,但使用add(IN),添加的元素会用指定的AggregateFunction进行聚合。

MapState<UK,UV>:

维护了一个映射列表,可以添加指定的键值对到状态中,可以获得当前所有映射的迭代器。put(UK,UV)、putAll(Map<UK,UV>添加映射。使用get(UK)见多特定的key。 可以使用entries()、keys()、values()分别检索映射,键和值的可迭代视图。isEmpty()判断是否包含键值对。

所有的状态都包含一个clean()的方法,用于对状态进行清除,清除当前key下的状态。

对于状态不一定存在于内存,可以保存在磁盘或者其他位置,另外从状态中获取的值取决于元素的key,所有的元素都是按key进行划分的。

3、状态的获取和初始化

状态通过 RuntimeContext 进行访问,因此只能在 rich functions中使用。

RichFunction中 RuntimeContext 提供如下方法:

ValueState getState(ValueStateDescriptor)

ReducingState getReducingState(ReducingStateDescriptor)

ListState getListState(ListStateDescriptor)

AggregatingState<IN, OUT> getAggregatingState(AggregatingStateDescriptor<IN, ACC, OUT>)

MapState<UK, UV> getMapState(MapStateDescriptor<UK, UV>)

通过这几个方法获取不同类型的状态。

ValueState样例:

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();

这个例子实现了一个简单的计数窗口。 我们把元组的第一个元素当作 key(在示例中都 key 都是 “1”)。 该函数将出现的次数以及总和存储在 “ValueState” 中。 一旦出现次数达到 2,则将平均值发送到下游,并清除状态重新开始。 请注意,我们会为每个不同的 key(元组中第一个元素)保存一个单独的值。

4、状态的有效期TTL

对于所有keyed state都可以设置一个有效期,过期之后,会对状态尽最大可能清除。

设置ttl需要首先定义一个StateTtlConfig配置对象。

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))
    // ttl更新策略
    .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
    // 数据在过期但还未被清理时的可见性设置
    .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
    .build();
    
ValueStateDescriptor<String> stateDescriptor = new ValueStateDescriptor<>("text state", String.class);
stateDescriptor.enableTimeToLive(ttlConfig);

有效期设置: .newBuilder(Time.seconds(1))

TTL更新策略:默认为OnCreateAndWrite

StateTtlConfig.UpdateType.OnCreateAndWrite - 仅在创建和写入时更新

StateTtlConfig.UpdateType.OnReadAndWrite - 读取时也更新

数据在过期但还未被清理时的可见性设置:默认为NeverReturnExpired

StateTtlConfig.StateVisibility.NeverReturnExpired - 不返回过期数据

StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp - 会返回过期但未清理的数据

需要注意的地方:

1、状态的上次修改的时间会和数据一起保存在state backend中,因此开启ttl会增加状态数据的存储。Heap state backend 会额外存储一个包括用户状态以及时间戳的 Java 对象,RocksDB state backend 会在每个状态值(list 或者 map 的每个元素)序列化后增加 8 个字节。

2、暂时只支持基于 processing time 的 TTL。

3、尝试从checkPoint、savePoint恢复时,TTL状态(是否开启)必须和之前保持一致,否则回导致StateMigrationException,

4、TTL 的配置并不会保存在 checkpoint/savepoint 中,仅对当前 Job 有效。

5、当前开启 TTL 的 map state 仅在用户值序列化器支持 null 的情况下,才支持用户值为 null。如果用户值序列化器不支持 null, 可以用 NullableSerializer包装一层。

6、启用 TTL 配置后,StateDescriptor 中的 defaultValue(已被标记 deprecated)将会失效。这个设计的目的是为了确保语义更加清晰,在此基础上,用户需要手动管理那些实际值为 null 或已过期的状态默认值。

5、广播状态Broadcast State

在一个流中的元素需要广播到下游任务使用的情形。广播状态一般适用于动态的任务,根据广播的流,动态的对数据进行处理,对于做什么操作,是能够根据广播流的数据来控制的。

例如:官网的文档中提到的关于广播流的使用。假设存在一个序列,序列中的元素是具有不同颜色与形状的图形,我们希望在序列里相同颜色的图形中寻找满足一定顺序模式的图形对(比如在红色的图形里,有一个长方形跟着一个三角形)。 同时,我们希望寻找的模式也会随着时间而改变。

示例代码:

根据key对数据进行划分

// 将图形使用颜色进行划分
KeyedStream<Item, Color> colorPartitionedStream = itemStream
                        .keyBy(new KeySelector<Item, Color>(){...});

用MapStateDescriptor创建一个类似于map的广播状态,并对规则进行广播。

// 一个 map descriptor,它描述了用于存储规则名称与规则本身的 map 存储结构
MapStateDescriptor<String, Rule> ruleStateDescriptor = new MapStateDescriptor<>(
			"RulesBroadcastState",
			BasicTypeInfo.STRING_TYPE_INFO,
			TypeInformation.of(new TypeHint<Rule>() {}));
		
// 广播流,广播规则并且创建 broadcast state
BroadcastStream<Rule> ruleBroadcastStream = ruleStream
                        .broadcast(ruleStateDescriptor);

将数据流和广播状态进行连接,捕获广播状态的数据。

DataStream<String> output = colorPartitionedStream
                 .connect(ruleBroadcastStream)
                 .process(
                     
                     // KeyedBroadcastProcessFunction 中的类型参数表示:
                     //   1. key stream 中的 key 类型
                     //   2. 非广播流中的元素类型
                     //   3. 广播流中的元素类型
                     //   4. 结果的类型,在这里是 string
                     
                     new KeyedBroadcastProcessFunction<Color, Item, Rule, String>() {
                         // 模式匹配逻辑
                     }
                 );

调用非广播流的.connect()方法来关联广播流,并传入这个广播流。 这个方法的返回参数是 BroadcastConnectedStream,具有类型方法 process(),传入一个特殊的 CoProcessFunction 来书写我们的模式识别逻辑。 具体传入 process() 的是哪个类型取决于非广播流的类型:

  • 如果流是一个 keyed 流,那就是 KeyedBroadcastProcessFunction 类型;
  • 如果流是一个 non-keyed 流,那就是 BroadcastProcessFunction 类型。

BroadcastProcessFunction

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;
}

KeyedBroadcastProcessFunction

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;
}

相同的方法:processElement和processBroadcastElement

processElement:负责处理主流的数据

processBroadcastElement:负责处理广播流的数据

两个方法的Context不同,但是有同样的方法

  1. 得到广播流的存储状态:ctx.getBroadcastState(MapStateDescriptor<K, V> stateDescriptor)

  2. 查询元素的时间戳:ctx.timestamp()

  3. 查询目前的Watermark:ctx.currentWatermark()

  4. 目前的处理时间(processing time):ctx.currentProcessingTime()

  5. 产生旁路输出:ctx.output(OutputTag outputTag, X value)

    在 getBroadcastState()方法中传入的 stateDescriptor 应该与调用 .broadcast(ruleStateDescriptor) 的参数相同。

对于广播流具有读写的权限,对于主流只有可读的权限。

因为flink不具备跨task的通信,所以给予了广播流的读写权限,这样在所有task中都能够看到,并且要求对这些元素的处理是一致的,那么所有的task得到的broadcast sate是一致的。

对于processBroadcastElement()的实现,必须在所有的并发实例中具有确定性的结果。

KeyedBroadcastProcessFunction和BroadcastProcessFunction的区别:

  1. processElement()的参数 ReadOnlyContext提供了方法能够访问 Flink 的定时器服务,可以注册事件定时器(event-time timer)或者处理时间的定时器(processing-time timer)。当定时器触发时,会调用 onTimer() 方法, 提供了 OnTimerContext,它具有 ReadOnlyContext 的全部功能,并且提供:
  • 查询当前触发的是一个事件还是处理时间的定时器
  • 查询定时器关联的key
  1. processBroadcastElement()方法中的参数 Context会提供方法 applyToKeyedState(StateDescriptor<S, VS> stateDescriptor, KeyedStateFunction<KS, S> function)。 这个方法使用一个 KeyedStateFunction能够对 stateDescriptor 对应的 state 中所有 key 的存储状态进行某些操作。目前 PyFlink 不支持 apply_to_keyed_state。
  2. 注册一个定时器只能在 KeyedBroadcastProcessFunction 的 processElement()方法中进行。 在 processBroadcastElement() 方法中不能注册定时器,因为广播的元素中并没有关联的 key。

KeyedBroadcastProcessFunction 的示例:

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

    // 存储部分匹配的结果,即匹配了一个元素,正在等待第二个元素
    // 我们用一个数组来存储,因为同时可能有很多第一个元素正在等待
    private final MapStateDescriptor<String, List<Item>> mapStateDesc =
        new MapStateDescriptor<>(
            "items",
            BasicTypeInfo.STRING_TYPE_INFO,
            new ListTypeInfo<>(Item.class));

    // 与之前的 ruleStateDescriptor 相同
    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();
            }
    
            // 不需要额外的 else{} 段来考虑 rule.first == rule.second 的情况
            if (shape.equals(rule.first)) {
                stored.add(value);
            }
    
            if (stored.isEmpty()) {
                state.remove(ruleName);
            } else {
                state.put(ruleName, stored);
            }
        }
    }
}

broadcast state 的重要注意事项:

  • 没有跨 task 通讯:如上所述,这就是为什么只有在 (Keyed)-BroadcastProcessFunction 中处理广播流元素的方法里可以更改 broadcast state 的内容。 同时,用户需要保证所有 task 对于 broadcast state 的处理方式是一致的,否则会造成不同 task 读取 broadcast state 时内容不一致的情况,最终导致结果不一致。
  • broadcast state 在不同的 task 的事件顺序可能是不同的:虽然广播流中元素的过程能够保证所有的下游 task 全部能够收到,但在不同 task 中元素的到达顺序可能不同。 所以 broadcast state 的更新不能依赖于流中元素到达的顺序
  • 所有的 task 均会对 broadcast state 进行 checkpoint:虽然所有 task 中的 broadcast state 是一致的,但当 checkpoint 来临时所有 task 均会对 broadcast state 做 checkpoint。 这个设计是为了防止在作业恢复后读文件造成的文件热点。当然这种方式会造成 checkpoint 一定程度的写放大,放大倍数为 p(=并行度)。Flink 会保证在恢复状态/改变并发的时候数据没有重复没有缺失。 在作业恢复时,如果与之前具有相同或更小的并发度,所有的 task 读取之前已经 checkpoint 过的 state。在增大并发的情况下,task 会读取本身的 state,多出来的并发(p_new - p_old)会使用轮询调度算法读取之前 task 的 state。
  • 不使用 RocksDB state backend: broadcast state 在运行时保存在内存中,需要保证内存充足。这一特性同样适用于所有其他 Operator State。

6、Checkpointing

flink中的算子都是有状态的。为了让状态进行容错,需要添加检查点,Checkpoint使flink能够恢复状态和流中的位置,从而向应用提供和无故障执行一样的语义。

6.1 checkpoint的前提:

1、一个能够重放的数据源。例如消息队列

2、存放状态的持久化存储。例如分部式文件系统

6.2 checkpoint的属性

开启:StreamExecutionEnvironment 的 enableCheckpointing(n)来启用 checkpoint,里面的 n 是进行 checkpoint 的间隔,单位毫秒。

属性:

精确一次(exactly-once):

checkpoint超时:

chekpoint可以容忍的失败次数:

并发checkpoint的数目:

  • 该选项不能和 “checkpoints 间的最小时间"同时使用。

externalized checkpoints:可以将checkpoint存储到外部系统例如:hdfs。Externalized checkpoints 将他们的元数据写到持久化存储上并且在 job 失败的时候不会被自动删除。

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

// 每 1000ms 开始一次 checkpoint
env.enableCheckpointing(1000);

// 高级选项:

// 设置模式为精确一次 (这是默认值)
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);

// 确认 checkpoints 之间的时间会进行 500 ms
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(500);

// Checkpoint 必须在一分钟内完成,否则就会被抛弃
env.getCheckpointConfig().setCheckpointTimeout(60000);

// 允许两个连续的 checkpoint 错误
env.getCheckpointConfig().setTolerableCheckpointFailureNumber(2);
        
// 同一时间只允许一个 checkpoint 进行
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);

// 使用 externalized checkpoints,这样 checkpoint 在作业取消后仍就会被保留
env.getCheckpointConfig().setExternalizedCheckpointCleanup(
        ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);

// 开启实验性的 unaligned checkpoints
env.getCheckpointConfig().enableUnalignedCheckpoints();

6.3 状态后端的选择

flink会将timer以及stateful的operator进行快照,人后存储,包括连接器(connector),窗口(window),以及用户自定义的状态。checkpoint存储在哪,依赖于存储的状态后端。

默认情况下,状态保存在taskmanager内存,checkpoint保存在jobManager的内存中。为了存储比较大的状态,flink支持各种途径存储checkpoint状态到其他state backends。StreamExecutionEnvironment.setStateBackend(…) 来配置所选的 state backends。

6.4 迭代作业的checkpoint

flink没有为迭代的作业提供一致性的保证,在迭代作业上开启checkpoint会导致异常。可以强制进行checkpoint:env.enableCheckpointing(interval, CheckpointingMode.EXACTLY_ONCE, force = true)。

6.5 部分任务结束后的 Checkpoint

flink支持在部分作业结束后进行checkpoint,如果一部分数据源是有限数据集,那么就可以出现这种情况。版本 1.15 开始,这一特性被默认打开。

关闭:

Configuration config = new Configuration();
config.set(ExecutionCheckpointingOptions.ENABLE_CHECKPOINTS_AFTER_TASKS_FINISH, false);
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(config);

但是会对正在运行的算子的影响。

在部分 Task 结束后的checkpoint中,Flink 对 UnionListState 进行了特殊的处理

UnionListState 一般用于实现对外部系统读取位置的一个全局视图(例如,用于记录所有 Kafka 分区的读取偏移)。 如果我们在算子的某个并发调用 close()方法后丢弃它的状态,我们就会丢失它所分配的分区的偏移量信息。 为了解决这一问题,对于使用 UnionListState的算子我们只允许在它的并发都在运行或都已结束的时候才能进行 checkpoint 操作。

ListState 一般不会用于类似的场景,但是用户仍然需要注意在调用 close()方法后进行的 checkpoint 会丢弃算子的状态并且 这些状态在算子重启后不可用。

任何支持并发修改操作的算子也可以支持部分并发实例结束后的恢复操作。从这种类型的快照中恢复等价于将算子的并发改为正在运行的并发实例数。

任务结束前等待最后一次 Checkpoint:

为了保证使用两阶段提交的算子可以提交所有的数据,任务会在所有算子都调用 finish()方法后等待下一次 checkpoint 成功后退出。 需要注意的是,这一行为可能会延长任务运行的时间,如果 checkpoint 周期比较大,这一延迟会非常明显。 极端情况下,如果 checkpoint 的周期被设置为 Long.MAX_VALUE,那么任务永远不会结束,因为下一次 checkpoint 不会进行。

7、State Backends

由 Flink 管理的 keyed state 是一种分片的键/值存储,每个 keyed state 的工作副本都保存在负责该键的 taskmanager 本地中。另外,Operator state 也保存在机器节点本地。Flink 定期获取所有状态的快照,并将这些快照复制到持久化的位置,例如分布式文件系统

Flink 管理的状态存储在 state backend中。Flink 有两种 state backend 的实现 – 一种基于 RocksDB 内嵌 key/value 存储将其工作状态保存在磁盘上的,另一种基于堆的 state backend,将其工作状态保存在 Java 的堆内存中。这种基于堆的 state backend 有两种类型:FsStateBackend,将其状态快照持久化到分布式文件系统;MemoryStateBackend,它使用 JobManager 的堆保存状态快照。

RocksDBStateBackend

Working State:本地磁盘(tmp dir)

状态备份:分布式文件系统

快照:全量 / 增量

特点:支持内存大小的状态,比基于对内存的后慢10倍

MemoryStateBackend

Working State:JVM Heap

状态备份: JobManager JVM Heap

快照:全量

特点:使用于小状态(本地)的测试和实验

FsStateBackend

Working State:JVM Heap

状态备份:分布式文件系统

快照:全量

特点:快速,需要大堆内存,受限于GC

三种状态后端的分析:基于堆内存的状态后端保存,访问设计堆上对象的读写。但是对于RocksDBStateBackend中的对象,访问需要序列化和反序列化,会有更大的开销。RocksDB状态量受限于本地磁盘的大小,但是rocksdb支持增量的快照,对于大量变化的缓慢状态应用来说是很好的。

精确一次(exactly once)

当应用程序发生错误时,结果可能发生丢失或者重复。flink根据应用程序和集群的配置产生一下结果:

flink不会从快照中恢复:at most once

没有任何丢失,但是可能得到重复冗余的结果:at least once

没有丢失和冗余重复:exactly once

flink通过回退和重新发送source数据从故障中恢复,当理想情况被描述为精确一次时,并不意味着每个时间将精确一次处理。相反,意味着每个事件都会影响flink管理的状态精确一次。

Barrier 只有在需要提供精确一次的语义保证时需要进行对齐(Barrier alignment)。如果不需要这种语义,可以通过配置 CheckpointingMode.AT_LEAST_ONCE 关闭 Barrier 对齐来提高性能。

端到端精确一次

为了实现端到端的精确一次,以便 sources 中的每个事件都仅精确一次对 sinks 生效,必须满足以下条件:

  1. 你的 sources 必须是可重放的,并且
  2. 你的 sinks 必须是事务性的(或幂等的)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值