目录
前言
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都必须认定数据提交要么成功执行,要么被终止然后回滚。