目录
前言
本文不介绍Flink简介以及其架构、特性等,仅是本人在学习flink过程中对代码方面做的一些笔记,如有不正确之处,欢迎指出。
Flink之WordCount
一个flink程序分为四个阶段:
首先用一段WordCount代码作为示例,说明一个Flink程序的基本流程。
// 1.创建流处理环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 2.接收socket文本流
val textDstream: DataStream[String] = env.socketTextStream("hd01", 12202)
// 3.flatMap和Map需要引用的隐式转换
import org.apache.flink.api.scala._
// 4.进行计算
val dataStream: DataStream[(String, Int)] = textDstream.flatMap(_.split("\\s")).filter(_.nonEmpty).map((_, 1)).keyBy(0).sum(1)
// 5.打印、设置并行度
dataStream.print().setParallelism(1)
// 6.启动executor,执行任务
env.execute("Socket stream word count")
代码解读:
1.创建执行环境(Environment)
和spark需要创建SparkContext一样,flink也需要创建一个ExecutionEnvironment。创建ExecutionEnvironment有三种方式,后面详细说。
2.创建流(Source)
根据数据源的不同,可能返回DataStream和DataSet两种数据类型。DataStream是流式数据,DataSet是类似sparkStreaming的批处理数据。数据源可以是文本文件、kafka、redis等,也可以是自定义的source,后面也会详细说。
3.导入隐式转换
DataStream是没有map、flatmap等方法的,需要导入隐式转换。
4.调用flatmap、map、keyby等算子进行计算(Transform)
其中flatmap、map、filter、keyby称为转化算子,sum则是滚动聚合算子(Rolling Aggregation)。需要注意的是:keyby所返回的数据不是DataStream,而是keyedStream,滚动聚合算子是针对keyedStream的每一个支流做聚合计算,DataStream是无法调用滚动聚合算子的。滚动聚合算子除了sum以外,还有min、max、minBy、maxBy。flink也可以自定义UTF函数,还有富函数、底层API、窗口与时间语义等后面都会详细说。
5.打印并设置并行度(Sink)
经过处理后的数据可以在sink阶段传递给不同地方,如控制台打印、kafka、mysql等。
另外,flink的每个算子后面都可以设置并行度,根据并行度以及API,会生成ExecutionGraph。flink中的执行图分为四层:StreamGraph -> JobGraph -> ExecutionGraph -> 物理执行图。本文不涉及这部分,后续在另一篇谈flink架构与特性中会说明。
6.启动任务
flink任务在环境对象调用execute方法时才会开始运行,参数为任务名。
Flink流处理API
一、Environment
1.两种Environment
Environment是Flink程序的入口,流处理和批处理的Environment是不同的,分别通过StreamExecutionEnvironment和ExecutionEnvironment来获得。
2.获取Environment的三种方式
方式一:getExecutionEnvironment
这种方式会根据程序运行环境自动获取ExecutionEnvironment。如果程序是独立调用的,则此方法返回本地执行环境;如果从命令行客户端调用程序以提交到集群,则此方法返回此集群的执行环境。是最常用的一种创建执行环境的方式。
//获取批处理执行环境
val env: ExecutionEnvironment = ExecutionEnvironment.getExecutionEnvironment
//获取流处理执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
方式二:createLocalEnvironment
返回本地执行环境,需要在调用时指定默认的并行度。
//获取本地执行环境,并设置并行度为1
val env = StreamExecutionEnvironment.createLocalEnvironment(1)
方式三:createRemoteEnvironment
返回集群执行环境,将Jar提交到远程服务器。需要在调用时指定JobManager的IP和端口号,并指定要在集群中运行的Jar包。
//获取集群环境
val env = ExecutionEnvironment.createRemoteEnvironment("jobmanage-hostname", port,"YOURPATH//wordcount.jar")
二、Source
source即是flink程序所处理的数据的来源,可以是一个集合(list)、文本文件、kafka等,也可以自定义source。
1.从集合中获取数据
//创建执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
//从集合中获取流
val stream1 = env.fromCollection(List("good","bey","word"))
2.从文本中获取流
//从文本中获取流,直接传入文件位置即可
val stream2 = env.readTextFile("FILE_PATH")
3.从kafka中获取流
从kafka中获取流,需要在pom中添加flink链接kafka的连接器依赖。
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka-0.11_2.11</artifactId>
<version>1.7.2</version>
</dependency>
在具体代码中,需要创建一个Properties对象用于传递kafka集群的相关信息及可选配置,用于实例化flink-kafka连接器,然后通过在addSource中传入连接器对象来获取kafka数据流。
//创建Properties对象,并设置相关参数
val properties = new Properties()
properties.setProperty("bootstrap.servers", "localhost: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")
//在addSource中传入连接器对象,获取流
val stream3 = env.addSource(new FlinkKafkaConsumer011[String]("kafka_test", new SimpleStringSchema(), properties))
3.自定义source
从获取kafka流的代码中就可以看出,获取流可以通过调用执行环境对象的addSource方法来获取,且addSource方法的参数是一个SourceFunction。所以,只要自定义实现SourceFunction,就可以实现自定义source。
具体实现步骤如下:
1.创建运行标记、重写cancel方法和run方法。
2.在run方法中使用SourceContext的collect方法返回生成的数据。
class MySensorSource extends SourceFunction[String]{
// flag: 表示数据源是否还在正常运行
var running: Boolean = true
//重现取消source方法,使其可以关闭
override def cancel(): Unit = {
running = false
}
//重写run方法,run方法是source生成数据的主要方法
//SourceFunction.SourceContext[String]的泛型与SourceFunction一致,可以是自定义的类,此处为String
override def run(ctx: SourceFunction.SourceContext[String]): Unit = {
// 初始化一个随机数发生器
val rand = new Random()
while(running){
// 生成随机数,作为ID
var str = "ID" + rand.nextInt()
// 获取当前时间戳,拼接ID
val curTime = System.currentTimeMillis()
var str = str + curTime
//返回给上下文环境
ctx.collect(str )
Thread.sleep(100)
}
}
}
source生成的数据可以是自定义的类,这样可以更细粒度、更方便、更明确地进行计算,以上demo生成的是String,如果要生成自定义类型的数据,只需要指定代码中的两个泛型即可。
三、Transform
Transform阶段可以通过flink自带的算子和自定义的函数进行计算,下面对flink常用算子进行介绍。
1.转换算子
map
对每条数据进行转换,与spark中的map一致(以下flatmap和filter也是,掌握spark的可直接跳过)。
//将每条数据都乘以2
val streamMap = stream.map { x => x * 2 }
flatmap
将每条数据通过一定逻辑进行展开,并分割成多个数据。如:
//将每条数据进行分割,分割符尾空格
val streamFlatMap = stream.flatMap{
x => x.split(" ")
}
filter
过滤,对每一条数据进行判断,返回结果为true的就留下,否则过滤掉。如:
//将每条数据对3取摩,留下哈希值等于1的数据
val streamFilter = stream.filter{
x => x % 3 == 1
}
keyBy
将一个流在逻辑上拆分成不相交的分区,每个分区包含具有相同key的元素,在内部以hash的形式实现的。但是实际上还是一个流,只是相当于对每条数据打上标签而已。DataStream调用keyBy后,返回的是KeyedStream。
keyBy的参数可以指定按照哪个关键字或者元组的哪个位置的数据进行keyed。如:
// Key by field "someKey"
dataStream.keyBy("someKey")
// Key by the first element of a Tuple(数组)
dataStream.keyBy(0)
KeyedStream可以调用滚动聚合算子对key相同的数据进行计算,之后可以通过reduce算子返回一个计算后的DataStream。
滚动聚合算子
这些算子可以针对KeyedStream的每一个支流做聚合。
- sum():计算分区内指定列或属性的总和。
- min():找出分区中最小的值。
- max():找出分区中最大的值。
- minBy():与min不同的是,min只能找出最小的值,而minBy则是可以找出最小值的整条数据。
- maxBy():与max不同的是,max只能找出最小的值,而maxBy则是可以找出最大值的整条数据。
reduce
一个分组数据流的聚合操作,合并当前的元素和上次聚合的结果,产生一个新的值,返回的流中包含每一次聚合的结果,而不是只返回最后一次聚合的最终结果。
也就是分别对应于keyBy、window/timeWindow 处理后的数据,根据ReduceFunction将元素与上一个reduce后的结果合并,产出合并之后的结果。
如文章开头的WordCount可以改为用reduce实现:
val dataStream: DataStream[(String, Int)] = textDstream.flatMap(_.split("\\s")).filter(_.nonEmpty).map((_, 1)).keyBy(0).reduce((x,y)=>(x._0, x._0 + y._0))
其中x是上一批次的结果,y是当前数据。
Split和Select
Split是把DataStream转换成SplitStream。可以把一个流在逻辑上拆分成多个流,与keyBy一样,经过split后的流会转化为SplitStream,只是在逻辑上分为了多个而已,实际上仍是一个流,可以通过Select选择分支流,来将流彻底地分开。
select可以选中SplitStream中的某个支流,并返回该流。
如:将user类型的数据流根据user的ID分为奇数ID和偶数ID
//根据ID进行split,对流中每条数据进行判断,splitFunction的返回值必须是一个可迭代对象,且返回值就是支流的标签。
val splitStream = stream2
.split( user => {
if (user.id % 2 == 1){
Seq("Odd ")
} else{
Seq("Even")
}
} )
//通过select取出打上Odd标签的支流
val odd = splitStream.select("Odd ")
//通过select取出打上Even标签的支流
val even= splitStream.select("Even")
通过select取出打上Odd和Even标签的支流
val all = splitStream.select("Odd ", "Even")
Connect和CoMap
Connect可以把两个且只能是两个DataStream合并成一个ConnectedStream,而在该ConnectedStream内部,仍是两个相互独立的DataStream,两个DataStream的数据类型可以不一致。
所以ConnectedStream的map、flatmap算子都有两个参数,分别是处理ConnectedStream内部两个DataStream的mapFunction或flatFunction,且两个流之间是可以共享状态的。
//从kafka的不同topic中获取两个流
val stream1 = env.addSource(new FlinkKafkaConsumer011[String]("test1", new SimpleStringSchema(), properties))
val stream2 = env.addSource(new FlinkKafkaConsumer011[String]("test2", new SimpleStringSchema(), properties))
//connect
val connected = stream1.connect(stream2 )
//ConnectedStreams的map需要传递两个function
val coMap = connected.map(
stream1=> (stream1._1, stream1._2),
stream2 => (stream2._1, stream2._2)
)
Union
union是把两个或两个以上的DataStream真正合并成一个DataStream,但是DataStream的数据类型必须一致。
//从kafka的不同topic中获取两个流
val stream1 = env.addSource(new FlinkKafkaConsumer011[String]("test1", new SimpleStringSchema(), properties))
val stream2 = env.addSource(new FlinkKafkaConsumer011[String]("test2", new SimpleStringSchema(), properties))
//union
val stream3 = stream1 .union(stream2 )
connect和union的区别
1.connect只能合并两个流,union可以将两个或两个以上的流进行合并。
2.connect可以将数据类型不一致的流进行合并,union只能合并数据类型一致的流。
3.connect合并后,得到的是ConnectedStream,union合并后得到的仍是DataStream。ConnectedStream内部仍是两个流,可以对其分别调用不同的transformat算子进行转换,且两个流共享状态,也就是说两个流之间的计算结果是可以相互依赖的。
四、UDF函数类
在之前调用流的算子时,可以发现每个转换算子除了可以传入一个方法对象以外,还可以传入一个类对象,如:
不同算子可以传入的类对象也是不同的,如map算子对应MapFunction类,fliter算子对应FilterFunction。这些类即UDF函数类,通过UDF函数类可以更细粒度地完成转换操作。
UDF函数类分为两种:普通函数类(Function Classes)和富函数类(Rich Functions)。
每个算子都有一个对应的函数接口和富函数接口,自定义UDF函数只需实现接口并重写其中的方法即可。
两种函数的区别为:普通函数类只需实现一个算子对应的方法,如MapFunction实现map方法,FilterFunction实现filter方法等。而富函数类除了算子方法外,还需要实现open方法、close方法、getRuntimeContext等具有生命周期特征的方法。
普通函数类
以map算子为例,对应的UDF函数抽象类为MapFunction,实现该接口,并重写map方法:
class UdfTest extends MapFunction[String]{
override def map(value: String): O = ???
}
UDF函数类与直接传入UDF函数方法的优点在于:函数方法会被每条数据调用一次;函数类只会实例化一次,每条数据调用的是函数类的map方法。所以一些只需创建一次,但每条数据计算都会使用到的变量或对象,就可以在创建函数类时创建,如jdbc等。
富函数
富函数可以说是同时实现了RichFunction和Function接口。如RichMapFunction继承了AbstractRichFunction抽象类,并实现了MapFunction,而AbstractRichFunction又实现了RichFunction接口,MapFunction实现了Function接口。生命周期方法就是在RichFunction中被定义的
public abstract class RichMapFunction<IN, OUT> extends AbstractRichFunction implements MapFunction<IN, OUT> {
private static final long serialVersionUID = 1L;
@Override
public abstract OUT map(IN value) throws Exception;
}
富函数的生命周期方法:
open():函数的初始化方法。在实际工作方法之前调用,因此适合一次性设置工作。如初始化一个连接器。
close():在最后一次调用主工作方法之后调用的,此方法可用于清理工作。如资源回收。
getRuntimeContext():获取RuntimeContext对象。
getIterationRuntimeContext():获取IterationRuntimeContext对象,多个RuntimeContext数量,等于并行度。
setRuntimeContext():设置函数的运行时上下文。在创建函数的并行实例时由框架调用。