9 状态编程和容错机制

9 状态编程和容错机制

流式计算分为无状态和有状态两种情况。无状态的计算观察每个独立事件,并根据最后一个事件输出结果。例如,流处理应用程序从传感器接收温度读数,并在温度超过 90 度时发出警告。有状态的计算则会基于多个事件输出结果。以下是一些例子。

  • 所有类型的窗口。例如,计算过去一小时的平均温度,就是有状态的计算。

  • 所有用于复杂事件处理的状态机。例如,若在一分钟内收到两个相差 20 度以上的温度读数,则发出警告,这是有状态的计算 。

  • 流与流之间的所有关联操作,以及流与静态表或动态表之间的关联操作,都是有状态的计算。

    下图展示了无状态流处理和有状态流处理的主要区别。无状态流处理分别接收每条数据记录(图中的黑条),然后根据最新输入的数据生成输出数据(白条)。有状态流处理会维护状态(根据每条输入记录进行更新),并基于最新输入的记录和当前的状态值生成输出记录(灰条)。

在这里插入图片描述

上图中输入数据由黑条表示。无状态流处理每次只转换一条输入记录,并且仅根据最新的输入记录输出结果(白条)。有状态 流处理维护所有已处理记录的状态值,并根据每条新输入的记录更新状态,因此输出记录(灰条)反映的是综合考虑多个事件之后的结果。尽管无状态的计算很重要,但是流处理对有状态的计算更感兴趣。事实上,正确地实现有状态的计算比实现无状态的计算难得多。旧的流处理系统并不支持有状态的计算,而新一代的流处理系统则将状态及其正确性视为重中之重。

  • 有状态的算子和应用程序

    Flink 内置的很多算子,数据源 source,数据存储 sink 都是有状态的,流中的数据都是 buffer records,会保存一定的元素或者元数据。例如: ProcessWindowFunction会缓存输入流的数据,ProcessFunction 会保存设置的定时器信息等等。

    在 Flink 中,状态始终与特定算子相关联。总的来说,有两种类型的状态:即算子状态和键控状态

    • 算子状态(operator state)

      算子状态的作用范围限定为算子任务。这意味着由同一并行任务所处理的所有数据都可以访问到相同的状态,状态对于同一任务而言是共享的。算子状态不能由相同或不同算子的另一个任务访问。
      在这里插入图片描述
      Flink 为算子状态提供三种基本数据结构:

      • 列表状态(List state):将状态表示为一组数据的列表。
      • 联合列表状态(Union list state):也将状态表示为数据的列表。它与常规列表状态的区别在于,在发生故障时,或者从保存点(savepoint)启动应用程序时如何恢复。
      • 广播状态(Broadcast state):如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应用广播状态。
    • 键控状态(keyed state)

      键控状态是根据输入数据流中定义的键(key)来维护和访问的。Flink 为每个键值维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护和处理这个 key 对应的状态。当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的 key。因此,具有相同 key 的所有数据都会访问相同的状态。Keyed State 很类似于一个分布式的 key-value map 数据结构,只能用于 KeyedStream(keyBy 算子处理之后)。
      在这里插入图片描述
      Flink 的 Keyed State 支持以下数据类型:

      • ValueState保存单个的值,值的类型为 T。

        • get 操作: ValueState.value()
        • set 操作: ValueState.update(T value)
      • ListState保存一个列表,列表里的元素的数据类型为 T。基本操作如下:

        • ListState.add(T value)
        • ListState.addAll(List values)
        • ListState.get()返回 Iterable
        • ListState.update(List values)
      • MapState<K, V>保存 Key-Value 对

        • MapState.get(UK key)
        • MapState.put(UK key, UV value)
        • MapState.contains(UK key)
        • MapState.remove(UK key)
      • ReducingState

      • AggregatingState<I, O>

      • State.clear()是清空操作

    package state;
    
    import org.apache.flink.api.common.functions.MapFunction;
    import org.apache.flink.streaming.api.checkpoint.ListCheckpointed;
    import org.apache.flink.streaming.api.datastream.DataStream;
    import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
    import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
    import pojo.SensorReading;
    
    import java.util.Collections;
    import java.util.List;
    
    /**
     * @author wangkai
     */
    public class OperatorState {
        public static void main(String[] args) throws Exception{
            StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
            env.setParallelism(1);
    
            DataStream<String> inputStream = env.socketTextStream("localhost", 7777);
    
            DataStream<SensorReading> dataStream = inputStream.map(line -> {
                String[] fields = line.split(",");
                return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
            });
    
            /**
             * 定义一个有状态的map操作,统计当前分区数据个数
             * */
            SingleOutputStreamOperator<Integer> map = dataStream.map(new MyCountMapper());
    
            map.print();
            env.execute("operator state");
        }
    
        public static class MyCountMapper implements MapFunction<SensorReading,Integer>, ListCheckpointed<Integer>{
    
            /**
             * 定义本地变量,作为算子状态
             * */
            private Integer count = 0;
            @Override
            public Integer map(SensorReading value) throws Exception {
                count++;
                return count;
            }
    
            @Override
            public List<Integer> snapshotState(long checkpointId, long timestamp) throws Exception {
                return Collections.singletonList(count);
            }
    
            @Override
            public void restoreState(List<Integer> state) throws Exception {
                for (Integer num:state
                     ) {
                    count += num;
                }
    
            }
        }
    }
    
    package state;
    
    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.configuration.Configuration;
    import org.apache.flink.streaming.api.datastream.DataStream;
    import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
    import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
    import pojo.SensorReading;
    
    /**
     * @author wangkai 
     */
    public class KeyedState {
        public static void main(String[] args) throws Exception{
            StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
            env.setParallelism(1);
    
            DataStream<String> inputStream = env.socketTextStream("10.204.51.45", 7777);
    
            DataStream<SensorReading> dataStream = inputStream.map(line -> {
                String[] fields = line.split(",");
                return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
            });
    
    
            SingleOutputStreamOperator<Integer> count = dataStream
                    .keyBy("id")
                    .map(new MyKeyedMapper());
    
            count.print("keyedCount");
    
            env.execute("keyedCount");
    
        }
    
        public static class MyKeyedMapper extends RichMapFunction<SensorReading,Integer>{
            /**
             * 声明状态
             * */
            private ValueState<Integer> keyedCount;
    
            @Override
            public void open(Configuration parameters) throws Exception {
                keyedCount = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("keyedCount - state",Integer.class,0));
            }
    
            @Override
            public Integer map(SensorReading value) throws Exception {
                Integer count = keyedCount.value();
                count++;
                keyedCount.update(count);
                return count;
            }
        }
    
    }
    
    
    package process;
    
    import org.apache.flink.api.common.functions.MapFunction;
    import org.apache.flink.api.common.state.ValueState;
    import org.apache.flink.api.common.state.ValueStateDescriptor;
    import org.apache.flink.configuration.Configuration;
    import org.apache.flink.streaming.api.datastream.DataStream;
    import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
    import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
    import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
    import org.apache.flink.util.Collector;
    import pojo.SensorReading;
    
    /**
     * @author wangkai
     *
     */
    public class ProcessFunction {
        public static void main(String[] args) throws Exception{
    
            /**
             *
             * 需求:监控温度传感器的温度值,如果温度值在 10 秒钟之内(processing time)
             * 连续上升,则报警
             * */
            StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    
            env.setParallelism(1);
    
            DataStream<String> source = env.socketTextStream("10.204.51.45", 7777);
    
    
            SingleOutputStreamOperator<SensorReading> dataStream = source.map(new MapFunction<String, SensorReading>() {
                @Override
                public SensorReading map(String value) throws Exception {
                    String[] split = value.split(",");
                    return new SensorReading(split[0], new Long(split[1]), new Double(split[2]));
                }
            });
    
            SingleOutputStreamOperator<String> warning = dataStream
                    .keyBy(SensorReading::getId)
                    .process(new TemIncWarning(10));
    
    
            warning.print("waring");
    
            env.execute("process test");
        }
    
        public static class TemIncWarning extends KeyedProcessFunction<String,SensorReading,String>{
            private Integer interval;
    
            /**
             * 声明状态,保存上次的温度值,定时器时间戳
             * */
            private ValueState<Double> lastTempState;
            private ValueState<Long> timerTsState;
    
    
            public TemIncWarning(Integer interval) {
                this.interval = interval;
            }
    
            @Override
            public void open(Configuration parameters) throws Exception {
                lastTempState = getRuntimeContext().getState(new ValueStateDescriptor<Double>("last-temp",Double.class,Double.MIN_VALUE));
                timerTsState = getRuntimeContext().getState(new ValueStateDescriptor<Long>("cur-timestamp", Long.class));
            }
    
            @Override
            public void processElement(SensorReading value, Context ctx, Collector<String> out) throws Exception {
                /**
                 * 取出状态
                 * */
                Double lastTemp = lastTempState.value();
                Long timeTs = timerTsState.value();
    
                    /**
                     * 如果温度上升并且没有定时器,注册10秒后的定时器,开始等待
                     * */
                if (value.getTemperature() > lastTemp && timeTs == null){
                    long ts = ctx.timerService().currentProcessingTime() + interval * 1000L;
                    ctx.timerService().registerProcessingTimeTimer(ts);
                    timerTsState.update(ts);
    
                    /**
                     * 如果温度下降,那么删除定时器
                     * */
                }else if( value.getTemperature() < lastTemp && timeTs != null){
                    ctx.timerService().deleteProcessingTimeTimer(timeTs);
                    timerTsState.clear();
                }
    
    
                /**
                 *更新状态
                 * */
                lastTempState.update(value.getTemperature());
    
            }
    
    
            @Override
            public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
                /**
                 * 触发定时器,报警
                 * */
                out.collect( "传感器" + ctx.getCurrentKey() + "的温度连续" + interval + "秒上升 ");
    
                timerTsState.clear();
            }
    
            @Override
            public void close() throws Exception {
                lastTempState.clear();
            }
        }
    
    }
    

    通过 RuntimeContext 注册 StateDescriptor。StateDescriptor 以状态 state 的名字和存储的数据类型为参数。在 open()方法中创建 state 变量。

  • 状态一致性

    当在分布式系统中引入状态时,自然也引入了一致性问题。一致性实际上是"正确性级别"的另一种说法,也就是说在成功处理故障并恢复之后得到的结果,与没有发生任何故障时得到的结果相比,前者到底有多正确?举例来说,假设要对最近一小时登录的用户计数。在系统经历故障之后,计数结果是多少?如果有偏差,是有漏掉的计数还是重复计数?

    • 一致性级别

      在流处理中,一致性可以分为 3 个级别:

      • at-most-once: 这其实是没有正确性保障的委婉说法——故障发生之后,计数结果可能丢失。同样的还有 udp。
      • at-least-once: 这表示计数结果可能大于正确值,但绝不会小于正确值。也就是说,计数程序在发生故障后可能多算,但是绝不会少算。
      • exactly-once: 这指的是系统保证在发生故障后得到的计数结果与正确值一致。

      曾经,at-least-once 非常流行。第一代流处理器(如 Storm 和 Samza)刚问世时只保证 at-least-once,原因有二 :

      • 这在基础架构层(决定什么代表正确,以及 exactly-once 的范围是什么)和实现层都很有挑战性。
      • 流处理系统的早期用户愿意接受框架的局限性,并在应用层想办法弥补(例如使应用程序具有幂等性,或者用批量计算层再做一遍计算)。

      最先保证 exactly-once 的系统(Storm Trident 和 Spark Streaming)在性能和表现力这两个方面付出了很大的代价。为了保证 exactly-once,这些系统无法单独地对每条记录运用应用逻辑,而是同时处理多条(一批)记录,保证对每一批的处理要么全部成功,要么全部失败。这就导致在得到结果前,必须等待一批记录处理结束。因此,用户经常不得不使用两个流处理框架(一个用来保证 exactly-once,另一个用来对每个元素做低延迟处理),结果使基础设施更加复杂。曾经,用户不得不在保证exactly-once 与获得低延迟和效率之间权衡利弊。Flink 避免了这种权衡。Flink 的一个重大价值在于,它既保证了 exactly-once,也具有低延迟和高吞吐的处理能力。从根本上说,Flink 通过使自身满足所有需求来避免权衡,它是业界的一次意义重大的技术飞跃。尽管这在外行看来很神奇,但是一旦了解,就会恍然大悟。

    • 端到端(end-to-end)状态一致性

      端到端的一致性保证,意味着结果的正确性贯穿了整个流处理应用的始终;每一个组件都保证了它自己的一致性,整个端到端的一致性级别取决于所有组件中一致性最弱的组件。具体可以划分如下:

      内部保证 —— 依赖 checkpoint

      source 端 —— 需要外部源可重设数据的读取位置

      sink 端 —— 需要保证从故障恢复时,数据不会重复写入外部系统而对于 sink 端,又有两种具体的实现方式:幂等(Idempotent)写入和事务性 (Transactional)写入:

      • 幂等写入

        所谓幂等操作,是说一个操作,可以重复执行很多次,但只导致一次结果更改,也就是说,后面再重复执行就不起作用了。

      • 事务写入

        需要构建事务来写入外部系统,构建的事务对应着 checkpoint,等到 checkpoint 真正完成的时候,才把所有对应的结果写入 sink 系统中。对于事务性写入,具体又有两种实现方式:预写日志(WAL)和两阶段提交(2PC)。DataStream API 提供了 GenericWriteAheadSink 模板类和 TwoPhaseCommitSinkFunction 接口,可以方便地实现这两种方式的事务性写入。
        在这里插入图片描述
        事务(Transaction)

        • 应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消
        • 具有原子性:一个事务中的一系列的操作要么全部成功,要么一个都不做

        实现思想:

        • 构建的事务对应着 checkpoint,等到 checkpoint 真正完成的时候,才把所有对应的结果写入 sink 系统中

        实现方式:

        • 预写日志(Write-Ahead-Log,WAL)

          把结果数据先当成状态保存,然后在收到 checkpoint 完成的通知时,一次性写入 sink 系统;简单易于实现,由于数据提前在状态后端中做了缓存,所以无论什么sink 系统,都能用这种方式一批搞定;DataStream API 提供了一个模板类:GenericWriteAheadSink,来实现这种事务性 sink。

        • 二阶段提交(Two-Phase-Commit,2PC)

          对于每个 checkpoint,sink 任务会启动一个事务,并将接下来所有接收的数据添加到事务里,然后将这些数据写入外部 sink 系统,但不提交它们 —— 这时只是“预提交”,当它收到 checkpoint 完成的通知时,它才正式提交事务,实现结果的真正写入,这种方式真正实现了 exactly-once,它需要一个提供事务支持的外部sink 系统。Flink 提供了 TwoPhaseCommitSinkFunction 接口

    • PC 对外部 sink 系统的要求

      外部 sink 系统必须提供事务支持,或者 sink 任务必须能够模拟外部系统上的事务,在 checkpoint 的间隔期间里,必须能够开启一个事务并接受数据写入,在收到 checkpoint 完成的通知之前,事务必须是“等待提交”的状态。 在故障恢复的情况下,这可能需要一些时间。如果这个时候sink系统关

      闭事务(例如超时了),那么未提交的数据就会丢失。sink 任务必须能够在进程失败后恢复事务,提交事务必须是幂等操作。

  • 检查点(checkpoint)

    Flink 具体如何保证 exactly-once 呢? 它使用一种被称为"检查点"(checkpoint)的特性,在出现故障时将系统重置回正确状态。下面通过简单的类比来解释检查点的作用。

    假设你和两位朋友正在数项链上有多少颗珠子,如下图所示。你捏住珠子,边数边拨,每拨过一颗珠子就给总数加一。你的朋友也这样数他们手中的珠子。当你分神忘记数到哪里时,怎么办呢? 如果项链上有很多珠子,你显然不想从头再数一遍,尤其是当三人的速度不一样却又试图合作的时候,更是如此(比如想记录前一分钟三人一共数了多少颗珠子,回想一下一分钟滚动窗口)。

    于是,你想了一个更好的办法: 在项链上每隔一段就松松地系上一根有色皮筋,将珠子分隔开; 当珠子被拨动的时候,皮筋也可以被拨动; 然后,你安排一个助手,让他在你和朋友拨到皮筋时记录总数。用这种方法,当有人数错时,就不必从头开始数。相反,你向其他人发出错误警示,然后你们都从上一根皮筋处开始重数,助手则会告诉每个人重数时的起始数值,例如在粉色皮筋处的数值是多少。

    Flink 检查点的作用就类似于皮筋标记。数珠子这个类比的关键点是: 对于指定的皮筋而言,珠子的相对位置是确定的; 这让皮筋成为重新计数的参考点。总状态(珠子的总数)在每颗珠子被拨动之后更新一次,助手则会保存与每根皮筋对应的检查点状态,如当遇到粉色皮筋时一共数了多少珠子,当遇到橙色皮筋时又是多少。当问题出现时,这种方法使得重新计数变得简单。

    • Flink 的检查点算法

      在过去的几十年中,关于连续处理系统的 recovery 机制,工业界和学术界提出了很多种解决办法,如: Distributed Snapshots: Determining Global States of Distributed Systems) 和 Naiad: A Timely Dataflow System。有一些系统如 Discretized Streams 和 Comet 会把连续处理当作 无状态的分布式批处理计算 来做状态恢复;对于有状态的 dataflow 系统,如:Naiad、SDGs、Piccolo 和 SEEP,它们是我们的主要关注点,它们使用 checkpoint 获取全局一致的 snapshot 来做故障恢复。

      关于 consistent global snapshot 的问题,自从在 Chandy 和 Lamport 的论文中提出来后,过去二十多年一直在被广泛地研究。全局 snapshot 理论上反映了作业执行的总体状态以及 operator 实例的可能状态。对于全局一致性 snapshot 算法,Naiad 中提出了一个简单但代价非常高昂的实现方案:

      1. 第一步,先停止计算;
      2. 第二步,开始做 snapshot;
      3. 第三步,如果 snapshot 完成了,每个 task 再恢复之前的计算。

      这个实现方案对吞吐和空间占用都有很大的影响,它并不是一个很好的方案。另一个实现方案,就是 Chandy-Lamport 算法,当前它已经应用在很多的系统中,它是异步地执行快照,并且要求上游数据源可以回溯(也就是要求数据源能够自己备份)。它是通过在数据流中发送 marker 来实现,marker 会触发 operator 和 state 的 snapshot。但这种算法还需要额外的存储空间用于上游数据量恢复,数据流的重新计算也会导致恢复时间较长(主要还是原生算法会对一些 record 也做相应的 snapshot,这会导致存储空间占用过高以及恢复时间过长)。本论文中提出的方案扩展了原生的 Chandy-Lamport 算法,但对于无环 graph 它不会备份未处理及通道中正在传输的 record,对于有环的 graph,它也只需要很少量的 record 备份。

      • Asynchronous Barrier Snapshot(ABS)
        • Problem Definition

          这里,我们这样定义一个 global snapshot(它需要包含所有的状态信息,这样才能保证 failover 之后能够正确恢复状态):
          G ∗ = ( T ∗ , E ∗ ) G* = (T*,E*) G=(T,E)
          它代表一个 execution graph G=(T,E)G=(T,E) 的一个全局快照,T∗T∗ 代表所有 task 状态的集合,E∗E∗ 代表所有 edge 状态的集合。也就是说:

          1. ∀t∈T,s∗t∈T∗∀t∈T,st∗∈T∗,T∗T∗ 会包含所有 Operator 的状态;
          2. E∗E∗ 会包含所有 channel 状态的集合,e∗e∗ 会包含 ee 中正在传输的所有 record。

          为了能够保证 recovery 后正确恢复状态信息,对于每个 G∗G∗,都需要保证以下两个特性:

          1. Termination:snapshot 能够在一定的时间内完成;
          2. Feasibility:它表示这个 snapshot 是有意义的,也就是说在 snapshot 期间尽管计算没有停止,也不会有任何信息丢失。
        • ABS for Acyclic Dataflows:非环 dataflow 中的 ABS 实现

        这里先看下在无环 dataflow 中 ABS 是如何实现的,因为 Flink 只支持有向无环图,所以这个就是 Flink checkpoint 的实现方案

        当把一个作业的执行逻辑划分为多个 stage 时,做 snapshot 不存储 channel 中的 state 是完全有可能的。如果一个 operator 已经完成了对输入的所有计算,并且数据已经完全输出出去,那么只对 operator 的 state 做 snapshot 就可以达到我们的要求。

        具体的实现就是:每个阶段的输入数据都会被周期性地插入一些特殊标记 —— barrier,这些 barrier 会推送到整个 dataflow 中直到 sink 节点(dataflow 中结束节点,它没有下游输出),每个 task 如果收到输入所有的 barrier 就开始做相应的 snapshot。这个算法的实现是有以下假设的:

        1. 网络传输是可靠的、可以做到 FIFO 传输,并且可以实现 blockedunblocked,如果一个通道是 blocked,它会把这个通道接收到的所有数据缓存起来先不发送,直接收到 unblocked 的信号才会发送;
        2. Task 可以在其对应的 channel 触发以下三种操作:blockedunblockedsend msgs,Broadcasting msgs 表示的是向下游所有的 channel 发送数据;
        3. 对于 source 节点来说,输入节点被抽象为 Nil 输入通道(一个虚拟通道)。

      ABS 算法的执行流程如下:

      1. 中心协调器周期性地在所有输入端插入 barrier;

      2. 当一个 source 节点接收到 barrier 时,它对当前的状态做下 snapshot,并且 broadcast barrier 到所有的下游节点(如上图中的 a 子图);

      3. 当一个非 source 节点从它的输入通道中接收到一个 barrier 时,它会 block 当前的 channel 直到接收该节点所有输入端发送的 barrier(如上图中的 b 子图以及代码第 9 行);

      4. 当从所有输入 channel 都接收到 barrier 之后,这个 task 会对当前状态做一个 snapshot,并且 broadcast 这个 barrier 到所有的输出端(如上图中的 c 子图以及代码第 12-13 行);

      5. 最后,这个 task 会 unblock 它所有的输入 channel,继续进行计算。

在这里插入图片描述
我们用一个例子来看检查点是如何运行的。Flink 为用户提供了用来定义状态的工具。例如对于输入的数据,我们按照第一个字进行分组并维护第二个字段的计数状态。 keyBy 算子用来将记录按照第一个元素(一个字符串)进行分组,根据该 key 将数据进行重新分区,然后将记录再发送给下一个算子: 有状态的
map 算子(mapWithState)。map 算子在接收到每个元素后,将输入记录的第二个字段的数据加到现有总数中,再将更新过的元素发射出去。下图表示程序的初始状态:
输入流中的 6 条记录被检查点分割线(checkpoint barrier)隔开,所有的 map 算子状态均为 0(计数还未开始)。所有 key 为 a 的记录将被顶层的 map 算子处理,所有 key 为 b的记录将被中间层的 map 算子处理,所有 key 为 c 的记录则将被底层的 map 算子处理。
在这里插入图片描述
上图是程序的初始状态。注意,a、b、c 三组的初始计数状态都是 0,即三个圆柱上的值。ckpt 表示检查点分割线(checkpoint barriers)。每条记录在处理顺序上严格地遵守在检查点之前或之后的规定,例如[“b”,2]在检查点之前被处理,[“a”,2]则在检查点之后被处理。
当该程序处理输入流中的 6 条记录时,涉及的操作遍布 3 个并行实例(节点、CPU内核等)。那么,检查点该如何保证 exactly-once 呢?检查点分割线和普通数据记录类似。它们由算子处理,但并不参与计算,而是会触发与检查点相关的行为。当读取输入流的数据源(在本例中与 keyBy 算子内联)遇到检查点屏障时,它将其在输入流中的位置保存到持久化存储中。如果输入流来自消息传输系统(Kafka),这个位置就是偏移量。Flink 的存储机制是插件化的,持久化存储可以是分布式文件系统,如 HDFS。下图展示了这个过程。
在这里插入图片描述
当 Flink 数据源(在本例中与 keyBy 算子内联)遇到检查点分界线(barrier)时,它会将其在输入流中的位置保存到持久化存储中。这让 Flink 可以根据该位置重启。检查点像普通数据记录一样在算子之间流动。当 map 算子处理完前 3 条数据并收到检查点分界线时,它们会将状态以异步的方式写入持久化存储,如下图所示。
在这里插入图片描述
位于检查点之前的所有记录([“b”,2]、[“b”,3]和[“c”,1])被 map 算子处理之后的情况。此时,持久化存储已经备份了检查点分界线在输入流中的位置(备份操作发生在barrier 被输入算子处理的时候)。map 算子接着开始处理检查点分界线,并触发将状态异步备份到稳定存储中这个动作。
当 map 算子的状态备份和检查点分界线的位置备份被确认之后,该检查点操作就可以被标记为完成,如下图所示。我们在无须停止或者阻断计算的条件下,在一个逻辑时间点(对应检查点屏障在输入流中的位置)为计算状态拍了快照。通过确保备份的状态和位置指向同一个逻辑时间点,后文将解释如何基于备份恢复计算,从而保证 exactly-once。值得注意的是,当没有出现故障时,Flink 检查点的开销极小,检查点操作的速度由持久化存储的可用带宽决定。回顾数珠子的例子: 除了因为数错而需要用到皮筋之外,皮筋会被很快地拨过。
在这里插入图片描述
检查点操作完成,状态和位置均已备份到稳定存储中。输入流中的所有数据记录都已处理完成。值得注意的是,备份的状态值与实际的状态值是不同的。备份反映的是检查点的状态。如果检查点操作失败,Flink 可以丢弃该检查点并继续正常执行,因为之后的某一个检查点可能会成功。虽然恢复时间可能更长,但是对于状态的保证依旧很有力。只有在一系列连续的检查点操作失败之后,Flink 才会抛出错误,因为这通常预示着 发生了严重且持久的错误。现在来看看下图所示的情况: 检查点操作已经完成,但故障紧随其后。
在这里插入图片描述
在这种情况下,Flink 会重新拓扑(可能会获取新的执行资源),将输入流倒回到上一个检查点,然后恢复状态值并从该处开始继续计算。在本例中,[“a”,2]、[“a”,2]和[“c”,2]这几条记录将被重播。下图展示了这一重新处理过程。从上一个检查点开始重新计算,可以保证在剩下的记录被处理之后,得到的 map 算子的状态值与没有发生故障时的状态值一致。
在这里插入图片描述
Flink 将输入流倒回到上一个检查点屏障的位置,同时恢复 map 算子的状态值。然后,Flink 从此处开始重新处理。这样做保证了在记录被处理之后,map 算子的状态值与没有发生故障时的一致。

  • Flink+Kafka 如何实现端到端的 exactly-once 语义

    我们知道,端到端的状态一致性的实现,需要每一个组件都实现,对于 Flink + Kafka 的数据管道系统(Kafka 进、Kafka 出)而言,各组件怎样保证 exactly-once语义呢?

    • 内部 —— 利用 checkpoint 机制,把状态存盘,发生故障的时候可以恢复,保证内部的状态一致性

    • source—— kafka consumer 作为 source,可以将偏移量保存下来,如果后续任务出现了故障,恢复的时候可以由连接器重置偏移量,重新消费数据,

      保证一致性

    • sink —— kafka producer 作为 sink,采用两阶段提交 sink,需要实现一个TwoPhaseCommitSinkFunction

​ Flink 由 JobManager 协调各个 TaskManager 进行 checkpoint 存储,checkpoint 保存在 StateBackend 中,默认 StateBackend 是内存级的,也可以改为文件级的进行持久化保存。

在这里插入图片描述
当 checkpoint 启动时,JobManager 会将检查点分界线(barrier)注入数据流;barrier 会在算子间传递下去。
在这里插入图片描述
每个算子会对当前的状态做个快照,保存到状态后端。对于 source 任务而言,就会把当前的 offset 作为状态保存起来。下次从 checkpoint 恢复时,source 任务可以重新提交偏移量,从上次保存的位置开始重新消费数据。
在这里插入图片描述

每个内部的 transform 任务遇到 barrier 时,都会把状态存到 checkpoint 里。sink 任务首先把数据写入外部 kafka,这些数据都属于预提交的事务(还不能被消费);当遇到 barrier 时,把状态保存到状态后端,并开启新的预提交事务。
在这里插入图片描述

当所有算子任务的快照完成,也就是这次的 checkpoint 完成时,JobManager 会向所有任务发通知,确认这次 checkpoint 完成。 当 sink 任务收到确认通知,就会正式提交之前的事务,kafka 中未确认的数据就改为“已确认”,数据就真正可以被消费了。
在这里插入图片描述
所以我们看到,执行过程实际上是一个两段式提交,每个算子执行完成,会进行“预提交”,直到执行完 sink 操作,会发起“确认提交”,如果执行失败,预提交会放弃掉。具体的两阶段提交步骤总结如下:

  • 第一条数据来了之后,开启一个 kafka 的事务(transaction),正常写入kafka 分区日志但标记为未提交,这就是“预提交”

  • jobmanager 触发 checkpoint 操作,barrier 从 source 开始向下传递,遇到barrier 的算子将状态存入状态后端,并通知 jobmanager

  • sink 连接器收到 barrier,保存当前状态,存入 checkpoint,通知jobmanager,并开启下一阶段的事务,用于提交下个检查点的数据

  • jobmanager 收到所有任务的通知,发出确认信息,表示 checkpoint 完成

  • sink 任务收到 jobmanager 的确认信息,正式提交这段时间的数据

  • 外部 kafka 关闭事务,提交的数据可以正常消费了。

    所以我们也可以看到,如果宕机需要通过 StateBackend 进行恢复,只能恢复所有确认提交的操作。

  • 选择一个状态后端(state backend)
    • MemoryStateBackend

      内存级的状态后端,会将键控状态作为内存中的对象进行管理,将它们存储在 TaskManager 的 JVM 堆上;而将 checkpoint 存储在 JobManager 的内存中。

    • FsStateBackend

      将 checkpoint 存到远程的持久化文件系统(FileSystem)上。而对于本地状态,跟 MemoryStateBackend 一样,也会存在 TaskManager 的 JVM 堆上。

    • RocksDBStateBackend

      将所有状态序列化后,存入本地的 RocksDB 中存储。

      package state;
      
      import org.apache.flink.api.common.restartstrategy.RestartStrategies;
      import org.apache.flink.runtime.state.filesystem.FsStateBackend;
      import org.apache.flink.streaming.api.CheckpointingMode;
      import org.apache.flink.streaming.api.environment.CheckpointConfig;
      import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
      
      /**
       * @author wangkai
       *
       */
      public class StateBackend {
          public static void main(String[] args) {
              StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
      
              /**
               * 状态后端配置
               * */
              env.setStateBackend(new FsStateBackend(" "));
      
              /**
               * 配置检查点
               *
               * */
              env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
              env.getCheckpointConfig().setCheckpointTimeout(60000);
              env.getCheckpointConfig().setMinPauseBetweenCheckpoints(500);
              env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
              env.getCheckpointConfig().setPreferCheckpointForRecovery(false);
              env.getCheckpointConfig().setTolerableCheckpointFailureNumber(0);
              env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.DELETE_ON_CANCELLATION);
      
              /**
               * 重启策略配置
               * */
              env.setRestartStrategy(RestartStrategies.fixedDelayRestart(3,1000));
      
      
          }
      }
      
      
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值