Distributed Cache为我们提供了一种扩展数据的方案,但有些时个并不能满足需求,如我们有一个MySql表中存储了部分一些字典数据,并且它可能随时更新,这时我们需要动态感知其变化(近实时)来对数据进行计算。

这时可以使用一个通用的做法:将小"表"广播出去。以下是我们运行类all.in.one.c06.Chapter06时,WebUI给出的图:

flink的广播流介绍 flink 广播_大数据

其主要的实现逻辑在

someDataStream
    .connect(toBroadcastStream.broadcast(descriptor))
    .process(new C06BroadcastProcessFunction(descriptor))
    .print()

将toBroadcastStream广播出去,使用connect将两个流连接,会得到一个BroadcastConnectedStream,然后分别处理当数据流和广播流数据到达时的逻辑即可。

在处理广播数据时,使用了方法ctx.getBroadcastState,它返回的是BroadcastState。它专为广播设计的,实现了Flink中的State接口。

State

State是Flink在流计算过程中,计算节点的中间计算结果或元数据属性,如聚合过程中计算中间聚合结果、Source为Kafka时记录offset等。官方说明即:State就是与时间相关的,Apache Flink任务的内部数据(计算数据和元数据属性)的快照。

State主要有两大作用:

  1. 增量计算:流计算中,计算场景大多数是增量计算,我们要基于上一次计算的结果之上进行处理,State提供了计算数据的存储能力(持久化)
  2. Failover:在出现机器、网络、脏数据等等原因出现程序错误任务重启时,为了保证我们可以重新恢复现场,需要State提供的数据持久化能力支持(Checkpoint)

简单介绍下Flink提供的四种常用state存储实现:

  1. 基于内存的HeapStateBackend: 在debug模式使用,不建议在生产模式下应用
  2. 基于HDFS的FsStateBackend: 分布式文件持久化,每次读写都产生网络IO,整体性能不佳
  3. 基于RocksDB的RocksDBStateBackend: 本地文件+异步HDFS持久化,生产环境常用
  4. 基于Niagara(Alibaba内部实现)NiagaraStateBackend: 分布式持久化,在Alibaba生产环境应用

除了上面提到的BroadcastState,State通常按以下方式划分:

  1. KeyedState:一般与KeyingBy产生的Keyed Stream配合使用,即每个key可以有它自己的值
  • ValueState<T>: 存储单值
  • ListState<T>: 存储列表
  • ReducingState<T>: 保存单值,但可以定义一个ReduceFunction来表示和原来已经存在的值进行何种方式的聚合
  • AggregatingState<IN, OUT>: 保存单值,与ReducingState唯一不同在于聚合后的值可能类型不同
  • MapState<UK, UV>: 保存映射列表
  • OperatorState: 定义Source时,保存source读取的位置(如kafka的offset)
package all.in.one.c06


import all.in.one.utils.random
import org.apache.flink.api.common.state.MapStateDescriptor
import org.apache.flink.streaming.api.functions.co.BroadcastProcessFunction
import org.apache.flink.streaming.api.functions.source.SourceFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector
import org.joda.time.DateTime


object Chapter06 extends App {


  val env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI()


  // 定义要广播的stream
  val toBroadcastStream = env.addSource(new SourceFunction[(String, String)] {
    var running = true


    override def run(ctx: SourceFunction.SourceContext[(String, String)]): Unit =
      do {
        val now = DateTime.now()
        val state = if (now.getSecondOfMinute % 2 == 0) "even" else "odd"
        val timeStr = now.toString("HH:mm:ss.SSS")
        ctx.collect(state -> timeStr)
        Thread.sleep(random.int(5000, 15000).toLong)
      } while (running)


    override def cancel(): Unit = running = false
  })
  // 定义一个正常stream
  val someDataStream = env.addSource(new SourceFunction[Int] {
    var running = true
    override def run(ctx: SourceFunction.SourceContext[Int]): Unit =
      do {
        ctx.collect(1)
        Thread.sleep(1000L)
      } while (running)
    override def cancel(): Unit = running = false
  })


  // 定义Descriptor
  val descriptor: MapStateDescriptor[String, String] =
    new MapStateDescriptor("C06 - broadcast", classOf[String], classOf[String])
  // 将数据流与广播流连接
  someDataStream
    .connect(toBroadcastStream.broadcast(descriptor))
    .process(new C06BroadcastProcessFunction(descriptor))
    .print()
  env.execute("Chapter 06")


  /**
    * 定义处理流的process function
    */
  class C06BroadcastProcessFunction(mapStateDescriptor: MapStateDescriptor[String, String])
      extends BroadcastProcessFunction[Int, (String, String), String] {


    /**
      * 当数据流数据到达的时候执行此方法处理
      */
    override def processElement(
        value: Int,
        ctx: BroadcastProcessFunction[Int, (String, String), String]#ReadOnlyContext,
        out: Collector[String]
    ): Unit = {
      val even = ctx.getBroadcastState(mapStateDescriptor).get("even")
      val odd = ctx.getBroadcastState(mapStateDescriptor).get("odd")
      val evenStr = if (even != null) s"even -> [$even]" else ""
      val oddStr = if (odd != null) s"odd -> [$odd]" else ""
      val toPrint =
        if (evenStr.isEmpty && oddStr.isEmpty) "无"
        else if (evenStr.isEmpty && oddStr.nonEmpty) oddStr
        else if (evenStr.nonEmpty && oddStr.isEmpty) evenStr
        else s"$evenStr, $oddStr"
      out.collect(toPrint)
    }


    /**
      * 当广播流数据到达的时候执行此方法处理
      */
    override def processBroadcastElement(
        value: (String, String),
        ctx: BroadcastProcessFunction[Int, (String, String), String]#Context,
        out: Collector[String]
    ): Unit = {
      // 将数据更新到state中
      ctx.getBroadcastState(mapStateDescriptor).put(value._1, value._2)
    }
  }


}

flink的广播流介绍 flink 广播_分布式_02

flink的广播流介绍 flink 广播_python_03

flink的广播流介绍 flink 广播_分布式_04