Flink编程模型
内容摘自《Flink原理、实战与性能优化》
文章目录
一.数据集类型
根据数据产生方式和数据产生是否含有边界(具有起始点和终止点)角度,将数据分为两种类型的数据集,一种是有界数据集,另外一种是无界数据集。
1.有界数据集
有界数据集具有时间边界,在处理过程中数据一定会在某个时间范围内起始和结束。对有界数据集的数据处理方式被称为批计算(Batch Processing)。
2.无界数据集
对于无界数据集,数据从开始生成就一直持续不断地产生新的数据,因此数据是没有边界的,例如服务器的日志、传感器信号数据等。对无界数据集的数据处理方式被称为流式数据处理,简称为流处理(Streaming Process)。
3.统一数据处理
有界数据集和无界数据集只是一个相对的概念,主要根据时间的范围而定,可以认为一段时间内的无界数据集其实就是有界数据集,同时有界数据集也可以通过一些方法转换为无界数据。
二.Flink编程接口
Flink根据数据集类型的不同将核心数据处理接口分为两大类,一类是支持批计算的接口DataSet API,另外一类是支持流计算的接口DataStreaming API。
同时Flink将数据处理接口抽象成四层,由上向下分别为SQL API、Table API、DataStreaming API/DataSet API以及Stateful Stream API。
(1) Table API
Table API将内存中的DataStream和DataSet数据集在原有的基础之上增加Schema信息,将数据类型统一成表结构,然后通过Table API提供的接口处理对应的数据集。SQL API则可以直接查询Table API中注册表中的数据表。
(2)DataStreaming API 和 DataSet API
DataStreaming API和DataSet API主要面向具有开发经验的用户,用户可以使用DataStreaming API处理无界流数据,使用DataSet API处理批量数据。
(3)Stateful Stream Process API
Stateful Stream Process API是Flink中处理Stateful Stream最底层的接口,用户可以使用Stateful Stream Process接口操作状态、时间等低层数据。
三.Flink程序结构
整个Flink程序一共分为5步,分别为1.设定Flink执行环境、2.创建和加载数据集、3.对数据集指定转换操作逻辑、4.指定计算结果输出位置、5.调用execute方法触发程序执行。对于所有Flink应用程序都基本含有这5个步骤。(下文中的所有代码使用的是scala语言)
1.Execution Environment
运行Flink程序的第一步就是获取相应的执行环境,执行环境决定了程序在什么环境(例如本地运行环境或者集群运行环境)中。同时不同的运行环境决定了应用的类型,比如StreamExecutionEnvironment是用来做流处理环境,ExecutionEnvironment是批处理环境。
//第一步:设定执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
2.初始化数据
创建完成ExecutionEnvironment后,需要将数据引入到Flink系统中。ExecutionEnvironment提供不同的数据接入接口完成数据的初始化,将外部数据转换成DataStream 或 DataSet 数据集。如一下代码所示,通过调用readTextFile()方法读取file:///pathfile路径中的数据并转换成DataStream数据集。
//第二步:指定数据源地址,读取输入数据
val text:DataStream[String] = env.readTextFile("file:///path/file")
3.执行转换操作
数据从外部系统读取并转换成DataStream或者DataSet数据集后,下一步就将对数据集进行各种转换操作。Flink中的Transformation操作都是通过不同的Operator来实现,每个Operator内部通过实现Function接口完成数据处理逻辑的定义。在DataStream API和DataSet API提供了大量的转换算子,例如map、flatMap、filter、keyBy等,用户只需要定义每种算子执行的函数逻辑,然后应用在数据转换操作Operato接口中即可。如下代码实现了对输入的文本数据集通过FlatMap算子转换成数组,然后过滤非空字段,将每个单词进行统计,得到最后的词频统计结果。
//第三步:对数据集指定转换操作逻辑
val counts:DataStream[(String,Int)] = text
.flatMap(_.toLowerCase.split(" "))//执行FlatMap转换操作
.map((_,1)) //执行map转换操作,转换成key-value接口
.keyBy(0) //按照指定key对数据重分区
.sum(1) //执行求和运算操作
4.指定计算结果输出位置
- 在DataStream数据经过不同的算子转换过程中,某些算子需要根据指定的key进行转换,常见的有join,coGroup,groupBy类算子,需要先将DataStream和DataSet数据集转换成相应的KeyedStream和GroupedDataSet,主要目的是将相同key值的数据路由到相同的Pipeline中,然后进行下一步的计算操作。
分区所使用的key可以通过两种方式指定:
(1)根据字段位置指定
在DataStreaming API中通过KeyBy()方法将DataStream数据集根据指定的key转换成重新分区的KeyedStream,如以下代码所示,对数据集按照相同key进行sum()聚合操作。
val dataStream:DataStream[(String,Int)] = env.fromElements(("a",1),("c",2))
//根据第一个字段重新分区,然后对第二个字段进行求和运算
val result = dataStream.keyby(0).sum(1)
(2)根据字段名称指定
使用字段名称需要DataStreaming中的数据结构类型必须是Tuple类或者POJOs类的。如以下代码所示,通过指定name字段名称来确定groupby的key字段。
val personDataSet = env.fromElements(new Person("Alex",18),new Person("Peter",43))
//指定name字段名称来确定groupby字段
personDataSet.GroupBy("name").max(1)
//如果程序中使用Tuple数据类型,通常情况下字段名称从1开始计算,字段位置索引从0开始计算
//以下代码中两种方式是等价的
val personDataStream = env.fromElements(("Alex",18),("Peter",43))
//通过名称指定第一个字段名称
personDataStream.keyBy("_1")
//通过位置指定第一个字段
personDataStream.keyBy(0)
-
数据集经过转换操作后,形成最终的结果数据集,一般需要将数据集输出在外部系统中或者输出在控制台之上。以下示例调用DataStream API中的writeAsText()和print()方法将数据集输出在文件和客户端中。
//将数据输出到文件中 counts.writeAsText("filepath") //将数据输出控制台 counts.print()
5.调用execute方法触发程序执行
所有的计算逻辑全部操作定义好之后,需要调用ExecutionEnvironment的execute()方法来触发应用程序的执行,其中execute()方法返回的结果类型为JobExecutionResult,里面包含了程序执行的时间和累加器等指标。
//调用StreamExecutionEnvironment的execute方法执行流式应用程序
env.execute("App Name")
四.Flink数据类型
-
数据类型支持
Flink支持非常完善的数据类型,数据类型的描述信息都是由TypeInformation定义,比较常用的TypeInformation由BasicTypeInfo、TupleTypeInfo、CaseClassTypeInfo、PojoTypeInfo类等。TypeInformation主要作用是为了在Flink系统内有效地对数据结构类型进行管理,能够在分布式计算过程中对数据的类型进行管理和推断。
-
1.原生数据类型
- Flink通过实现BasicTypeInfo数据类型,能够支持任意Java原生基本类型(装箱)或String类型,例如Integer、String、Double等,如以下代码所示,通过从给定的元素集中创建DataStream数据集。
//创建Int类型的数据集 val intStream:DataStream[Int] = env.fromElements(3,1,2,1,5) //创建String类型的数据集 val stringStream:DataStream[String] = env.fromElements("hello","flink")
- Flink实现另外一种TypeInformation是BasicArrayTypeInfo,对应的是Java基本类型数组(装箱)或String对象的数组,如下代码通过使用Array数组和List集合创建DataStream数据集。
//通过从数组中创建数据集 val IntStream:DataStream[Int] = env.fromCollection(Array(3,1,2,1,5)) //通过List集合创建数据集 val IntStream:DataStream[Int] = env.fromCollection(List(3,1,2,1,5))
-
2.Java Tuples类型
通过定义TupleTypeInfo来描述Tuple类型数据,Flink在Java接口中定义了元组(Tuple)供用户使用。Flink Tuples是固定长度固定类型的Java Tuple实现,不支持空值存储。目前支持任意的Flink Java Tuple类型字段数量上限为25,如果字段数量超过上限,可以通过继承Tuple类的方式进行拓展。如下代码所示,创建Tuple数据类型数据集。
//通过实例化Tuple2创建具有两个元素的数据集 val tupleStream2:DataStream[Tuple2[String,Int]] = env.fromElements(new Tuple2("a",1),new Tuple2("b",2))
-
3.Scala Case Class类型
Flink通过实现CaseClassTypeInfo支持任意的Scala Case Class,包括Scala tuples类型,支持的字段数量上限为22,,支持通过字段名称和位置索引获取指标,不支持存储空值。如下代码实例所示,定义WordCount Case Class数据类型,然后通过fromElements方法创建input数据集,调用keyBy()方法对数据集根据word字段重新分区。
//定义WordCount Case Class数据结构 case class WordCount(word:String,count:Int) //通过fromElements方法创建数据集 val input = env.fromElements(WordCount("hello",1),WordCount("world",2)) val keyStream1 = input.keyBy("word")//根据word字段为分区字段 val keyStream2 = input.keyBy(0)//也可以通过指定position分区
通过使用Scala Tuple创建DataStream数据集,其他的使用方式和Case Class相似。需要注意的是,如果根据名称获取字段,可以使用Tuple中的默认字段名称。
//通过scala Tuple创建具有两个元素的数据集 val tupleStream:DataStream[Tuple2[String,Int]] = env.fromElements(("a",1),("b",2)) //使用默认字段名称获取字段,其中_1表示tuple第一个字段 tupleStream.keyBy("_1")
-
4.POJOs类型
POJOs类可以完成复杂数据结构的定义,Flink通过实现PojoTypeInfo来描述任意的POJOs,包括Java和Scala类,包括Java和Scala类。再Flink中使用POJOs类可以通过字段名称获取字段,例如
dataStream.join(ohterStream).where("name").equalTo("personName")
对于用户做数据处理则非常透明和简单,如果在Flink中使用POJOs数据类型,需要遵循以下要求:
- POJOs类必须是Public修饰且必须独立定义,不能是内部类;
- POJOs类中必须含有默认空构造器;
- POJOs类中所有的Fields必须是Public或者具有Public修饰的getter和setter方法;
- POJOs类中的字段类型必须是Flink支持的。
代码如下所示:(java代码)
//定义Java Person类,具有public修饰符 public class Person{ //字段具有public修饰符 public String name; public int age; //具有默认空构造器 public Person(){ } public Person(String name,int age){ this.name = name; this.age = age; } }
定义好POJOs Class后,就可以在Flink环境中使用了,如下代码所示。POJOs类仅支持字段名称指定字段,如代码中通过Person name来指导keyBy字段。
//使用fromElements接口构建Person类的数据集 val personStream = env.fromElements(new Person("Peter",14),new Person("Linda",25)) //通过Person.name来指定keyby字段 personStream.keyBy("name")
Scala POJOs数据结构定义如下,使用方式与Java POJOs相同。
class Person(var name: String,var age:Int){ //默认空构造器 def this(){ this(null,-1) } }
-
5.Flink Value类型
Value数据类型实现了org.apache.flink.types.Value,其中包括read()和write()两个方法完成序列化和反序列化操作,相对于通用的序列化工具会有着比较高校的性能。目前Flink提供了内建的Value类型由IntValue、DoubleValue以及StringValue等,用户可以结合原生数据类型和Value类型使用。
-
6.特殊数据类型
在Flink中也支持一些比较特殊的数据类型,例如Scala中的List、Map、Either、Opiton、Try数据类型,以及Java中Either数据类型,还有Hadoop的Writable数据类型。
-
-
TypeInformation信息获取
通常情况下Flink都能进行正常的数据类型推断,并选择合适的serializers以及comparators。在某些情况下Flink不能直接做到数据类型推断,例如定义函数时如果使用到了泛型,JVM就会出现类型擦除的问题。在Scala API和Java API中,Flink分别使用了不同的方式重构了数据类型信息。
-
1.Scala API类型信息
Scala API通过使用Manifest和类标签,在编译器运行时获取类型信息,即使是在函数定义中使用了泛型,也不会像Java API出现类型擦除的问题,这使得Scala API具有非常精密的类型管理机制。
当使用ScalaAPI开发Flink应用,如果使用到Flink已经通过TypeInformation定义的数据类型,TypeInformation不会自动创建,而是使用隐式参数的方式引入,需要将TypeInformation类隐式参数引入到当前程序环境中,代码实例如下:
import org.apache.flink.api.scala._
-
2.Java API类型信息
由于Java的泛型会出现类型擦除问题,Flink通过Java反射机制尽可能重构类型信息,例如使用函数签名以及子类的信息等。同时类型推断在当输出类型依赖于输入参数类型时相对比较容易做到,但是如果函数的输出类型不依赖于输入参数的类型信息,这个时候就需要借助于类型提示**(Types Hint)**来告诉系统函数中闯入的参数类型信息和输出参数信息。如下代码通过returns方法中传入TypeHint< Integer >实例指定输出参数类型,帮助Flink系统对输出类型进行数据类型参数的推断和收集。
//定义Type Hint输出类型参数 DataStream<Integer> typeStream = input .flatMap(new MyMapFunction<String,Integer>()) .retyrbs(new TypeHint<Integer>(){});//通过returns方法指定返回参数类型 //定义泛型函数,输入参数类型为<T,O>,输出参数类型为O class MyMapFunction<T,O> implements MapFunction<T,O> { public void flatMap(T value,Collector<O> out){ //定义计算逻辑 } }
在使用Java API定义POJOs类型数据时,PojoTypeInformation为POJOs类中的所有字段创建序列化器,对于标准的类型,例如Integer、String、Long等类型是通过Flink自带的序列化器进行数据序列化,对于其他类型数据都是直接调用Kryo序列化工具来进行序列化。
如果用户想使用Kryo序列化工具来序列化POJOs所有字段,则在ExecutionConfig中调用enableForceKryo()来开启Kryo序列化
final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); env.getConfig().enableForceKyro();
如果默认的Kyro序列化类不能序列化POJOs对象,通过调用ExecutionConfig的addDefaultKryoSerializer()方法向Kryo中添加自定义的序列化器。
env.getConfig().addDefaultKryoSerializer(Class<?> type,Class<? extends Serializer<?>> serializerClass)
-
-
自定义TypeInformation
Flink提供了可插拔的TypeInformationFactory让用户将自定义的TypeInformation注册到Flink类型系统中。
五.批处理WordCount
package com.atguigu.wc
import org.apache.flink.api.scala.ExecutionEnvironment
import org.apache.flink.api.scala._
//批处理的WordCount
object WordCount {
def main(args:Array[String]): Unit={
//创建一个批处理的执行环境
val env:ExecutionEnvironment = ExecutionEnvironment.getExecutionEnvironment
//定义转换操作
//1.从文件中读取数据
val inputPath:String = "D:\\software\\IDEA\\work_space\\src\\main\\resources\\hello.txt"
val inputDataSet:DataSet[String] = env.readTextFile(inputPath)//scala具有自动类型推断,类型可写可不写
//2.对数据进行转换处理统计,先分词,再按照word进行分组,最后进行聚合统计
val resultDataSet:DataSet[(String,Int)] = inputDataSet.
flatMap(_.split(" "))
.map((_,1))//第一个元素为word,第二个为1
.groupBy(0)
.sum(1) //按照第一个元素作为Key进行分组,针对第二个元素进行求和统计
//打印输出
resultDataSet.print()
}
}
六.流处理的WordCount
package com.atguigu.wc
import org.apache.flink.api.java.utils.ParameterTool
import org.apache.flink.streaming.api.scala._
//流处理word Count
object StreamWordCount {
def main(args: Array[String]): Unit = {
//1.创建流处理的执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
//env.setParallelism(4)
//1.5 从外部命令中提取参数,作为socket主机名和端口号
val paramTool:ParameterTool = ParameterTool.fromArgs(args)
val host:String = paramTool.get("host")
val port:Int = paramTool.getInt("port")
//2.接收一个socket文本流
val inputDataStream = env.socketTextStream(host,port)
//3.进行转换处理统计
val resultDataStream = inputDataStream
.flatMap(_.split(" "))
.filter(_.nonEmpty)
.map((_,1))
.keyBy(0)
.sum(1)
/* 可以在每个计算过程中设置并行参数
比如:
val resultDataStream = inputDataStream
.flatMap(_.split(" "))
.filter(_.nonEmpty)
.map((_,1)).setParallelism(4)
.keyBy(0)
.sum(1)
一般用于读写操作,设置并行参数为1实现串行
*/
//4.打印输出
resultDataStream.print()
//5.启动一个进程监听事件(事件:数据到来)
env.execute("stream word count")
}
}