Flink学习笔记(四)

自定义生成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)的一致性,因为所有算子都会保存检查点并恢复其所有状态,这样一来所有的输入流就都会被重置到检查点完成时的位置

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RE:0-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值