Flink State and Checkpoint、Scala中获取类对象、通过checkpoint来恢复之前的状态、process

Flink State and Checkpoint

Flink 通过 State 和 Checkpoint 来实现容错和数据处理的唯一一次

State

丰富的State API。

  • ValueState<T>: 保存一个可以更新和检索的值(如上所述,每个值都对应到当前的输入数据的 key,因此算子接收到的每个 key 都可能对应一个值)。 这个值可以通过 update(T) 进行更新,通过 T value() 进行检索。
  • ListState<T>: 保存一个元素的列表。可以往这个列表中追加数据,并在当前的列表上进行检索。可以通过 add(T) 或者 addAll(List<T>) 进行添加元素,通过 Iterable<T> get() 获得整个列表。还可以通过 update(List<T>) 覆盖当前的列表。
  • ReducingState<T>: 保存一个单值,表示添加到状态的所有值的聚合。接口与 ListState 类似,但使用 add(T) 增加元素,会使用提供的 ReduceFunction 进行聚合。
  • AggregatingState<IN, OUT>: 保留一个单值,表示添加到状态的所有值的聚合。和 ReducingState 相反的是, 聚合类型可能与 添加到状态的元素的类型不同。 接口与 ListState 类似,但使用 add(IN) 添加的元素会用指定的 AggregateFunction 进行聚合。
  • MapState<UK, UV>: 维护了一个映射列表。 你可以添加键值对到状态中,也可以获得反映当前所有映射的迭代器。使用 put(UK,UV) 或者 putAll(Map<UK,UV>) 添加映射。 使用 get(UK) 检索特定 key。 使用 entries()keys()values() 分别检索映射、键和值的可迭代视图。你还可以通过 isEmpty() 来判断是否包含任何键值对。

所有类型的状态还有一个clear() 方法,清除当前 key 下的状态数据,也就是当前输入元素的 key。

在之前我们的示例代码中统计单词的数量是通过 keyBy 之后做 sum 求和得出结果,而 Flink 的数据是一条一条处理的,那么能不能不用sum来求单词的数量呢??

package com.shujia.flink.state

import org.apache.flink.api.common.functions.MapFunction
import org.apache.flink.streaming.api.scala._

import scala.collection.mutable

object Demo1NoStateWC {
  def main(args: Array[String]): Unit = {
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment

    val linesDS: DataStream[String] = env.socketTextStream("master", 8888)

    val kvDS: DataStream[(String, Int)] = linesDS.flatMap(_.split(",")).map((_, 1))

    val keyByDS: KeyedStream[(String, Int), String] = kvDS.keyBy(_._1)

    val countDS: DataStream[(String, Int)] = keyByDS.map(new MapFunction[(String, Int), (String, Int)] {
      //统计单词的数量
      var count = 0
      
      override def map(value: (String, Int)): (String, Int) = {
      
        count += 1
        
        (value._1, count)
        
      }
    })

    countDS.print()

    env.execute()

  }
}

但是这样会有问题

这是为什么呢?

因为每一个并行度中的 task 都会定义一个 var count = 0 来统计流过这个task的单词数量

java 和 jva 被分给一个task进行处理

所以同一个task中流过的数据,都会对该task中定义的 count 进行修改

那怎么办呢??

package com.shujia.flink.state

import org.apache.flink.api.common.functions.MapFunction
import org.apache.flink.streaming.api.scala._

import scala.collection.mutable

object Demo1NoStateWC {
  def main(args: Array[String]): Unit = {
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment

    val linesDS: DataStream[String] = env.socketTextStream("master", 8888)

    val kvDS: DataStream[(String, Int)] = linesDS.flatMap(_.split(",")).map((_, 1))

    val keyByDS: KeyedStream[(String, Int), String] = kvDS.keyBy(_._1)

    val countDS: DataStream[(String, Int)] = keyByDS.map(new MapFunction[(String, Int), (String, Int)] {

      /** 
        * 下面的方式会产生一个问题
        *
        * map集合的数据保存在内存中
        * 任务失败重启之后 map 集合中的数据会丢失
        * 可以使用MySQL或者Redis来存数据,但是这样会降低Flink的性能(增加了网络开销)
        *
        * flink的状态(之前的计算结果)
        * 状态和普通的集合变量的区别
        *
        * flink的状态会被checkpoint持久化到hdfs中,如果任务失败了
        * 重新启动,可以从hdfs中恢复任务,保证之前的计算结果不丢失
        *
        */
        
      //key 是单词,value 是单词的数量
      //使用 HashMap 保存之前的计算结果
      val map = new mutable.HashMap[String, Int]()

      override def map(value: (String, Int)): (String, Int) = {

        val word: String = value._1

        //从map集合中获取单词的数量,如果有就返回,如果没有就返回0
        val count: Int = map.getOrElse(word, 0)

        //统计新的单词的数量
        val newCount: Int = count + 1

        //覆盖map集合
        map.put(word, newCount)

        (word, newCount)
      }
    })

    countDS.print()

    env.execute()

  }
}
    * flink的状态(之前的计算结果)
    * 状态和普通的集合变量的区别
    *
    * flink的状态会被checkpoint持久化到hdfs中,如果任务失败了
    * 重新启动,可以从hdfs中恢复任务,保证之前的计算结果不丢失

ValueState

单值状态

classOf[] -- Scala中获取类对象

package com.shujia.flink.state

import org.apache.flink.api.common.functions.{RichMapFunction, RuntimeContext}
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.scala._

object Demo2ValueState {
  def main(args: Array[String]): Unit = {
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment

    val linesDS: DataStream[String] = env.socketTextStream("master", 8888)

    val kvDS: DataStream[(String, Int)] = linesDS.flatMap(_.split(",")).map((_, 1))

    val keyByDS: KeyedStream[(String, Int), String] = kvDS.keyBy(_._1)

    val countDS: DataStream[(String, Int)] = keyByDS.map(new RichMapFunction[(String, Int), (String, Int)] {

      /**
        * ValueState:单值状态,为每一个key保存一个值
        * 保证在状态中的数据不会因为flink程序挂断而丢失
        * checkpoint不能在本地执行,需要将代码提交到集群中,状态的数据需要保存在hdfs中
        *
        */
        
      var valueState: ValueState[Int] = _

      /**
        * open:在map之前执行,一般用于建立网络链接
        * 还用于初始化状态
        *
        */
        
      override def open(parameters: Configuration): Unit = {

        //1、获取flink的环境对象,通过环境初始化状态
        val context: RuntimeContext = getRuntimeContext

        //2、创建状态的描述对象,需要指定 状态名 和 状态保存的数据类型的类对象
        //classOf[] -- Scala中获取类对象
        val valueStateDesc = new ValueStateDescriptor[Int]("count", classOf[Int])

        //3、获取状态
        valueState = context.getState(valueStateDesc)
      }

      override def map(value: (String, Int)): (String, Int) = {

        /**
          * 使用状态统计单词的数量
          *
          */

        //获取之前的单词数量
        val count: Int = valueState.value()

        //计算新的数量
        val newCount: Int = count + 1

        //更新状态
        valueState.update(newCount)

        //返回数据
        (value._1, newCount)
      }
    })

    countDS.print()

    env.execute()

  }
}

为了观察 ValueState 的效果,需要开启 Checkpoint 然后在集群中运行

Checkpoint

开启 Checkpoint 比较麻烦,东西有点多,可以去官网找一下开启时需要指定的参数

package com.shujia.flink.state

import org.apache.flink.api.common.functions.{RichMapFunction, RuntimeContext}
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.configuration.Configuration
import org.apache.flink.runtime.state.filesystem.FsStateBackend
import org.apache.flink.streaming.api.CheckpointingMode
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.environment.CheckpointConfig.ExternalizedCheckpointCleanup

object Demo3Checkpoint {
  def main(args: Array[String]): Unit = {
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment

    /**
      * checkpoint : 定时将flink执行过程中的状态保存到状态后端
      * 只保存最新一次成功的checkpoint
      *
      *
      * 任务执行失败之后可以从最新的checkpoint恢复任务
      * flink run -c com.shujia.flink.state.Demo3Checkpoint -s hdfs://master:9000/flink/checkpoint/a306d8690ca7718aec6a425b0d8ee171/chk-645 flink-1.0.jar
      *
      * -s : 指定恢复任务的位置,需要指定到chk-***
      *
      */

    // 每 1000ms 开始一次 checkpoint
    env.enableCheckpointing(1000)

    // 高级选项:
    // 设置模式为精确一次 (这是默认值)
    env.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)

    // 确认 checkpoints 之间的时间会进行 500 ms
    env.getCheckpointConfig.setMinPauseBetweenCheckpoints(500)

    // Checkpoint 必须在一分钟内完成,否则就会被抛弃
    env.getCheckpointConfig.setCheckpointTimeout(60000)

    // 同一时间只允许一个 checkpoint 进行
    env.getCheckpointConfig.setMaxConcurrentCheckpoints(1)

    // 当作业取消时,保留作业的 checkpoint。注意,这种情况下,需要手动清除该作业保留的 checkpoint。默认是不保留
    //ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION:在取消作业时保留checkpoint。请注意,在这种情况下,您必须在取消后手动清理检查点状态。
    //ExternalizedCheckpointCleanup.DELETE_ON_CANCELLATION:取消作业时删除checkpoint。仅当作业失败时,检查点状态才可用。
    env.getCheckpointConfig.enableExternalizedCheckpoints(ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION)

    //将状态保存到hdfs的状态后端
    //在这里指定状态保存到HDFS中的路径
    val stateBackend = new FsStateBackend("hdfs://master:9000/flink/checkpoint")

    //设置状态后端
    env.setStateBackend(stateBackend)


    val linesDS: DataStream[String] = env.socketTextStream("master", 8888)

    val kvDS: DataStream[(String, Int)] = linesDS.flatMap(_.split(",")).map((_, 1))

    val keyByDS: KeyedStream[(String, Int), String] = kvDS.keyBy(_._1)

    val countDS: DataStream[(String, Int)] = keyByDS.map(new RichMapFunction[(String, Int), (String, Int)] {

      /**
        * ValueState: 单值状态,为每一个key保存一个值
        * 保证在状态中的数据不会因为flink程序挂断而丢失
        * checkpoint不能再本地执行,需要将代码提交到服务器中,状态的数据需要保存再hdfs中
        *
        */
        
      var valueState: ValueState[Int] = _

      /**
        * open:方法在map之前执行,一般用于建立网络链接
        * 还用于初始化状态
        *
        */
        
      override def open(parameters: Configuration): Unit = {

        //1、获取flink的环境对象,通过环境初始化状态
        val context: RuntimeContext = getRuntimeContext

        //创建状态的描述对象,需要指定状态保存的数据类型和状体名
        val valueStateDesc = new ValueStateDescriptor[Int]("count", classOf[Int])

        //获取状态
        valueState = context.getState(valueStateDesc)
      }

      override def map(value: (String, Int)): (String, Int) = {

        /**
          * 使用状态统计单词的数量
          *
          */

        //获取之前的单词数量
        val count: Int = valueState.value()

        //计算新的数量
        val newCount: Int = count + 1

        //更新状态
        valueState.update(newCount)

        //返回数据
        (value._1, newCount)
      }
    })

    countDS.print()

    env.execute()

  }
}

通过checkpoint来恢复之前的状态

将代码打成jar包,提交到集群中运行

在job执行的过程中,将job取消,如何通过checkpoint来恢复之前的状态??

2、通过flink命令

flink run -c 主类名 -s 指定恢复任务的位置(hdfs上的路径需要加上前缀 hdfs://master:9000 ,需要指定到chk-***) jar包名

State

ListState

集合状态

package com.shujia.flink.state

import java.lang

import org.apache.flink.api.common.functions.RuntimeContext
import org.apache.flink.api.common.state.{ListState, ListStateDescriptor}
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 Demo4ListState {
  def main(args: Array[String]): Unit = {

    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment

    val studentDS: DataStream[String] = env.socketTextStream("master", 8888)

    /**
      * 实时统计每个班级的平均年龄
      *
      */
      
    val kvDS: DataStream[(String, Int)] = studentDS.map(stu => {
      val split: Array[String] = stu.split(",")
      val clazz: String = split(4)
      val age: Int = split(2).toInt
      (clazz, age)
    })
    
    //按照班级分组
    val keyByDS: KeyedStream[(String, Int), String] = kvDS.keyBy(_._1)

    /**
      * process : flink底层的api,一条一条的将数据传到后面的方法中
      * 可以在process中操作flink的时间、状态和事件
      *
      * KeyedProcessFunction[K,I,O]
      * K -- key的类型
      * I -- 输入数据类型
      * O -- 输出数据类型
      */

    val avgAgeDS: DataStream[(String, Double)] = keyByDS.process(new KeyedProcessFunction[String, (String, Int), (String, Double)] {

      /**
        * open : 在processElementz之前执行
        * 有open方法,当然也有close方法
        */

      /**
        * 集合状态 : 为每一个key保存一个集合
        *
        */
        
      var listState: ListState[Int] = _

      override def open(parameters: Configuration): Unit = {
        //获取flink环境
        val context: RuntimeContext = getRuntimeContext

        //创建集合状态的描述对象
        val listStateDesc = new ListStateDescriptor[Int]("ages", classOf[Int])
        
        //通过 context 也能获取其他的状态
        listState = context.getListState(listStateDesc)
      }

      /**
        * processElement : 每一条数据执行一次
        *
        * @param kv  :一条数据
        * @param ctx : 上下文对象
        * @param out : 用于将数据发送到下游
        */
        
      override def processElement(kv: (String, Int),
                                  ctx: KeyedProcessFunction[String, (String, Int), (String, Double)]#Context,
                                  out: Collector[(String, Double)]): Unit = {
        //接收元组中元素
        val (clazz, age) = kv
        
        //将每一条数据的年龄保存到集合状态中
        listState.add(age)

        //从状态中获取所有的年龄计算平均值
        val iterable: lang.Iterable[Int] = listState.get()

        //导入一个隐式转换将java的迭代器转换成scala的集合
        import scala.collection.JavaConversions._
        val ages: List[Int] = iterable.iterator().toList

        val avgAge: Double = ages.sum.toDouble / ages.length

        //将数据发送到下游
        out.collect(clazz, avgAge)
      }
    })

    avgAgeDS.print()

    env.execute()

  }
}

ReducingState

聚合状态

package com.shujia.flink.state

import org.apache.flink.api.common.functions.{ReduceFunction, RuntimeContext}
import org.apache.flink.api.common.state.{ReducingState, ReducingStateDescriptor}
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 Demo5ReducingState {
  def main(args: Array[String]): Unit = {
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment

    val linesDS: DataStream[String] = env.socketTextStream("master", 8888)

    val kvDS: DataStream[(String, Int)] = linesDS.flatMap(_.split(",")).map((_, 1))

    val keyByDS: KeyedStream[(String, Int), String] = kvDS.keyBy(_._1)

    // KeyedProcessFunction[K,I,O]
    // K -- key的类型
    // I -- 输入数据的类型
    // O -- 输出数据的类型
    val countDS: DataStream[(String, Int)] = keyByDS.process(new KeyedProcessFunction[String, (String, Int), (String, Int)] {

      var reducingState: ReducingState[Int] = _

      override def open(parameters: Configuration): Unit = {
        val context: RuntimeContext = getRuntimeContext

        val reduceStateDesc = new ReducingStateDescriptor[Int](
          "count", // 名称
          new ReduceFunction[Int] { // 聚合的方法
            override def reduce(x: Int, y: Int): Int = x + y
          },
          classOf[Int] // 数据类型
        )

        //获取聚合状态
        reducingState = context.getReducingState(reduceStateDesc)
      }

      override def processElement(
                                   value: (String, Int),
                                   ctx: KeyedProcessFunction[String, (String, Int), (String, Int)]#Context,
                                   out: Collector[(String, Int)]): Unit

      = {

        //向聚合状态中增加一个元素,在聚合状态的内部会自动进行聚合计算
        reducingState.add(1)

        //获取聚合的结果
        val count: Int = reducingState.get()

        //将数据发送到下游
        out.collect((value._1, count))
      }
    }
    )

    countDS.print()

    env.execute()

  }
}
posted @ 2022-03-21 23:40  赤兔胭脂小吕布  阅读(273)  评论(0编辑  收藏  举报