DataSet/DataStream
Flink中定义两个特殊类来代表数据:DataSet、DataStream,**两者与Java中的集合不同,它们是不可变的,也就是一旦被创建其中的数据不能被增添或者删除,同时它们允许数据可重复。**如果你使用如map、filter等算子,那么新的DataSet/DataStream就在每使用transformations算子之后就会被创建。
程序流程
Flink的程序编写极为规律,每个程序都包含以下部分,这里以WordCount举例,示例数据内容如下:
hadoop,hadoop,hadoop
flink,flink
spark
获取Execution Environment
//流处理
val env = StreamExecutionEnvironment.getExecutionEnvironment
//批处理
val env = ExecutionEnvironment.getExecutionEnvironment
虽然官方提供以下三类获取Environment,但是通常只需要使用getExecutionEnvironment(),它会根据环境来正确地的创建Environment,如果程序执行在IDE编辑器或者作为常规的Java程序执行,它就会在你机器上创建一个本地的Environment;如果使用命令行方式调用Jar包,Flink cluster manager就会执行对应的main方法同时getExecutionEnvironment()就会返回用于集群环境的Environment。
getExecutionEnvironment()
createLocalEnvironment()
createRemoteEnvironment(host: String, port: Int, jarFiles: String*)
加载/创建数据(Source)
如果你想读取文件数据可以用以下方法:
val ds = env.readTextFile("input/wc.txt")
指定Transformations算子
每个transformation算子都会产生新的DataSet/DataStream,以下就是WordCount统计所用到算子:
//wordcount统计
val result = ds.flatMap(_.split(","))
.map((_, 1))
.keyBy(0)
.sum(1)
指定计算之后结果输出到哪里(Sink)
print()方法是让结果打印在控制台以便观察,你也可以使用writeAsText(path: String)来写入文件中。
result.print()
触发
这里需要提醒的是,只有流处理需要显式触发,而批处理只需要指定输出即可。
env.execute(this.getClass.getSimpleName)
⚠️注意:以上操作不会像Java程序一样在main方法被启动之后立刻执行相关操作,而是将每个操作添加在Flink维护的有向无环图(简称DAG)中,只有当被触发之后整个DAG才开始执行,这也就是Lazy Evaluation(懒加载)。
**
并行度Parallel
提出问题
如果你按照上述WordCount例子运行的话,你应该能看到以下输出内容。很明显与Spark的不太一样,为什么会出现(flink,1)、(hadoop,1)等输出内容呢?原因是Flink对比Spark来说,数据不会以批次处理,而是真正的实时处理,当第一个hadoop被加载立即输出(hadoop,1),等到第二个hadoop到达时就变成(hadoop,2),以此类推。
11> (hadoop,1)
11> (hadoop,2)
11> (hadoop,3)
1> (spark,1)
10> (flink,1)
10> (flink,2)
不知道你是否注意到结果之前的数字,比如11、1、10,这些代表什么?为什么hadoop前面会是11,spark前面是1,flink前面是10?要搞清楚这些问题就必须先了解Flink中的并行度。
什么是并行度
前一篇介绍了Flink是分布式且任务并行执行的计算引擎,在执行过程中,数据流会有一个或者多个分区,每个操作Task也会有一个或者多个子操作Task,这里的并行度也就是指定的操作有多少个子操作Task,同一个代码中每个操作的并行度都不一定相同。
Flink中有两类操作:one-to-one和redistributing都与并行度有关:
- One-to-one
例如Source与map()之间,保留数据的顺序和分区,类似与Spark的窄依赖。
- Redistributing
例如map()和keyBy/window、keyBy/window和Sink之间数据重新分配到不同子Task上。不同算子会有不同分配方式:keyBy() 通过Key值来Hash进行重新分配、broadcast()和rebalance()则是随机分配。
回答问题
回到之前的问题,输出结果之前的数字其实就是线程号,经过keyBy()重新分配之后同一key都在一个线程中处理。如果不想看到这些线程号,只需要在print()后面加上setParallelism(1)就行。其实有很多中设置并行度方法,之后系列文章会详细写一部分。
多种指定Key的方式
一些transformations算子,例如:join, coGroup, keyBy, groupBy需要使用者指定Key,以便之后的Reduce, GroupReduce, Aggregate, Windows等算子计算。
Tuples指定Key
上方WC例子就是在Tuple中通过index来指定Key的,代码如下:
val result = ds.flatMap(_.split(","))
.map((_, 1))
.keyBy(0)
.sum(1)
⚠️需要注意的是,如果你有个如下所示的DataStream,key(0)则是指的其中的Tuple2<Integer,Float>,如果你想指定Tuple2中的Integer,可以使用类似于_._1._1。
DataStream<Tuple3<Tuple2<Integer, Float>,String,Long>> ds;
字段指定Key
这个是基于字段名来指定Key,特别适合嵌套类型,例如Tuple和POJO。首先得定义一个WC POJO来接受数据,建议使用case class,代码如下:
case class WC(word:String,count:Int)
input.flatMap(_.split(","))
.map(ele => {
WC(ele, 1)
})
.keyBy("word")
.sum("count")
Key Selector方法指定Key
Key Selector方法通过使用者将数据作为输入,并返回所想要的Key,代码如下:
input.flatMap(_.split(","))
.map(x => {
WC(x, 1)
})
.keyBy(_.word)
.sum("count")
Flink支持的数据类型
- Java Tuples and Scala Case Classes
- Java POJOs
_ 1. 类必须为public_
_ 2. 必须含有无参构造函数_
_ 3. 有所有属性的get/set方法_
_ 4. 支持序列化_
- Primitive Types:Flink支持所有Java和Scala原生类型,例如:Integer,String和Double
- Regular Classes:支持大部分Java和Scala的API和类,除开无法序列化的,例如IO流、文件指针等
- Values:手动序列化和反序列化,通过实现org.apache.flinktypes.Value接口中的read和write方法
- Hadoop Writables:实现org.apache.hadoop.Writable接口的类型
- Special Types:Scala的Either,Option和Try
多种实现Transformation方法
Lambda Functions
就像前面的例子一样,使用Lambda表达式来实现自己的Transformation方法。
env.readTextFile("input/access.log")
.map(x => {
val splits = x.split(",")
(splits(0).toLong,splits(1),splits(2).toInt)
})
.filter(_._3 > 5000)
.print()
Rich Functions
所有的Transformation方法都有对应的Rich方法,例如map就对应RichMapFunction。Rich Function不仅让使用者自己实现相关方法(例如map、filter等),还提供生命周期方法(例如open、close)以及RunTimeContext用来传递参数、广播变量等。
//两种方式:匿名内部类以及创建继承相关Rich的新类
//注意map和filter
env.readTextFile("input/access.log")
.map(new RichMapFunction[String,Access] {
//setup methods
override def open(parameters: Configuration): Unit = {
println("~~~~~~~~~open~~~~~~~~~~~~")
super.open(parameters)
}
override def map(input: String): Access = {
val splits = input.split(",")
Access(splits(0),splits(1),splits(2).toInt)
}
//teardown methods
override def close(): Unit = super.close()
})
.filter(new MyFilterFun)
.print().setParallelism(1)
class MyFilterFun extends RichFilterFunction[Access]{
override def filter(input: Access): Boolean = {
input.flow > 5000
}
}
累加器
Flink内置两类累加器,这些都是实现Accumulator接口:
如何使用
第一步,在你自己实现的transformation方法中创建一个累加器对象;
第二步,在Rich Function的open方法中注册累加器;
第三步,使用累加器;
第四步,使用JobExecutionResult对象查看累加器结果,代码如下:
//利用累加器来统计多少不符合标准的数据
env.readTextFile("input/access.txt")
.map(new RichMapFunction[String, Access] {
//创建累加器
private val total = new IntCounter()
private val wrong = new IntCounter()
override def open(parameters: Configuration): Unit = {
//注册累加器
getRuntimeContext.addAccumulator("num_total", total)
getRuntimeContext.addAccumulator("num_wrong", wrong)
}
override def close(): Unit = super.close()
override def map(input: String): Access = {
//使用累加器
total.add(1)
try {
val splits = input.split(",")
Access(splits(0), splits(1), splits(2).toInt)
} catch {
case e: Exception => {
wrong.add(1)
null
}
}
}
})
//查看结果
val myJobExecutionResult = env.execute(this.getClass.getSimpleName)
val resultTotal = myJobExecutionResult.getAccumulatorResult[Int]("num_total")
val resultWrong = myJobExecutionResult.getAccumulatorResult[Int]("num_wrong")
println("total num is " + resultTotal + ",wrong num is " + resultWrong)
上文所涉及到的示例代码以及数据皆已上传到Github上,如有需要请直接clone到本地直接运行,🔗链接如下:
github.com/liverrrr/fl…
上文如有错误或是纰漏,👏欢迎各位下方评论指出,大家一起交流学习📖