Flink 状态管理

Flink 状态管理

通常意义上,函数里所有需要任务去维护并用来计算结果的数据都属于任务的状态,可以把状态想象成任务的业务逻辑所需要访问的本地或实例变量

如上图,任务首先会接受一些输入数据。在处理这些数据的过程中,任务对其状态进行读取或更新,并根据状态的输入数据计算结果。我们以一个持续计算接收到多少条记录的简单任务为例。当任务收到一个新的记录后,首先会访问状态获取当前统计的记录数目,然后把数目增加并更新状态,最后将更新后的状态数目发送出去。

Flink会负责进行状态的管理,包括状态一致性、故障处理以及高效存取相关的问题都由Flink负责搞定,这样开发人员就可以专注于自己的应用逻辑。

在Flink中,状态都是和特定operator(算子)相关联,为了让Flink的Runtime(运行)层知道算子有哪些状态,算子需要自己对其进行注册。根据作用域的不同,状态可以分为以下两类

  • operator state(算子状态)
  • keyed state(键值分区状态)

算子状态

算子状态的作用域是某个算子任务,这意味着所有在同一个并行任务之内的记录都能访问到相同的状态**(每一个并行的子任务都共享一个状态)。算子状态不能通过其他任务访问,无论该任务是否来自相同算子(相同算子的不同任务之间也不能访问)**。

Flink为算子状态提供了三种数据结构

  1. 列表状态(list state):将状态表示为一组数据的列表。(每一个并行的子任务共享一个状态)
  2. 联合列表状态(union list state):同样将状态表示为数据的列表,但在进行故障恢复或者从某个保存点(savepoint)启动应用的时候,状态恢复的方式和普通的列表状态有所不同。(把之前的每一个状态广播到对应的每一个算子中)
  3. 广播状态(broadcast state):专门为那些需要保证算子的每个任务状态都相同的场景而设计。(把同一个状态广播给所有算子子任务)

键值分区状态

键值分区状态会按照算子输入记录所定义的键值来进行维护或访问。Flink为每个键值都维护了一个状态实例,该实例总是位于那个处理对应键值记录的算子任务上。当任务在处理一个记录时,会自动把状态的访问范围限制为当前记录的键值,因此所有键值相同的记录都能访问到一样的状态。

Flink为键值分区状态提供以下几种数据结构

  1. 单值状态(value state):每个键对应存储一个任意类型的值。
  2. 列表状态(list state):每个键对应存储一个值的列表。
  3. 映射状态(map state):每个键对应存储一个键值映射。
  4. 聚合状态(Reducing state & Aggregating State):每个键对应存储一个用于聚合操作的列表

状态后端(State Backends)

有状态算子的任务通常会对每一条到来的记录读写状态,因此高效的状态访问对于记录处理的低延迟而言至关重要。为了保证快速访问状态,每个并行任务都会把状态维护在本地。至于状态具体的存储、访问和维护,则是由一个名为状态后端的可拔插(pluggable) 组件来决定。状态后端主要负责两件事情:本地状态管理和将状态以检查点的形式写入远程存储

目前,Flink提供了三种状态后端,状态后端的选择会影响有状态应用的鲁棒性及性能。

  1. MemoryStateBackend

    • MemoryStateBackend将状态以常规对象的方式存储在TaskManager进程的JVM堆,并在生成Checkpoints时会将状态发送至JobManager并保存到它的堆内存中。
    • 如果状态过大,则可能导致JVM上的任务由于OutOfMemoryError而终止,并且可能由于堆中放置了过多常驻内存的对象而引发垃圾回收停顿问题。
    • 由于内存具有易失性,所以一旦JobManager出现故障就会导致状态丢失,因此MemoryStateBackend通常用于开发和调试。
    • 内存访问速度快,延迟低,但容错性也低。
  2. FsStateBackend

    • 与MemoryStateBackend一样将本地状态存储在TaskManager进程的JVM堆里,不同的是将Checkpoints存到了远程持久化文件系统(FileSystem)中。
    • 受到TaskManager内存大小的限制,并且也可能导致垃圾回收停顿问题。
    • FsStateBackend既让本地访问享有内存的速度,又可以支持故障容错。
  3. RocksDBStateBackend

    • RocksDBStateBackend会将全部状态序列化后存到本地RocksDB实例中

    • 由于磁盘I/O以及序列化/反序列化对象的性能开销,相较于内存中维护状态而言, 读写性能会偏低。

    • RocksDB的支持并不直接包含在Flink中,需要额外引入依赖

      1
      2
      3
      4
      5
      
      <dependency> 
        <groupId>org.apache.flink</groupId> 
        <artifactId>flink-statebackend-rocksdb_2.12</artifactId> 
        <version>1.12.1</version>
      </dependency>
      

有状态算子的扩缩容

流式应用的一项基本需求是根据输入数据到达速率的变化调整算子的并行度。对于无状态的算子扩缩容很容易,但是对于有状态算子来说,这就变的复杂了很多。因为我们需要把状态重新分组,分配到与之前数量不等的并行任务上

针对不同类型状态的算子,Flink提供了四种扩缩容模式

  1. 键值分区状态
  2. 算子列表状态
  3. 算子联合列表状态
  4. 算子广播状态

键值分区状态

带有键值分区状态的算子在扩缩容时会根据新的任务数量对键值重新分区,但为了降低状态在不同任务之间迁移的必要成本,Flink不会对单独的键值实施再分配,而是会把所有键值分为不同的键值组(Key group)。每个键值组都包含了部分键值,Flink以此为单位把键值分配给不同任务。

算子列表状态

带有算子列表状态的算子在扩缩容时会对列表中的条目进行重新分配。理论上,所有并行算子任务的列表条目会被统一收集起来,随后均匀分配到更少或更多的任务之上。如果列表条目的数量小于算子新设置的并行度,部分任务在启动时的状态就可能为空。

算子联合列表状态

带有算子联合列表状态的算子会在扩缩容时把状态列表的全部条目广播到全部任务上,随后由任务自己决定哪些条目应该保留,哪些应该丢弃。

算子广播状态

带有算子广播状态的算子在扩缩容时会把状态拷贝到全部新任务上,这样做的原因是广播状态能确保所有任务的状态相同。在缩容的情况下,由于状态经过复制不会丢失,我们可以简单的停掉多出的任务。

Built with Hugo
主题 StackJimmy 设计