【Flink】状态与容错

445 阅读6分钟

「这是我参与2022首次更文挑战的第24天,活动详情查看:2022首次更文挑战

一、概述

状态,其实指的是 Flink 程序的中间计算结果。

Flink 支持了不同类型的状态,并且针对状态的持久化还提供了专门的机制和状态管理器。

Flink 根据是否需要保存中间结果, 把计算分为有状态计算和无状态计算:

  • 有状态计算: 依赖之前或之后的事件

  • 无状态计算: 独立

Flink 的官网同样给出了适用于状态计算的几种情况:

  • 复杂事件处理获取符合某一特定时间规则的事件

  • 聚合计算

  • 机器学习的模型训练

  • 使用历史的数据进行计算

根据数据结构不同, Flink 定义了多种 state , 应用于不同的场景:

  • ValueState: 即类型为 T 的单值状态。这个状态与对应的 key 绑定, 是最简单的状态了。它可以通过 update 方法更新状态值, 通过 value() 方法获取状态值。

  • ListState: 即 key 上的状态值为一个列表。可以通过 add 方法往列表中附加值; 也可以通过 get() 方法返回一个 Iterable<T> 来遍历状态值。

  • ReducingState: 这种状态通过用户传入的 reduceFunction, 每次调用 add 方法添加值的时候, 会调用 reduceFunction, 最后合并到一个单一的状态值。

  • FoldingState: 跟 ReducingState 有点类似, 不过它的状态值类型可以与 add 方法中传入的元素类型不同(这种状态将会在 Flink 未来版本中被删除)。

  • MapState: 即状态值为一个 map。用户通过 putputAll 方法添加元素。

每一类状态有以下两种托管方式:

  1. 托管方式(Managed State):这类状态的数据结构由引擎定义,Flink 运行时负责序列化及写入状态后端。当并行度改变时,Flink 引擎负责重新拆分托管状态到各实例上。以外,引擎负责优化托管状态的存储效率。

  2. 非托管方式(Raw State):这类状态由应用程序定义,引擎以字节流格式写入状态后端。

状态分类

Flink 中,根据数据集是否按照某一个 Key 进行分区,将状态分为:

  • Keyed State

  • Operator StateNon-Keyed State)两种类型。

如下图所示,Keyed State 是经过分区后的流上状态,每个 Key 都有自己的状态。

图中的八边形、圆形和三角形分别管理各自的状态,并且只有指定的 key 才能访问和更新自己对应的状态。

2021-04-1719-06-27.png

Keyed State 不同的是,Operator State 可以用在所有算子上,每个算子子任务或者说每个算子实例共享一个状态,流入这个算子子任务的数据可以访问和更新这个状态。每个算子子任务上的数据共享自己的状态。

但是有一点需要说明的是,无论是 Keyed State 还是 Operator StateFlink 的状态都是基于本地的,即每个算子子任务维护着这个算子子任务对应的状态存储,算子子任务之间的状态不能相互访问。

案例 - 利用 state 求平均值

原始数据:

(1,3)(1,5)(1,7)(1,4)(1,2)

思路:

  1. 读数据源
  2. 将数据源根据 key 分组
  3. 按照 key 分组策略, 对流式数据调用状态化处理

状态处理:每当第一个元素的和达到二,就把第二个元素的和与第一个元素的和相除,最后输出。

import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.StateTtlConfig;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.time.Time;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;


public class StateDemo {

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

        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        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("submit job");
    }

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

        private transient ValueState<Tuple2<Long, Long>> sum;
        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();
            }
        }

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


            // 可以通过传递配置在任何状态描述符中启用 TTL 功能
            StateTtlConfig ttlConfig = StateTtlConfig
                    .newBuilder(Time.seconds(10))
                    .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
                    .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
                    .build();

            descriptor.enableTimeToLive(ttlConfig);

            sum = getRuntimeContext().getState(descriptor);
        }
    }
}

输出结果如下:

6> (1,4)
6> (1,6)

通过继承 RichFlatMapFunction 来访问 State,通过 getRuntimeContext().getState(descriptor) 来获取状态的句柄。

二、状态后端种类和配置

Flink 的状态数据可以存在 JVM 的堆内存或者堆外内存中,当然也可以借助第三方存储。

默认情况下,Flink 的状态会保存在 taskmanager 的内存中,Flink 提供了三种可用的状态后端用于在不同情况下进行状态后端的保存:

  • MemoryStateBackend

  • FsStateBackend

  • RocksDBStateBackend

(1)MemoryStateBackend

MemoryStateBackendstate 数据存储在内存中,一般用来进行本地调试用,在使用 MemoryStateBackend 时需要注意的一些点包括:

每个独立的状态(state)默认限制大小为 5MB,可以通过构造函数增加容量 状态的大小不能超过 akkaFramesize 大小(默认 10MB) 聚合后的状态必须能够放进 JobManager 的内存中。即,JobManager 收到的 state 数据总和不能超过 JobManager 内存。

下图表示了 MemoryStateBackend 的数据存储位置:

2021-04-2117-36-40.png

MemoryStateBackend 适合的场景:

  • 本地开发和调试
  • 状态很小的作业

MemoryStateBackend 可以通过在代码中显示指定:

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new MemoryStateBackend(DEFAULT_MAX_STATE_SIZE,false));

其中,new MemoryStateBackend(DEFAULT_MAX_STATE_SIZE, false) 中的 false 代表关闭异步快照机制。

很明显 MemoryStateBackend 适用于本地调试使用,来记录一些状态很小的 Job 状态信息。

(2)FsStateBackend

FsStateBackend 会把状态数据保存在 TaskManager 的内存中。

CheckPoint 时,将状态快照写入到配置的文件系统目录中,少量的元数据信息存储到 JobManager 的内存中。

使用 FsStateBackend 需要指定一个文件路径,一般来说是 HDFS 的路径,例如,hdfs://namenode:40010/flink/checkpoints

FsStateBackend 适合的场景:

  • 大状态、长窗口、大键值(键或者值很大)状态的作业
  • 适合高可用方案

下图表示了 FsStateBackend 的数据存储位置:

2021-04-2117-37-15.png

同样可以在代码中显示指定:

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new FsStateBackend("hdfs://namenode:40010/flink/checkpoints", false));

(3)RocksDBStateBackend

RocksDBStateBackendFsStateBackend 有一些类似,首先它们都需要一个外部文件存储路径,比如 HDFShdfs://namenode:40010/flink/checkpoints,此外也适用于大作业、状态较大、全局高可用的那些任务。

但是与 FsStateBackend 不同的是,RocksDBStateBackend 将正在运行中的状态数据保存在 RocksDB 数据库中,RocksDB 数据库默认将数据存储在 TaskManager 运行节点的数据目录下。

这意味着,RocksDBStateBackend 可以存储远超过 FsStateBackend 的状态,可以避免向 FsStateBackend 那样一旦出现状态暴增会导致 OOM,但是因为将状态数据保存在 RocksDB 数据库中,吞吐量会有所下降。

此外,需要注意的是,RocksDBStateBackend 是唯一支持增量快照的状态后端。

下图表示了 RocksDBStateBackend 的数据存储位置:

2021-04-2117-39-46.png

RocksDBStateBackend 适用于以下场景:

  • 超大状态、超长窗口(天)、大键值状态的作业

  • 适合高可用模式