注:次文档参考 【尚硅谷】大数据高级 flink技术精讲(2020年6月) 编写。
1.由于视频中并未涉及到具体搭建流程,Flink 环境搭建部分并未编写。
2.视频教程 Flink 版本为 1.10.0,此文档根据 Flink v1.11.1 进行部分修改。
3.文档中大部分程序在 Windows 端运行会有超时异常,需要打包后在 Linux 端运行。
4.程序运行需要的部分 Jar 包,请很具情况去掉 pom 中的 “scope” 标签的再进行打包,才能在集群上运行。
5.原始文档在 Markdown 中编写,此处目录无法直接跳转。且因字数限制,分多篇发布
此文档仅用作个人学习,请勿用于商业获利。
根据哪种时间进行计算要根据不同的计算需求,
比如 星球大战系列电影,前传的上映时间要晚于前三部,对于观影来说更希望按照故事发生先后顺序看。但对于统计票房来说是按照上映时间统计。
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)
当 Flink 以 EventTime 模式处理数据流时,他会根据数据里的时间戳来处理基于时间的算子。但由于网络,分布式等原因,会导致乱序数据的产生。
对于乱序数据来说,遇到一个时间戳达到了窗口关闭时间,不应该立刻触发窗口计算,而是等待一段时间,等迟到的数据来了再关闭窗口。
WaterMark 定义
WaterMark 的大小,需要在延迟性 和 计算结果的准确性间衡量。
WaterMark 特点
多分区之间的 WaterMark 传递中,在每个分区中,会根据当前分区的上游分区个数,创建对应的 PartitionWaterMark。
每个 PartitionWaterMark 中记录了这个上游分区的 WaterMark,并根据上游发送的数据进行更新。
向下游广播的 WaterMark 是这个分区中所有 PartitionWaterMark 最小的那个。
WaterMark 的设定
处理乱序数据的三重保证
窗口有两个重要操作:触发计算,清空状态(关闭窗口)
自定义一个周期性生成 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)
}
}
普通的 transform 算子,只能获取当前的数据,或者加上聚合状态。
如果是 RichFunction,可以有生命周期方法,还可以获取运行时上下文,进行状态编程,
但是他们都不能获取 时间戳和 WaterMark 相关的信息。
ProcessFunction 是唯一可以获取到时间相关信息的 API。
ProcessFunction 可以实现 RichFunction 能实现的方法,另外还可以获取 timestamp 和 Watermark。
可以注册定时器,指定某个时间点发生的操作。
还可以输出侧输出流。
Flink 提供了以下 ProcessFunction:
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()
}
}
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))
}
}
}
在 Flink 中,状态始终和特定的算子相关联。
为了使运行时的 Flink 了解算子的状态,算子需要预先注册其状态。
总的来说有以下几种类型的状态:
算子状态的作用范围限定为 当前的算子任务。由同一个并行任务所处理的所有数据都可以访问到相同的状态。
状态对于同一子任务而言是共享的。
算子状态不能由相同或不同算子的另一个子任务访问。
算子状态的数据结构
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;
}
键控状态是根据输入数据流中定义的键(key)来维护和访问的。
Flink 为每个 Key 维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护和处理这个 Key 对应的状态。
当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的 Key。
算子状态的数据结构
键控状态的使用
// 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()
}
}
选择一个状态后端
定义
# 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(""))
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()
}
}
什么是状态
状态是针对每个算子而言,在每个并行任务中用于计算结果的数据。
可以看作是一个本地变量,一般放在本地内存。
Flink 会统一进行数据类型的管理,方便进行读写传输以及容错保证。
状态分类
operator state: 对于当前任务所有输入的数据可见,当前任务输入的所有数据都可以访问同一份状态。
keyed state: 状态只针对当前 key 的数据可见。对每个 Key 维护和管理一份状态实例。
有几种状态后端
状态编程需要获取运行时上下文,所以在 富函数、ProcessFunction 等中都可以实现。
使用 keyed state,必须在 keyBy 之后的操作中(基于一个 KeyedStream)。
所有算子都可以有状态。
map/filter/flatmap 可以通过实现 RichFunction 定义状态;
reduce/aggregate/window 本来就是有状态,是 flink 底层直接管理的,也可以实现 RichFunction 定义状态。
ProcessFunction 是一类特殊的函数类,是 .process() 方法的参数,它也实现了 RichFunction 接口,是一个特殊的富函数。
DataStream/KeyedStream/WindowedStream 等都可以调用 .process() 方法,传入的是不同的 stream
什么是状态一致性
状态一致性分类
Flink 故障恢复机制的核心,就是应用状态的一致性检查点。
有状态流应用的一致检查点,其实就是 所有任务的状态,在某个时间点的一份快照。存储的是这个时间点,已经处理完的这条数据的偏移量,和这个数据处理完后当前所有状态的值。
在执行流应用程序期间,Flink 会定期保存状态的一致性检查点。
如果发生故障,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。当所有任务都确认已成功将状态保存到检查点时,检查点就真正完成了。
Flink 还提供了可以自定义的镜像保存功能,就是保存点(savepoints)。
原则上,创建保存点使用的算法与检查点完全相同,因此保存点可以认为就是具有一些额外元数据的检查点。
Flink 不会自动创建保存点,因此用户(或者外部调度程序)必须明确地触发创建操作。
保存点是一个强大的功能。除了故障恢复外,保存点可以用于:有计划的手动备份,更新应用程序,版本迁移,暂停和重启应用,等等
// 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())
Flink 中的 checkpoint,保存的是所有任务状态的快照。
这个状态要求是所有任务都处理完同一个数据之后的状态。
FLink checkpoint 算法基于 Chandy-Lamport 算法的分布式快照。
barrier 用于分隔不同的 checkpoint,对于每个人物而言,收到 barrier 就意味着要开始做 状态 的保存。算法中需要对不同上游分区发来的 barrier 进行对齐。
checkpoint 的存储位置,由状态后端(state backend)决定,一般是放在远程持久化存储空间(fs 或 rocksdb)
JobManager 触发一个 checkpoint 操作,会把 checkpoint 中所有任务状态的拓扑结构保存下来。
目前我们看到的一致性保证都是由流处理器实现的,也就是说都是在Flink流处理器内部保证的;而在真实应用中,流处理应用除了流处理器以外还包含了数据源(例如 Kafka)和输出到持久化系统
端到端的一致性保证,意味着结果的正确性贯穿了整个流处理应用的始终;每一个组件都保证了它自己的一致性
整个端到端的一致性级别取决于所有组件中一致性最弱的组件
预写日志(WAL)
把结果数据先当成状态保存,然后在收到 checkpoint 完成的通知时,一次性写入 sink 系统
简单易于实现,由于数据提前在状态后端中做了缓存,所以无论什么 sink 系统,都能用这种方式一批搞定
DataStream API 提供了一个模板类: GenericWriteAheadSink 来实现这种事务性 sink
两阶段提交(2PC)
对于每个 checkpoint,sink 任务会启动一个事务,并将接下来所有接收的数据添加到事务里。
然后将这些数据写入外部 sink 系统,但不提交它们——这时只是“预提交”。
当它收到 checkpoint 完成的通知时,它才正式提交事务,实现结果的真正写入。
这种方式真正实现了 exactly-once,它需要一个提供事务支持的外部 sink 系统。Flink 提供了TwoPhaseCommitSinkFunction 接口。
2PC 对外部 sink 系统的要求
不同source和sink的一致性保证
Exactly-once 两阶段提交步骤
Flink 对批处理和流处理,提供了统一的上层 API。
Table APl 是一套内嵌在 Java 和 Scala 语言中的查询 API,它允许以非常直观的方式组合来自一些关系运算符的查询。
Flink 的 SQL 支持基于实现了 SQL 标准的 Apache Calcite。
org.apache.flink
flink-table-planner-blink_2.11
1.11.1
provided
org.apache.flink
flink-csv
1.11.1
provided
// 创建表的执行环境
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")
}
}
创建表的执行环境,需要将 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
TableEnvironment 维护着一个由标识符(identifier)创建的表 catalog 的映射。
标识符由三个部分组成: catalog 名称、数据库名称 以及 对象名称。
Table 可以是虚拟的(视图 VIEWS)也可以是常规的(表 TABLES)。
表可以是临时的,并与单个 Flink 会话的生命周期相关。也可以是永久的,并且在多个 Flink 会话和群集中可见。
永久表需要 catalog(例如 Hive Metastore)以维护表的元数据。一旦永久表被创建,它将对任何连接到 catalog 的 Flink 会话可见且持续存在,直至被明确删除。
另一方面,临时表通常保存于内存中并且仅在创建它们的 Flink 会话持续期间存在。这些表对于其它会话是不可见的。它们不与任何 catalog 或者数据库绑定但可以在一个命名空间(namespace)中创建。即使它们对应的数据库被删除,临时表也不会被删除。
屏蔽 - Shadowing
可以使用与已存在的永久表相同的标识符去注册临时表。临时表会屏蔽永久表,并且只要临时表存在,永久表就无法访问。所有使用该标识符的查询都将作用于临时表。
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") // 创建临时表
连接到 文件系统 & 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")
}
}
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
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)
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")
}
}
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._
临时视图(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'")
// 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)
Table 可以被转换成 DataStream 或 DataSet。通过这种方式,定制的 DataSet 或 DataStream 程序就可以在 Table API 或者 SQL 的查询结果上运行了。
将 Table 转换为 DataStream 或者 DataSet 时,你需要指定生成的 DataStream 或者 DataSet 的数据类型,即,Table 的每行数据要转换成的数据类型。通常最方便的选择是转换成 Row 。以下列表概述了不同选项的功能:
流式查询(streaming query)的结果表会动态更新,即,当新纪录到达查询的输入流时,查询结果会改变。因此,像这样将动态查询结果转换成 DataStream 需要对表的更新方式进行编码。
将 Table 转换为 DataStream 有两种模式:
// 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)
// 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)
DataStream 中的数据类型,与表的 Scheme 之间的对应关系,可以有两种: 基于字段名称,基于字段位置。
// 基于名称重命名,可以自定义字段顺序
val sensorTable = tableEnv.fromDataStream(dataStream, 'timestamp as 'ts, 'id as 'myId, 'temperature)
// 根据 DataStream 中的位置进行一一对应,可以直接重命名,但不能调整字段顺序
val sensorTable = tableEnv.fromDataStream(dataStream, 'myId, 'ts)
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")
对于流式查询,需要声明如何在表和外部连结器之间执行转换。与外部系统交换的消息类型,由更新模式(Update Mode) 指定。
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
}
}
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)
}
}
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")
可以创建 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")
Table API 提供了一种机制来解释计算 Table 的逻辑和优化查询计划。 这是通过 Table.explain() 方法或者 StatementSet.explain() 方法来完成的。Table.explain() 返回一个 Table 的计划。StatementSet.explain() 返回多 sink 计划的结果。它返回一个描述三种计划的字符串:
println(table.explain())
println(tableEnvironment.explain())
关系代数 / SQL | 流处理 |
---|---|
关系(或表)是有界(多)元组集合。 | 流是一个无限元组序列。 |
对批数据(例如关系数据库中的表)执行的查询可以访问完整的输入数据。 | 流式查询在启动时不能访问所有数据,必须“等待”数据流入。 |
批处理查询在产生固定大小的结果后终止。 | 流查询不断地根据接收到的记录更新其结果,并且始终不会结束。 |
尽管存在这些差异,但是使用关系查询和 SQL 处理流并不是不可能的。高级关系数据库系统提供了一个称为 物化视图(Materialized Views) 的特性。物化视图被定义为一条 SQL 查询,就像常规的虚拟视图一样。与虚拟视图相反,物化视图缓存查询的结果,因此在访问视图时不需要对查询进行计算。缓存的一个常见难题是防止缓存为过期的结果提供服务。当其定义查询的基表被修改时,物化视图将过期。 即时视图维护(Eager View Maintenance) 是一种一旦更新了物化视图的基表就立即更新视图的技术。
如果我们考虑以下问题,那么即时视图维护和流上的SQL查询之间的联系就会变得显而易见:
动态表 是 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)]
流式表查询的处理过程:
注意: 动态表首先是一个逻辑概念。在查询执行期间不一定(完全)物化动态表。
查询分为两种:
一个查询是产生一个只追加的表还是一个更新的表有一些含义:
许多(但不是全部)语义上有效的查询可以作为流上的连续查询进行评估。有些查询代价太高而无法计算,这可能是由于它们需要维护的状态大小,也可能是由于计算更新代价太高。
状态大小: 连续查询在无界流上计算,通常应该运行数周或数月。因此,连续查询处理的数据总量可能非常大。必须更新先前输出的结果的查询需要维护所有输出的行,以便能够更新它们。例如,第一个查询示例需要存储每个用户的 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
);
动态表可以像普通数据库表一样通过 INSERT、UPDATE 和 DELETE 来不断修改。
在将动态表转换为流或将其写入外部系统时,需要对这些更改进行编码。Flink的 Table API 和 SQL 支持三种方式来编码一个动态表的变化:
在创建表的 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")
在 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 后缀修饰的字段名字是否是已有字段,事件时间字段可以是:
不管在哪种情况下,事件时间字段都表示 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
略