文章目录
一、Flink简介
1、Flink概念
Flink是为分布式、高性能、随时可用以及准确的流处理应用程序打造的开源流处理框架。
2、Flink特点
- 事件驱动型
事件驱动型应用是一类具有状态的应用,它从一个或多个事件流提取数据,并根据到来的事件触发计算、状态更新或其他外部动作。
- 基于流的世界观
在 Flink 的世界观中,一切都是由流组成的,离线数据是有界的流;实时数据是一个没有界限的流:这就是所谓的有界流和无界流,这种以流为世界观的架构,获得的最大好处就是具有极低的延迟。
- 分层api
Ø越顶层越抽象,表达含义越简明,使用越方便
Ø越底层越具体,表达能力越丰富,使用越灵活
你可以在表与 DataStream/DataSet 之间无缝切换,以允许程序将 Table API 与 DataStream 以及 DataSet 混合使用。
-
支持事件时间(event-time)和处理时间(processing-time)语义
-
精确一次(exactly-once)的状态一致性保证
-
低延迟,每秒处理数百万个事件,毫秒级延迟
-
与众多常用存储系统的连接
-
高可用,动态扩展,实现7*24小时全天候运行
3、Flink 和 Spark Streaming
-
流(stream)和微批(micro-batching)
-
数据模型
–spark 采用 RDD 模型,spark streaming 的 DStream 实际上也就是一组 一组的小批数据 RDD 的集合
–flink 基本数据模型是数据流,以及事件(Event)序列
-
运行时架构
–spark 是批计算,将 DAG 划分为不同的 stage,一个完成后才可以计算下一个
–flink 是标准的流执行模式,一个事件在一个节点处理完后可以直接发往下一个节点进行处理
4、Flink on Yarn
Flink提供了两种在yarn上运行的模式,分别为Session-Cluster和Per-Job-Cluster模式。
1) Per-Job-Cluster 模式:
每次提交都会创建一个新的flink集群,任务之间互相独立,互不影响,方便管理。任务执行完成之后创建的集群也会消失。
2) Session-cluster 模式:
在yarn中初始化一个flink集群,开辟指定的资源,以后提交任务都向这里提交。这个flink集群会常驻在yarn集群中,除非手工停止。
二、Flink运行架构
1、Flink运行时的组件
2、任务提交流程
Flink任务提交后,Client向HDFS上传Flink的Jar包和配置,之后向Yarn ResourceManager提交任务,ResourceManager分配Container资源并通知对应的NodeManager启动ApplicationMaster,ApplicationMaster启动后加载Flink的Jar包和配置构建环境,然后启动JobManager,之后ApplicationMaster向ResourceManager申请资源启动TaskManager,ResourceManager分配Container资源后,由ApplicationMaster通知资源所在节点的NodeManager启动TaskManager,NodeManager加载Flink的Jar包和配置构建环境并启动TaskManager,TaskManager启动后向JobManager发送心跳包,并等待JobManager向其分配任务。
3、一些概念
Flink中每一个worker(TaskManager)都是一个JVM进程**,它可能会在**独立的线程上执行一个或多个subtask。为了控制一个worker能接收多少个task,worker通过task slot来进行控制(一个worker至少有一个task slot)。
Task Slot是静态的概念,是指TaskManager具有的并发执行能力,
并行度parallelism是动态概念,即TaskManager运行程序时实际使用的并发能力
所有的Flink程序都是由三部分组成的: Source、Transformation和Sink。
一个特定算子的子任务(subtask)的个数被称之为其并行度(parallelism)。一般情况下,一个流程序的并行度,可以认为就是其所有算子中最大的并行度。
相同并行度的one to one操作,Flink这样相连的算子链接在一起形成一个task,原来的算子成为里面的一部分。
三、Flink流处理API
1、Environment
val env = StreamExecutionEnvironment.getExecutionEnvironment //获取环境
env.setsetParallelism(1) //设置并行度
2、Source
-
从集合读取数据
fromCollection
val stream1 = env .fromCollection(List( SensorReading("sensor_1", 1547718199, 35.8), SensorReading("sensor_6", 1547718201, 15.4), SensorReading("sensor_7", 1547718202, 6.7), SensorReading("sensor_10", 1547718205, 38.1) ))
-
从文件读取数据
readTextFile
val stream2 = env.readTextFile("YOUR_FILE_PATH")
- 以kafka消息队列的数据作为来源
//添加依赖
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka-0.11_2.12</artifactId>
<version>1.10.1</version>
</dependency>
val properties = new Properties()
properties.setProperty("bootstrap.servers", "localhost:9092")
properties.setProperty("group.id", "consumer-group")
val stream = env.addSource(new FlinkKafkaConsumer011[String]("sensor", new SimpleStringSchema(), properties))
3、Transform
- map
DataStream -->DataStream 映射
- flatMap
def flatMap[A,B](as: List[A])(f: A ⇒ List[B]): List[B]
DataStream -->DataStream
- Filter
DataStream -->DataStream
- KeyBy
DataStream → KeyedStream:逻辑地将一个流拆分成不相交的分区,每个分区包含具有相同key的元素,在内部以hash的形式实现的。分组
- 滚动聚合算子
这些算子可以针对KeyedStream的每一个支流做聚合。
l sum()
l min()
l max()
l minBy()
l maxBy()
- Reduce
KeyedStream → DataStream:一个分组数据流的聚合操作,合并当前的元素和上次聚合的结果,产生一个新的值,返回的流中包含每一次聚合的结果,而不是只返回最后一次聚合的最终结果。
- Split 和 Select
Split:分流:DataStream → SplitStream:根据某些特征把一个DataStream拆分成两个或者多个DataStream。
val splitStream = stream2
.split( sensorData => {
if (sensorData.temperature > 30) Seq("high") else Seq("low")
} )
select:SplitStream→DataStream:从一个SplitStream中获取一个或者多个DataStream。
val high = splitStream.select("high")
- Connect和 CoMap
Connect:DataStream,DataStream → ConnectedStreams:连接两个保持他们类型的数据流,两个数据流被Connect之后,只是被放在了一个同一个流中,内部依然保持各自的数据和形式不发生任何变化,两个流相互独立。
CoMap:ConnectedStreams → DataStream:作用于ConnectedStreams上,功能与map和flatMap一样,对ConnectedStreams中的每一个Stream分别进行map和flatMap处理。
val connected = warning.connect(low)
val coMap = connected.map(
warningData => (warningData._1, warningData._2, "warning"),
lowData => (lowData.id, "healthy")
)
- Union
DataStream → DataStream:对两个或者两个以上的DataStream进行union操作,产生一个包含所有DataStream元素的新DataStream。
1. Union之前两个流的类型必须是一样,Connect可以不一样,在之后的coMap中再去调整成为一样的。
- Connect只能操作两个流,Union可以操作多个。
4、Sink
- Kafka
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka-0.11_2.12</artifactId>
<version>1.10.1</version>
</dependency>
stream.addSink(new FlinkKafkaProducer011[String]("localhost:9092", "test", new SimpleStringSchema()))
- Redis
<dependency>
<groupId>org.apache.bahir</groupId>
<artifactId>flink-connector-redis_2.11</artifactId>
<version>1.0</version>
</dependency>
//定义一个redis的mapper类,用于定义保存到redis时调用的命令:
class MyRedisMapper extends RedisMapper[SensorReading]{
override def getCommandDescription: RedisCommandDescription = {
new RedisCommandDescription(RedisCommand.HSET, "sensor_temperature")
}
override def getValueFromData(t: SensorReading): String = t.temperature.toString
override def getKeyFromData(t: SensorReading): String = t.id
}
//主函数中调用
val conf = new FlinkJedisPoolConfig.Builder().setHost("localhost").setPort(6379).build()
dataStream.addSink( new RedisSink[SensorReading](conf, new MyRedisMapper) )
5、实现UDF函数
Flink暴露了所有udf函数的接口(实现方式为接口或者抽象类)。
下面例子实现了FilterFunction接口:
//实现一个自己的类,继承相应的Function方法
class FilterFilter extends FilterFunction[String] {
override def filter(value: String): Boolean = {
value.contains("flink")
}
}
val flinkTweets = tweets.filter(new FlinkFilter) //传入相应的对象
//还可以将函数实现成匿名类
val flinkTweets = tweets.filter(
new RichFilterFunction[String] {
override def filter(value: String): Boolean = {
value.contains("flink")
}
}
)
//匿名函数
val flinkTweets = tweets.filter(_.contains("flink"))
2、富函数(Rich Functions)
它与常规函数的不同在于,可以获取运行环境的上下文,并拥有一些生命周期方法,所以可以实现更复杂的功能。
Rich Function有一个生命周期的概念。典型的生命周期方法有:
l open()方法是rich function的初始化方法,当一个算子例如map或者filter被调用之前open()会被调用。
l close()方法是生命周期中的最后一个调用的方法,做一些清理工作。
l getRuntimeContext()方法提供了函数的RuntimeContext的一些信息,例如函数执行的并行度,任务的名字,以及state状态
class MyFlatMap extends RichFlatMapFunction[Int, (Int, Int)] {
var subTaskIndex = 0
override def open(configuration: Configuration): Unit = {
subTaskIndex = getRuntimeContext.getIndexOfThisSubtask
// 以下可以做一些初始化工作,例如建立一个和HDFS的连接
}
override def flatMap(in: Int, out: Collector[(Int, Int)]): Unit = {
if (in % 2 == subTaskIndex) {
out.collect((subTaskIndex, in))
}
}
override def close(): Unit = {
// 以下做一些清理工作,例如断开和HDFS的连接。
}
}
四、Flink中的Window
1、window概述
streaming流式计算是一种被设计用于处理无限数据集的数据处理引擎,而无限数据集是指一种不断增长的本质上无限的数据集,而window是一种切割无限数据为有限块进行处理的手段。
Window是无限数据流处理的核心,Window将一个无限的stream拆分成有限大小的”buckets”桶,我们可以在这些桶上做计算操作。
2、类型
- 时间窗口(Time Window)
Ø滚动时间窗口
Ø滑动时间窗口
Ø会话窗口
- 计数窗口(Count Window)
Ø滚动计数窗口
Ø滑动计数窗口
3、window的API
**window需要用在keyBy之后。 ** KeyedStream ===》WindowedStream
//滚动时间窗口
val minTempPerWindow = dataStream
.map(r => (r.id, r.temperature))
.keyBy(_._1)
.timeWindow(Time.seconds(15)) //一个参数
//滑动时间窗口
val minTempPerWindow = dataStream
.map(r => (r.id, r.temperature))
.keyBy(_._1)
.timeWindow(Time.seconds(15), Time.seconds(5)) //1:窗口时长 2:滑动步长
//滚动计数窗口
val minTempPerWindow = dataStream
.map(r => (r.id, r.temperature))
.keyBy(_._1)
.countWindow(5) //一个参数
//滑动计数窗口
val windowedStream: WindowedStream[(String, Int), Tuple, GlobalWindow] = keyedStream.countWindow(10,2)
//会话窗口(session window)
keyedStream.window(EventTimeSessionWindows.withGap(Time.seconds(10)))
- window function 定义了要对窗口中收集的数据做的计算操作,主要可以分为两类:
l 增量聚合函数(incremental aggregation functions)
每条数据到来就进行计算,保持一个简单的状态。典型的增量聚合函数有ReduceFunction, AggregateFunction。
l 全窗口函数(full window functions)
先把窗口所有数据收集起来,等到计算的时候会遍历所有数据。ProcessWindowFunction就是一个全窗口函数。
五、时间语义与Wartermark
1、时间语义
Event Time:是事件创建的时间。它通常由事件中的时间戳描述,例如采集的日志数据中,每一条日志都会记录自己的生成时间,Flink通过时间戳分配器访问事件时间戳。
Ingestion Time:是数据进入Flink的时间。
Processing Time:是每一个执行基于时间操作的算子的本地系统时间,与机器相关,默认的时间属性就是Processing Time。
•我们往往更关心事件时间(Event Time)
2、Event Time
在Flink的流式处理中,绝大部分的业务都会使用eventTime,一般只在eventTime无法使用时,才会被迫使用ProcessingTime或者IngestionTime。
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 从调用时刻开始给env创建的每一个stream追加时间特征
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
3、Watermark
由于网络、分布式等原因,会导致乱序数据的产生。乱序,就是指Flink接收到的事件的先后顺序不是严格按照事件的Event Time顺序排列的。
如果只根据eventTime决定window的运行,我们不能明确数据是否全部到位,但又不能无限期的等下去,此时必须要有个机制来保证一个特定的时间后,必须触发window去进行计算了,这个特别的机制,就是Watermark。
-
Watermark是一种衡量Event Time进展的机制。
-
Watermark是用于处理乱序事件的,而正确的处理乱序事件,通常用Watermark机制结合window来实现。
-
数据流中的Watermark用于表示timestamp小于Watermark的数据,都已经到达了,因此,window的执行也是由Watermark触发的。
-
Watermark可以理解成一个延迟触发机制,我们可以设置Watermark的延时时长t,每次系统会校验已经到达的数据中最大的maxEventTime,然后认定eventTime小于maxEventTime - t的所有数据都已经到达,如果有窗口的停止时间等于maxEventTime – t,那么这个窗口被触发执行。
watermark 的传递:取最小的
- Watermark的引入
提前指定 Event Time 的使用,一定要指定数据源中的时间戳
dataStream.assignTimestampsAndWatermarks( new BoundedOutOfOrdernessTimestampExtractor[SensorReading] //数据类型
(Time.milliseconds(1000)) {//指定推迟时间
//重写方法 ,指定Event Time时间
override def extractTimestamp(element: SensorReading): Long = {
element.timestamp
}
} )
Event Time的使用一定要指定数据源中的时间戳。
Flink暴露了TimestampAssigner接口供我们实现,使我们可以自定义如何从事件数据中抽取时间戳。
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 从调用时刻开始给env创建的每一个stream追加时间特性
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
// 每隔5秒产生一个watermark AssignerWithPeriodicWatermarks
env.getConfig.setAutoWatermarkInterval(5000)
val readings: DataStream[SensorReading] = env
.addSource(new SensorSource)
.assignTimestampsAndWatermarks(new MyAssigner()) //通过实现类来创建WaterMark
MyAssigner有两种类型
l AssignerWithPeriodicWatermarks
l AssignerWithPunctuatedWatermarks
AssignerWithPeriodicWatermarks:周期性的生成watermark:默认周期是200毫秒。可以使用ExecutionConfig.setAutoWatermarkInterval()方法进行设置。
//也可以继承AssignerWithPeriodicWatermarks来实现周期性生成Watermark
class MyAssigner extends AssignerWithPeriodicWatermarks[SensorReading]{
val bound: Long = 60 * 1000 // 延时为1分钟
var maxTs: Long = Long.MinValue // 观察到的最大时间戳
override def getCurrentWatermark: Watermark = {
new Watermark(maxTs - bound) //获取当前Watermark
}
override def extractTimestamp(r: SensorReading, previousTS: Long) = {
maxTs = maxTs.max(r.timestamp) //更新事件时间戳
r.timestamp //将当前事件的时间戳作为Event Time
}
}
一种简单的特殊情况是,如果我们事先得知数据流的时间戳是单调递增的,也就是说没有乱序,那我们可以使用assignAscendingTimestamps,这个方法会直接使用数据的时间戳生成watermark。
val stream: DataStream[SensorReading] = ...
val withTimestampsAndWatermarks = stream.assignAscendingTimestamps(e => e.timestamp)
而对于乱序数据流,如果我们能大致估算出数据流中的事件的最大延迟时间,就可以使用如下代码:
val withTimestampsAndWatermarks = stream.assignTimestampsAndWatermarks(
new SensorTimeAssigner
)
class SensorTimeAssigner extends BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(5)) {//设定最大延迟时间为5s
// 抽取时间戳
override def extractTimestamp(r: SensorReading): Long = r.timestamp
}
AssignerWithPunctuatedWatermarks:
间断式地生成watermark。和周期性生成的方式不同,这种方式不是固定时间的,而是可以根据需要对每条数据进行筛选和处理。
eg:
class PunctuatedAssigner extends AssignerWithPunctuatedWatermarks[SensorReading] {
val bound: Long = 60 * 1000 // 延时为1分钟
override def checkAndGetNextWatermark(r: SensorReading, extractedTS: Long): Watermark = {
if (r.id == "sensor_1") {
new Watermark(extractedTS - bound)
} else {
null
}
}
override def extractTimestamp(r: SensorReading, previousTS: Long): Long = {
r.timestamp
}
}
4、另外说明
可以通过
keybyStream.timeWindow(Time.seconds(15))
.allowedLateness(Time.minutes(1)) //窗口推迟1min关闭,时间也是按照Water Mark的时间,迟到的数据每来一条,进行一次计算
.sideOutputLateData(new OutputTag[(String, Double, Long)]("late")) //侧输出流,需要手动聚合
六、ProcessFunction API
Process Function用来构建事件驱动的应用以及实现自定义的业务逻辑(使用之前的window函数和转换算子无法实现)。可以访问时间戳、watermark以及注册定时事件。还可以输出特定的一些事件,例如超时事件等。
Flink提供了8个Process Function:
· ProcessFunction
· KeyedProcessFunction
· CoProcessFunction
· ProcessJoinFunction
· BroadcastProcessFunction
· KeyedBroadcastProcessFunction
· ProcessWindowFunction
· ProcessAllWindowFunction
1、KeyedProcessFunction
KeyedProcessFunction用来操作KeyedStream。所有的Process Function都继承自RichFunction接口,所以都有open()、close()和getRuntimeContext()等方法。而KeyedProcessFunction[KEY, IN, OUT]还额外提供了两个方法:
· processElement(v: IN, ctx: Context, out: Collector[OUT]), 流中的每一个元素都会调用这个方法,调用结果将会放在Collector数据类型中输出。Context可以访问元素的时间戳,元素的key,以及TimerService时间服务。Context还可以将结果输出到别的流(side outputs)。
· onTimer(timestamp: Long, ctx: OnTimerContext, out: Collector[OUT])是一个回调函数。当之前注册的定时器触发时调用。参数timestamp为定时器所设定的触发的时间戳。Collector为输出结果的集合。OnTimerContext和processElement的Context参数一样,提供了上下文的一些信息,例如定时器触发的时间信息(事件时间或者处理时间)。
2、TimerService 和 定时器(Timers)
Context和OnTimerContext所持有的TimerService对象拥有以下方法:
· currentProcessingTime(): Long 返回当前处理时间
· currentWatermark(): Long 返回当前watermark的时间戳
· registerProcessingTimeTimer(timestamp: Long): Unit 会注册当前key的processing time的定时器。当processing time到达定时时间时,触发timer。
· registerEventTimeTimer(timestamp: Long): Unit 会注册当前key的event time 定时器。当水位线大于等于定时器注册的时间时,触发定时器执行回调函数。
· deleteProcessingTimeTimer(timestamp: Long): Unit 删除之前注册处理时间定时器。如果没有这个时间戳的定时器,则不执行。
· deleteEventTimeTimer(timestamp: Long): Unit 删除之前注册的事件时间定时器,如果没有此时间戳的定时器,则不执行。
当定时器timer触发时,会执行回调函数onTimer()。注意定时器timer只能在keyed streams上面使用。
例子:
监控温度传感器的温度值,如果温度值在一秒钟之内(processing time)连续上升,则报警。
val warnings = readings
.keyBy(_.id)
.process(new TempIncreaseAlertFunction)
//[String, SensorReading, String] 1:分组ID 2:输入数据 3:输出数据
class TempIncreaseAlertFunction extends KeyedProcessFunction[String, SensorReading, String] {
// 保存上一个传感器温度值
lazy val lastTemp: ValueState[Double] = getRuntimeContext.getState(
new ValueStateDescriptor[Double]("lastTemp", Types.of[Double])
)
// 保存注册的定时器的时间戳
lazy val currentTimer: ValueState[Long] = getRuntimeContext.getState(
new ValueStateDescriptor[Long]("timer", Types.of[Long])
)
override def processElement(r: SensorReading,
ctx: KeyedProcessFunction[String, SensorReading, String]#Context,
out: Collector[String]): Unit = {
// 取出上一次的温度
val prevTemp = lastTemp.value()
// 将当前温度更新到上一次的温度这个变量中
lastTemp.update(r.temperature)
val curTimerTimestamp = currentTimer.value()
if (prevTemp == 0.0 || r.temperature < prevTemp) {
// 温度下降或者是第一个温度值,删除定时器
ctx.timerService().deleteProcessingTimeTimer(curTimerTimestamp)
// 清空状态变量
currentTimer.clear()
} else if (r.temperature > prevTemp && curTimerTimestamp == 0) {
// 温度上升且我们并没有设置定时器
val timerTs = ctx.timerService().currentProcessingTime() + 1000
ctx.timerService().registerProcessingTimeTimer(timerTs)
currentTimer.update(timerTs)
}
}
override def onTimer(ts: Long,
ctx: KeyedProcessFunction[String, SensorReading, String]#OnTimerContext,
out: Collector[String]): Unit = {
out.collect("传感器id为: " + ctx.getCurrentKey + "的传感器温度值已经连续1s上升了。")
currentTimer.clear()
}
}
3、侧输出流(SideOutput)
process function的side outputs功能可以产生多条流,并且这些流的数据类型可以不一样。
实例:
val monitoredReadings: DataStream[SensorReading] = readings
.process(new FreezingMonitor)//分流,得到的是主输出流
monitoredReadings
.getSideOutput(new OutputTag[String]("freezing-alarms")) // 侧输出流
.print()
readings.print()
class FreezingMonitor extends ProcessFunction[SensorReading, SensorReading] {
// 定义一个侧输出标签
lazy val freezingAlarmOutput: OutputTag[String] =
new OutputTag[String]("freezing-alarms")
override def processElement(r: SensorReading,
ctx: ProcessFunction[SensorReading, SensorReading]#Context,
out: Collector[SensorReading]): Unit = {
// 温度在32F以下时,输出警告信息至侧输出流
if (r.temperature < 32.0) {
ctx.output(freezingAlarmOutput, s"Freezing Alarm for ${r.id}")
}
// 所有数据直接常规输出到主流
out.collect(r)
}
}
七、状态编程和容错机制
Flink 会进行状态管理,包括状态一致性、故障处理以及高效存储和访问,以便开发人员可以专注于应用程序的逻辑。
在 Flink 中,状态始终与特定算子相关联
总的来说,有两种类型的状态:
l 算子状态(operator state)
l 键控状态(keyed state)
算子状态的作用范围限定为算子任务。这意味着由同一并行任务所处理的所有数据都可以访问到相同的状态,状态对于同一任务而言是共享的。算子状态不能由相同或不同算子的另一个任务访问。
Flink为算子状态提供三种基本数据结构:
列表状态(List state)
将状态表示为一组数据的列表。
联合列表状态(Union list state)
也将状态表示为数据的列表。它与常规列表状态的区别在于,在发生故障时,或者从保存点(savepoint)启动应用程序时如何恢复。
广播状态(Broadcast state)
如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应用广播状态。
键控状态是根据输入数据流中定义的键(key)来维护和访问的。Flink为每个键值维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护和处理这个key对应的状态。当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的key。因此,具有相同key的所有数据都会访问相同的状态。Keyed State很类似于一个分布式的key-value map数据结构,只能用于KeyedStream(keyBy算子处理之后)。
Flink的Keyed State支持以下数据类型:
l ValueState[T]保存单个的值,值的类型为T。
o get操作: ValueState.value()
o set操作: ValueState.update(value: T)
l ListState[T]保存一个列表,列表里的元素的数据类型为T。基本操作如下:
o ListState.add(value: T)
o ListState.addAll(values: java.util.List[T])
o ListState.get()返回Iterable[T]
o ListState.update(values: java.util.List[T])
l MapState[K, V]保存Key-Value对。
o MapState.get(key: K)
o MapState.put(key: K, value: V)
o MapState.contains(key: K)
o MapState.remove(key: K)
l ReducingState[T]
l AggregatingState[I, O]
State.clear()是清空操作。
eg:
val alerts: DataStream[(String, Double, Double)] = keyedData.flatMap(new TemperatureAlertFunction(1.7))
//一定使用富函数
class TemperatureAlertFunction(val threshold: Double) extends RichFlatMapFunction[SensorReading, (String, Double, Double)] { //[输入数据类型,输出类型]
private var lastTempState: ValueState[Double] = _ //状态保存,使用键控状态的值类型
//初始化
override def open(parameters: Configuration): Unit = {
//定义状态标签,保存的数据类型
val lastTempDescriptor = new ValueStateDescriptor[Double]("lastTemp", classOf[Double])
//通过上下文获取状态
lastTempState = getRuntimeContext.getState[Double](lastTempDescriptor)
}
//或者可以直接使用lazy
//lazy val lastTempState: ValueState[Double]
// =getRuntimeContext.getState[Double](new ValueStateDescriptor[Double]("lastTemp", classOf[Double])
override def flatMap(reading: SensorReading,out: Collector[(String, Double, Double)]): Unit = {
//取出状态值
val lastTemp = lastTempState.value()
val tempDiff = (reading.temperature - lastTemp).abs
if (tempDiff > threshold) {
//输出
out.collect((reading.id, reading.temperature, tempDiff))
}
//更新状态值
this.lastTempState.update(reading.temperature)
}
}
2、检查点
Flink具体如何保证exactly-once呢? 它使用一种被称为"检查点"(checkpoint)的特性,在出现故障时将系统重置回正确状态。检查点是Flink最有价值的创新之一,因为它使Flink可以保证exactly-once,并且不需要牺牲性能。
- 检查点的实现算法
Flink 的改进实现
—— 基于 Chandy-Lamport 算法的分布式快照
—— 将检查点的保存和数据处理分离开,不暂停整个应用
Ø检查点分界线(CheckpointBarrier)
•Flink 的检查点算法用到了一种称为分界线(barrier)的特殊数据形式,用来把一条流上数据按照不同的检查点分开
•分界线之前到来的数据导致的状态更改,都会被包含在当前分界线所属的检查点中;而基于分界线之后的数据导致的所有更改,就会被包含在之后的检查点中
①现在是一个有两个输入流的应用程序,用并行的两个Source任务来读取
②JobManager 会向每个 source 任务发送一条带有新检查点 ID 的消息,通过这种方式来启动检查点
③•数据源将它们的状态写入检查点,并发出一个检查点 barrier
•状态后端在状态存入检查点之后,会返回通知给 source 任务,source 任务就会向 JobManager 确认检查点完成
④•分界线对齐:barrier 向下游传递,sum 任务会等待所有输入分区的 barrier 到达
•对于barrier已经到达的分区,继续到达的数据会被缓存
•而barrier尚未到达的分区,数据会被正常处理
⑤•当收到所有输入分区的 barrier 时,任务就将其状态保存到状态后端的检查点中,然后将 barrier 继续向下游转发
⑥•向下游转发检查点 barrier 后,任务继续正常的数据处理
⑦•Sink 任务向 JobManager 确认状态保存到 checkpoint 完毕
•当所有任务都确认已成功将状态保存到检查点时,检查点就真正完成了
3、状态一致性
对于流处理器内部来说,所谓的状态一致性,其实就是我们所说的计算结果要保证准确。一条数据不应该丢失,也不应该重复计算。在遇到故障时可以恢复状态,恢复以后的重新计算,结果应该也是完全正确的。
Flink 使用了一种轻量级快照机制 —— 检查点(checkpoint)来保证 exactly-once 语义
应用状态的一致检查点,是 Flink 故障恢复机制的核心
端到端(end-to-end)状态一致性
端到端的一致性保证,意味着结果的正确性贯穿了整个流处理应用的始终;每一个组件都保证了它自己的一致性
•内部保证 —— checkpoint
•source 端 —— 可重设数据的读取位置
•sink 端 —— 从故障恢复时,数据不会重复写入外部系统
Ø幂等写入
Ø事务写入
- 事务写入
构建的事务对应着checkpoint,等到 checkpoint 真正完成的时候,才把所有对应的结果写入 sink 系统中。
实现方式:
①预写日志
•把结果数据先当成状态保存,然后在收到 checkpoint 完成的通知时,一次性写入 sink 系统
•简单易于实现,由于数据提前在状态后端中做了缓存,所以无论什么 sink **系统,**都能用这种方式一批搞定
•DataStream API 提供了一个模板类:GenericWriteAheadSink,来实现这种事务性 sink
②两阶段提交(Two-Phase-Commit,2PC)
•对于每个 checkpoint,sink 任务会启动一个事务,并将接下来所有接收的数据添加到事务里
•然后将这些数据写入外部 sink 系统,但不提交它们 —— 这时只是“预提交”
•当它收到 checkpoint 完成的通知时,它才正式提交事务,实现结果的真正写入
Ø这种方式真正实现了 exactly-once,它需要一个提供事务支持的外部 sink 系统。Flink 提供了 TwoPhaseCommitSinkFunction 接口。
2PC 对外部 sink 系统的要求
•外部 sink 系统必须提供事务支持,或者 sink 任务必须能够模拟外部系统上的事务
•在 checkpoint 的间隔期间里,必须能够开启一个事务并接受数据写入
•在收到 checkpoint 完成的通知之前,事务必须是“等待提交”的状态。在故障恢复的情况下,这可能需要一些时间。如果这个时候sink系统关闭事务(例如超时了),那么未提交的数据就会丢失
•sink 任务必须能够在进程失败后恢复事务
•提交事务必须是幂等操作
Flink+Kafka 端到端状态一致性的保证
•内部 —— 利用 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关闭事务,提交的数据可以正常消费了。