流式计算分为无状态和有状态两种情况。无状态的计算观察每个独立事件,并根据最后一个事件输出结果。例如,流处理应用程序从传感器接收温度读数,并在温度超过90度时发出警告。有状态的计算则会基于多个事件输出结果。以下是一些例子。
所有类型的窗口。例如,计算过去一小时的平均温度,就是有状态的计算。
所有用于复杂事件处理的状态机。例如,若在一分钟内收到两个相差20度以上的温度读数,则发出警告,这是有状态的计算。
流与流之间的所有关联操作,以及流与静态表或动态表之间的关联操作,都是有状态的计算。
下图展示了无状态流处理和有状态流处理的主要区别。无状态流处理分别接收每条数据记录(图中的黑条),然后根据最新输入的数据生成输出数据(白条)。有状态流处理会维护状态(根据每条输入记录进行更新),并基于最新输入的记录和当前的状态值生成输出记录(灰条)。
上图中输入数据由黑条表示。无状态流处理每次只转换一条输入记录,并且仅根据最新的输入记录输出结果(白条)。有状态 流处理维护所有已处理记录的状态值,并根据每条新输入的记录更新状态,因此输出记录(灰条)反映的是综合考虑多个事件之后的结果。
尽管无状态的计算很重要,但是流处理对有状态的计算更感兴趣。事实上,正确地实现有状态的计算比实现无状态的计算难得多。旧的流处理系统并不支持有状态的计算,而新一代的流处理系统则将状态及其正确性视为重中之重。
1. 有状态的算子和应用程序
Flink内置的很多算子,数据源source,数据存储sink都是有状态的,流中的数据都是buffer records,会保存一定的元素或者元数据。例如: ProcessWindowFunction会缓存输入流的数据,ProcessFunction会保存设置的定时器信息等等。
在Flink中,状态始终与特定算子相关联。总的来说,有两种类型的状态:
算子状态(operator state)
键控状态(keyed state)
1.1 算子状态(operator state)
算子状态的作用范围限定为算子任务。这意味着由同一并行任务所处理的所有数据都可以访问到相同的状态,状态对于同一任务而言是共享的。算子状态不能由相同或不同算子的另一个任务访问,即:即使同一个算子的并行任务也不能相互访问状态。
Flink为算子状态提供三种基本数据结构:
1.列表状态(List state): 将状态表示为一组数据的列表;每个并行子任务只会保留一个列表,也就是当前并行子任务上所有状态项的集合,状态项是可以重新分配的最细粒度,彼此完全独立。
如果并行度调整了,列表状态中的元素项被统一收集起来,相当于多个分区的列表合成大列表,再均匀分配所有并行任务
2.联合列表状态(Union list state):也将状态表示为数据的列表;所有分区的所有数据都会访问同一个状态。
3.广播状态(Broadcast state):状态像广播一样到所有并行子任务,底层中广播状态类似映射状态的键值对保存,并且基于广播流来创建。
1.1.1 一个案例
案例有问题,不能通过编译,还不知道哪里的问题
import java.util
import org.apache.flink.api.common.functions.RichMapFunction
import org.apache.flink.streaming.api.checkpoint.ListCheckpointed
import org.apache.flink.streaming.api.scala._
object OperatorStateTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val inputStream: DataStream[String] = env.socketTextStream("hadoop", 7777)
val resultStream = inputStream.map(new MyMapper())
resultStream.print()
env.execute("XXX")
}
}
// 实现记录经map流过多少条数据
class MyMapper() extends RichMapFunction[SensorReading9, Long] with ListCheckpointed[Long] {
// 定义记录常量
var count: Long = 0L
// 实现没经一条数据,记录数增1
override def map(value: SensorReading9): Long = {
count += 1
count
}
// 对记录数进行快照设置检查点 这里检查点是一个list 是因为分布式架构 并行任务各维护一个count
override def snapshotState(checkpointId: Long, timestamp: Long): util.List[Long] = {
val stateList = new util.ArrayList[Long]()
stateList.add(count)
stateList
}
// 进行故障恢复
override def restoreState(state: util.List[Long]): Unit = {
val iter = state.iterator()
while (iter.hasNext) {
count += iter.next()
}
// for(countState <- state){
// count += countState
// }
}
}
case class SensorReading9(id: String, timestamp: Long, temperature: Double)
1.2 键控状态(keyed state)
键控状态是根据输入数据流中定义的键(key)来维护和访问的。Flink为每个键值维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护和处理这个key对应的状态。当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的key。因此,具有相同key的所有数据都会访问相同的状态。Keyed State很类似于一个分布式的key-value map数据结构,只能用于KeyedStream(keyBy算子处理之后)
Flink的Keyed State支持以下数据类型:
- ValueState[T]保存单个的值,值的类型为T。
get操作: ValueState.value()
set操作: ValueState.update(value: T)
- ListState[T]保存一个列表,列表里的元素的数据类型为T。基本操作如下:
ListState.add(value: T)
ListState.addAll(values: java.util.List[T])
ListState.get()返回Iterable[T]
ListState.update(values: java.util.List[T])
- MapState[K, V]保存Key-Value对。
MapState.get(key: K)
MapState.put(key: K, value: V)
MapState.contains(key: K)
MapState.remove(key: K)
- ReducingState[T]
- AggregatingState[I, O]
- State.clear()是清空操作
1.2.1 ValueState案例:
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.api.java.tuple.Tuple
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector
object StateTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val inputStream = env.socketTextStream("hadoop", 7777)
val dataStream = inputStream.map {
data => {
val dataArray = data.split(",")
SensorReading6(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
}
}
val resultStream = dataStream.keyBy(0).process(new MyProcessor(39))
resultStream.print()
env.execute("XXXXX")
}
}
// 注意第一个泛型是key的类型,必须是Tuple类型
class MyProcessor(threshold: Double) extends KeyedProcessFunction[Tuple, SensorReading6, (String, Double, Double)] {
// 定义状态有两种方法
// 1.直接在KeyedProcessFunction子类中创建
// 这里必须用懒加载,因为在程序刚编译截断是没有上下文环境的
// lazy val myState = getRuntimeContext.getState(new ValueStateDescriptor[Int]("my-state",classOf[Int]))
// 2.在RichFunction生命周期函数中创建
// KeyedProcessFunction是继承RichFunction接口的,只要程序启动后,控制生命周期的RichFunction的方法中自然也会有运行上下文环境
var myState: ValueState[Double] = _
override def open(parameters: Configuration): Unit = {
myState = getRuntimeContext.getState(new ValueStateDescriptor[Double]("my-state", classOf[Double]))
}
override def processElement(input: SensorReading6, context: KeyedProcessFunction[Tuple, SensorReading6, (String, Double, Double)]#Context, out: Collector[(String, Double, Double)]): Unit = {
// 获取状态
val lastTemp = myState.value()
//更新状态
myState.update(input.temperature)
// 输出
if (input.temperature > lastTemp && threshold < input.temperature - lastTemp) {
// 当温度上升且上升温度大于阈值输出相关信息
out.collect((input.id, lastTemp, input.temperature))
}
}
}
case class SensorReading6(id: String, timestamp: Long, temperature: Double)
1.2.2 RichMapFunction和RichFlatMapFunction实现状态定义
Map及MapFunction是不能定义状态的,但是RichMapFunction可以定义状态
import org.apache.flink.api.common.functions.{RichFlatMapFunction, RichMapFunction}
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector
object RichFunctionMapTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val inputStream: DataStream[String] = env.socketTextStream("hadoop", 7777)
val keyedStream = inputStream.map {
data =>
val splitData = data.split(",")
SensorReading7(splitData(0), splitData(1).toLong, splitData(2).toDouble)
}.keyBy(0)
// 调用 必须是keyedStream
val resultStreamWithMap = keyedStream.map(new TempChangeWarning(30))
val resultStreamWithFlatMap = keyedStream.map(new TempChangeWarning(30))
resultStreamWithMap.print()
resultStreamWithFlatMap.print()
env.execute("XXXX")
}
}
// 自定义RichMapFunction 一次输出一个结果
class TempChangeWarning(threshold: Double) extends RichMapFunction[SensorReading7, (String, Double, Double)] {
// 定义状态变量 上一次的温度
private var lastTempState: ValueState[Double] = _
override def open(parameters: Configuration): Unit = {
lastTempState = getRuntimeContext.getState(new ValueStateDescriptor[Double]("last-temp", classOf[Double]))
}
override def map(value: SensorReading7): (String, Double, Double) = {
// 从上一次状态中取出上次温度值
val lastTemp = lastTempState.value()
// 更新状态
lastTempState.update(value.temperature)
// 跟当前温度值计算差值,然后和阈值比较,如果大于阈值就报警
val diff = (value.temperature - lastTemp).abs
if (diff > threshold) {
(value.id, lastTemp, value.temperature)
} else {
// 这里必须有返回值 因此对于map来说必须有返回值,适合输入和输出成对出现的场景
("", 10, 10)
}
}
}
//自定义RichFlatMapFunction
class TempWarningWithFlatmap(threshold: Double) extends RichFlatMapFunction[SensorReading7, (String, Double, Double)] {
lazy val lastTempState: ValueState[Double] = getRuntimeContext.getState(new ValueStateDescriptor[Double]("last-temp", classOf[Double]))
override def flatMap(value: SensorReading7, out: Collector[(String, Double, Double)]) = {
// 从上一次状态中取出上次温度值
val lastTemp = lastTempState.value()
// 更新状态
lastTempState.update(value.temperature)
// 跟当前温度值计算差值,然后和阈值比较,如果大于阈值就报警
val diff = (value.temperature - lastTemp).abs
// flapMap可以输出满足一定条件的结果,不满足可以不输出
if (diff > threshold) {
out.collect((value.id, lastTemp, value.temperature))
}
}
}
case class SensorReading7(id: String, timestamp: Long, temperature: Double)
1.2.3 FlatMapWithState实现状态定义
import org.apache.flink.streaming.api.scala._
object RichFunctionMapTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val inputStream: DataStream[String] = env.socketTextStream("hadoop", 7777)
val keyedStream = inputStream.map {
data =>
val splitData = data.split(",")
SensorReading8(splitData(0), splitData(1).toLong, splitData(2).toDouble)
}.keyBy(0)
// 泛型 输出类型 状态类型
val resultStream = keyedStream.flatMapWithState[(String, Double, Double), Double]({
// 输入数据但还无状态记录的情况 不会向外输出结果 但保存本次状态
case (inputData: SensorReading8, None) => (List.empty, Some(inputData.temperature))
//输入数据已经有状态记录的情况
case (inputData: SensorReading8, lastTemp: Some[Double]) => {
// 计算上次温度和本次温度的差值
val diff = (inputData.temperature - lastTemp.get).abs
if (diff > 10.0) {
// 输出结果 并更新状态
(List((inputData.id, lastTemp.get, inputData.temperature)), Some(inputData.temperature))
} else {
(List.empty, Some(inputData.temperature))
}
}
})
resultStream.print()
env.execute("XXXX")
}
}
case class SensorReading8(id: String, timestamp: Long, temperature: Double)
2. 状态后端
每传入一条数据,有状态的算子任务都会读取和更新状态
由于有效的状态访问对于处理数据的低延迟至关重要,因此每个并行任务都会在本地维护其状态,以确保快速的状态访问
状态的存储、访问以及维护,由一个可插入的组件决定,这个组件就叫做状态后端(state backend)
状态后端主要负责两件事:本地的状态管理,以及将检查点(checkpoint)状态写入远程存储
MemoryStateBackend
内存级的状态后端,会将键控状态作为内存中的对象进行管理,将它们存储在 TaskManager 的 JVM 堆上,而将 checkpoint 存储在 JobManager 的内存中
特点:快速、低延迟,但不稳定
FsStateBackend
将 checkpoint 存到远程的持久化文件系统(FileSystem)上,而对于本地状态,跟 MemoryStateBackend 一样,也会存在 TaskManager 的 JVM 堆上
同时拥有内存级的本地访问速度,和更好的容错保证
RocksDBStateBackend
将所有状态序列化后,存入本地的 RocksDB 中存储。
注意:RocksDB的支持并不直接包含在flink中,需要引入依赖:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-statebackend-rocksdb_2.11</artifactId>
<version>1.10.0</version>
</dependency>