1. Flink中的状态

  • 由一个任务维护,并且用来计算某个结果的所有数据,都属于这个任务的状态
  • 可以认为状态就是一个本地变量,可以被任务的业务逻辑访问
  • Flink 会进行状态管理,包括状态一致性、故障处理以及高效存储和访问,以便开发人员可以专注于应用程序的逻辑

2. 管理状态的原因

有状态的计算是流处理框架要实现的重要功能,因为稍复杂的流处理场景都需要记录状态,然后在新流入数据的基础上不断更新状态。下面的几个场景都需要使用流处理的状态功能:

  • 数据流中的数据有重复,我们想对重复数据去重,需要记录哪些数据已经流入过应用,当新数据流入时,根据已流入过的数据来判断去重
  • 检查输入流是否符合某个特定的模式,需要将之前流入的元素以状态的形式缓存下来。比如,判断一个温度传感器数据流中的温度是否在持续上升
  • 对一个时间窗口内的数据进行聚合分析,分析一个小时内某项指标的75分位或99分位的数值
  • 在线机器学习场景下,需要根据新流入数据不断更新机器学习的模型参数

Flink的一个算子有多个子任务,每个子任务分布在不同实例上,我们可以把状态理解为某个算子子任务在其当前实例上的一个变量,变量记录了数据流的历史信息。当新数据流入时,我们可以结合历史信息来进行计算。实际上,Flink的状态是由算子的子任务来创建和管理的。一个状态更新和获取的流程如下图所示,一个算子子任务接收输入流,获取对应的状态,根据新的计算结果更新状态。一个简单的例子是对一个时间窗口内输入流的某个整数字段求和,那么当算子子任务接收到新元素时,会获取已经存储在状态中的数值,然后将当前输入加到状态上,并将状态数据更新。

3. Flink的状态分类

3.1 Managed State和Raw State

Flink有两种基本类型的状态:托管状态(Managed State)和原生状态(Raw State)。
Managed State是由Flink管理的,Flink帮忙存储、恢复和优化,Raw State是开发者自己管理的,需要自己序列化。
两者的具体区别有:

  • 从状态管理的方式上来说,Managed State由Flink Runtime托管,状态是自动存储、自动恢复的,Flink在存储管理和持久化上做了一些优化。当我们横向伸缩,或者说我们修改Flink应用的并行度时,状态也能自动重新分布到多个并行实例上。Raw State是用户自定义的状态
  • 从状态的数据结构上来说,Managed State支持了一系列常见的数据结构,如ValueState、ListState、MapState等。Raw State只支持字节,任何上层数据结构需要序列化为字节数组。使用时,需要用户自己序列化,以非常底层的字节数组形式存储,Flink并不知道存储的是什么样的数据结构
  • 从具体使用场景来说,绝大多数的算子都可以通过继承Rich函数类或其他提供好的接口类,在里面使用Managed State。Raw State是在已有算子和Managed State不够用时,用户自定义算子时使用

对Managed State继续细分,它又有两种类型:Keyed StateOperator State

3.1.1 Operator State

算子状态(Operator State)的作用范围限定为算子任务。这意味着由同一并行任务所处理的所有数据都可以访问到相同的状态, 状态对于同一任务而言是共享的。算子状态不能由相同或不同算子的另一个任务访问。

Flink 为算子状态提供三种基本数据结构:

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

3.1.2 Keyed State

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

Flink 的 Keyed State 支持以下数据类型:

  • ValueState[T]保存单个的值,值的类型为 T
    • 获取状态
      • ValueState.value()
    • 更新状态
      • ValueState.update(value: T)
  • ListState[T]保存一个列表,列表里的元素的数据类型为 T
    • 添加元素
      • ListState.add(value: T)
      • ListState.addAll(values: java.util.List[T])
    • 获取列表
      • oListState.get() 返回 Iterable[T]
    • 更新列表
      • ListState.update(values: java.util.List[T])
  • MapState[K, V]保存 Key-Value 键值对
    • 获取某个key下的value
      • MapState.get(key: K)
    • 对某个key设置value
      • MapState.put(key: K, value: V)
    • 判断某个key是否存在
      • MapState.contains(key: K)
    • 删除某个key以及对应的value
      • MapState.remove(key: K)
    • 返回MapState中所有的元素
      • MapState.entries()
    • 返回一个迭代器
      • MapState.iterator()
  • ReducingState[T]
  • AggregatingState[I, O]
    • ReducingState[T]和AggregatingState[IN, OUT]与ListState[T]同属于MergingState[T]。与ListState[T]不同的是,ReducingState[T]只有一个元素,而不是一个列表。它的原理是新元素通过add(value: T)加入后,与已有的状态元素使用ReduceFunction合并为一个元素,并更新到状态里。AggregatingState[IN, OUT]与ReducingState[T]类似,也只有一个元素,只不过AggregatingState[IN, OUT]的输入和输出类型可以不一样。ReducingState[T]和AggregatingState[IN, OUT]与窗口上进行ReduceFunction和AggregateFunction很像,都是将新元素与已有元素做聚合

注意,Flink的核心代码目前使用Java实现的,而Java的很多类型与Scala的类型不太相同,比如List和Map。这里不再详细解释Java和Scala的数据类型的异同,但是开发者在使用Scala调用这些接口,比如状态的接口,需要注意将Java的类型转为Scala的类型。对于List和Map的转换,只需要需要引用import scala.collection.JavaConversions._,并在必要的地方添加后缀asScala或asJava来进行转换。此外,Scala和Java的空对象使用习惯不太相同,Java一般使用null表示空,Scala一般使用None。

4. 状态一致性

当在分布式系统中引入状态时, 自然也引入了一致性问题。一致性实际上是“正确性级别”的另一种说法,也就是说在成功处理故障并恢复之后得到的结果,与没有发生任何故障时得到的结果相比, 前者到底有多正确?
在流处理中, 一致性可以分为 3 个级别:

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

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

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

最先保证 exactly-once 的系统(Storm Trident 和 Spark Streaming)在性能和表现力这两个方面付出了很大的代价。为了保证 exactly-once,这些系统无法单独地对每条记录运用应用逻辑, 而是同时处理多条(一批)记录, 保证对每一批的处理要么全部成功,要么全部失败。这就导致在得到结果前,必须等待一批记录处理结束。因此, 用户经常不得不使用两个流处理框架(一个用来保证 exactly-once, 另一个用来对每个元素做低延迟处理), 结果使基础设施更加复杂。曾经, 用户不得不在保证exactly-once 与获得低延迟和效率之间权衡利弊。Flink 避免了这种权衡。
Flink 的一个重大价值在于,它既保证了 exactly-once, 也具有低延迟和高吞吐的处理能力


部分内容摘自https://zhuanlan.zhihu.com/p/104171679