flink State 和 checkpoint,重启策略 原理详解

目录

前言

State

keyed State

1.ValueState 

2.ListState  

3.MapState

4.ReducingState

5.AggregatingState

6.FoldingState

Operator State

 

存储状态

MemoryStateBackend      

FsStateBackend 

RocksDBStateBackend 

 

重启策略

checkpoint

checkpoint配置

通过checkpoint恢复某个版本数据

savePoint

 checkpoint原理

checkpoint优化

flink的两阶段提交



 

前言

Flink容错机制主要依靠state 和 checkpoint的功能。

flink通过state来存储计算结果(因为逐条计算,数据增量计算) ,流计算特有。

所谓的状态指的是,在流处理过程中那些需要记住的数据,而这些数据既可以包括业务数据,也可以包括元数据。Flink 本身提供了不同的状态管理器来管理状态,并且这个状态可以非常大。

flink State中主要分为 Operator State(task级别) 以及 Keyed State (针对每个task中的每个key).

flink checkpoint可以根据最近的checkpoint版本在故障,重启,集群升级一些状况中进行恢复。默认checkpoint功能是disabled的。

 

State

keyed State

基于KeyedStream上的状态,比如:keyby,groupby,partitionby等。每个key都有属于自己的State.key与key之间的State是不可见的。

keyedState状态有六种类型

  • 1.ValueState 

每个key都对应的一个值状态。 
这个值可以通过update(T) 进行更新,
通过 T value() 进行检索

 例

//每当第一个元素的和达到二,就把第二个元素的和与第一个元素的和相除
public class StateDemo {


    public static void main(String[] args) throws Exception {

        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        //env.setStateBackend(new MemoryStateBackend(MemoryStateBackend.DEFAULT_MAX_STATE_SIZE,false)); //false 代表关闭异步快照机制
        //env.setStateBackend(new FsStateBackend("hdfs://namenode:40010/flink/checkpoints", false));
        env.fromElements(Tuple2.of(1L, 3L), Tuple2.of(1L, 5L), Tuple2.of(1L, 7L), Tuple2.of(1L, 5L), Tuple2.of(1L, 2L))
                .keyBy(0)
                .flatMap(new CountWindowAverage())
                .printToErr();
        env.execute("start..");

    }

    public static class CountWindowAverage extends RichFlatMapFunction<Tuple2<Long, Long>, Tuple2<Long, Long>> {

        private transient ValueState<Tuple2<Long, Long>> sum;

        @Override
        public void open(Configuration parameters) throws Exception {

            ValueStateDescriptor<Tuple2<Long, Long>> descriptor = new ValueStateDescriptor<>(
                    "average",//state的名字
                    TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {})); //设置默认值

            //StateTtlConfig 用于设置state的TTL属性   表示当上次更新时间戳+
            final StateTtlConfig ttlConfig = StateTtlConfig.newBuilder(Time.seconds(10))
                    //表明当状态创建或每次写入时都会更新时间戳
                    .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)  //  选项: Disabled、OnCreateAndWrite、OnReadAndWrite
                    //一旦这个状态过期了,那么永远不会被返回给调用方,只会返回空状态,避免了过期状态带来的干扰
                    .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)  //选项:  ReturnExpiredIfNotCleanedUp、NeverReturnExpired
                    .setTtlTimeCharacteristic(ProcessingTime)  //TTL
                    .build();

            descriptor.enableTimeToLive(ttlConfig);  //设置

            //来获取状态的句柄
            sum = getRuntimeContext().getState(descriptor);
        }

        @Override
        public void flatMap(Tuple2<Long, Long> input, Collector<Tuple2<Long, Long>> out) throws Exception {
            Tuple2<Long, Long> currentSum;
            //访问ValueState
            if (sum.value() == null) {
                currentSum = Tuple2.of(0L, 0L);
            } else {
                currentSum = sum.value();
            }

            //更新
            currentSum.f0 += 1;
            //第二个元素加1
            currentSum.f1 += input.f1;

            //更新state
            sum.update(currentSum);

            //如果count的值大于等于2,求知道并清空state
            if (currentSum.f0 >= 2) {
                out.collect(new Tuple2<>(input.f0, currentSum.f1 / currentSum.f0));
                sum.clear();
            }


        }
    }


}

 


 

  • 2.ListState  

保存一个元素的列表。可以往这个列表中追加数据,并在当前的列表上进行检索。
可以通过 add(T) 或者 addAll(List<T>) 进行添加元素,
通过 Iterable<T> get() 获得整个列表。
还可以通过 update(List<T>) 覆盖当前的列表	。

 例

//针对每个用户,返回最近三次时间间隔内最少的间隔差
public class ListStateDemo {

    public static void main(String[] args) throws Exception {

        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        final DataStreamSource<String> source = env.socketTextStream("192.168.8.174", 9999);

        final SingleOutputStreamOperator<Tuple2<String, Long>> map = source.map(new MapFunction<String, Tuple2<String, Long>>() {
            @Override
            public Tuple2<String, Long> map(String value) throws Exception {
                final String[] split = value.split(",");
                String userid = split[0];
                String time = split[1];
                return new Tuple2<>(userid, Long.valueOf(time));
            }
        });

        map.keyBy(x -> x.f0).flatMap(new CountAverageWithList()).printToErr();

        env.execute("start...");
    }


}


class CountAverageWithList extends RichFlatMapFunction<Tuple2<String, Long>, HashMap<String, Long>> {

    private ListState<Tuple2<String, Long>> liststate;

    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
        //初始化状态值
        final ListStateDescriptor<Tuple2<String, Long>> listState =
                new ListStateDescriptor<>("listState", TypeInformation.of(new TypeHint<Tuple2<String, Long>>() {
                }));
        liststate = getRuntimeContext().getListState(listState);
    }


    @Override
    public void flatMap(Tuple2<String, Long> value, Collector<HashMap<String, Long>> collector) throws Exception {
        //获取当前key的状态
        // final Iterable<Tuple2<String, Long>> currentState = liststate.get();

        if (liststate == null) {
            liststate.addAll(Collections.emptyList());
        }

        liststate.add(value);
        Iterator<Tuple2<String, Long>> iterator = liststate.get().iterator();
        ArrayList<Tuple2<String, Long>> allElementList = Lists.newArrayList(iterator);
        if (allElementList.size() >= 3) {
            final HashMap<String, Long> map = new HashMap<>();
            Long minTime = 0L;
            for (int i = 0; i < allElementList.size(); i++) {

                if (i != 0) {
                    final long time = Math.abs(allElementList.get(i).f1 - allElementList.get(i - 1).f1);
                    if (minTime != 0) {
                        minTime = (time > minTime ? minTime : time);
                    } else {
                        minTime = time;
                    }
                    map.put(allElementList.get(i).f0, minTime);
                }
            }
            collector.collect(map);
        }
    }

}

 


  • 3.MapState

维护了一个映射列表。 你可以添加键值对到状态中,也可以获得 反映当前所有映射的迭代器
。使用 put(UK,UV) 或者 putAll(Map<UK,UV>) 添加映射。
 使用 get(UK) 检索特定 key。
 使用 entries(),keys() 和 values() 分别检索映射、 键和值的可迭代视图。

// 使用MapState求取每个key对应的平均值
object MapStateDemo {

  def main(args: Array[String]): Unit = {


    val env = StreamExecutionEnvironment.getExecutionEnvironment

    env.fromCollection(List(
      (1L, 3d),
      (1L, 5d),
      (1L, 7d),
      (2L, 4d),
      (2L, 2d),
      (2L, 6d)
    )).keyBy(_._1)
      .flatMap(new CountAverageMapState)
      .print()
    env.execute()


  }


}

class CountAverageMapState extends RichFlatMapFunction[(Long, Double), (Long, Double)] {

  private var mapState: MapState[String, Double] = _

  初始化获取mapState对象
  override def open(parameters: Configuration): Unit = {
    val mapStateOperate = new MapStateDescriptor[String, Double](
      "mapStateOperate",
      classOf[String],
      classOf[Double])

    mapState = getRuntimeContext.getMapState(mapStateOperate)
  }

  override def flatMap(input: (Long, Double), out: Collector[(Long, Double)]): Unit = {

    //将相同的key对应的数据放到一个map集合当中去,就是这种对应  key -> Map((key1, value1),(key2, value2))
    //每次都构建一个map集合
    mapState.put(UUID.randomUUID().toString, input._2)
    import scala.collection.JavaConverters._
    //获取map集合当中所有的value,我们每次将数据的value给放到map的value里面去
    val listState: List[Double] = mapState.values().iterator().asScala.toList
    if (listState.size >= 3) {
      var count = 0L
      var sum = 0d
      for (eachState <- listState) {
        count += 1
        sum += eachState
      }
      out.collect(input._1, sum / count)
    }

  }
}

 


  • 4.ReducingState

保存一个单值,表示添加到状态的所有值的聚合。
接口与ListState类似,但使用add(T) 增加元素,会使用提供的 ReduceFunction 进行聚合。

 例

/**
 * ReducingState<T> :这个状态为每一个 key 保存一个聚合之后的值
 * get() 获取状态值
 * add()  更新状态值,将数据放到状态中
 * clear() 清除状态
 */
object ReduceingStateOperate {


  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.api.scala._
    env.fromCollection(List(
      (1L, 3d),
      (1L, 5d),
      (1L, 7d),
      (2L, 4d),
      (2L, 2d),
      (2L, 6d)
    )).keyBy(_._1)
      .flatMap(new CountAverageReduceStage)
      .print()

    env.execute()
  }
}

class CountAverageReduceStage extends RichFlatMapFunction[(Long, Double), (Long, Double)] {

  //定义ReducingState
  private var reducingState: ReducingState[Double] = _

  //定义一个计数器
  var counter = 0L

  override def open(parameters: Configuration): Unit = {

    val reduceSum = new ReducingStateDescriptor[Double](
      "reduceSum",
      new ReduceFunction[Double] {
        override def reduce(value1: Double, value2: Double): Double = {
          value1 + value2
        }
      },
      classOf[Double])

    //初始化获取reducingState对象
    reducingState = getRuntimeContext.getReducingState[Double](reduceSum)

  }

  override def flatMap(input: (Long, Double), out: Collector[(Long, Double)]): Unit = {
    //计数器+1
    counter += 1

    //添加数据到reducingState
    reducingState.add(input._2)

    out.collect(input._1, reducingState.get() / counter)
  }
}

 


 

  • 5.AggregatingState

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

/**
 * 将相同key的数据聚合成为一个字符串
 */
object AggregrageStateOperate {

  def main(args: Array[String]): Unit = {

    val env = StreamExecutionEnvironment.getExecutionEnvironment

    env.fromCollection(List(
      (1L, 3d),
      (1L, 5d),
      (1L, 7d),
      (2L, 4d),
      (2L, 2d),
      (2L, 6d)
    )).keyBy(_._1)
      .flatMap(new AggregrageState)
      .print()

    env.execute()
  }
}

/**
 * (1L, 3d),
 * (1L, 5d),
 * (1L, 7d),   把相同key的value拼接字符串:Contains-3-5-7
 */
class AggregrageState extends RichFlatMapFunction[(Long, Double), (Long, String)] {

  //定义AggregatingState
  private var aggregateTotal: AggregatingState[Double, String] = _

  override def open(parameters: Configuration): Unit = {
    /**
     * name: String,
     * aggFunction: AggregateFunction[IN, ACC, OUT],
     * stateType: Class[ACC]
     */
    val aggregateStateDescriptor = new AggregatingStateDescriptor[Double, String, String](
      "aggregateState",
      new AggregateFunction[Double, String, String] {
        //创建一个初始值
        override def createAccumulator(): String = {
          "Contains"
        }

        //对数据进行累加
        override def add(value: Double, accumulator: String): String = {
          accumulator + "-" + value
        }

        //获取累加的结果
        override def getResult(accumulator: String): String = {
          accumulator
        }

        //数据合并的规则
        override def merge(a: String, b: String): String = {
          a + "-" + b
        }
      },
      classOf[String])

    //获取AggregatingState对象
    aggregateTotal = getRuntimeContext.getAggregatingState(aggregateStateDescriptor)
  }

  override def flatMap(input: (Long, Double), out: Collector[(Long, String)]): Unit = {
    aggregateTotal.add(input._2)
    out.collect(input._1, aggregateTotal.get())
  }

}

 


  • 6.FoldingState

保留一个单值,表示添加到状态的所有值的聚合。
 与ReducingState 相反,聚合类型可能与添加到状态的元素类型不同。
接口与 ListState 类似,但使用 add(T)添加的元素会用指定的 FoldFunction 折叠成聚合值。
在Flink1.4中弃用,未来版本将被完全删除。

 

Operator State

可以用在所有算子上。每个算子实例共享一个状态。流入这个算子任务的数据可以访问和更新这个状态。

举例:Flink中的Kafka Connector,就使用了operator state。它会在每个connector实例中,保存该实例中消费topic的所有(partition, offset)映射。

/**
 * 实现每两条数据进行输出打印一次,不用区分数据的key
 */
object OperatorListState {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.api.scala._
    val sourceStream: DataStream[(String, Int)] = env.fromCollection(List(
      ("spark", 3),
      ("hadoop", 5),
      ("hive", 7),
      ("flume", 9)
    ))

    sourceStream.addSink(new OperateTaskState).setParallelism(1)

    env.execute()
  }

}

class OperateTaskState extends SinkFunction[(String, Int)] {
  //定义一个list 用于我们每两条数据打印一下
  private var listBuffer: ListBuffer[(String, Int)] = new ListBuffer[(String, Int)]

  override def invoke(value: (String, Int), context: SinkFunction.Context[_]): Unit = {
    listBuffer.+=(value)

    if (listBuffer.size == 2) {
      println(listBuffer)

      //清空state状态
      listBuffer.clear()
    }
  }
}

 

 


存储状态

env.setStateBackend(new MemoryStateBackend(MemoryStateBackend.DEFAULT_MAX_STATE_SIZE,false)); 
false 代表关闭异步快照机制

StateBackend定义了State如何存储。目前提供了三种不同形式的存储。

  • MemoryStateBackend      

 会将state保存在taskManager的内存中。将checkpoint保存在jobManager的内存中。

 每个独立的状态(state)默认限制大小为 5MB,
 

 适用场景

(1)本地调试
(2)flink任务状态数据量较小的场景

 

  • FsStateBackend 

 会将state保存在taskManager的内存中,将checkpoint保存在文件系统中(例hdfs)。

 适用场景

(1)大状态、长窗口、大key/value状态的的任务
(2)全高可用配置
  • RocksDBStateBackend 

会将state保存在内置RocksDB中,将checkpoint保存在文件系统中(例hdfs)。RocksDB 数据库默认将数据存储在 TaskManager 运行节点的数据目录下。

 适用场景

(1)大状态、长窗口、大key/value状态的的任务
(2)全高可用配置

 

 


 

重启策略

flink提供了多种类型级别的重启策略。常用的重启策略包括:

  • 固定延迟重启策略模式
env.setRestartStrategy(RestartStrategies.fixedDelayRestart(
        3, // 重启次数
        Time.of(5, TimeUnit.SECONDS) // 时间间隔
));
  • 失败率重启策略模式
env.setRestartStrategy(RestartStrategies.failureRateRestart(
        3, // 每个时间间隔的最大故障次数
        Time.of(5, TimeUnit.MINUTES), // 测量故障率的时间间隔
        Time.of(5, TimeUnit.SECONDS) //  每次任务失败时间间隔
));
  • 无重启策略模式   
final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
env.setRestartStrategy(RestartStrategies.noRestart());

flink在判断使用那种重启策略的时候做了默认约定。

  • 如果用户没有配置checkpoint,那么默认不会重启。
  • 如果用户配置了checkpoint,但没设置重启策略,默认按固定延迟重启策略模式进行重启。

 


 

checkpoint

flink的checkpoint实际上就是对state的快照。

 

checkpoint配置

//默认checkpoint功能是disabled的,想要使用的时候需要先启用
// 每隔1000 ms进行启动一个检查点【设置checkpoint的周期】
environment.enableCheckpointing(1000);
// 高级选项:
// 设置模式为exactly-once (这是默认值)
environment.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
// 确保检查点之间有至少500 ms的间隔【checkpoint最小间隔】
environment.getCheckpointConfig.setMinPauseBetweenCheckpoints(500);
// 检查点必须在一分钟内完成,或者被丢弃【checkpoint的超时时间】
environment.getCheckpointConfig.setCheckpointTimeout(60000);
// 同一时间只允许进行一个检查点
environment.getCheckpointConfig.setMaxConcurrentCheckpoints(1);
// 表示一旦Flink处理程序被cancel后,会保留Checkpoint数据,以便根据实际需要恢复到指定的Checkpoint【详细解释见备注】

/**
  * ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION:表示一旦Flink处理程序被cancel后,会保留Checkpoint数据,以便根据实际需要恢复到指定的Checkpoint
  * ExternalizedCheckpointCleanup.DELETE_ON_CANCELLATION: 表示一旦Flink处理程序被cancel后,会删除Checkpoint数据,只有job执行失败的时候才会保存checkpoint
  */
environment.getCheckpointConfig.enableExternalizedCheckpoints(ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);

 

 

 

通过checkpoint恢复某个版本数据

默认情况下,如果设置了Checkpoint选项,则Flink只保留最近成功生成的1个Checkpoint,而当Flink程序失败时,可以从最近的这个Checkpoint来进行恢复。

flink run -m yarn-cluster -yn 2 -yjm 1024 -ytm 1024 -s hdfs://node01:8020/fsStateBackend/971ae7ac4d5f20e704747ea7c549b356/chk-50/_metadata -c com.kaikeba.checkpoint.TestCheckPoint original-flink_study-1.0-SNAPSHOT.jar

savePoint

savePoint是checkpoint的一种特殊实现,是用户以手动命令触发checkpoint,并将结果持久化到指定的存储目录中。

应用场景:

  • 1.应用程序代码升级

通过触发保存点并从该保存点处运行新版本,下游的应用程序并不会察觉到不同

  • 2.Flink版本更新

Flink 自身的更新也变得简单,因为可以针对正在运行的任务触发保存点,并从保存点处用新版本的 Flink 重启任务。

  • 3.维护和迁移

使用保存点,可以轻松地“暂停和恢复”应用程序

savePoint使用

1.取消任务并手动触发savepoint   ( 需要在conf/flink-conf.yaml 配置 state.savepoints.dir)

##【针对on yarn模式需要指定-yid参数】

flink cancel -s [targetDirectory] jobId [-yid yarnAppId]

例如:

flink cancel 8d1bb7f88a486815f9b9cf97c304885b -yid application_1594807273214_0004

2.从指定savepoint启动job

flink run -s savepointPath [runArgs]

##例如:
flink run -m yarn-cluster -yn 2 -yjm 1024 -ytm 1024 -s hdfs://node01:8020/flink/savepoints/savepoint-8d1bb7-c9187993ca94 -c MainClass original-flink_study-1.0-SNAPSHOT.jar

3.清除savepoint数据

 flink savepoint -d savepointPath

 checkpoint原理

1.在JobManager端的checkPoint Coordinator(协调器)会向任务中所有的task周期性的发送barrier(栅栏-数据批次快照)进行快照请求

2.这些barrier会被插入到数据流中,作为数据的一部分和数据一起向下流动,source Task接受到barrier后, 会把当前自己的state进行snapshot(保存到状态后端)。每个Checkpoint Barrier有一个ID,表示该段数据属于哪次Checkpoint

3.source向checkpoint coordinator(协调器)确认snapshot已经完成。

4.source继续向下游transformation operator发送 barrier。

5.transformation operator重复向下游operator发送barrier.

此时因为下游operator并行度的关系,就会存在多个流通道。
barrier在传播过程中就需要进行对齐。
当operator接收到快照的barrierN后并不能直接处理之后的数据,而是需要等待其他输入快照的barrierN.
如果接收不到其他流的barrierN,会将没接收到的barrierN的数据缓存起来。
随着snapshotN保存到状态后端,当通过checkpoint恢复数据时也会优先处理存储的数据。

6.直到sink operator向协调器确认snapshot完成。

7.coordinator确认完成本周期的snapshot已经完成。

checkpoint优化

因为一致性快照这种方式保证了数据的一致性,但是也有了潜在问题。

1.每次进行checkpoint前,都需要暂停处理流入数据,执行完快照才会继续进行。 barrier在对齐过程中,需要等待其他流通道的barrier完成。可能会有数据流阻塞。

  Flink提供了异步快照机制。

异步快照:barrier不需要等待全部对齐完成才继续处理后续数据。通过异步完成,并异步向Coordinator发送消息。

 

flink的两阶段提交

主要依托checkpoint机制来实现,类似checkpoint,JobMaster相当于协调者,所有的处理节点相当于执行者,start-checkpoint消息相当于pre-commit消息,每个处理节点的checkpoint相当于pre-commit过程,checkpoint ack消息相当于执行者反馈信息,最后callback消息相当于commit消息,完成具体的提交动作.

 

kafka-flink-kafka支持exactly-once的流程:

 

1

  • Phase 1: Pre-commit 预提交

    • Flink的JobManager向source注入checkpoint barrier以开启这snapshot,barrier从source流向sink(当Source收到Barrier后,将自身的状态进行保存,后端可以根据配置进行选择,==这里的状态是指消费的每个分区对应的offset==。然后将Barrier发送给下一个Operator),

    • 每个进行snapshot的算子成功snapshot后,都会向JobManager发送ACK.

    • 当sink完成snapshot后, 向JobManager发送ACK的同时向kafka进行pre-commit.预提交成功后,Kafka Sink会向Kafka进行真正的事务Commit。

  • Phase 2: Commit 实际提交

    • 当JobManager接收到所有算子的ACK后, 就会通知所有的算子这次checkpoint已经完成

    • Sink接收到这个通知后, 就向kafka进行commit, 正式把数据写入到kafka

不同阶段fail over的recovery举措:

(1) 在pre-commit前fail over, 系统恢复到最近的checkponit

(2) 在pre-commit后,commit前fail over,系统恢复到刚完成pre-commit时的状态

 

因此,所有opeartor必须对checkpoint最终结果达成共识:

即所有operator都必须认定数据提交要么成功执行,要么被终止然后回滚。

 

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘狗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值