Flink - 尚硅谷- 大数据高级 Flink 技术精讲 - 2

  • 七、Flink 时间语义与 Watermark
    • 7.1 Flink 中的时间语义
    • 7.2 设置 Event Time
    • 7.3 水位线 - Watermark
      • 7.3.1 基本概念
      • 7.3.2 WaterMark 传递
      • 7.3.3 WaterMark 注意点
      • 7.3.4 Watermark Demo
  • 八、ProcessFunction API(底层 API)
    • 8.1 KeyedProcessFunction 和 定时器(Timers)
    • 8.2 侧输出流(SlideOutput)
    • 8.3 CoProcessFunction
  • 九、状态编程和容错机制
    • 9.1 Flink 中的状态
      • 9.1.1 算子状态(Operator State)
      • 9.1.2 键控状态(Keyed State)
      • 9.1.3 状态后端(State Backends)
      • 9.1.4 Demo
      • 9.1.5 总结
    • 9.2 状态一致性
      • 9.2.1 概念
      • 9.2.2 一致性检查点(checkpoint)
      • 9.2.3 从检查点恢复状态
      • 9.2.4 Flink 检查点算法
      • 9.2.5 保存点(save points)
      • 9.2.6 配置
      • 9.2.7 总结
    • 9.3 状态一致性分类
      • 9.3.1 端到端(end to end)一致性
      • 9.3.2 端到端的 exactly-once 保证
      • 9.3.3 Flink + Kafka 如何实现端到端的 exactly-once
  • 十、Table API & SQL
    • 10.1 定义
    • 10.2 pom
    • 10.3 两种 planner(old & blink) 的区别
  • 十一、Table API & SQL 调用
    • 11.1 基本程序结构
    • 11.2 创建表环境
    • 11.3 在 Catalog 中注册表
      • 11.3.1 表的概念
      • 11.3.2 临时表 - TemporaryTable 和 永久表 - PermanentTable
      • 11.3.3 创建表
      • 11.3.4 连接到外部系统
    • 11.4 表的查询
      • 11.4.1 Table API
      • 11.4.2 SQL
      • 11.4.3 Demo
    • 11.5 Table、View、流 的转换
      • 11.5.1 Scala 隐式转换
      • 11.5.2 DataSet/DataStream to View
      • 11.5.3 DataStream/DataSet to Table
      • 11.5.4 Table to DataStream
      • 11.5.5 Table to DataSet
      • 11.5.6 数据类型与 Schema 的映射
    • 11.6 表的输出
      • 11.6.1 更新模式
      • 11.6.2 输出到文件
      • 11.6.3 输出到 Kafka
      • 11.6.4 输出到 ES
      • 11.6.5 输出到 Mysql
    • 11.7 Explaining
  • 十二、Table API & SQL 流式概念
    • 12.1 动态表
      • 12.1.1 DataStream 上的关系查询
      • 12.1.2 动态表 & 连续查询(Continuous Query)
      • 12.1.3 更新和追加查询
      • 12.1.4 查询限制
      • 12.1.5 表到流的转换
    • 12.2 时间属性
      • 12.2.1 处理时间
      • 12.2.2 事件时间
  • 十三、Table API
  • 十四、Table API 自定义函数

注:次文档参考 【尚硅谷】大数据高级 flink技术精讲(2020年6月) 编写。

1.由于视频中并未涉及到具体搭建流程,Flink 环境搭建部分并未编写。
2.视频教程 Flink 版本为 1.10.0,此文档根据 Flink v1.11.1 进行部分修改。
3.文档中大部分程序在 Windows 端运行会有超时异常,需要打包后在 Linux 端运行。
4.程序运行需要的部分 Jar 包,请很具情况去掉 pom 中的 “scope” 标签的再进行打包,才能在集群上运行。
5.原始文档在 Markdown 中编写,此处目录无法直接跳转。且因字数限制,分多篇发布
此文档仅用作个人学习,请勿用于商业获利。

七、Flink 时间语义与 Watermark

7.1 Flink 中的时间语义

  • Event Time : 事件产生的时间
  • Ingestion Time : 数据到达 Flink 的时间
  • Processing Time : 执行操作算子的本地系统事件,与机器相关

根据哪种时间进行计算要根据不同的计算需求,
比如 星球大战系列电影,前传的上映时间要晚于前三部,对于观影来说更希望按照故事发生先后顺序看。但对于统计票房来说是按照上映时间统计。

7.2 设置 Event Time

    val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    // 从调用时刻开始给 env 创建的每一个 stream 追加时间特征
    environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    // alternatively:
    // environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    // environment.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime)
    // environment.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime)

    val sourceStream: DataStream[String] = environment.socketTextStream("localhost", 7777)

    // Transform
    val sourceDataStream: DataStream[SensorReading] = sourceStream
      .map((data: String) => {
        val dataArray: Array[String] = data.split(",")
        SensorReading(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
      }).assignAscendingTimestamps(_.timestamp * 1000L)

7.3 水位线 - Watermark

7.3.1 基本概念

当 Flink 以 EventTime 模式处理数据流时,他会根据数据里的时间戳来处理基于时间的算子。但由于网络,分布式等原因,会导致乱序数据的产生。

对于乱序数据来说,遇到一个时间戳达到了窗口关闭时间,不应该立刻触发窗口计算,而是等待一段时间,等迟到的数据来了再关闭窗口。

WaterMark 定义

  • WaterMark 是一种衡量 EventTime 进展的机制,可以设定延迟触发窗口
  • WaterMark 是用于处理乱序事件的,而正确的处理乱序事件,通常用 WaterMark 机制结合 window 来实现
  • 数据流中的 WaterMark 用于表示 timestamp 小于 WaterMark 的数据都已经到达了,因此,window 的执行也是由 WaterMark 触发的
  • WaterMark 用来让程序自己平衡延迟和结果正确性

WaterMark 的大小,需要在延迟性 和 计算结果的准确性间衡量。

WaterMark 特点

  • WaterMark 是一条特殊的数据记录
  • WaterMark 必须单调递增,以确保任务的事件时间时钟在向前推进
  • WaterMark 与数据的时间戳相关

7.3.2 WaterMark 传递

多分区之间的 WaterMark 传递中,在每个分区中,会根据当前分区的上游分区个数,创建对应的 PartitionWaterMark。
每个 PartitionWaterMark 中记录了这个上游分区的 WaterMark,并根据上游发送的数据进行更新。
向下游广播的 WaterMark 是这个分区中所有 PartitionWaterMark 最小的那个。

Flink - 尚硅谷- 大数据高级 Flink 技术精讲 - 2_第1张图片

7.3.3 WaterMark 注意点

  • WaterMark 就是事件时间,表示事件的处理程度
  • WaterMark 主要用来处理乱序数据,一般就是直接定义一个延迟时间,延迟触发窗口操作
  • WaterMark 延迟时间的设置,一般要根据数据的乱序情况来定,通常设置成最大乱序程度
  • 关窗操作,必须是时间进展到窗口关闭时间,事件时间语义下就是 WaterMark 达到窗口关闭的时间
  • WaterMark 代表的含义是,之后就不会再来时间戳比 WaterMark 更新的数值小的数据
  • 如果有不同的上游分区,当前任务会对他们创建各自分区的 WaterMark,当前任务的 WaterMark 就是最小的那个

WaterMark 的设定

  • 如果 WaterMark 设置的延迟太久,收到结果的速度可能会很慢,解决办法是在 WaterMark 到达之前输出一个近似结果
  • 如果 WaterMark 到达的太早,则可能收到错误结果,不过 Flink 处理迟到数据的机制可以解决这个问题
    • .allowedLateness(Time.minutes(1)) // 允许窗口在输出结果后保留一段时间,后续到达的这个时间窗内的每条数据都会根据这个时间窗内上次的结果数据重新计算,并再次输出
    • .sideOutputLateData(new OutputTag[SensorReading](“late data”)) // 将迟到数据放到侧输出流

处理乱序数据的三重保证

  • WaterMark 设置延迟时间
  • window 的 allowedLateness 设置窗口允许处理迟到数据的时间
  • window 的 sideOutputLateData 可以将迟到的数据写入侧输出流

窗口有两个重要操作:触发计算,清空状态(关闭窗口)

7.3.4 Watermark Demo

自定义一个周期性生成 WaterMark 的 Assigner

      // .assignTimestampsAndWatermarks(new MyPeriodicWaterMarkAssigner(5000L))
      .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(5)) {
        override def extractTimestamp(element: SensorReading): Long = element.timestamp * 1000L
      })

      
class MyPeriodicWaterMarkAssigner(lateness: Long) extends AssignerWithPeriodicWatermarks[SensorReading] {
  // 需要两个关键参数,延迟时间 和 当前所有数据中的最大时间戳
  //  val lateness: Long = 5000L
  var currentMaxTimestampMillis: Long = Long.MinValue + lateness
  var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")

  // 默认每隔 200ms 调用 getCurrentWatermark 生成 Watermark
  // 修改生成 Watermark 的时间间隔 environment.getConfig.setAutoWatermarkInterval(100)
  override def getCurrentWatermark: Watermark = new Watermark(currentMaxTimestampMillis - lateness)

  // 每条数据调用 extractTimestamp 生成 EventTime
  override def extractTimestamp(element: SensorReading, recordTimestamp: Long): Long = {
    currentMaxTimestampMillis = currentMaxTimestampMillis.max(element.timestamp * 1000L)
    printInfo(element: SensorReading)
    // 获取 event time
    element.timestamp * 1000L
  }

  def printInfo(element: SensorReading): Unit = {
    println("Key : [" + element.id +
      "], EventTime : [" + element.timestamp * 1000L + "|" + sdf.format(element.timestamp * 1000L) +
      "], CurrentMaxTimeMillis : [" + currentMaxTimestampMillis + "|" + sdf.format(currentMaxTimestampMillis) +
      "], Watermark : [" + this.getCurrentWatermark.getTimestamp + "|" + sdf.format(this.getCurrentWatermark.getTimestamp) +
      "]")
  }
}

自定义一个断点式生成 WaterMark 的 Assigner

      .assignTimestampsAndWatermarks(new MyPunctuatedWaterMarkAssigner)


// 每条数据都会触发下面两个操作,更新 WaterMark
class MyPunctuatedWaterMarkAssigner extends AssignerWithPunctuatedWatermarks[SensorReading] {
  val lateness: Long = 5000L

  override def checkAndGetNextWatermark(lastElement: SensorReading, extractedTimestamp: Long): Watermark = {
    if (lastElement.id == "sensor_1") new Watermark(extractedTimestamp - lateness) else null
  }

  override def extractTimestamp(element: SensorReading, recordTimestamp: Long): Long = element.timestamp * 1000L
}

WatermarkStrategy

      .assignTimestampsAndWatermarks(
        WatermarkStrategy.forBoundedOutOfOrderness[SensorReading](Duration.ofSeconds(5))
          .withTimestampAssigner(new SerializableTimestampAssigner[SensorReading] {
            override def extractTimestamp(element: SensorReading, recordTimestamp: Long): Long = element.timestamp
          })
      )

Full Code

package com.mso.flink.stream.time

import java.text.SimpleDateFormat
import java.time.Duration

import org.apache.flink.api.common.eventtime._
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.{AssignerWithPeriodicWatermarks, AssignerWithPunctuatedWatermarks}
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.WindowFunction
import org.apache.flink.streaming.api.watermark.Watermark
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector

import scala.collection.mutable.ArrayBuffer
import scala.util.Sorting

// 输入数据的样例类
case class SensorReading(id: String, timestamp: Long, temperature: Double)

object WaterMarkDemo {
  def main(args: Array[String]): Unit = {
    val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    // 从调用时刻开始给 env 创建的每一个 stream 追加时间特征
    environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    environment.getConfig.setAutoWatermarkInterval(100)

    val sourceStream: DataStream[String] = environment.socketTextStream("localhost", 7777)

    // Transform
    val waterMarkStream: DataStream[SensorReading] = sourceStream
      .map((data: String) => {
        val dataArray: Array[String] = data.split(",")
        SensorReading(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
      })
      .assignTimestampsAndWatermarks(
        WatermarkStrategy.forBoundedOutOfOrderness[SensorReading](Duration.ofSeconds(5))
          .withTimestampAssigner(new SerializableTimestampAssigner[SensorReading] {
            override def extractTimestamp(element: SensorReading, recordTimestamp: Long): Long = element.timestamp
          })
      )

    //      .assignTimestampsAndWatermarks(new MyPeriodicWaterMarkAssigner(5000L))
    //      .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(5)) {
    //        override def extractTimestamp(element: SensorReading): Long = element.timestamp * 1000L
    //      })
    //
    //      .assignTimestampsAndWatermarks(new MyPunctuatedWaterMarkAssigner)

    waterMarkStream.keyBy(data => data.id)
      // 使用滚动窗口,窗口大小为 10s
      .timeWindow(Time.seconds(10))
      .apply(new MyWindowFunction)
      .print("WaterMark demo")

    environment.execute()
  }
}

// 自定义一个周期性生成 WaterMark 的 Assigner
class MyPeriodicWaterMarkAssigner(lateness: Long) extends AssignerWithPeriodicWatermarks[SensorReading] {
  // 需要两个关键参数,延迟时间 和 当前所有数据中的最大时间戳
  //  val lateness: Long = 5000L
  var currentMaxTimestampMillis: Long = Long.MinValue + lateness
  var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")

  // 默认每隔 200ms 调用 getCurrentWatermark 生成 Watermark
  // 修改生成 Watermark 的时间间隔 environment.getConfig.setAutoWatermarkInterval(100)
  override def getCurrentWatermark: Watermark = new Watermark(currentMaxTimestampMillis - lateness)

  // 每条数据调用 extractTimestamp 生成 EventTime
  override def extractTimestamp(element: SensorReading, recordTimestamp: Long): Long = {
    currentMaxTimestampMillis = currentMaxTimestampMillis.max(element.timestamp * 1000L)
    printInfo(element: SensorReading)
    // 获取 event time
    element.timestamp * 1000L
  }

  def printInfo(element: SensorReading): Unit = {
    println("Key : [" + element.id +
      "], EventTime : [" + element.timestamp * 1000L + "|" + sdf.format(element.timestamp * 1000L) +
      "], CurrentMaxTimeMillis : [" + currentMaxTimestampMillis + "|" + sdf.format(currentMaxTimestampMillis) +
      "], Watermark : [" + this.getCurrentWatermark.getTimestamp + "|" + sdf.format(this.getCurrentWatermark.getTimestamp) +
      "]")
  }
}

// 自定义一个断点式生成 WaterMark 的 Assigner
// 每条数据都会触发下面两个操作,更新 WaterMark
class MyPunctuatedWaterMarkAssigner extends AssignerWithPunctuatedWatermarks[SensorReading] {
  val lateness: Long = 5000L

  override def checkAndGetNextWatermark(lastElement: SensorReading, extractedTimestamp: Long): Watermark = {
    if (lastElement.id == "sensor_1") new Watermark(extractedTimestamp - lateness) else null
  }

  override def extractTimestamp(element: SensorReading, recordTimestamp: Long): Long = element.timestamp * 1000L
}

// IN, OUT, KEY, W <: Window
class MyWindowFunction extends WindowFunction[SensorReading, String, String, TimeWindow] {
  /**
   *
   * @param key    : 输入的数据类型
   * @param window : 窗口
   * @param input  : 窗口里面所有的数据,都封装在 input 中
   * @param out    : 输出的数据
   */
  override def apply(key: String, window: TimeWindow, input: Iterable[SensorReading], out: Collector[String]): Unit = {
    val arrBuf: ArrayBuffer[Long] = ArrayBuffer[Long]()
    val ite: Iterator[SensorReading] = input.iterator
    while (ite.hasNext) {
      val tup2: SensorReading = ite.next()
      arrBuf.append(tup2.timestamp)
    }
    val arr: Array[Long] = arrBuf.toArray
    Sorting.quickSort(arr)
    val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
    val result: String = "聚合数据的 key 为 : [" + key.toString +
      "], " + "窗口当中数据的条数为 : [" + arr.length +
      "], " + "窗口当中第一条数据为 : [" + sdf.format(arr.head * 1000L) +
      "], " + "窗口当中最后一条数据为 : [" + sdf.format(arr.last * 1000L) +
      "], " + "窗口起始时间为 : [" + sdf.format(window.getStart) +
      "], " + "窗口结束时间为 : [" + sdf.format(window.getEnd) +
      "], " + "如果看到这个结果证明窗口已经运行了"
    out.collect(result)
  }
}

八、ProcessFunction API(底层 API)

普通的 transform 算子,只能获取当前的数据,或者加上聚合状态。
如果是 RichFunction,可以有生命周期方法,还可以获取运行时上下文,进行状态编程,
但是他们都不能获取 时间戳和 WaterMark 相关的信息。

ProcessFunction 是唯一可以获取到时间相关信息的 API。
ProcessFunction 可以实现 RichFunction 能实现的方法,另外还可以获取 timestamp 和 Watermark。
可以注册定时器,指定某个时间点发生的操作。
还可以输出侧输出流。

Flink 提供了以下 ProcessFunction:

  • ProcessFunction - DataStrem
  • KeyedProcessFunction - KeyedStream
  • CoProcessFunction - ConnectedStream
  • ProcessJoinFunction
  • BroadcastProcessFunction
  • KeyedBroadcastProcessFunction
  • ProcessWindowFunction
  • ProcessAllWindowFunction

8.1 KeyedProcessFunction 和 定时器(Timers)

Code

package com.mso.flink.stream.process

import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.api.common.typeinfo.{TypeInformation, Types}
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector

// 输入数据的样例类
case class SensorReading(id: String, timestamp: Long, temperature: Double)

object KeyedProcessFunctionDemo {
  def main(args: Array[String]): Unit = {
    val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    // 从调用时刻开始给 env 创建的每一个 stream 追加时间特征
    environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    environment.getConfig.setAutoWatermarkInterval(100)

    val sourceStream: DataStream[String] = environment.socketTextStream("localhost", 7777)

    // Transform
    val dataStream: DataStream[SensorReading] = sourceStream
      .map((data: String) => {
        val dataArray: Array[String] = data.split(",")
        SensorReading(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
      })

    // 检测每一个温度传感器,温度是否在 10s 内连续上升
    val waringStream: DataStream[String] = dataStream.keyBy(data => data.id)
      .process(new MyKeyedProcessFunction(10000L))

    waringStream.print("KeyedProcessFunctionDemo demo")
    environment.execute()
  }
}

// 自定义 KeyedProcessFunction
class MyKeyedProcessFunction(myInterval: Long) extends KeyedProcessFunction[String, SensorReading, String] {
  // 由于需要跟之前的温度值做对比,所以将上一个温度保存成状态
  //    lazy val lastTempState: ValueState[Double] = getRuntimeContext.getState(new ValueStateDescriptor[Double]("last temp state", classOf[Double]))
  lazy val lastTempState: ValueState[Double] = getRuntimeContext.getState(new ValueStateDescriptor[Double]("last temp state", TypeInformation.of(classOf[Double])))

  // 为了方便删除定时器,还需要保存定时器的时间戳
  lazy val curTimerTsState: ValueState[Long] = getRuntimeContext.getState(new ValueStateDescriptor("cur timestamp state", classOf[Long]))

  // 传入的每条数据都会调用这个方法
  override def processElement(value: SensorReading,
                              ctx: KeyedProcessFunction[String, SensorReading, String]#Context,
                              out: Collector[String]): Unit = {
    // 首先取出上一条数据的 温度状态 和 定时器状态
    val lastTemp = lastTempState.value()
    val curTimerTs = curTimerTsState.value()
    // 更新温度值状态为当前数据的温度值
    lastTempState.update(value.temperature)

    // 判断当前温度值,如果比之前温度高 并且 没有定时器,注册 10s 定时器
    if (value.temperature > lastTemp && curTimerTs == 0) {
      // 使用 Flink 程序的处理时间创建定时器
      val ts: Long = ctx.timerService().currentProcessingTime() + myInterval
      ctx.timerService().registerProcessingTimeTimer(ts)
      curTimerTsState.update(ts)
    } else if (value.temperature < lastTemp) {
      // 如果温度下降,删除定时器
      // 注意此处时间为 deleteProcessingTimeTimer 而非  deleteEventTimeTimer
      ctx.timerService().deleteProcessingTimeTimer(curTimerTs)
      // 清空状态
      curTimerTsState.clear()
    }
  }

  /**
   * 定时器触发,说明 10s 内温度没有下降,报警
   *
   * @param timestamp 若有不同的定时器,可以根据 timestamp 进行区分
   * @param ctx
   * @param out
   */
  override def onTimer(timestamp: Long,
                       ctx: KeyedProcessFunction[String, SensorReading, String]#OnTimerContext,
                       out: Collector[String]): Unit = {
    out.collect(ctx.getCurrentKey + " -> 温度连续" + myInterval / 1000 + "s 内上升")
    curTimerTsState.clear()
  }
}

8.2 侧输出流(SlideOutput)

Code

package com.mso.flink.stream.process

import org.apache.flink.streaming.api.functions.ProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector

object SlideOutputDemo {
  def main(args: Array[String]): Unit = {
    val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    val sourceStream: DataStream[String] = environment.socketTextStream("localhost", 7777)

    // Transform
    val dataStream: DataStream[SensorReading] = sourceStream
      .map((data: String) => {
        val dataArray: Array[String] = data.split(",")
        SensorReading(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
      })

    val highTempStream: DataStream[SensorReading] = dataStream.process(new SplitTempProcess(30))
    val lowTempStream: DataStream[(String, Double, Long)] = highTempStream.getSideOutput(new OutputTag[(String, Double, Long)]("lowTempStream"))

    highTempStream.print("high")
    lowTempStream.print("low")
    environment.execute()
  }
}

class SplitTempProcess(threshold: Int) 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)]("lowTempStream"), (value.id, value.temperature, value.timestamp))
    }
  }
}

8.3 CoProcessFunction

九、状态编程和容错机制

9.1 Flink 中的状态

在 Flink 中,状态始终和特定的算子相关联。
为了使运行时的 Flink 了解算子的状态,算子需要预先注册其状态。

总的来说有以下几种类型的状态:

  • 算子状态(Operator State) : 算子状态的作用范围限定为算子任务
  • 键控状态(Keyed State) : 根据输入数据流中定义的键(Key)来维护和访问

9.1.1 算子状态(Operator State)

算子状态的作用范围限定为 当前的算子任务。由同一个并行任务所处理的所有数据都可以访问到相同的状态
状态对于同一子任务而言是共享的
算子状态不能由相同或不同算子的另一个子任务访问。

算子状态的数据结构

  • 列表状态(List state)- 将状态表示为一组数据的列表
  • 联合列表状态(Union list state) - 将状态表示为数据的列表。发生故障时,或者从保存点启动应用程序时如何恢复
  • 广播状态(Broadcast state) - 如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应用广播状态
public interface CheckpointedFunction {

	/**
	 * This method is called when a snapshot for a checkpoint is requested. This acts as a hook to the function to
	 * ensure that all state is exposed by means previously offered through {@link FunctionInitializationContext} when
	 * the Function was initialized, or offered now by {@link FunctionSnapshotContext} itself.
	 *
	 * @param context the context for drawing a snapshot of the operator
	 * @throws Exception Thrown, if state could not be created ot restored.
	 */
	void snapshotState(FunctionSnapshotContext context) throws Exception;

	/**
	 * This method is called when the parallel function instance is created during distributed
	 * execution. Functions typically set up their state storing data structures in this method.
	 *
	 * @param context the context for initializing the operator
	 * @throws Exception Thrown, if state could not be created ot restored.
	 */
	void initializeState(FunctionInitializationContext context) throws Exception;
}

9.1.2 键控状态(Keyed State)

键控状态是根据输入数据流中定义的键(key)来维护和访问的。
Flink 为每个 Key 维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护和处理这个 Key 对应的状态。
当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的 Key。

算子状态的数据结构

  • 值状态(Value state) - 将状态表示为单个的值
    • ValueState.value()
    • ValueState.update(value: T)
  • 列表状态(List state) - 将状态表示为一组数据的列表
    • ListState.add(value: T)
    • ListState.addAll(values: java.util.List[T])
    • ListState.get()
    • ListState.update(values: java.util.List[T])
  • 映射状态(Map state) - 将状态表示为一组 Key-Value 对
    • MapState.get(key: K)
    • MapState.put(key: K, value: V)
    • MapState.contains(key: K)
    • MapState.remove(key: K)
  • 聚合状态(Reducing state & Aggregating state) - 将状态表示为一个用于聚合操作的列表
    • ReducingState[T] // 同 ListState
    • AggregatingState[I, O]

键控状态的使用

//  keyed state demo
class MyProcessFunction extends KeyedProcessFunction[String, SensorReading, Int] {
  // Fun 1 - use lazy
  lazy val myState: ValueState[Int] = getRuntimeContext
    .getState(new ValueStateDescriptor[Int]("my-state", classOf[Int]))
  //  // Fun 2 - use open
  //  var myState2: ValueState[Int] = _
  //  override def open(parameters: Configuration): Unit = {
  //    myState2 = getRuntimeContext.getState(new ValueStateDescriptor[Int]("my-state2", classOf[Int]))
  //  }

  lazy val myListState: ListState[String] = getRuntimeContext
    .getListState(new ListStateDescriptor[String]("my-list-state", classOf[String]))
  lazy val myMapState: MapState[String, Double] = getRuntimeContext
    .getMapState(new MapStateDescriptor[String, Double]("my-map-state", classOf[String], classOf[Double]))

  private val myReducingState: ReducingState[SensorReading] = getRuntimeContext
    .getReducingState(new ReducingStateDescriptor[SensorReading]("my-reducing-state",
      new ReduceFunction[SensorReading] {
        override def reduce(value1: SensorReading, value2: SensorReading): SensorReading = {
          SensorReading(value1.id, value1.timestamp.max(value2.timestamp), value1.temperature.min(value2.temperature))
        }
      },
      classOf[SensorReading]
    ))

  override def processElement(value: SensorReading,
                              ctx: KeyedProcessFunction[String, SensorReading, Int]#Context,
                              out: Collector[Int]): Unit = {
    myState.value()
    myState.update(1)
    myListState.add("hello")
    myListState.addAll(new util.ArrayList[String]())
    myMapState.put("sensor_1", 10.0)
    myMapState.get("sensor_1")
    myReducingState.add(value)
    myReducingState.clear()
  }
}

9.1.3 状态后端(State Backends)

  • 每传入一条数据,有状态的算子任务会读取和更新状态
  • 由于有效的状态访问对于处理数据的低延迟至关重要,因此每个并行任务都会在本地维护其状态,以确保快速的状态访问
  • 状态的存储、访问和维护,由一个可插入的组件决定,这个组件叫做 状态后端
  • 状态后端主要负责两件事,本地的状态管理,以及将检查点状态(checkpoint)写入远程存储

选择一个状态后端

  • MemoryStateBackend
    • 内存级的状态后端,会将键控状态作为内存中的对象进行管理,将它们存储在 TaskManager 的 JVM 堆上,而将 checkpoint 存储在 JobManager 的内存中
    • 特点:快速、低延迟,但不稳定
  • FsStateBackend
    • 将 checkpoint 存到远程的持久化文件系统(FileSystem)上,而对于本地状态,跟 MemoryStateBackend 一样,也会存在 TaskManager 的 JVM 堆上
    • 同时拥有内存级的本地访问速度,和更好的容错保证
  • RocksDBStateBackend
    • 将所有状态序列化后,存入本地的 RocksDB 中存储。

定义

# pom for  RocksDBStateBackend
        
        
            org.apache.flink
            flink-statebackend-rocksdb_2.11
            1.11.1
            provided
        

# Code
    // 配置状态后端。也可在配置文件中配置
    //      # Supported backends are 'jobmanager', 'filesystem', 'rocksdb', or the
    //      # state.backend: filesystem
    //      # state.checkpoints.dir: hdfs://namenode-host:port/flink-checkpoints
    //      # state.savepoints.dir: hdfs://namenode-host:port/flink-checkpoints
    environment.setStateBackend(new MemoryStateBackend)
    environment.setStateBackend(new FsStateBackend("file://"))
    environment.setStateBackend(new RocksDBStateBackend(""))

9.1.4 Demo

package com.mso.flink.stream.state

import java.util

import org.apache.flink.api.common.functions.{ReduceFunction, RichFlatMapFunction, RichMapFunction}
import org.apache.flink.api.common.state._
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector

// 输入数据的样例类
case class SensorReading(id: String, timestamp: Long, temperature: Double)

object StateDemo {
  def main(args: Array[String]): Unit = {
    val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    // 从调用时刻开始给 env 创建的每一个 stream 追加时间特征
    environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    // 配置状态后端。也可在配置文件中配置
    //      # Supported backends are 'jobmanager', 'filesystem', 'rocksdb', or the
    //      # state.backend: filesystem
    //      # state.checkpoints.dir: hdfs://namenode-host:port/flink-checkpoints
    //      # state.savepoints.dir: hdfs://namenode-host:port/flink-checkpoints
    //    environment.setStateBackend(new MemoryStateBackend)
    //    environment.setStateBackend(new FsStateBackend("file://"))
    //    environment.setStateBackend(new RocksDBStateBackend(""))

    val sourceStream: DataStream[String] = environment.socketTextStream("localhost", 7777)

    // Transform
    val dataStream: DataStream[SensorReading] = sourceStream
      .map((data: String) => {
        val dataArray: Array[String] = data.split(",")
        SensorReading(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
      })

    // 判断相邻两次温度差值是否大于 10
    val resultStream: DataStream[(SensorReading, Double, Double)] = dataStream
      .keyBy(data => data.id)
      .map(new MyMapFunction(10))

    val resultStream2: DataStream[(SensorReading, Double, Double)] = dataStream
      .keyBy(data => data.id)
      .flatMap(new MyFlatMapFunction(10))
    val resultStream3: DataStream[(SensorReading, Double, Double)] = dataStream
      .keyBy(data => data.id)
      // .flatMapWithState[(输出类型), 状态类型]()
      .flatMapWithState[(SensorReading, Double, Double), Double]({
        case (inputData: SensorReading, None) => (List.empty, Some(inputData.temperature))
        case (inputData: SensorReading, lastTemp: Some[Double]) => {
          val tempDiff: Double = (inputData.temperature - lastTemp.get).abs
          if (tempDiff > 10) {
            (List((inputData, lastTemp.get, tempDiff)), Some(inputData.temperature))
          } else {
            (List.empty, Some(inputData.temperature))
          }
        }
      })
    resultStream.print("State demo")
    resultStream2.print("State demo2")
    resultStream3.print("State demo3")
    environment.execute()
  }
}

// 自定义 RichMapFunction,对每条数据都要求有输出结果
class MyMapFunction(threshold: Double) extends RichMapFunction[SensorReading, (SensorReading, Double, Double)] {
  // 定义状态变量
  private var lastTempState: ValueState[Double] = _

  override def open(parameters: Configuration): Unit = {
    lastTempState = getRuntimeContext.getState(new ValueStateDescriptor[Double]("last temperature", classOf[Double]))
  }

  override def map(value: SensorReading): (SensorReading, Double, Double) = {
    val lasttemp = lastTempState.value()
    lastTempState.update(value.temperature)

    val tempDiff = (value.temperature - lasttemp).abs
    if (tempDiff > threshold) {
      (value, lasttemp, tempDiff)
    } else {
      (value, -256, -256)
    }
  }
}

// 自定义 FlatMapFunction,对每条数据都可以输出多条也可以不输出
class MyFlatMapFunction(threshold: Double) extends RichFlatMapFunction[SensorReading, (SensorReading, Double, Double)] {
  // 定义状态变量
  lazy val lastTempState: ValueState[Double] = getRuntimeContext
    .getState(new ValueStateDescriptor[Double]("last temperature2", classOf[Double]))

  override def flatMap(value: SensorReading, out: Collector[(SensorReading, Double, Double)]): Unit = {
    val lasttemp = lastTempState.value()
    lastTempState.update(value.temperature)

    val tempDiff = (value.temperature - lasttemp).abs
    if (tempDiff > threshold) {
      out.collect((value, lasttemp, tempDiff))
    }
  }
}

//  keyed state demo
class MyProcessFunction extends KeyedProcessFunction[String, SensorReading, Int] {
  // Fun 1 - use lazy
  lazy val myState: ValueState[Int] = getRuntimeContext
    .getState(new ValueStateDescriptor[Int]("my-state", classOf[Int]))
  //  // Fun 2 - use open
  //  var myState2: ValueState[Int] = _
  //  override def open(parameters: Configuration): Unit = {
  //    myState2 = getRuntimeContext.getState(new ValueStateDescriptor[Int]("my-state2", classOf[Int]))
  //  }

  lazy val myListState: ListState[String] = getRuntimeContext
    .getListState(new ListStateDescriptor[String]("my-list-state", classOf[String]))
  lazy val myMapState: MapState[String, Double] = getRuntimeContext
    .getMapState(new MapStateDescriptor[String, Double]("my-map-state", classOf[String], classOf[Double]))

  private val myReducingState: ReducingState[SensorReading] = getRuntimeContext
    .getReducingState(new ReducingStateDescriptor[SensorReading]("my-reducing-state",
      new ReduceFunction[SensorReading] {
        override def reduce(value1: SensorReading, value2: SensorReading): SensorReading = {
          SensorReading(value1.id, value1.timestamp.max(value2.timestamp), value1.temperature.min(value2.temperature))
        }
      },
      classOf[SensorReading]
    ))

  override def processElement(value: SensorReading,
                              ctx: KeyedProcessFunction[String, SensorReading, Int]#Context,
                              out: Collector[Int]): Unit = {
    myState.value()
    myState.update(1)
    myListState.add("hello")
    myListState.addAll(new util.ArrayList[String]())
    myMapState.put("sensor_1", 10.0)
    myMapState.get("sensor_1")
    myReducingState.add(value)
    myReducingState.clear()
  }
}

9.1.5 总结

什么是状态

状态是针对每个算子而言,在每个并行任务中用于计算结果的数据。
可以看作是一个本地变量,一般放在本地内存。
Flink 会统一进行数据类型的管理,方便进行读写传输以及容错保证。

状态分类

operator state: 对于当前任务所有输入的数据可见,当前任务输入的所有数据都可以访问同一份状态。
keyed state: 状态只针对当前 key 的数据可见。对每个 Key 维护和管理一份状态实例。

有几种状态后端

  • 内存级
  • 文件级
  • RocksDB

状态编程需要获取运行时上下文,所以在 富函数、ProcessFunction 等中都可以实现。

使用 keyed state,必须在 keyBy 之后的操作中(基于一个 KeyedStream)。
所有算子都可以有状态。
map/filter/flatmap 可以通过实现 RichFunction 定义状态;
reduce/aggregate/window 本来就是有状态,是 flink 底层直接管理的,也可以实现 RichFunction 定义状态。
ProcessFunction 是一类特殊的函数类,是 .process() 方法的参数,它也实现了 RichFunction 接口,是一个特殊的富函数。
DataStream/KeyedStream/WindowedStream 等都可以调用 .process() 方法,传入的是不同的 stream

9.2 状态一致性

9.2.1 概念

什么是状态一致性

  • 有状态的流处理,内部每个算子任务都可以有自己的状态对于流处理器内部来说,
  • 所谓的状态一致性,其实就是我们所说的计算结果要保证准确。
  • 一条数据不应该丢失,也不应该重复计算
  • 在遇到故障时可以恢复状态,恢复以后的重新计算,结果应该也是完全正确的。

状态一致性分类

  • at-most-once : 最多一次。当任务故障时,最简单的做法是什么都不敢,既不恢复丢失的状态,也不重播丢失的数据。
  • at-least-once : 在大多数的真实应用场景,我们需要不丢失事件。所有的事件都得到了处理,而一些事件还可能被处理多次。
  • exactly-once : 处理且仅处理一次。

9.2.2 一致性检查点(checkpoint)

Flink 故障恢复机制的核心,就是应用状态的一致性检查点。
有状态流应用的一致检查点,其实就是 所有任务的状态,在某个时间点的一份快照。存储的是这个时间点,已经处理完的这条数据的偏移量,和这个数据处理完后当前所有状态的值。

  • Flink 使用了一种轻量级快照机制——检查点(checkpoint)来保证 exactly-once 语义
  • 有状态流应用的一致检查点,其实就是:所有任务的状态,在某个时间点的一份拷贝(一份快照)。而这个时间点,应该是所有任务都恰好处理完一个相同的输入数据的时候。
  • 应用状态的一致检查点,是 Flink 故障恢复机制的核心

9.2.3 从检查点恢复状态

在执行流应用程序期间,Flink 会定期保存状态的一致性检查点。
如果发生故障,Flink 将会使用最近的检查点一致性恢复应用程序的状态,并重新启动处理程序。

遇到故障后的处理流程:

  • Step 1 : 重启应用
  • Step 2 : 从 Checkpoint 中读取状态,将状态重置。从检查点重新启动应用程序后,其内部状态与检查点完成时的状态完全相同
  • Step 3 : 开始消费并处理检查点到发生故障之间的所有数据。这种检查点的保存和恢复机制可以为应用程序状态提供 精确一次(exactly-once) 的一致性,因为所有算子都会保存检查点并恢复其所有状态,这样依赖所有的输入流都会被重置到检查点完成时的位置。

9.2.4 Flink 检查点算法

基于 Chandy-Lamport 算法的分布式快照。
将检查点的保存和数据处理分离开,不暂停整个应用。

Flink 的检查点算法用到了一种称为分界线(barrier)的特殊数据形式,用来把一条流上数据按照不同的检查点分开。
JobManager 发出快照通知,并在 source 数据流中某个 offset 处插入一个 Barrier,并向所有分区广播出去,通知所有分区记录快照。

流程示例

前提 :有两个输入流的应用程序。

Step 1 : JobManager 会向每个 Source 任务发送一条带有新检查点 ID 的消息,通过这种方式来启动检查点。
Step 2 : 数据源将它们的状态写入检查点,并发出一个检查点 barrier。
Step 3 : 状态后端在状态存入检查点之后,会返回通知给 source 任务,source 任务就会向 JobManager 确认检查点完成。
Step 4 : 分界线对齐。barrier 向下游传递,子任务会等待所有输入分区的 barrier 到达。对于 barrier 已经到达的分区,继续到达的数据会被缓存;而 barrier 尚未到达的分区,数据会被正常处理。
Step 5 : 当收到所有输入分区的 barrier 时,任务就将其状态保存到状态后端的检查点中,然后将 barrier 继续向下游转发。向下游转发检查点 barrier 后,任务继续正常的数据处理。
Step 6 : Sink 任务向 JobManager 确认状态保存到 checkpoin。当所有任务都确认已成功将状态保存到检查点时,检查点就真正完成了。

9.2.5 保存点(save points)

Flink 还提供了可以自定义的镜像保存功能,就是保存点(savepoints)。

原则上,创建保存点使用的算法与检查点完全相同,因此保存点可以认为就是具有一些额外元数据的检查点。

Flink 不会自动创建保存点,因此用户(或者外部调度程序)必须明确地触发创建操作。

保存点是一个强大的功能。除了故障恢复外,保存点可以用于:有计划的手动备份,更新应用程序,版本迁移,暂停和重启应用,等等

9.2.6 配置

    // checkpoint 配置
    environment.enableCheckpointing(10000L) // 触发检查点的间隔时间
    environment.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)
    environment.getCheckpointConfig.setCheckpointTimeout(30000L)
    environment.getCheckpointConfig.setMaxConcurrentCheckpoints(3)
    environment.getCheckpointConfig.setMinPauseBetweenCheckpoints(5000L)
    environment.getCheckpointConfig.setPreferCheckpointForRecovery(false)
    environment.getCheckpointConfig.setTolerableCheckpointFailureNumber(3)

    // 重启策略
    environment.setRestartStrategy(RestartStrategies.fixedDelayRestart(3, 10000L))
    // environment.setRestartStrategy(RestartStrategies.failureRateRestart(5, Time.of(5, TimeUnit.MINUTES), Time.of(10, TimeUnit.SECONDS)))
    environment.setRestartStrategy(RestartStrategies.failureRateRestart(5, Time.minutes(5), Time.seconds(10)))
    environment.setRestartStrategy(RestartStrategies.noRestart())

9.2.7 总结

Flink 中的 checkpoint,保存的是所有任务状态的快照。
这个状态要求是所有任务都处理完同一个数据之后的状态。

FLink checkpoint 算法基于 Chandy-Lamport 算法的分布式快照。

barrier 用于分隔不同的 checkpoint,对于每个人物而言,收到 barrier 就意味着要开始做 状态 的保存。算法中需要对不同上游分区发来的 barrier 进行对齐。

checkpoint 的存储位置,由状态后端(state backend)决定,一般是放在远程持久化存储空间(fs 或 rocksdb)

JobManager 触发一个 checkpoint 操作,会把 checkpoint 中所有任务状态的拓扑结构保存下来。

9.3 状态一致性分类

9.3.1 端到端(end to end)一致性

目前我们看到的一致性保证都是由流处理器实现的,也就是说都是在Flink流处理器内部保证的;而在真实应用中,流处理应用除了流处理器以外还包含了数据源(例如 Kafka)和输出到持久化系统

端到端的一致性保证,意味着结果的正确性贯穿了整个流处理应用的始终;每一个组件都保证了它自己的一致性

整个端到端的一致性级别取决于所有组件中一致性最弱的组件

9.3.2 端到端的 exactly-once 保证

  • 内部保证 - checkpoint
  • source 端 - 可重设数据的读取位置
  • sink 端 - 从故障恢复时,数据不会重复写入外部系统
    • 幂等写入 : 一个操作,可以重复执行很多次,但只导致一次结果更改,也就是后面重复执行就不起作用了。比如 Mysql 根据唯一性索引,Hbase 根据 rowkey 等写入。
    • 事务写入 : 构建的事务对应 checkpoint,等到 checkpoint 真正完成的时候,才把所有对应的结果写入 sink 系统中。
      • 预写日志(WAL)
      • 两阶段提交(2PC)

预写日志(WAL)

把结果数据先当成状态保存,然后在收到 checkpoint 完成的通知时,一次性写入 sink 系统
简单易于实现,由于数据提前在状态后端中做了缓存,所以无论什么 sink 系统,都能用这种方式一批搞定
DataStream API 提供了一个模板类: GenericWriteAheadSink 来实现这种事务性 sink

两阶段提交(2PC)

对于每个 checkpoint,sink 任务会启动一个事务,并将接下来所有接收的数据添加到事务里。
然后将这些数据写入外部 sink 系统,但不提交它们——这时只是“预提交”。
当它收到 checkpoint 完成的通知时,它才正式提交事务,实现结果的真正写入。
这种方式真正实现了 exactly-once,它需要一个提供事务支持的外部 sink 系统。Flink 提供了TwoPhaseCommitSinkFunction 接口。

2PC 对外部 sink 系统的要求

  • 外部 sink 系统必须提供事务支持,或者 sink 任务必须能够模拟外部系统上的事务
  • 在 checkpoint 的间隔期间里,必须能够开启一个事务并接受数据写入
  • 在收到 checkpoint 完成的通知之前,事务必须是“等待提交”的状态。在故障恢复的情况下,这可能需要一些时间。如果这个时候 sink 系统关闭事务(例如超时了),那么未提交的数据就会丢失
  • sink 任务必须能够在进程失败后恢复事务
  • 提交事务必须是幕等操作

不同source和sink的一致性保证

Flink - 尚硅谷- 大数据高级 Flink 技术精讲 - 2_第2张图片

9.3.3 Flink + Kafka 如何实现端到端的 exactly-once

  • 内部 : 利用 checkpoint 机制,把状态存盘,发生故障的时候可以恢复,保证内部的状态一致性
  • source : kafka consumer 作为 source,可以将偏移量保存下来,如果后续任务出现了故障,恢复的时候可以由连接器重置偏移量,重新消费数据,保证一致性
  • sink : kafka producer 作为 sink,采用两阶段提交 sink,需要实现一个 TwoPhaseCommitSinkFunction

Exactly-once 两阶段提交步骤

  • 第一条数据来了之后,开启一个 kafka 的事务(transaction),正常写入 kafka 分区日志但标记为未提交,这就是“预提交”
  • jobmanager 触发 checkpoint 操作,barrier 从 source 开始向下传递,遇到 barrier 的算子将状态存入状态后端,并通知 jobmanager
  • sink 连接器收到 barrier,保存当前状态,存入 checkpoint,通知 jobmanager,并开启下一阶段的事务,用于提交下个检查点的数据
  • jobmanager 收到所有任务的通知,发出确认信息,表示 checkpoint 完成
  • sink 任务收到 jobmanager 的确认信息,正式提交这段时间的数据
  • 外部 kafka 关闭事务,提交的数据可以正常消费了。

十、Table API & SQL

10.1 定义

Flink 对批处理和流处理,提供了统一的上层 API。
Table APl 是一套内嵌在 Java 和 Scala 语言中的查询 API,它允许以非常直观的方式组合来自一些关系运算符的查询。
Flink 的 SQL 支持基于实现了 SQL 标准的 Apache Calcite。

10.2 pom

        
        
        
            org.apache.flink
            flink-table-planner-blink_2.11
            1.11.1
            provided
        
        
        
            org.apache.flink
            flink-csv
            1.11.1
            provided
        

10.3 两种 planner(old & blink) 的区别

  • 批流统一 : Blink 将批处理作业,视为流式处理的特殊情况。所以,blink 不支持表和 Dataset 之间的转换,批处理作业将不转换为 DataSet 应用程序,而是跟流处理一样,转换为 DataStream 程序来处理。
  • 因为批流统一,Blink planner 也不支持 BatchTableSource,而使用有界的 StreamTableSource 代替。
  • Blink planner 只支持全新的目录,不支持已弃用的 ExternalCatalog。
  • 旧 planner 和 Blink planner 的 FilterableTableSource实现不兼容。旧的 planner 会把 PlannerExpressions 下推到 filterableTableSource 中,而 blink planner 则会把 Expressions 下推。
  • 基于字符串的键值配置选项仅适用于 Blink planner。
  • PlannerConfig 在两个 planner 中的实现不同。
  • Blink planner 会将多个 sink 优化在一个 DAG 中(仅在 TableEnvironment 上受支持,而在 StreamTableEnvironment 上不受支持)。而旧 planner 的优化总是将每一个 sink 放在一个新的 DAG 中,其中所有 DAG 彼此独立。
  • 旧的 planner 不支持目录统计,而 Blink planner 支持。

十一、Table API & SQL 调用

11.1 基本程序结构

// 创建表的执行环境
val tableEnv = ... 

// 注册一张表,用于读取数据
tableEnv.connect(...).createTemporaryTable("inputTable")
// 注册一张表,用于把计算结果输出
tableEnv.connect(...).createTemporaryTable("outputTable")

// Tranfsform 通过Table API 查询算子,得到一张结果表
val result = tableEnv.from("inputTable").select(...)
// Tranfsform 通过SQL查询语句,得到一张结果表
val sqlResult = tableEnv.sq1Query("SELECT...FROM inputTable...")

// Sink 将结果表写入输出表中
result.insertInto("outputTable")

Demo

package com.mso.flink.table

import org.apache.flink.api.java.utils.ParameterTool
import org.apache.flink.api.scala._
import org.apache.flink.table.api._
//import org.apache.flink.table.api.bridge.java._
import org.apache.flink.table.api.bridge.scala._

// 输入数据的样例类
case class SensorReading(id: String, timestamp: Long, temperature: Double)

object DataSetTableDemo {
  def main(args: Array[String]): Unit = {
    // 创建一个批处理的执行环境
    val environment: ExecutionEnvironment = ExecutionEnvironment.getExecutionEnvironment

    // 从文件中红读取数据
    val params: ParameterTool = ParameterTool.fromArgs(args)
    val inputDataSet: DataSet[String] = environment.readTextFile(params.get("path"))

    // Transform
    val dataSet: DataSet[SensorReading] = inputDataSet
      .map((data: String) => {
        val dataArray: Array[String] = data.split(",")
        SensorReading(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
      })

    // 根据执行环境,创建一个批处理 Table 环境
    val tableEnv: BatchTableEnvironment = BatchTableEnvironment.create(environment)

    // 基于数据流转换成一张表,然后进行操作
    val dataTable: Table = tableEnv.fromDataSet(dataSet)

    // 使用 Table API 查询
    val resultTable: Table = dataTable
      .select($"id", $"temperature").filter($"id".isEqual("sensor_1"))
    val resultTable2: Table = dataTable.select("id, temperature").filter("id=='sensor_1'")

    // 使用 SQL 查询
    val resultSqlTable: Table = tableEnv.sqlQuery("select id, temperature from " + dataTable + " where id = 'sensor_1'")
    // Or use view
    tableEnv.createTemporaryView("dataView", dataTable)
    val resultSqlTable2: Table = tableEnv.sqlQuery("select id, temperature from dataView where id = 'sensor_1'")

    // 转换为数据流并打印输出
    resultTable.printSchema() // 打印表结构
    val resultStream: DataSet[(String, Double)] = resultTable.toDataSet[(String, Double)]
    val resultSqlStream: DataSet[(String, Double)] = resultSqlTable.toDataSet[(String, Double)]
    resultStream.printOnTaskManager("resultStream")
    resultSqlStream.printOnTaskManager("resultSqlStream")
    tableEnv.toDataSet[(String, Double)](resultSqlTable2).print("resultSqlTable2")

    environment.execute("DataSetTableDemo")
  }
}

11.2 创建表环境

创建表的执行环境,需要将 flink 流处理的执行环境传入

package com.mso.flink.table

object TableEnvironmentDemo {
  def main(args: Array[String]): Unit = {
    // **********************
    // 1. 创建表环境
    // **********************
    // 1.1 创建 流查询 环境
    // 1.1.1 Flink 老版本
    import org.apache.flink.table.api.EnvironmentSettings
    val fStreamSettings: EnvironmentSettings = EnvironmentSettings.newInstance().useOldPlanner().inStreamingMode().build()

    import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
    import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment
    val fStreamEnvironment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    val fStreamTableEnv: StreamTableEnvironment = StreamTableEnvironment.create(fStreamEnvironment, fStreamSettings)

    // 1.1.2 blink 版本
    import org.apache.flink.table.api.EnvironmentSettings
    val bStreamSettings: EnvironmentSettings = EnvironmentSettings.newInstance().useBlinkPlanner().inStreamingMode().build()

    import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
    import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment
    val bStreamEnvironment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    val bStreamTableEnv: StreamTableEnvironment = StreamTableEnvironment.create(bStreamEnvironment, bStreamSettings)

    // 1.2 创建 批查询 环境
    // 1.2.1 Flink 老版本
    import org.apache.flink.api.scala.ExecutionEnvironment
    import org.apache.flink.table.api.bridge.scala.BatchTableEnvironment
    val fBatchEnvironment: ExecutionEnvironment = ExecutionEnvironment.getExecutionEnvironment
    val fBatchTableEnv: BatchTableEnvironment = BatchTableEnvironment.create(fBatchEnvironment)

    // 1.2.2 blink 版本
    import org.apache.flink.table.api.{EnvironmentSettings, TableEnvironment}
    val bBatchSettings: EnvironmentSettings = EnvironmentSettings.newInstance().useBlinkPlanner().inBatchMode().build()
    val bBatchTableEnv: TableEnvironment = TableEnvironment.create(bBatchSettings)
  }
}

TableEnvironment 是 flink中 集成 TableAPI 和 SQL 的核心概念,所有对表的操作都基于 TableEnvironment

  • 注册 Catalog
  • 在 Catalog 中注册表
  • 执行 SQL 查询
  • 注册用户自定义函数(UDF)

11.3 在 Catalog 中注册表

11.3.1 表的概念

TableEnvironment 维护着一个由标识符(identifier)创建的表 catalog 的映射。
标识符由三个部分组成: catalog 名称、数据库名称 以及 对象名称。

Table 可以是虚拟的(视图 VIEWS)也可以是常规的(表 TABLES)。

  • 视图 可以从已经存在的 Table 中创建,一般是 Table API 或者 SQL 的查询结果。
  • 常规表 描述的是外部数据,例如文件、数据库表或者消息队列的数据,也可以直接从 DataStream 转换而来.

11.3.2 临时表 - TemporaryTable 和 永久表 - PermanentTable

表可以是临时的,并与单个 Flink 会话的生命周期相关。也可以是永久的,并且在多个 Flink 会话和群集中可见。

永久表需要 catalog(例如 Hive Metastore)以维护表的元数据。一旦永久表被创建,它将对任何连接到 catalog 的 Flink 会话可见且持续存在,直至被明确删除。

另一方面,临时表通常保存于内存中并且仅在创建它们的 Flink 会话持续期间存在。这些表对于其它会话是不可见的。它们不与任何 catalog 或者数据库绑定但可以在一个命名空间(namespace)中创建。即使它们对应的数据库被删除,临时表也不会被删除。

屏蔽 - Shadowing

可以使用与已存在的永久表相同的标识符去注册临时表。临时表会屏蔽永久表,并且只要临时表存在,永久表就无法访问。所有使用该标识符的查询都将作用于临时表。

11.3.3 创建表

TableEnvironemnt 可以调用 .connect() 方法,连接外部系统,并调用 .createTemporaryTable() 方法,在 Catalog 中注册表。

虚拟表

在 SQL 的术语中,Table API 的对象对应于视图(虚拟表)。它封装了一个逻辑查询计划。它可以通过以下方法在 catalog 中创建:

// get a TableEnvironment
val tableEnv = ... // see "Create a TableEnvironment" section

// table is the result of a simple projection query 
val projTable: Table = tableEnv.from("X").select(...)

// register the Table projTable as table "projectedTable"
tableEnv.createTemporaryView("projectedTable", projTable)

Connector Tables

另外一个方式去创建 TABLE 是通过 connector 声明。Connector 描述了存储表数据的外部系统。存储系统例如 Apache Kafka 或者常规的文件系统都可以通过这种方式来声明。

tableEnv
  .connect(...)   // 定义表的数据来源 和 外部系统建立连接
  .withFormat(...)    // 定义数据格式化方法
  .withSchema(...)    // 定义表结构
  .inAppendMode()
  .createTemporaryTable("MyTable")  // 创建临时表

11.3.4 连接到外部系统

连接到 文件系统 & Kafka

package com.mso.flink.table

import org.apache.flink.api.java.utils.ParameterTool
import org.apache.flink.streaming.api.scala._
import org.apache.flink.table.api._
import org.apache.flink.table.api.bridge.scala._
import org.apache.flink.table.descriptors.{Csv, FileSystem, Kafka, Schema}

object TableConnectDemo {
  def main(args: Array[String]): Unit = {
    val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    val environmentSettings: EnvironmentSettings = EnvironmentSettings.newInstance().useBlinkPlanner().inStreamingMode().build()
    val tableEnvironment: StreamTableEnvironment = StreamTableEnvironment.create(environment, environmentSettings)

    // **********************
    // 2. 连接外部系统
    // **********************
    // 2.1 连接到文件系统
    val parameterTool: ParameterTool = ParameterTool.fromArgs(args)
    val filePath: String = parameterTool.get("path")
    tableEnvironment.connect(new FileSystem().path(filePath))
      .withFormat(new Csv().fieldDelimiter(',')) // 读取数据之后的格式化方法
      .withSchema(
        new Schema()
          .field("id", DataTypes.STRING())
          .field("timestamp", DataTypes.BIGINT())
          .field("temperature", DataTypes.DOUBLE())
      ) // 定义表结构
      .createTemporaryTable("CSV_input_table") // 注册一张表

    // 转换成流打印输出
    val sensorTable: Table = tableEnvironment.from("CSV_input_table")
    sensorTable.toAppendStream[(String, Long, Double)].print("CSV_input_table")

    // 2.2 连接到 Kafka
    tableEnvironment.connect(
      new Kafka()
        //        .version("0.11")
        .version("universal")
        .topic("sensor")
        .property("bootstrap.servers", "test01:9092")
        .property("zookeeper.connect", "test01:2181")
        .property("group.id", "test-group")
        .property("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
        .property("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
        .property("auto.offset.reset", "latest")
    )
      .withFormat(new Csv().fieldDelimiter(',')) // 读取数据之后的格式化方法
      .withSchema(
        new Schema()
          .field("id", DataTypes.STRING())
          .field("timestamp", DataTypes.BIGINT())
          .field("temperature", DataTypes.DOUBLE())
      ) // 定义表结构
      .createTemporaryTable("Kafka_input_table") // 注册一张表
    val sensorKafkaTable: Table = tableEnvironment.from("Kafka_input_table")
    sensorKafkaTable.toAppendStream[(String, Long, Double)].print("Kafka_input_table")

    environment.execute("TableConnectDemo")
  }
}

11.4 表的查询

11.4.1 Table API

Table API 是集成在 scala 和 Java 语言内的查询 API。

Table API 是基于 Table 类的,该类表示一个表(流或批处理),并提供使用关系操作的方法。这些方法返回一个新的 Table 对象,该对象表示对输入 Table 进行关系操作的结果。
一些关系操作由多个方法调用组成,例如 table.groupBy(…).select(),其中 groupBy(…) 指定 table 的分组,而 select(…) 在 table 分组上的投影。

Demo

// get a TableEnvironment
val tableEnv = ... // see "Create a TableEnvironment" section

// register Orders table

// scan registered Orders table
val orders = tableEnv.from("Orders")
// compute revenue for all customers from France
val revenue = orders
  .filter($"cCountry" === "FRANCE")
  .groupBy($"cID", $"cName")
  .select($"cID", $"cName", $"revenue".sum AS "revSum")

// emit or convert Table
// execute query

11.4.2 SQL

Flink SQL 是基于实现了SQL标准的 Apache Calcite 的。SQL 查询由常规字符串指定。

sqlQuery

// get a TableEnvironment
val tableEnv = ... // see "Create a TableEnvironment" section

// register Orders table

// compute revenue for all customers from France
val revenue = tableEnv.sqlQuery("""
  |SELECT cID, cName, SUM(revenue) AS revSum
  |FROM Orders
  |WHERE cCountry = 'FRANCE'
  |GROUP BY cID, cName
  """.stripMargin)

// emit or convert Table
// execute query

executeSql

// get a TableEnvironment
val tableEnv = ... // see "Create a TableEnvironment" section

// register "Orders" table
// register "RevenueFrance" output table

// compute revenue for all customers from France and emit to "RevenueFrance"
tableEnv.executeSql("""
  |INSERT INTO RevenueFrance
  |SELECT cID, cName, SUM(revenue) AS revSum
  |FROM Orders
  |WHERE cCountry = 'FRANCE'
  |GROUP BY cID, cName
  """.stripMargin)

11.4.3 Demo

package com.mso.flink.table

import org.apache.flink.api.java.utils.ParameterTool
import org.apache.flink.streaming.api.scala._
import org.apache.flink.table.api._
import org.apache.flink.table.api.bridge.scala._
import org.apache.flink.table.descriptors.{Csv, FileSystem, Schema}

object TableQueryDemo {
  def main(args: Array[String]): Unit = {
    val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    val settings: EnvironmentSettings = EnvironmentSettings.newInstance().useBlinkPlanner().build()
    val tableEnvironment: StreamTableEnvironment = StreamTableEnvironment.create(environment, settings)

    // 注册一张表
    val parameterTool: ParameterTool = ParameterTool.fromArgs(args)
    val filePath: String = parameterTool.get("path")
    tableEnvironment.connect(new FileSystem().path(filePath))
      .withFormat(new Csv)
      .withSchema(
        new Schema()
          .field("id", DataTypes.STRING())
          .field("timestamp", DataTypes.BIGINT())
          .field("temperature", DataTypes.DOUBLE())
      )
      .createTemporaryTable("CSV_input_table")

    // **********************
    // 3. 表的查询
    // **********************

    // 3.1 简单查询
    val sourceTable: Table = tableEnvironment.from("CSV_input_table")
    val resultTable: Table = sourceTable
      .select('id, 'temperature)
      .filter('id === "sensor_1")

    // 3.2 SQL 查询
    val resultSqlTable: Table = tableEnvironment
      .sqlQuery(
        """
          |select id, temperature
          |from CSV_input_table
          |where id = 'sensor_1'
          |""".stripMargin)

    // 3.3 简单聚合
    val aggResultTable: Table = sourceTable
      .groupBy($"id")
      .select('id, 'id.count() as 'idCount)

    // 3.4 SQL 简单聚合
    val aggSqlResultTable: Table = tableEnvironment
      .sqlQuery("select id, count(id) as cnt from CSV_input_table group by id")

    // print
    resultTable.toAppendStream[(String, Double)].print("resultTable")
    tableEnvironment.toAppendStream[(String, Double)](resultSqlTable).print("resultSqlTable")

    aggResultTable.toRetractStream[(String, Long)].print("aggResultTable")
    tableEnvironment.toRetractStream[(String, Long)](aggSqlResultTable).print("aggSqlResultTable")

    environment.execute("TableQueryDemo")
  }
}

11.5 Table、View、流 的转换

11.5.1 Scala 隐式转换

Scala Table API 含有对 DataSet、DataStream 和 Table 类的隐式转换。
通过为 Scala DataStream API 导入以下包,可以启用这些转换。

import org.apache.flink.api.scala._
OR 
import org.apache.flink.streaming.api.scala._

import org.apache.flink.table.api._
import org.apache.flink.table.api.bridge.scala._

11.5.2 DataSet/DataStream to View

临时视图(Temporary View)

// get TableEnvironment 
// registration of a DataSet is equivalent
val tableEnv: StreamTableEnvironment = ... // see "Create a TableEnvironment" section

val stream: DataStream[(Long, String)] = ...

// register the DataStream as View "myTable" with fields "f0", "f1"
tableEnv.createTemporaryView("myTable", stream)

// register the DataStream as View "myTable2" with fields "myLong", "myString"
tableEnv.createTemporaryView("myTable2", stream, 'myLong, 'myString)

---------------------------------------------------------------------------------------------------------
// 基于 DataStream 创建临时视图
tableEnv.createTemporaryView('sensorView', dataStream)
tableEnv.createTemporaryView('sensorView', dataStream, 'id, 'timestamp as 'ts, 'temperature)

// 基于 Table 创建临时视图
tableEnv.createTemporaryView('sensorView', sensorTable)

Demo

    // 基于数据流转换成一张表,然后进行操作
    val dataTable: Table = tableEnv.fromDataSet(xxx)

    // 使用 SQL 查询
    val resultSqlTable: Table = tableEnv.sqlQuery("select id, temperature from " + dataTable + " where id = 'sensor_1'")

    // Or use view
    tableEnv.createTemporaryView("dataView", dataTable)
    val resultSqlTable2: Table = tableEnv.sqlQuery("select id, temperature from dataView where id = 'sensor_1'")

11.5.3 DataStream/DataSet to Table

// get TableEnvironment
// registration of a DataSet is equivalent
val tableEnv = ... // see "Create a TableEnvironment" section

val stream: DataStream[(Long, String)] = ...

// convert the DataStream into a Table with default fields "_1", "_2"
val table1: Table = tableEnv.fromDataStream(stream)

// convert the DataStream into a Table with fields "myLong", "myString"
val table2: Table = tableEnv.fromDataStream(stream, $"myLong", $"myString")

Demo

val dataStream: DataStream[SensorReading] = ...
// 将 DataStream 转换为 表
val sensorTable: Table = tableEnv.fromDataStream(dataStream)
// Or 将 DataStream 转换为 表,并指定字段
val sensorTable = tableEnv.fromDataStream(dataStream, 'id, 'timestamp as 'ts, 'temperature)

11.5.4 Table to DataStream

Table 可以被转换成 DataStream 或 DataSet。通过这种方式,定制的 DataSet 或 DataStream 程序就可以在 Table API 或者 SQL 的查询结果上运行了。

将 Table 转换为 DataStream 或者 DataSet 时,你需要指定生成的 DataStream 或者 DataSet 的数据类型,即,Table 的每行数据要转换成的数据类型。通常最方便的选择是转换成 Row 。以下列表概述了不同选项的功能:

  • Row: 字段按位置映射,字段数量任意,支持 null 值,无类型安全(type-safe)检查。
  • POJO: 字段按名称映射(POJO 必须按 Table 中字段名称命名),字段数量任意,支持 null 值,无类型安全检查。
  • Case Class: 字段按位置映射,不支持 null 值,有类型安全检查。
  • Tuple: 字段按位置映射,字段数量少于 22(Scala)或者 25(Java),不支持 null 值,无类型安全检查。
  • Atomic Type: Table 必须有一个字段,不支持 null 值,有类型安全检查。

流式查询(streaming query)的结果表会动态更新,即,当新纪录到达查询的输入流时,查询结果会改变。因此,像这样将动态查询结果转换成 DataStream 需要对表的更新方式进行编码。

将 Table 转换为 DataStream 有两种模式:

  • Append Mode: 仅当动态 Table 仅通过 INSERT 更改进行修改时,才可以使用此模式,即,它仅是追加操作,并且之前输出的结果永远不会更新。
  • Retract Mode: 任何情形都可以使用此模式。它使用 boolean 值对 INSERT 和 DELETE 操作的数据进行标记。
// get TableEnvironment. 
// registration of a DataSet is equivalent
val tableEnv: StreamTableEnvironment = ... // see "Create a TableEnvironment" section

// Table with two fields (String name, Integer age)
val table: Table = ...

// convert the Table into an append DataStream of Row
val dsRow: DataStream[Row] = tableEnv.toAppendStream[Row](table)

// convert the Table into an append DataStream of Tuple2[String, Int]
val dsTuple: DataStream[(String, Int)] dsTuple = 
  tableEnv.toAppendStream[(String, Int)](table)

// convert the Table into a retract DataStream of Row.
//   A retract stream of type X is a DataStream[(Boolean, X)]. 
//   The boolean field indicates the type of the change. 
//   True is INSERT, false is DELETE.
val retractStream: DataStream[(Boolean, Row)] = tableEnv.toRetractStream[Row](table)

11.5.5 Table to DataSet

// get TableEnvironment 
// registration of a DataSet is equivalent
val tableEnv = BatchTableEnvironment.create(env)

// Table with two fields (String name, Integer age)
val table: Table = ...

// convert the Table into a DataSet of Row
val dsRow: DataSet[Row] = tableEnv.toDataSet[Row](table)

// convert the Table into a DataSet of Tuple2[String, Int]
val dsTuple: DataSet[(String, Int)] = tableEnv.toDataSet[(String, Int)](table)

11.5.6 数据类型与 Schema 的映射

DataStream 中的数据类型,与表的 Scheme 之间的对应关系,可以有两种: 基于字段名称,基于字段位置。

  • 基于名称(name-based)
// 基于名称重命名,可以自定义字段顺序
val sensorTable = tableEnv.fromDataStream(dataStream, 'timestamp as 'ts, 'id as 'myId, 'temperature)
  • 基于位置(position-based)
// 根据 DataStream 中的位置进行一一对应,可以直接重命名,但不能调整字段顺序
val sensorTable = tableEnv.fromDataStream(dataStream, 'myId, 'ts)

11.6 表的输出

Table 通过写入 TableSink 输出。TableSink 是一个通用接口,用于支持多种文件格式(如 CSV、Apache Parquet、Apache Avro)、存储系统(如 JDBC、Apache HBase、Apache Cassandra、Elasticsearch)或消息队列系统(如 Apache Kafka、RabbitMQ)。

批处理 Table 只能写入 BatchTableSink,而流处理 Table 需要指定写入 AppendStreamTableSink,RetractStreamTableSink 或者 UpsertStreamTableSink。

方法 Table.executeInsert(String tableName) 将 Table 发送至已注册的 TableSink。该方法通过名称在 catalog 中查找 TableSink 并确认Table schema 和 TableSink schema 一致。

Demo

// get a TableEnvironment
val tableEnv = ... // see "Create a TableEnvironment" section

// create an output Table
val schema = new Schema()
    .field("a", DataTypes.INT())
    .field("b", DataTypes.STRING())
    .field("c", DataTypes.LONG())

tableEnv.connect(new FileSystem("/path/to/file"))
    .withFormat(new Csv().fieldDelimiter('|').deriveSchema())
    .withSchema(schema)
    .createTemporaryTable("CsvSinkTable")

// compute a result Table using Table API operators and/or SQL queries
val result: Table = ...

// emit the result Table to the registered TableSink
result.executeInsert("CsvSinkTable")

11.6.1 更新模式

对于流式查询,需要声明如何在表和外部连结器之间执行转换。与外部系统交换的消息类型,由更新模式(Update Mode) 指定。

  • 追加(Append)模式
  • 撤回(Retract)模式
  • 更新插入(Upsert)模式

11.6.2 输出到文件

package com.mso.flink.table

import org.apache.flink.api.java.utils.ParameterTool
import org.apache.flink.streaming.api.scala._
import org.apache.flink.table.api._
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment
import org.apache.flink.table.descriptors.{Csv, FileSystem, Schema}

object TableOutputToFileDemo {
  def main(args: Array[String]): Unit = {
    val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    val settings: EnvironmentSettings = EnvironmentSettings.newInstance().useBlinkPlanner().build()
    val tableEnvironment: StreamTableEnvironment = StreamTableEnvironment.create(environment, settings)

    // 从文件中红读取数据
    val params: ParameterTool = ParameterTool.fromArgs(args)
    val inputDataSet: DataStream[String] = environment.readTextFile(params.get("path"))

    // Transform
    val dataStream: DataStream[SensorReading] = inputDataSet
      .map((data: String) => {
        val dataArray: Array[String] = data.split(",")
        SensorReading(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
      })

    // **********************
    // 4. 表的输出 - File
    // **********************

    // Option 1. Use TableEnvironment - tableEnvironment.executeSql()
    // Source Table. Register the DataStream as View "sourceTable" with fields "id", "ts", "temp"
    tableEnvironment.createTemporaryView("sourceTable", dataStream, 'id, 'timestamp as 'ts, 'temperature as "temp")

    // Sink Table. Register the output table "CsvSinkTable" with fields "id", "ts", "temp"
    // 注:输出到文件仅支持追加模式
    tableEnvironment.connect(new FileSystem().path(params.get("outputPath")))
      .withFormat(new Csv().fieldDelimiter('|').deriveSchema())
      .withSchema(new Schema()
        .field("id", DataTypes.STRING())
        .field("ts", DataTypes.BIGINT())
        .field("temp", DataTypes.DOUBLE()))
      .createTemporaryTable("CsvSinkTable")

    // Do Sink. Emit the result Table to the registered TableSink
    tableEnvironment.executeSql(
      """
        |INSERT INTO CsvSinkTable
        |select id, ts, temp
        |from sourceTable
        |where id = 'sensor_1'
        |""".stripMargin)

    // Option 2。 Use Table API - Table.executeInsert()
    // Source Table
    val sourceTable: Table = tableEnvironment.fromDataStream(dataStream, 'id, 'timestamp as 'ts, 'temperature as "temp")

    // Sink Table. Register the output table "CsvSinkTable" with fields "id", "ts", "temp"
    tableEnvironment.connect(new FileSystem().path(params.get("outputPath2")))
      .withFormat(new Csv().fieldDelimiter('|').deriveSchema())
      .withSchema(new Schema()
        .field("id", DataTypes.STRING())
        .field("ts", DataTypes.BIGINT())
        .field("temp", DataTypes.DOUBLE()))
      .createTemporaryTable("CsvSinkTable2")
    // Table API
    val resultTable: Table = sourceTable
      .select('id, 'ts, 'temp)
      .filter('id === "sensor_1")
    // Do Sink. Emit the result Table to the registered TableSink
    resultTable.executeInsert("CsvSinkTable2")

    //  ~/flink-1.11.1/bin/flink run -c com.mso.flink.table.TableOutputToFileDemo FlinkPractice-1.0-SNAPSHOT-jar-with-dependencies.jar --path /home/flink/sensor.txt --outputPath /home/flink/output.txt --outputPath2 /home/flink/output2.txt

  }
}

11.6.3 输出到 Kafka

package com.mso.flink.table

import org.apache.flink.streaming.api.scala._
import org.apache.flink.table.api.bridge.scala._
import org.apache.flink.table.api.{DataTypes, EnvironmentSettings}
import org.apache.flink.table.descriptors.{Csv, Kafka, Schema}

object TableOutputKafkaDemo {
  def main(args: Array[String]): Unit = {
    val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    val settings: EnvironmentSettings = EnvironmentSettings.newInstance().useBlinkPlanner().build()
    val tableEnvironment: StreamTableEnvironment = StreamTableEnvironment.create(environment, settings)

    // 连接到 Kafka,注册一张 source 表
    tableEnvironment.connect(
      new Kafka()
        .version("universal")
        .topic("sensor")
        .property("bootstrap.servers", "test01:9092")
        .property("zookeeper.connect", "test01:2181")
        .property("group.id", "test-group")
        .property("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
        .property("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
        .property("auto.offset.reset", "latest")
    )
      .withFormat(new Csv().fieldDelimiter(',')) // 读取数据之后的格式化方法
      .withSchema(
        new Schema()
          .field("id", DataTypes.STRING())
          .field("timestamp", DataTypes.BIGINT())
          .field("temperature", DataTypes.DOUBLE())
      ) // 定义表结构
      .createTemporaryTable("Kafka_input_table")

    // 连接到 Kafka,注册一张 sink 表
    tableEnvironment.executeSql(
      """
        |CREATE TABLE Kafka_output_table (
        | id STRING,
        | temp DOUBLE
        |) WITH (
        | 'connector' = 'kafka',
        | 'topic' = 'flink-sink',
        | 'properties.bootstrap.servers' = 'test01:9092',
        | 'properties.group.id' = 'testGroup',
        | 'format' = 'csv',
        | 'scan.startup.mode' = 'earliest-offset'
        |)
        |""".stripMargin)

    // Do Sink
    // 注:输出到 Kafka 仅支持追加模式
    tableEnvironment.executeSql(
      """
        |INSERT INTO Kafka_output_table
        |select id, temperature
        |from Kafka_input_table
        |where id = 'sensor_1'
        |""".stripMargin)
  }
}

11.6.4 输出到 ES

    tableEnv.connect(
      new Elasticsearch()
        .version("6")
        .host("localhost", 9200, "http")
        .index("sensor")
        .documentType("temp")
    )
      .inUpsertMode()
      .withFormat(new Json())
      .withSchema(new Schema()
        .field("id", DataTypes.STRING())
        .field("count", DataTypes.BIGINT())
      )
      .createTemporaryTable("esoutputTable")
    aggResultTable.insertInto("esOutputTable")

11.6.5 输出到 Mysql

可以创建 Table 来描述 MySql 中的数据,作为输入和输出

    val sinkDDL: String =
      """
        |create table jdbcoutputTable(
        |id varchar(20) not null,
        |cnt bigint not null
        |) with (
        | 'connector' = 'jdbc',
        | 'url' = 'jdbc:mysql://localhost:3306/mdb',
        | 'table-name' = 'sensor_count'
        | 'connector.driver' = 'com.mysql.jdbc.Driver',
        | 'connector.username' = 'root',
        | 'connector.password' = '123456'
        |)
        |""".stripMargin

    tableEnvironment.sqlUpdate(sinkDDL) //执行 DDL 创健装

    aggResultsqlTable.insertInto("jdbcOutputTable")

11.7 Explaining

Table API 提供了一种机制来解释计算 Table 的逻辑和优化查询计划。 这是通过 Table.explain() 方法或者 StatementSet.explain() 方法来完成的。Table.explain() 返回一个 Table 的计划。StatementSet.explain() 返回多 sink 计划的结果。它返回一个描述三种计划的字符串:

  • 关系查询的抽象语法树(the Abstract Syntax Tree),即未优化的逻辑查询计划,
  • 优化的逻辑查询计划
  • 物理执行计划。
    println(table.explain())
    println(tableEnvironment.explain())

十二、Table API & SQL 流式概念

12.1 动态表

12.1.1 DataStream 上的关系查询

关系代数 / SQL 流处理
关系(或表)是有界(多)元组集合。 流是一个无限元组序列。
对批数据(例如关系数据库中的表)执行的查询可以访问完整的输入数据。 流式查询在启动时不能访问所有数据,必须“等待”数据流入。
批处理查询在产生固定大小的结果后终止。 流查询不断地根据接收到的记录更新其结果,并且始终不会结束。

尽管存在这些差异,但是使用关系查询和 SQL 处理流并不是不可能的。高级关系数据库系统提供了一个称为 物化视图(Materialized Views) 的特性。物化视图被定义为一条 SQL 查询,就像常规的虚拟视图一样。与虚拟视图相反,物化视图缓存查询的结果,因此在访问视图时不需要对查询进行计算。缓存的一个常见难题是防止缓存为过期的结果提供服务。当其定义查询的基表被修改时,物化视图将过期。 即时视图维护(Eager View Maintenance) 是一种一旦更新了物化视图的基表就立即更新视图的技术。

如果我们考虑以下问题,那么即时视图维护和流上的SQL查询之间的联系就会变得显而易见:

  • 数据库表是 INSERT、UPDATE 和 DELETE DML 语句的 stream 的结果,通常称为 changelog stream 。
  • 物化视图被定义为一条 SQL 查询。为了更新视图,查询不断地处理视图的基本关系的changelog 流。
  • 物化视图是流式 SQL 查询的结果。

12.1.2 动态表 & 连续查询(Continuous Query)

动态表 是 Flink 的支持流数据的 Table API 和 SQL 的核心概念。与表示批处理数据的静态表不同,动态表是随时间变化的。可以像查询静态批处理表一样查询它们。查询动态表将生成一个 连续查询 。一个连续查询永远不会终止,结果会生成一个动态表。查询不断更新其(动态)结果表,以反映其(动态)输入表上的更改。本质上,动态表上的连续查询非常类似于定义物化视图的查询。

需要注意的是,连续查询的结果在语义上总是等价于以批处理模式在输入表快照上执行的相同查询的结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HIRvD705-1598264647076)(https://ci.apache.org/projects/flink/flink-docs-release-1.11/fig/table-streaming/stream-query-stream.png)]

流式表查询的处理过程:

  • Step 1 : 将流转换为动态表。
  • Step 2 : 在动态表上计算一个连续查询,生成一个新的动态表。
  • Step 3 : 生成的动态表被转换回流。

注意: 动态表首先是一个逻辑概念。在查询执行期间不一定(完全)物化动态表。

12.1.3 更新和追加查询

查询分为两种:

  • 更新:查询更新先前输出的结果,即定义结果表的 changelog 流包含 INSERT 和 UPDATE 操作。
  • 追加:查询只附加到结果表,即结果表的 changelog 流只包含 INSERT 操作。

一个查询是产生一个只追加的表还是一个更新的表有一些含义:

  • 产生更新更改的查询通常必须维护更多的状态(请参阅以下部分)。
  • 将 append-only 的表转换为流与将已更新的表转换为流是不同的(参阅表到流的转换章节)。

12.1.4 查询限制

许多(但不是全部)语义上有效的查询可以作为流上的连续查询进行评估。有些查询代价太高而无法计算,这可能是由于它们需要维护的状态大小,也可能是由于计算更新代价太高。

状态大小: 连续查询在无界流上计算,通常应该运行数周或数月。因此,连续查询处理的数据总量可能非常大。必须更新先前输出的结果的查询需要维护所有输出的行,以便能够更新它们。例如,第一个查询示例需要存储每个用户的 URL 计数,以便能够增加该计数并在输入表接收新行时发送新结果。如果只跟踪注册用户,则要维护的计数数量可能不会太高。但是,如果未注册的用户分配了一个惟一的用户名,那么要维护的计数数量将随着时间增长,并可能最终导致查询失败。

SELECT user, COUNT(url)
FROM clicks
GROUP BY user;

计算更新: 有些查询需要重新计算和更新大量已输出的结果行,即使只添加或更新一条输入记录。显然,这样的查询不适合作为连续查询执行。下面的查询就是一个例子,它根据最后一次单击的时间为每个用户计算一个 RANK。一旦 click 表接收到一个新行,用户的 lastAction 就会更新,并必须计算一个新的排名。然而,由于两行不能具有相同的排名,所以所有较低排名的行也需要更新。

SELECT user, RANK() OVER (ORDER BY lastLogin)
FROM (
  SELECT user, MAX(cTime) AS lastAction FROM clicks GROUP BY user
);

12.1.5 表到流的转换

动态表可以像普通数据库表一样通过 INSERT、UPDATE 和 DELETE 来不断修改。

  • 它可能是一个只有一行、不断更新的表;
  • 也可能是一个 insert-only 的表,没有 UPDATE 和 DELETE 修改;
  • 或者介于两者之间的其他表。

在将动态表转换为流或将其写入外部系统时,需要对这些更改进行编码。Flink的 Table API 和 SQL 支持三种方式来编码一个动态表的变化:

  • Append-only 流: 仅通过 INSERT 操作修改的动态表可以通过输出插入的行转换为流。
  • Retract 流: retract 流包含两种类型的 message: add messages 和 retract messages 。通过将INSERT 操作编码为 add message、将 DELETE 操作编码为 retract message、将 UPDATE 操作编码为更新(先前)行的 retract message 和更新(新)行的 add message,将动态表转换为 retract 流。下图显示了将动态表转换为 retract 流的过程。
  • Upsert 流: upsert 流包含两种类型的 message: upsert messages 和delete messages。转换为 upsert 流的动态表需要(可能是组合的)唯一键。通过将 INSERT 和 UPDATE 操作编码为 upsert message,将 DELETE 操作编码为 delete message ,将具有唯一键的动态表转换为流。消费流的算子需要知道唯一键的属性,以便正确地应用 message。与 retract 流的主要区别在于 UPDATE 操作是用单个 message 编码的,因此效率更高。下图显示了将动态表转换为 upsert 流的过程。

12.2 时间属性

12.2.1 处理时间

在创建表的 DDL 中定义

处理时间属性可以在创建表的 DDL 中用计算列的方式定义,用 PROCTIME() 就可以定义处理时间。

CREATE TABLE user_actions (
  user_name STRING,
  data STRING,
  user_action_time AS PROCTIME() -- 声明一个额外的列作为处理时间属性
) WITH (
  ...
);

SELECT TUMBLE_START(user_action_time, INTERVAL '10' MINUTE), COUNT(DISTINCT user_name)
FROM user_actions
GROUP BY TUMBLE(user_action_time, INTERVAL '10' MINUTE);

在 DataStream 到 Table 转换时定义

处理时间属性可以在 schema 定义的时候用 .proctime 后缀来定义。时间属性一定不能定义在一个已有字段上,所以它只能定义在 schem 定义的最后。

val stream: DataStream[(String, String)] = ...

// 声明一个额外的字段作为时间属性字段
val table = tEnv.fromDataStream(stream, $"UserActionTimestamp", $"user_name", $"data", $"user_action_time".proctime)

val windowedTable = table.window(Tumble over 10.minutes on $"user_action_time" as "userActionWindow")

使用 TableSource 定义

处理时间属性可以在实现了 DefinedProctimeAttribute 的 TableSource 中定义。逻辑的时间属性会放在 TableSource 已有物理字段的最后

// 定义一个由处理时间属性的 table source
class UserActionSource extends StreamTableSource[Row] with DefinedProctimeAttribute {

	override def getReturnType = {
		val names = Array[String]("user_name" , "data")
		val types = Array[TypeInformation[_]](Types.STRING, Types.STRING)
		Types.ROW(names, types)
	}

	override def getDataStream(execEnv: StreamExecutionEnvironment): DataStream[Row] = {
		// create stream
		val stream = ...
		stream
	}

	override def getProctimeAttribute = {
		// 这个名字的列会被追加到最后,作为第三列
		"user_action_time"
	}
}

// register table source
tEnv.registerTableSource("user_actions", new UserActionSource)

val windowedTable = tEnv
	.from("user_actions")
	.window(Tumble over 10.minutes on $"user_action_time" as "userActionWindow")

12.2.2 事件时间

在 DDL 中定义

事件时间属性可以用 WATERMARK 语句在 CREATE TABLE DDL 中进行定义。WATERMARK 语句在一个已有字段上定义一个 watermark 生成表达式,同时标记这个已有字段为时间属性字段。

CREATE TABLE user_actions (
  user_name STRING,
  data STRING,
  user_action_time TIMESTAMP(3),
  -- 声明 user_action_time 是事件时间属性,并且用 延迟 5 秒的策略来生成 watermark
  WATERMARK FOR user_action_time AS user_action_time - INTERVAL '5' SECOND
) WITH (
  ...
);

SELECT TUMBLE_START(user_action_time, INTERVAL '10' MINUTE), COUNT(DISTINCT user_name)
FROM user_actions
GROUP BY TUMBLE(user_action_time, INTERVAL '10' MINUTE);

在 DataStream 到 Table 转换时定义

事件时间属性可以用 .rowtime 后缀在定义 DataStream schema 的时候来定义。时间戳和 watermark 在这之前一定是在 DataStream 上已经定义好了。

在从 DataStream 到 Table 转换时定义事件时间属性有两种方式。取决于用 .rowtime 后缀修饰的字段名字是否是已有字段,事件时间字段可以是:

  • 在 schema 的结尾追加一个新的字段
  • 替换一个已经存在的字段。

不管在哪种情况下,事件时间字段都表示 DataStream 中定义的事件的时间戳。

// Option 1:

// 基于 stream 中的事件产生时间戳和 watermark
val stream: DataStream[(String, String)] = inputStream.assignTimestampsAndWatermarks(...)

// 声明一个额外的逻辑字段作为事件时间属性
val table = tEnv.fromDataStream(stream, $"user_name", $"data", $"user_action_time".rowtime)


// Option 2:

// 从第一个字段获取事件时间,并且产生 watermark
val stream: DataStream[(Long, String, String)] = inputStream.assignTimestampsAndWatermarks(...)

// 第一个字段已经用作事件时间抽取了,不用再用一个新字段来表示事件时间了
val table = tEnv.fromDataStream(stream, $"user_action_time".rowtime, $"user_name", $"data")

// Usage:

val windowedTable = table.window(Tumble over 10.minutes on $"user_action_time" as "userActionWindow")

使用 TableSource 定义

事件时间属性可以在实现了 DefinedRowTimeAttributes 的 TableSource 中定义。getRowtimeAttributeDescriptors() 方法返回 RowtimeAttributeDescriptor 的列表,包含了描述事件时间属性的字段名字、如何计算事件时间、以及 watermark 生成策略等信息。

同时需要确保 getDataStream 返回的 DataStream 已经定义好了时间属性。 只有在定义了 StreamRecordTimestamp 时间戳分配器的时候,才认为 DataStream 是有时间戳信息的。 只有定义了 PreserveWatermarks watermark 生成策略的 DataStream 的 watermark 才会被保留。反之,则只有时间字段的值是生效的。

// 定义一个有事件时间属性的 table source
class UserActionSource extends StreamTableSource[Row] with DefinedRowtimeAttributes {

	override def getReturnType = {
		val names = Array[String]("user_name" , "data", "user_action_time")
		val types = Array[TypeInformation[_]](Types.STRING, Types.STRING, Types.LONG)
		Types.ROW(names, types)
	}

	override def getDataStream(execEnv: StreamExecutionEnvironment): DataStream[Row] = {
		// 构造 DataStream
		// ...
		// 基于 "user_action_time" 定义 watermark
		val stream = inputStream.assignTimestampsAndWatermarks(...)
		stream
	}

	override def getRowtimeAttributeDescriptors: util.List[RowtimeAttributeDescriptor] = {
		// 标记 "user_action_time" 字段是事件时间字段
		// 给 "user_action_time" 构造一个时间属性描述符
		val rowtimeAttrDescr = new RowtimeAttributeDescriptor(
			"user_action_time",
			new ExistingField("user_action_time"),
			new AscendingTimestamps)
		val listRowtimeAttrDescr = Collections.singletonList(rowtimeAttrDescr)
		listRowtimeAttrDescr
	}
}

// register the table source
tEnv.registerTableSource("user_actions", new UserActionSource)

val windowedTable = tEnv
	.from("user_actions")
	.window(Tumble over 10.minutes on $"user_action_time" as "userActionWindow")

十三、Table API

详见官网:Table API

十四、Table API 自定义函数

你可能感兴趣的:(Flink,flink)