什么是Flink
Apache Flink是由Apache软件基金会开发的开源流处理框架,其核心是用Java和Scala编写的分布式流数据流引擎
Flink可以做什么
Flink以数据并行和流水线方式执行任意流数据程序,Flink的流水线运行时系统可以执行批处理和流处理程序。此外,Flink的运行时本身也支持迭代算法的执行。
- 实时推荐系统
- 实时报表
- 实时数仓与ETL
- 实时欺诈与实时信用评估
- 大数据安全监测
- 等等
目前,阿里巴巴、腾讯、美团、华为、滴滴出行、携程、饿了么、爱奇艺、有赞、唯品会等大厂都已经将 Flink 实践于公司大型项目中
和其他所有的计算框架一样,flink也有一些基础的开发步骤以及基础,核心的API,从开发步骤的角度来讲,主要分为四大部分
- Environment(环境)
- Source(输入)
- Transform(转换)
- Sink(输出)
Environment
要了解一个系统,一般都是从架构开始。我们关心的问题是:系统部署成功后各个节点都启动了哪些服务,各个服务之间又是怎么交互和协调的。下方是 Flink 集群启动后架构图。
当 Flink 集群启动后,首先会启动一个 JobManger 和一个或多个的 TaskManager。由 Client 提交任务给 JobManager,JobManager 再调度任务到各个 TaskManager 去执行,然后 TaskManager 将心跳和统计信息汇报给 JobManager。TaskManager 之间以流的形式进行数据的传输。上述三者均为独立的 JVM 进程。
- Client 为提交 Job 的客户端,可以是运行在任何机器上(与 JobManager 环境连通即可)。提交 Job 后,Client可以结束进程(Streaming的任务),也可以不结束并等待结果返回。
- JobManager 主要负责调度 Job 并协调 Task 做 checkpoint,职责上很像 Storm 的 Nimbus。从
Client 处接收到 Job 和 JAR 包等资源后,会生成优化后的执行计划,并以 Task 的单元调度到各个 TaskManager去执行。 - TaskManager 在启动的时候就设置好了槽位数(Slot),每个 slot 能启动一个 Task,Task 为线程。从JobManager 处接收需要部署的 Task,部署启动后,与自己的上游建立 Netty 连接,接收数据并处理。
Flink架构的详细解读可以点击这里
在编写代码时,flink获取执行环境非常简单
// 批处理环境
val env = ExecutionEnvironment.getExecutionEnvironment
// 流式数据处理环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
Source
flink本身封装好的有许多开箱即用的source方法,如:
- readTextFile
- socketTextStream
- readFile
- fromCollection
还有一些是集成flink的连接器,如kafka,MySQL等等,这些只需要导入连接器的依赖,按照需要填好参数调用addSource即可,也十分方便
val properties = new Properties()
properties.setProperty("bootstrap.servers", "hadoop02:9092")
properties.setProperty("group.id", "consumer-group")
properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
properties.setProperty("auto.offset.reset", "latest")
val kafkaDS: DataStream[String] = env.addSource(
new FlinkKafkaConsumer011[String](
"sensor",
new SimpleStringSchema(),
properties)
)
这些都是flink自身或者别人封装好的source,我们拿过来用就可以了,如果有些时候没有这些封装好的source,我没有可以自己写一个source类继承 SourceFunction,然后使用使用addSource加入我们自定义的source即可。
class MySensorSource() extends SourceFunction[SensorReading]{
var running:Boolean = true
override def run(sourceContext: SourceFunction.SourceContext[SensorReading]): Unit = {
val cur = 1.to(10).map(i=>("sensor_"+i , System.currentTimeMillis() , 36.5+Random.nextGaussian()))
while (running){
cur.foreach(t=>sourceContext.collect(SensorReading(t._1,t._2,t._3)))
Thread.sleep(500)
}
}
override def cancel(): Unit = running = false
}
Transform
在Spark中,算子分为转换算子和行动算子,转换算子的作用可以通过算子方法的调用将一个RDD转换另外一个RDD,Flink中也存在同样的操作,可以将一个数据流转换为其他的数据流。
flink提供了许多封装好的方法,可以直接调用,比如:map,reduce,keyby,filter,select,union,comap,flatmap等等。如果这些方法满足不了我们处理的逻辑,我们可以自定义udf函数。Flink 暴露了所有 udf 函数的接口(实现方式为接口或者抽象类)。例MapFunction,FilterFunction,ProcessFunction 等等。
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")
}
})
我们 filter 的字符串"flink"还可以当作参数传进去。
val tweets: DataStream[String] = ...
val flinkTweets = tweets.filter(new KeywordFilter("flink"))
class KeywordFilter(keyWord: String) extends FilterFunction[String] {
override def filter(value: String): Boolean = {
value.contains(keyWord)
}
}
“富函数”是 DataStream API 提供的一个函数类的接口,所有 Flink 函数类都
有其 Rich 版本。它与常规函数的不同在于,可以获取运行环境的上下文,并拥有一
些生命周期方法,所以可以实现更复杂的功能。
Rich Function 有一个生命周期的概念。典型的生命周期方法有:
- open()方法是 rich function 的初始化方法,当一个算子例如 map 或者 filter 被调用之前
open()会被调用。 - close()方法是生命周期中的最后一个调用的方法,做一些清理工作。
- 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))
}
}
}
Sink
Flink 没有类似于 spark 中 foreach 方法,让用户进行迭代的操作。虽有对外的输出操作都要利用 Sink 完成。最后通过类似如下方式完成整个任务最终输出操作。
Sink和Source一样,除了flink自带的一些方法还有一些集成的连接器(如kafka,elastic search,Redis),如果我们要输出的格式没有封装好的sink,我们就需要自定义了。
class MyJdbcSink() extends RichSinkFunction[SensorReading]{
var conn: Connection = _
var insertStmt: PreparedStatement = _
var updateStmt: PreparedStatement = _
override def open(parameters: Configuration): Unit = {
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/work", "root", "root")
insertStmt = conn.prepareStatement("INSERT INTO flink_sensor (id, sensor, time) VALUES (?, ?, ?)")
updateStmt = conn.prepareStatement("UPDATE flink_sensor SET sensor = ?,time = ? WHERE id = ?")
}
override def invoke(value: SensorReading, context: SinkFunction.Context[_]): Unit = {
updateStmt.setDouble(1,value.sensor)
updateStmt.setString(2,value.time.toString)
updateStmt.setString(3,value.id)
updateStmt.execute()
if(updateStmt.getUpdateCount == 0){
insertStmt.setString(1,value.id)
insertStmt.setDouble(2,value.sensor)
insertStmt.setString(3,value.time.toString)
insertStmt.execute()
}
}
override def close(): Unit = {
insertStmt.close()
updateStmt.close()
conn.close()
}
}
下一篇是flink的一些高级操作,比如开窗,定时,状态恢复,Wartermark等等。。