文章目录
自定义生成waterMark
定义一个周期性的waterMark
//TODO 自定义一个周期型生成waterMark的Assigner
class MyWMAssigner1(lateness : Long) extends AssignerWithPeriodicWatermarks[SensorReading]{
// TODO 需要两个关键参数,延迟时间,和当前数据中心 最大的时间戳
//val lateness : Long = 1000L
var maxTs: Long = Long.MinValue + lateness
override def getCurrentWatermark: Watermark = //此方法返回一个waterMark,类通过调用此方法获得watermar
new Watermark(maxTs - lateness)
override def extractTimestamp(element: SensorReading, previousElementTimestamp: Long): Long = {
maxTs = maxTs.max(element.timestamp * 1000L)
element.timestamp * 1000L
}
}
定义一个断点式的非周期的waterMark
//TODO 定义一个断点式生成waterMark的Assigner
class MyWMAssinger3 extends AssignerWithPunctuatedWatermarks[SensorReading]{
var lateness : Long = 1000L
override def checkAndGetNextWatermark(lastElement: SensorReading, extractedTimestamp: Long): Watermark = {
if (lastElement.id == "sensor_1"){
new Watermark(extractedTimestamp - lateness)
}else
null
}
override def extractTimestamp(element: SensorReading, previousElementTimestamp: Long): Long = {
element.timestamp*1000L
}
}
五、ProcessFunction API (底层API)
普通的Transform算子,只能获取当前的数据,或者加上聚合状态
如果是RichFunction,可以有声明周期方法,还可以获取运行时上下文,进行状态编程
但是它们都不能获取时间戳和waterMark相关的信息。
Process Function是唯一可以获取到时间相关信息的API
可以获取到timestamp和watermark
可以注册定时器,制定某个时间点发生的操作。
还可以输出测输出流。
KeyedProcessFunction
KeyedProcessFunction用来操作KeyedStream。KeyedProcessFunction会处理流的每一个元素,输出为0个、1个或者多个元素。所有的Process Function都继承自RichFunction接口,所以都有open()、close()和getRuntimeContext()等方法。而KeyedProcessFunction[KEY, IN, OUT]还额外提供了两个方法:
- processElement(v: IN, ctx: Context, out: Collector[OUT]), 流中的每一个元素都会调用这个方法,调用结果将会放在Collector数据类型中输出。Context可以访问元素的时间戳,元素的key,以及TimerService时间服务。Context还可以将结果输出到别的流(side outputs)。
- onTimer(timestamp: Long, ctx: OnTimerContext, out: Collector[OUT])是一个回调函数。当之前注册的定时器触发时调用。参数timestamp为定时器所设定的触发的时间戳。Collector为输出结果的集合。OnTimerContext和processElement的Context参数一样,提供了上下文的一些信息,例如定时器触发的时间信息(事件时间或者处理时间)。
TimerService 和 定时器(Timers)
Context和OnTimerContext所持有的TimerService对象拥有以下方法:
- currentProcessingTime(): Long 返回当前处理时间
- currentWatermark(): Long 返回当前watermark的时间戳
- registerProcessingTimeTimer(timestamp: Long): Unit 会注册当前key的processing time的定时器。当processing time到达定时时间时,触发timer。
- registerEventTimeTimer(timestamp: Long): Unit 会注册当前key的event time 定时器。当水位线大于等于定时器注册的时间时,触发定时器执行回调函数。
- deleteProcessingTimeTimer(timestamp: Long): Unit 删除之前注册处理时间定时器。如果没有这个时间戳的定时器,则不执行。
- deleteEventTimeTimer(timestamp: Long): Unit 删除之前注册的事件时间定时器,如果没有此时间戳的定时器,则不执行。
- 当定时器timer触发时,会执行回调函数onTimer()。注意定时器timer只能在keyed streams上面使用。
// 自定义 KeyedProcessFunction
class TempIncreWarning(interval: Long) extends KeyedProcessFunction[Tuple, SensorReading, String]{
// 由于需要跟之前的温度值做对比,所以将上一个温度保存成状态
lazy val lastTempState: ValueState[Double] = getRuntimeContext.getState( new ValueStateDescriptor[Double]("lastTemp", classOf[Double]))
// 为了方便删除定时器,还需要保存定时器的时间戳
lazy val curTimerTsState: ValueState[Long] = getRuntimeContext.getState( new ValueStateDescriptor[Long]("cur-timer-ts", classOf[Long]) )
override def processElement(value: SensorReading, ctx: KeyedProcessFunction[Tuple, SensorReading, String]#Context, out: Collector[String]): Unit = {
// 首先取出状态
val lastTemp = lastTempState.value()
val curTimerTs = curTimerTsState.value()
// 将上次温度值的状态更新为当前数据的温度值
lastTempState.update(value.temperature)
// 判断当前温度值,如果比之前温度高,并且没有定时器的话,注册10秒后的定时器
if( value.temperature > lastTemp && curTimerTs == 0 ){
val ts = ctx.timerService().currentProcessingTime() + interval
ctx.timerService().registerProcessingTimeTimer(ts)
curTimerTsState.update(ts)
}
// 如果温度下降,删除定时器
else if( value.temperature < lastTemp ){
ctx.timerService().deleteProcessingTimeTimer(curTimerTs)
// 清空状态
curTimerTsState.clear()
}
}
// 定时器触发,说明10秒内没有来下降的温度值,报警
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Tuple, SensorReading, String]#OnTimerContext, out: Collector[String]): Unit = {
val key = ctx.getCurrentKey.asInstanceOf[Tuple1[String]].f0
out.collect( "温度值连续" + interval/1000 + "秒上升" )
curTimerTsState.clear()
}
}
侧输出流
大部分的DataStream API的算子的输出是单一输出,也就是某种数据类型的流。
除了split算子,可以将一条流分成多条流,这些流的数据类型也都相同。process function的side outputs功能可以产生多条流,并且这些流的数据类型可以不一样。
一个side output可以定义为OutputTag[X]对象,X是输出流的数据类型。process function可以通过Context对象发射一个事件到一个或者多个side outputs。
package day5
import com.atguigu.apitest.SensorReading
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.streaming.api.functions.ProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector
/**
* Copyright (c) 2018-2028 尚硅谷 All Rights Reserved
*
* Project: FlinkTutorial
* Package: day5
* Version: 1.0
*
* Created by wushengran on 2020/4/21 10:17
*/
object SideOutputTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val inputStream = env.socketTextStream("hadoop102", 7777)
val dataStream: DataStream[SensorReading] = inputStream
.map( data => {
val dataArray = data.split(",")
SensorReading( dataArray(0), dataArray(1).toLong, dataArray(2).toDouble )
} )
// 用 ProcessFunction的侧输出流实现分流操作
val highTempStream: DataStream[SensorReading] = dataStream
.process( new SplitTempProcessor(30.0) )
val lowTempStream = highTempStream.getSideOutput( new OutputTag[(String, Double, Long)]("low-temp") )
// 打印输出
highTempStream.print("high")
lowTempStream.print("low")
env.execute("side output job")
}
}
// 自定义 ProcessFunction,用于区分高低温度的数据
class SplitTempProcessor(threshold: Double) extends ProcessFunction[SensorReading, SensorReading]{
override def processElement(value: SensorReading, ctx: ProcessFunction[SensorReading, SensorReading]#Context, out: Collector[SensorReading]): Unit = {
// 判断当前数据的温度值,如果大于阈值,输出到主流;如果小于阈值,输出到侧输出流
if( value.temperature > threshold ){
out.collect(value)
} else {
ctx.output( new OutputTag[(String, Double, Long)]("low-temp"), (value.id, value.temperature, value.timestamp) )
}
}
}
六、状态管理
Flink中的状态
状态是针对每个算子而言,在每个并行任务中用于计算结果的数据
可以看做是一个本地变量,一般放在本地内存:flink会统一进行数据类型的管理,方便进行读写传输以及容错保证。
•由一个任务维护,并且用来计算某个结果的所有数据,都属于这个任务的状态
•可以认为状态就是一个本地变量,可以被任务的业务逻辑访问
•Flink 会进行状态管理,包括状态一致性、故障处理以及高效存储和访问,以便开发人员可以专注于应用程序的逻辑
•在 Flink 中,状态始终与特定算子相关联
•为了使运行时的 Flink 了解算子的状态,算子需要预先注册其状态
总的说来,有两种类型的状态:
•算子状态(Operator State)
•算子状态的作用范围限定为算子任务
•键控状态(Keyed State)
根据输入数据流中定义的键(key)来维护和访问
算子状态(Operator State)
•算子状态的作用范围限定为算子任务,由同一并行任务所处理的所有数据都可以访问到相同的状态
•状态对于同一子任务而言是共享的
•算子状态不能由相同或不同算子的另一个子任务访问
算子状态数据结构
列表状态(List state)
•将状态表示为一组数据的列表
联合列表状态(Union list state)
•也将状态表示为数据的列表。它与常规列表状态的区别在于,在发生故障时,或者从保存点(savepoint)启动应用程序时如何恢复
广播状态(Broadcast state)
•如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应用广播状态。
键控状态(Keyed State)
- 键控状态是根据**输入数据流中定义的键(key)**来维护和访问的
- Flink 为每个 key 维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护和处理这个 key 对应的状态
- 当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的 key
键控状态数据结构
值状态(Value state)
•将状态表示为单个的值
列表状态(List state)
•将状态表示为一组数据的列表
映射状态(Map state)
•将状态表示为一组 Key-Value 对
聚合状态(Reducing state & Aggregating State)
•将状态表示为一个用于聚合操作的列表
状态后端(State Backends)
- 每传入一条数据,有状态的算子任务都会读取和更新状态
- 由于有效的状态访问对于处理数据的低延迟至关重要,因此每个并行任务都会在本地维护其状态,以确保快速的状态访问
- 状态的存储、访问以及维护,由一个可插入的组件决定,这个组件就叫做状态后端(state backend)
负责:
1.本地状态管理
2.将检查点转台写入远程存储。
MemoryStateBackend
- 内存级的状态后端,会将键控状态作为内存中的对象进行管理,将它们存储在 TaskManager 的 JVM 堆上,而将 checkpoint 存储在 JobManager 的内存中
- 特点:快速、低延迟,但不稳定
FsStateBackend
- 将 checkpoint 存到远程的持久化文件系统(FileSystem)上,而对于本地状态,跟 MemoryStateBackend 一样,也会存在 TaskManager 的 JVM 堆上
- 同时拥有内存级的本地访问速度,和更好的容错保证
RocksDBStateBackend
- 将所有状态序列化后,存入本地的 RocksDB 中存储。
// 配置状态后端
env.setStateBackend( new MemoryStateBackend() )
env.setStateBackend( new FsStateBackend(".") )
env.setStateBackend( new RocksDBStateBackend("", true) )
val warningStream: DataStream[(String, Double, Double)] = dataStream
// .map( new MyMapper() )
.keyBy("id")
// .flatMap( new TempChangeWarningWithFlatmap(10.0) )
.flatMapWithState[(String, Double, Double), Double]({
case (inputData: SensorReading, None) => (List.empty, Some(inputData.temperature))
case (inputData: SensorReading, 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))
}
}
})
// 自定义 RichFlatMapFunction,可以输出多个结果
class TempChangeWarningWithFlatmap(threshold: Double) extends RichFlatMapFunction[SensorReading, (String, Double, Double)]{
lazy val lastTempState: ValueState[Double] = getRuntimeContext.getState(new ValueStateDescriptor[Double]("last-temp", classOf[Double]))
override def flatMap(value: SensorReading, out: Collector[(String, Double, Double)]): Unit = {
// 从状态中取出上次的温度值
val lastTemp = lastTempState.value()
// 更新状态
lastTempState.update(value.temperature)
// 跟当前温度值计算差值,然后跟阈值比较,如果大于就报警
val diff = (value.temperature - lastTemp).abs
if( diff > threshold ){
out.collect( (value.id, lastTemp, value.temperature) )
}
}
}
懒加载是必要的(lazy),否则会出现以下情况。
// 自定义 RichMapFunction
class TempChangeWarning(threshold: Double) extends RichMapFunction[SensorReading, (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: SensorReading): (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
( value.id, 0.0, 0.0 )
}
}
// operator state示例
class MyMapper() extends RichMapFunction[SensorReading, Long] with ListCheckpointed[java.lang.Long]{
// lazy val countState: ValueState[Long] = getRuntimeContext.getState( new ValueStateDescriptor[Long]("count", classOf[Long]) )
var count: Long = 0L
override def map(value: SensorReading): Long = {
count += 1
count
}
override def restoreState(state: util.List[java.lang.Long]): Unit = {
val iter = state.iterator()
while(iter.hasNext){
count += iter.next()
}
// for( countState <- state ){
// count += countState
// }
}
override def snapshotState(checkpointId: Long, timestamp: Long): util.List[java.lang.Long] = {
val stateList = new util.ArrayList[java.lang.Long]()
stateList.add(count)
stateList
}
}
状态总结
map/filter/flatmap本来是无状态的,但是可以通过实现RichFunction,在其中自定义状态进行操作。
reduce/aggregate/window本来就有状态,是flink底层直接管理的,我们也可以实现RichFunction自定义状态
Process Function是一类特殊的函数类,是.process()方法的参数,他也实现了RichFunction接口,是一个特殊的富函数
DataStream/KeyedStream/ConnectedStream/WindowedStream等等都可以调用.process()方法,传入的是不同的Process Function。
七、容错机制
一致性检查点(checkpoint)
-
Flink 故障恢复机制的核心,就是应用状态的一致性检查点
-
有状态流应用的一致检查点,其实就是所有任务的状态,在某个时间点的一份拷贝(一份快照);这个时间点,应该是所有任务都恰好处理完一个相同的输入数据的时候
-
保存的不是数据,而是状态
从检查点恢复状态
- 在执行流应用程序期间,Flink 会定期保存状态的一致检查点
- 如果发生故障, Flink 将会使用最近的检查点来一致恢复应用程序的状态,并重新启动处理流程
第一步:重启应用
第二步:从checkpoint中读取状态,将状态重置
从检查点重新启动应用 程序后,其内部状态与检查点完成时的状态完全相同。
第三步:开始消费并处理检查点到发生故障之间的所有数据
这种检查点的保存和恢复机制可以为应用程序状态提供“精确一次”(exactly-once)的一致性,因为所有算子都会保存检查点并恢复其所有状态,这样一来所有的输入流就都会被重置到检查点完成时的位置