Flink程序是常规程序,可对分布式集合进行转换(例如,过滤,映射,更新状态,联接,分组,定义窗口,聚合)。集合最初是从源创建的(例如,通过读取文件,kafka主题或本地内存中的集合)。结果通过接收器返回,接收器可以将数据写入(分布式)文件或标准输出(例如,命令行终端)。Flink程序可以在各种上下文中运行,独立运行或嵌入其他程序中。执行可以在本地JVM或许多计算机的群集中进行。
根据数据源的类型(即有界或无界源),您将编写批处理程序或流程序,其中DataSet API用于批处理,而DataStream API用于流。本指南将介绍两个API共有的基本概念,但是请参考流式传输指南 和 批处理指南,以获取有关使用每个API编写程序的具体信息。
Flink具有特殊的类DataSet,DataStream用于表示程序中的数据。可以将它们视为包含重复项的不可变数据集合。DataSet数据是有限的,对于DataStream许多元素,它们可以是无限的。
这些集合在某些关键方面与常规Java集合不同。首先,它们是不可变的,这意味着一旦创建它们就不能添加或删除元素。也不能简单地检查其中的元素。
集合最初通过Flink程序添加源创建或从别的集合转换而来,通过使用API方法转化它们衍生的map,filter等等。
Flink程序看起来像转换数据集合的常规程序。每个程序都包含相同的基本部分:
请注意,在包org.apache.flink.api.java中可以找到Java DataSet API的所有核心类, 而在org.apache.flink.streaming.api中可以找到Java DataStream API的类 。
这StreamExecutionEnvironment是所有Flink程序的基础。可以使用以下静态方法获得一个StreamExecutionEnvironment:
getExecutionEnvironment()
createLocalEnvironment()
createRemoteEnvironment(String host, int port, String... jarFiles)
通常,只需要使用getExecutionEnvironment(),因为这将根据上下文执行正确的操作:如果是在IDE中执行程序或作为常规Java程序执行,它将创建一个本地环境,该环境将在本地计算机上执行程序。如果是从程序创建的JAR文件,并通过命令行调用它 ,则Flink集群管理器将执行程序中的main方法,getExecutionEnvironment()并将返回用于在集群上执行程序的执行环境。
为了指定数据源,执行环境有几种使用各种方法从文件读取的方法:可以逐行,以CSV文件的形式读取它们,或使用完全自定义的数据输入格式。要将文本文件读取为一系列行,可以使用:
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<String> text = env.readTextFile("file:///path/to/file");
这将提供一个DataStream,然后可以在其上应用转换以创建新的派生DataStream。
可以通过使用转换函数在DataStream上调用方法来应用转换。例如,转换如下所示:
DataStream<String> input = ...;
DataStream<Integer> parsed = input.map(new MapFunction<String, Integer>() {
@Override
public Integer map(String value) {
return Integer.parseInt(value);
}
});
通过将原始集合中的每个String转换为Integer,将创建一个新的DataStream。
一旦有了包含最终结果的DataStream,就可以通过创建接收器将其写入外部系统。这些只是创建接收器的一些示例方法:
writeAsText(String path)
print()
对于指定的完整程序,需要执行程序调用 execute()触发执行StreamExecutionEnvironment。根据ExecutionEnvironment执行类型的不同,执行将在本地计算机上触发或程序提交的集群上执行。
该execute()方法返回一个JobExecutionResult,其中包含执行时间和累加器结果。
所有Flink程序都是延迟执行的:执行程序的main方法时,不会直接进行执行程序的数据加载和转换。而是将创建每个操作并将其添加到程序的计划中。当调用execute()显式触发执行时,实际上才会执行这些操作。程序是在本地执行还是在群集上执行取决于执行环境的类型。
懒加载使您可以构建复杂的程序,Flink将其作为一个整体计划的单元执行。
某些转换【join,coGroup,keyBy,groupBy】要求在dataSet上定义键。其他转换【Reduce,GroupReduce,Aggregate,Windows】允许在应用键之前将数据分组。
dataSet分组操作:
DataSet<...> input = // [...]
DataSet<...> reduced = input.groupBy(/*define key here*/)
.reduceGroup(/*do something*/);
dataStream分组操作:
DataStream<...> input = // [...]
DataStream<...> windowed = input.keyBy(/*define key here*/)
.window(/*window specification*/);
Flink的数据模型不是基于键值对。因此,无需将数据集类型实际打包到键和值中。键是“虚拟的”:将它们定义为对实际数据的功能,以指导分组操作。
1.元组键值对操作
最简单的情况是在元组的一个或多个字段上对元组进行分组。
1.1根据元组的第一个字段(整数类型之一)进行分组。
DataStream<Tuple3<Integer,String,Long>> input = // [...]
KeyedStream<Tuple3<Integer,String,Long>,Tuple> keyed = input.keyBy(0)
1.2将元组分组在由第一字段和第二字段组成的复合键上。
DataStream<Tuple3<Integer,String,Long>> input = // [...]
KeyedStream<Tuple3<Integer,String,Long>,Tuple> keyed = input.keyBy(0,1)
1.3嵌套元组
DataStream<Tuple3<Tuple2<Integer, Float>,String,Long>> ds;
指定keyBy(0)将导致系统将完整字符Tuple2用作键(以Integer和Float作为键)。如果要指向到嵌套中Tuple2,则必须使用字段表达式匹配键。
1.4字段表达式定义键
可以使用基于字符串的字段表达式来引用嵌套字段,并定义用于分组,排序,联接或联合分组的键。
字段表达式使选择(嵌套)复合类型(例如Tuple和POJO类型)中的字段变得非常容易。
在下面的示例中,我们有一个WCPOJO,其中有两个字段“ word”和“ count”。要按字段分组word,我们只需将其名称传递给keyBy()函数即可。
case class WC(word : String, num : Int) // 样例类
val counts = text.flatMap(_.split(" ").filter(_.nonEmpty))
.map(WC(_, 1))
.groupBy("word")//根据第word进行分组
.sum("num") // 分组求和
.setParallelism(1) // 设置并行度
.sortPartition("num", Order.DESCENDING) // 降序排序
通过字段名称选择POJO字段。例如,"user"引用POJO类型的“用户”字段。
通过其字段名称或字段索引选择元组字段。
可以在POJO和元组中选择嵌套字段。使用"_“或”."。例如,“user.zip"引用存储在POJO类型的“用户”字段中的POJO的“ zip”字段。支持POJO和元组的任意嵌套和混合,例如”_2.user.zip"或"user._4.1.zip"。
字段表达式示例:
class WC(var complex: ComplexNestedClass, var count: Int) {
def this() { this(null, 0) }
}
class ComplexNestedClass(
var someNumber: Int,
someFloat: Float,
word: (Long, Long, String),
hadoopCitizen: IntWritable) {
def this() { this(0, 0, (0, 0, ""), new IntWritable(0)) }
}
大多数转换算子需要用户定义具体的功能。本节列出了如何指定它们的不同方法:
1.filter
.filter(_.endsWith("k")) // 过滤
val counts = text.flatMap(_.split(" ").filter(_.nonEmpty))
.map(_ => 1)
.reduce{(x, y) => x + y}
class MyMapFunction extends RichMapFunction[String, Int] {
def map(in: String):Int = { in.length }
}
应用:
val counts = text.flatMap(_.split(" ").filter(_.nonEmpty))
.map(new MyMapFunction())
.reduce{(x, y) => x + y}
执行结果:
也可以定义为匿名类:
val counts = text.flatMap(_.split(" ").filter(_.nonEmpty))
.map(new MyMapFunction())
.map (new RichMapFunction[Int, Int] { // 匿名类
def map(in: Int):Int = { in + 1 }
})
.reduce{(x, y) => x + y}
执行结果:
Flink对可以在DataSet或DataStream中的元素类型设置了一些限制。原因是系统分析类型以确定有效的执行策略。数据类型有七种不同的类别:
DataStream<Tuple2<String, Integer>> wordCounts = env.fromElements(
new Tuple2<String, Integer>("hello", 1),
new Tuple2<String, Integer>("world", 2));
wordCounts.map(new MapFunction<Tuple2<String, Integer>, Integer>() {
@Override
public Integer map(Tuple2<String, Integer> value) throws Exception {
return value.f1;
}
});
wordCounts.keyBy(0); // also valid .keyBy("f0")
如果Flink将Java和Scala类满足以下要求,则它们被视为特殊的POJO数据类型:
POJO样例类:
case class WordWithCount(word : String, count : Int){
def this(){
this(null, 0)
}
}
执行代码:
val input = execution.fromElements(
new WordWithCount("hello", 1),
new WordWithCount("word", 2))
val result = input
.groupBy("word")
.sum("count") // 分组求和
.setParallelism(1) // 设置并行度
.sortPartition("count", Order.DESCENDING) // 降序排序
result.print()