Flink项目的理念是:Apache Flink是为分布式、高性能、随时可用以及准确的流处理应用程序打造的开源流处理框架。
Apache Flink是一个框架和分布式处理引擎,用于对无界和有界数据流进行有状态计算。Flink被设计在所有常见的集群环境中运行,以内存执行速度和任意规模来执行计算。
Flink能够提供毫秒级别的延迟,同时保证了数据处理的低延迟、高吞吐和结果的正确性,还提供了丰富的时间类型和窗口计算、Exactly-once(就一次)语义支持,另外还可以进行状态管理,并提供了 CEP(复杂事件处理)的支持。
在spark中,一切都是由批次组成的,离线数据是一个大批此,而实时数据是由一个一个无限的小批次组成的;
在flink中,一切都是由流组成的,离线数据是有界限的流,实时数据是一个没有界限的流,这就是所谓的有界流和无界流。
无界数据流
无界数据流有一个开始但是没有结束,它们不会在生成时终止并提供数据,必须连续处理无界流,也就是说必须再获取后立即处理event;
对于无界数据流是无法等待所有数据都到达,因为输入是无界的,并且再任何时间点都不会完成;
处理无界数据通常要求以特定顺序(例如事件发生的顺序)获取event,以便能够推断结果完整性。
有界数据流
有界数据流有明确定义的开始和结束,可以在执行任何计算之前通过获取所有数据来处理有界流;
处理有界流不需要有序获取,因为可以始终对有界数据集进行排序,有界流的处理也称为批处理。
这种以流为世界观的架构,获得的最大好处就是具有极低的延迟。
时间驱动型应用是一类具有状态的应用,它从一个或多个事件流提取数据,并根据到来的时间触发计算、状态更新或其他外部动作
Flink提供的最高层级的抽象是SQL
越顶层越抽象,表达含义越明显,使用越方便
越底层越具体,表达能力越丰富,使用越灵活
Flink几大模块
这个问题是一个非常宏观的问题,因为两个框架的不同点非常之多,但是在面试时有非常重要的一点一定要回答出来:Flink是标准的实时处理引擎,基于事件驱动;SparkStreaming是微批(Micro-Batch)的模型。
SparkStreaming | Flink | |
---|---|---|
架构模型 | 主要角色包括:Master、Worker、Driver和Executor | 主要包括:JobManager、TaskManager和Slot |
任务调度 | 连续不间断的生成微小的数据批次,构建有向无环图DAG,会依次创建DStreamGraph、JobGenerator、JobScheduler | 根据用户提交的代码生成StreamGraph,经过优化生成JobGraph,然后提交给JobManager进行处理,JobManager会根据JobGraph生成ExecutionGraph,ExecutionGraph是Flink调度最核心的数据结构,JobManager根据ExecutionGraph对Job进行调度 |
时间机制 | 只支持处理时间 | 支持处理时间、事件时间、注入时间;同时也支持watermark机制来处理滞后数据 |
容错机制 | 可以设置checkpoint,假如发生故障并重启,可以从上次checkpoint之处恢复,但是这个行为只能使得数据不丢失,可能会重复处理,不能做到恰好一次处理语义 | 使用两阶段提交(2PC)协议来解决可能重复处理问题,做到恰好一次处理语义 |
Storm | Flink | |
---|---|---|
状态管理 | 无状态,需用户自行进行状态管理 | 有状态 |
窗口支持 | 对事件窗口支持较弱,缓存整个窗口的所有数据,窗口结束时一起计算 | 窗口支持较为完善,自带一些窗口聚合方法,并且会自动管理窗口状态 |
消息投递 | At Most Once/At Least Once | At Most Once/At Least Once/Exactly Once |
容错方式 | ACK机制:对每个消息进行全链路跟踪,失败或超时进行重发 | 检查点机制:通过分布式一致性快照机制,对数据流和算子状态进行保存;在发生错误时,使系统能够进行回滚 |
应用现状 | 在美团点评实时计算业务中已有较为成熟的运用,有管理平台、常用API和相应的文档,大量实时作业基于Storm构建 | 在美团点评实时计算业务中已有一定应用,但是管理平台、API及文档等仍需进一步完善 |
创建一个执行环境,表示当前执行程序的上下文;如果程序是独立调用的,则此方法返回本地执行环境;如果从命令行客户端调用程序以提交到集群,则此方法返回此集群的执行环境
也就是说,getExecutionEnvironment会根据查询运行的方式决定返回什么样的运行环境,是最常用的一种创建执行环境的方式
val env: ExecutionEnvironment = ExecutionEnvironment.getExecutionEnvironment
val env = StreamExecutionEnvironment.getExecutionEnvironment
算子 | 说明 |
---|---|
map | 将输入数据一对一处理 |
flatMap | 将输入数据打散成一个List/将输入的list数据组合在一起 |
filter | 过滤出表达式返回true的数据 |
keyBy | DataStream → KeyedStream:分区不分流,只是将流对象转换,数据形式没改变只是可以进行聚合操作,其实就是分组 |
Rolling Aggregation | 滚动聚合算子(sum(), min(), max(), minBy(), maxBy())进行keyBy后可以执行聚合算子 |
reduce | KeyedStream → DataStream:一个分组数据流的聚合操作,合并当前的元素和上次聚合的结果,产生一个新的值,返回的流包含每一次聚合的结果,而不是只返回最后一次聚合的最终结果 |
split | 将数据流DataStream转换为两个或者多个SplitStream |
select | 从split分割而开的两个或者多个SplitStream获取出来两个或多个DataStream |
connect | 将两个DataStream转换成一个ConnectedStream 但是内部还是各自管理 |
coMap/coFlatMap | 对connect算子的结果ConnectedStream进行各自的map/flatMap操作 |
union | 可以将多个(数据类型相同的)流DataStream合并为一个DataStream |
DataStream → SplitStream:根据某些特征把一个DataStream拆分成两个或者多个DataStream
SplitStream →DataStream:从一个SplitStream中获取一个或者多个DataStream
DataStream, DataStream → ConnectedStreams:连接两个保持他们类型的数据流,两个数据流被Connect之后,只是被放在了一个同一个流中,内部依然保持各自的数据和形式不发生任何变化,两个流相互独立
ConnectedStreams → DataStream:作用于ConnectedStreams上,功能与map和flatMap一样,对ConnectedStreams中的每一个Stream分别进行map和flatMap处理
DataStream → DataStream:对两个或者两个以上的DataStream进行union操作,产生一个包含所有DataStream元素的新DataStream
基础数据类型
Flink支持所有的Java和Scala基础数据类型:Int,Double,Long,String…
Java和Scala元组(Tuples)
Scala样例类(case classes)
Java简单对象(pojos)
其他(Arrays,Lists,Maps,Enums等)
Flink对Java和Scala中的一些特殊目的的类型也都是支持的,比如Java的ArrayList,HashMap,Enum等
Flink暴露了所有UDF函数的接口(实现方式为接口或者抽象类)
例如MapFunction,FilterFunction,ProcessFunction等
还可以将函数实现成匿名类
package com.streamapi
import org.apache.flink.api.common.functions.{FilterFunction, RichFilterFunction, RichFlatMapFunction}
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector
object UDFTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val stream = env.readTextFile("source/sensor.txt")
val value = stream.map(line => {
// e.g. sensor_2,1600828094726,113.45370583283331
val splited = line.split(",")
SensorReading(splited(0), splited(1).trim.toLong, splited(2).trim.toDouble)
})
// 自定义Fliter
val dataStream = value.filter(new UDFFilter())
// 可以将函数实现成匿名类
// 使用RichFilterFunction
value.filter(new RichFilterFunction[SensorReading] {
override def filter(t: SensorReading): Boolean = {
t.id == "sensor_8"
}
// 可以做一些预操作
override def open(parameters: Configuration): Unit = super.open(parameters)
})
dataStream.print("filter")
env.execute("udf filter")
}
}
// 自定义Fliter函数
class UDFFilter() extends FilterFunction[SensorReading] {
override def filter(t: SensorReading): Boolean = {
t.id == "sensor_8"
}
}
value.filter(new RichFilterFunction[SensorReading] {
override def filter(t: SensorReading): Boolean = {
t.id == "sensor_8"
}
// 可以做一些预操作
override def open(parameters: Configuration): Unit = super.open(parameters)
})
“富函数”是DataStream API提供的一个函数类的接口,所有Flink函数类都有其Rich版本
它与常规函数的不同在于,可以获取运行环境的上下文,并拥有一些生命周期方法,所以可以实现更复杂的功能
Rich Functions
有一个生命周期的概念,典型的生命周期方法有:
// 自定义FlatMap函数
class UDFFlatMap extends RichFlatMapFunction[Int, (Int, Int)] {
var subTaskIndex = 0
override def open(parameters: Configuration): Unit = {
subTaskIndex = getRuntimeContext.getIndexOfThisSubtask
// 还可以做一些初始化工作,例如建立一个和HDFS的连接
}
override def flatMap(in: Int, collector: Collector[(Int, Int)]): Unit = {
if (in % 2 == subTaskIndex) {
collector.collect((subTaskIndex, in))
}
}
override def close(): Unit = {
// 以下做一些清理工作,例如断开和HDFS的连接
}
}
这里重点介绍KeyedProcessFunction
KeyedProcessFunction用来操作KeyedStream
KeyedProcessFunction会处理流的每一个元素,输出为0个、1个或者多个元素
所有的Process Function都继承自RichFunction接口,所以都有open()、close()和getRuntimeContext()等方法;而KeyedProcessFunction[KEY, IN, OUT]还额外提供了两个方法:
Context和OnTimerContext所持有的TimerService对象拥有以下方法:
当定时器timer触发时,会执行回调函数onTimer();注意定时器timer只能在keyed streams上面使用
class TempIncreaseAlertFunction() extends KeyedProcessFunction[String, SensorReading, String] {
// 定义一个状态,用来保存上一个传感器温度值
lazy val lastTemp = getRuntimeContext.getState(
new ValueStateDescriptor[Double]("lastTemp", Types.of[Double])
)
// 定义一个状态,用来保存注册的定时器的时间戳
lazy val currentTimer = getRuntimeContext.getState(
new ValueStateDescriptor[Long]("timer", Types.of[Long])
)
override def processElement(value: SensorReading, ctx: KeyedProcessFunction[String, SensorReading, String]#Context, out: Collector[String]): Unit = {
// 取出上一次的温度
val prevTemp = lastTemp.value()
// 将当前温度更新到上一次的温度这个变量中
lastTemp.update(value.temperature)
// 获取定时器的时间
val currentTimerTimestamp = currentTimer.value()
if (prevTemp == 0.0 || value.temperature < prevTemp) {
// 温度下降或者是第一个温度值,删除定时器
ctx.timerService().deleteProcessingTimeTimer(currentTimerTimestamp)
// 清空状态变量
currentTimer.clear()
} else if (value.temperature > prevTemp && currentTimerTimestamp == 0) {
// 温度上升并且没有设置定时器 才开始 注册定时器
// 获取当前处理时间,延迟1s
val timerTs = ctx.timerService().currentProcessingTime() + 1000
ctx.timerService().registerProcessingTimeTimer(timerTs)
// 把定时时间保存到状态
currentTimer.update(timerTs)
}
}
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, SensorReading, String]#OnTimerContext, out: Collector[String]): Unit = {
// 输出报警信息
out.collect("传感器id为:" + ctx.getCurrentKey + "的传感器温度值已经连续1s上升了。")
// 清空状态
currentTimer.clear()
}
}
大部分的DataStream API的算子的输出是单一输出,也就是某种数据类型的流
除了split算子,可以将一条流分成多条流,这些流的数据类型也都相同
ProcessFunction的SideOutPut功能可以产生多条流,并且这些流的数据类型可以不一样
一个SideOutPut可以定义为OutPutTag[X]对象,X是输出流的数据类型
ProcessFunction可以通过Context对象发射一个事件到一个或者多个SideOutPut
package com.api
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.ProcessFunction
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.util.Collector
object SideOutput {
def main(args: Array[String]): Unit = {
// 环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 设置事件时间
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
// 全局并行度
env.setParallelism(2)
val stream = env.socketTextStream("localhost", 9999)
val dataStream: DataStream[SensorReading] = stream.map(line => {
val splited = line.split(",")
SensorReading(splited(0), splited(1).trim.toLong, splited(2).trim.toDouble)
})
// 有界无序,能大致估算出数据流中的事件的最大延迟时间
.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(1)) {
override def extractTimestamp(element: SensorReading): Long = element.timestamp * 1000
})
val resDStream = dataStream.keyBy(_.id)
// 监控温度传感器的温度值,将温度值低于32F的温度输出到side output
.process(new FreezingMonitor())
resDStream.print("process data")
// 侧输出流
resDStream.getSideOutput(new OutputTag[String]("freezing-alarms")).print("SideOutput")
env.execute("SideOutput")
}
}
// 定义样例类,传感器id,时间戳,温度
case class SensorReading(id: String, timestamp: Long, temperature: Double)
class FreezingMonitor() extends ProcessFunction[SensorReading, SensorReading] {
// 定义一个侧输出标签
lazy val freezingAlarmOutput = new OutputTag[String]("freezing-alarms")
override def processElement(value: SensorReading, ctx: ProcessFunction[SensorReading, SensorReading]#Context, out: Collector[SensorReading]): Unit = {
// 温度在32F以下时,则在侧输出流输出警告信息
if (value.temperature < 32.0) {
ctx.output(freezingAlarmOutput, s"Freezing Alarm for ${value.id}")
}
// 所有数据直接床柜输出到主流
out.collect(value)
}
}
对于两条输入流,DataStream API提供了CoProcessFunction这样的low-level操作
CoProcessFunction提供了操作每一个输入流的方法:processElement1()和processElement2()
类似于ProcessFunction,这两种方法都通过Context对象来调用
这个Context对象可以访问事件数据,定时器时间戳,TimerService,以及side outputs
CoProcessFunction也提供了onTimer()回调函数