大数据开发Flink State的容错和一致性(第五十三篇)

83 阅读8分钟

一、State的容错与一致性

如何保证Flink流式任务故障后恢复到之前的状态?

也就是将算子的运行结果恢复到任务停止之前的样子。为了实现状态的容错,Flink需要依赖于Checkpoint机制。因为Checkpoint可以将任务数据持久化保存,供任务恢复时使用。

针对流式计算任务,在故障后恢复状态数据的时候,会涉及到三种语义。第一种:

  1. 至少一次:At-least-once

    这种语义可能会导致数据恢复时,重复处理数据

  2. 至多一次:At-most-once

    这种语义可能会导致数据恢复时,丢失数据

  3. 仅一次:Exactly-once

    这种语义可以保证数据只对结果影响一次,可以保证结果的准确性,不会出现重复或者丢失的情况。这里的仅一次语义表示数据对最终结果的影响只有一次,并不是说数据只被处理一次

如果要实现流式计算任务中数据的一致性,其实就是想要流式计算任务中实现这种仅一次语义。针对流式计算任务中,仅一次语义的实现思路其实有很多种。

实现思路解释
At-least-once+去重需要自己维护去重功能,中等难度,性能开销中等
At-least-once+幂等需要依赖于存储系统的特性,实现简单,性能开销较低
State+Checkpoint需要借助于快照功能,实现简单,性能开销较低

幂等操作的含义:表示一条命令重复执行多次,对最终的结果只有一次影响。比如:针对Redis中的set命令,我们set a 1这个命令不管执行多少次。a的值始终都是1,那么这个命令我们就可以认为是幂等操作。那针对redis里面的incr命令,我们执行incr a,此时每执行一次,a的值就会加1,所以多次执行会导致结果发生变化,那么这个命令就不是幂等操作,我们在使用At-least-once语义的时候,它可能会导致数据重复,借助于幂等操作也是可以实现Exactly-once

State一致性

二、如何实现Flink任务的端到端的一致性

Flink可以借助于Checkpoint机制保证系统内部状态的一致性,但是一个Flink流式计算任务,它需要有Source以及系统内部(也就是算子)以及Sink这三个部分组成,那这个时候应该如何实现Flink流计算任务整个链路的一致性保证?也可以称为是端到端的一致性。端到端的一致性意味着要保证从Source端、系统内部、到最终Sink整个阶段的一致性,每一个阶段负责保证自己的一致性。整个端到端的一致性级别取决于所有阶段中一致性最弱的那个阶段

  1. Source端:外部数据源需要支持故障恢复时数据的重放。

    在流式计算任务中,常用的外部数据源是Kafka,Kafka可以支持数据重放机制,可以通过控制消费偏移量实现数据重新消费

  2. 系统内部:依赖于CheckPoint机制实现

  3. Sink端:目的地存储系统需要保证数据恢复时不会重复写入

    • 幂等写入

    • 事务写入。构建的事务对应着Checkpoint真正完成的时候才把所有对应的结果写入目的地存储系统中,针对事务写入这种机制。有两种实现方式:

      • 预写日志(WAL)-无法100%保证Exactly-once语义。它会使用Operator State来存储数据,在任务发生故障时,可以恢复,不会导致数据丢失,几乎适合所有外部系统,不能保证提供100%端到端的仅一次语义,因为预写日志在极端情况下会将数据写入多次。比如:外部系统不支持原子性的写入多条数据,那么在向外部系统写入数据的时候,可能就会出现部分数据已经写入,但是此时任务出现故障,这样导致剩余一部分数据没有写入的情况,当下次恢复的时候会重写全部数据,这样就会出现部分数据重复了
      • 两阶段提交(2PC)-可以100%保证Exactly-once语义。如果外部系统自身就支持事务,比如MySQL、Kafka,这个时候我们可以使用两阶段提交的方式,最终保证端到端的一致性。但是它牺牲了延迟,此时输出数据不再是实时写入到外部系统。而是分批次的提交,这一次checkpoint成功了,它才会把这一批数据真正写入到外部系统。

目前来说,没有完美的故障恢复和仅一次语义保证机制

三、Checkpoint(快照)机制详解

  1. Checkpoint是Flink中实现State容错和一致性最核心的功能,它能够根据配置周期性的基于流计算中的状态生成快照,从而将这些数据定期持续化存储下来,当Flink程序一旦意外故障崩溃时,在重新运行程序的时候可以从这些快照进行恢复,从而修正因为故障带来的数据异常。
  2. 默认情况下,Checkpoint机制是处于禁用状态的
  3. 支持两种语义级别:Exactly-once(默认)和At-least-once(会延迟几毫秒)

如果某个需求对数据的准确度要求不是特别高,但是需要计算和传输的数据量比较大,还需要低延迟。那么这时候就可以考虑使用At-least-once语义。

package com.strivelearn.flink.checkpoint;
​
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.runtime.state.storage.FileSystemCheckpointStorage;
import org.apache.flink.streaming.api.CheckpointingMode;
import org.apache.flink.streaming.api.environment.CheckpointConfig;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
​
/**
 * @author strivelearn
 * @version CheckpointCoreConf.java, 2023年02月05日
 */
public class CheckpointCoreConf {
    public static void main(String[] args) {
        StreamExecutionEnvironment executionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment();
        executionEnvironment.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);
        // 开启checkpoint,并且指定自动执行的间隔时间 单位毫秒 ,下面我设置了2分钟
        executionEnvironment.enableCheckpointing(1000 * 60 * 2);
        // 高级选项
        // 获取Checkpoint的配置对象
        CheckpointConfig checkpointConfig = executionEnvironment.getCheckpointConfig();
        // 设置语义模式为Exactly_once
        checkpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
        // 设置两次Checkpoint之间的最小间隔时间(设置5s)表示生成Checkpoint完成后的5s内不会开始生成新的Checkpoint
        checkpointConfig.setMinPauseBetweenCheckpoints(1000 * 5);
        // 设置最多同时运行几个checkpoint(默认是1,建议使用1,这样可以减少占用资源)
        checkpointConfig.setMaxConcurrentCheckpoints(1);
        // 设置一次checkpoint的执行超时时间,达到超时时间后会被取消执行。可以避免Checkpoint执行时间过长,下面设置了6分钟
        checkpointConfig.setCheckpointTimeout(1000 * 60 * 6);
        // 设置允许连续Checkpoint失败次数(默认是0,表示Checkpoint只要执行失败任务也会立刻失败)
        // 偶尔的失败,可能由于一些特殊情况(网络问题),这种应该不应该导致任务执行失败,设置3为容错值
        checkpointConfig.setTolerableCheckpointFailureNumber(3);
        // 设置在手工停止任务时是否保留之前生成的Checkpoint数据
        // RETAIN_ON_CANCELLATION:在任务故障和手工停止任务时都会保留之前生成的Checkpoint数据
        // DELETE_ON_CANCELLATION:只有在任务故障才会保留,如果手工停止任务会删除之前生成的数据
        checkpointConfig.setExternalizedCheckpointCleanup(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
        // 设置Checkpoint后的状态数据的存储的位置
        // 支持 JobManagerCheckpointStorage(默认)存储在JobManager节点的JVM堆内存中
        // 与FileSystemCheckpointStorage 存储在hdfs
        checkpointConfig.setCheckpointStorage(new FileSystemCheckpointStorage("hdfs://xxx:9001/flink-checkpoint"));
    }
}
3.1、保存多个Checkpoint

如果在任务中开启了Checkpoint,则Flink只会保留最近成功生成的1份Checkpoint数据。当Flink程序故障重启时,可以从最近的这份Checkpoint数据进行恢复,但是我们希望能够保留多份Checkpoint数据,这样我们可以根据实际情况来选择其中的一份进行恢复,这样更加灵活。比如:最近2小时的数据处理有问题,我们希望将整个状态还原到2小时之前,就需要用到2小时之前的Checkpoint进行恢复

  • state.checkpoints.num-retained:20

在Flink的配置文件中,flink-conf.yaml文件中,添加这个配置,上面配置也就是保存最近20份checkpoint数据。这个配置改完后,针对整个客户端有效,只要在这个客户端上提交的任务,它都会使用这个配置

3.2、Checkpoint的代码实战
package com.strivelearn.flink.checkpoint;
​
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.environment.CheckpointConfig;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
​
/**
 * 有状态的单词计数+Checkpoint
 *
 * @author strivelearn
 * @version WordCountStateWithCheckpoint.java, 2023年02月05日
 */
public class WordCountStateWithCheckpoint {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment executionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment();
        executionEnvironment.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);
        // 开启checkpoint,方便测试开启10s执行一次
        executionEnvironment.enableCheckpointing(1000 * 10);
        // 获取checkpoint配置
        CheckpointConfig checkpointConfig = executionEnvironment.getCheckpointConfig();
        // 在任务故障和手工停止任务时都会保留之前生成的Checkpoint数据
        checkpointConfig.setExternalizedCheckpointCleanup(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
        // 设置checkpoint后的状态数据的存储位置
        checkpointConfig.setCheckpointStorage("hdfs://192.168.234.100:9000/flink-checkpoint/wordcount");
        DataStreamSource<String> stringDataStreamSource = executionEnvironment.socketTextStream("192.168.234.100", 9001);
        KeyedStream<Tuple2<String, Integer>, Tuple> tuple2TupleKeyedStream = stringDataStreamSource.flatMap(new FlatMapFunction<String, String>() {
            @Override
            public void flatMap(String line, Collector<String> out) throws Exception {
                String[] words = line.split(" ");
                for (String word : words) {
                    out.collect(word);
                }
            }
        }).map(new MapFunction<String, Tuple2<String, Integer>>() {
            @Override
            public Tuple2<String, Integer> map(String value) throws Exception {
                return new Tuple2<>(value, 1);
            }
        }).keyBy(0);
​
        tuple2TupleKeyedStream.map(new RichMapFunction<Tuple2<String, Integer>, Tuple2<String, Integer>>() {
            // 声明一个ValueState类型的状态变量,存储设备上一次收到的温度数据
            private ValueState<Integer> countState;
​
            // 任务初始化的时候这个方法会执行一次
            @Override
            public void open(Configuration parameters) throws Exception {
                // 注册状态
                ValueStateDescriptor<Integer> valueStateDescriptor = new ValueStateDescriptor<>("countState", Integer.class);
                countState = getRuntimeContext().getState(valueStateDescriptor);
            }
​
            @Override
            public Tuple2<String, Integer> map(Tuple2<String, Integer> value) throws Exception {
                // 从状态中获取这个key之前出现的次数
                Integer lastNum = countState.value();
                // 当前出现的次数
                Integer currentNum = value.f1;
                if (lastNum == null) {
                    lastNum = 0;
                }
                Integer sum = lastNum + currentNum;
                countState.update(sum);
                // 返回单词及单词出现的总次数
                return new Tuple2<>(value.f0, sum);
            }
        }).print();
        executionEnvironment.execute("WordCountStateWithCheckpoint");
    }
}
  1. 启动hdfs

  2. 启动socket

    nc -l 9001

  3. 输入测试数据

    image-20230205232610672

  4. 控制台查看数据

    image-20230205232638236

  5. 查看hdfs checkpoint存储的数据

    image-20230205232943714

  6. 如果要恢复指定的checkpoint

    把jar包代码提交到服务器上,使用命令指定使用哪份checkpoint的数据:

    -s hdfs://192.168.234.100:9000//flink-checkpoint/wordcount/f574a2024b5fda0fd81e4ba97edbe2cc/chk-24 -c