目录
1.DataSet and DataStream
2.Anatomy of a Flink Program(Flink程序剖析)
3.Lazy Evaluation(延迟执行)
4.Specifying Keys(key的定义)
1.Define keys for Tuples(元组键)
2.Define keys using Field Expressions(字段表达式键)
3.Define keys using Key Selector Functions(Key选择器函数)
5.Specifying Transformation Functions(转换函数)
1.实现接口
2.匿名内部类
3.Java 8 Lambdas
4.Rich functions
6.Flink数据类型
1.Tuples and Case Classes
2.POJOs
3.Primitive Types(基本类型)
4.Values
5.Hadoop Writables
6.Special Types(特殊类型)
7.Type Erasure & Type Inference(类型擦除和类型推断)
7.Accumulators & Counters(累加器和计数器)
Flink程序是实现分布式集合转换的常规程序(例如:filter,map,update state,join,group,window,aggregate)。集合最初是由source创建的(例如:读文件,kafka,本地文件,内存集合)。结果通过sink返回,例如,可以将数据写入分布式文件系统,标准输出(命令行终端)。Flink程序可以在各种各样的环境中运行,standalone,嵌入到其他程序等。可以在本地JVM中执行,也可以在集群中执行。
根据数据源的类型,分为有界或无界的source,可以编写一个批处理或流处理程序,其中DataSet API用于批处理,DataStream API用于流处理。本文档将介绍两种API常见的基本概念。
注意:在实际展示如何使用这些API的例子,我们将使用StreamingExecutionEnvironment 和DataStreamAPI。在DataSet API中概念完全相同,有ExecutionEnvironment 和DataSetAPI替代。
Flink有特殊的类DataSet和DataStream来表示程序中的数据。你可以将它们认为包含副本的不可变的数据集合。DataSet数据集是有界的,而DataStream数据元素是无界的。
这些集合在某些关键的方面与常规的Java集合不同。首先,它们是不可变的,这意味着一旦创建了它们,就不能添加或删除元素。也不能简单的检查内部的元素。一个集合最初是通过在Flink程序中添加一个source来创建的,而新的集合则是通过诸如map,filter等API方法来转换它们的。
Flink程序看起来像普通的数据集合转换程序。每个程序有相同的基本组成部分:
1、获取一个执行环境(ExecutionEnvironment)
2、加载或创建初始化数据(Load/create)
3、指定该数据的转换操作(transformation)
4、指定在存储的计算结果(sink)
5、触发程序执行(execute())
现在我们将对每一个步骤做一个概述,请参考相应部分以获得更多的详细信息。
请注意,Java DataSet API的所有核心类在org.apache.flink.api.java中,Java DataStream API在org.apache.flink.streaming.api。
StreamExecutionEnvironment是所有Flink程序的基础。你可以使用下面的静态方法获取该对象的实例:
getExecutionEnvironment()
createLocalEnvironment()
createRemoteEnvironment(String host, int port, String... jarFiles)
通常,你只需要使用getExecutionEnvironment(),因为这将根据上下文来初始化环境。如果你在IDE中执行程序或作为一个常规的Java程序,它将创建一个本地环境,它将在你本地机器上执行程序。如果创建了一个Jar文件,并通过命令行调用它,那么Flink集群管理器将执行main方法,并且调用getExecutionEnvironment()返回一个执行环境,然后在集群上执行你的程序。
对于指定的数据源,执行环境有多种方式使用各种方法来读取文件,对于CSV可以逐行读取,或者使用完全自定义的数据输入格式。只需要将文本文件作为序列的行读取。
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream text = env.readTextFile("file:///path/to/file");
这将为你返回一个DataStream,然后你可以应用转换来创建新的派生DataStream。你可以使用转换函数调用DataStream的转换方法。例如,map转换:
DataStream input = ...;
DataStream parsed = input.map(new MapFunction() {
@Override
public Integer map(String value) {
return Integer.parseInt(value);
}
});
这将通过将原始集合中的每个字符串转换为整数来创建新的DataStream.
一旦有了一个最终结果的DataStream,就可以通过创建一个sink来讲其写入外部系统。这些只是创建一个sink的实例方法:
writeAsText(String path)
print()
只要你指定了需要触发执行的完整程序,通过调用StreamExecutionEnvironment的execute()方法来执行,依靠ExecutionEnvironment的类型,将在本地机器触发执行,或者在集群上提交你的程序。execute()方法返回一个JobExecutionResult,它包含执行时间和累加器结果。
所有的Flink程序都是延迟执行的。当程序的main方法被执行时,加载数据和转换不会直接发生。相反,每个操作都被创建并添加到程序的计划中。当执行环境中的execute()显示地触发执行时,才开始执行实际的操作。程序在本地执行还是集群上执行取决于执行环境的类型。
延迟执行可以让你创建复杂的程序,Flink作为一个整体计划单元执行
一些转换(join,cogroup,keyBy,groupBy)要求在一个元素的集合上指定一个key。其他转换(reduce,groupReduce,aggregate,window)允许数据在应用之前被分组在一个key上。
// A DataSet is grouped as
DataSet<...> input = // [...]
DataSet<...> reduced = input
.groupBy(/*define key here*/)
.reduceGroup(/*do something*/);
// while a key can be specified on a DataStream using
DataStream<...> input = // [...]
DataStream<...> windowed = input
.keyBy(/*define key here*/)
.window(/*window specification*/);
Flink的数据模型不是基于key-value的。因此,你不需要物理地将数据集类型封装成keys和values。keys是虚拟的:它们被定义为对实际数据的函数,以指导分组操作符。
注意:在接下来的讨论中将使用DataStream API和keyBy。对于DataSet API 只需使用DataSet 和groupBy替代。
最简单的情况是在元组的一个或多个字段上对元组进行分组:
元组被分组在第一个字段(整数类型)
DataStream> input = // [...]
KeyedStream,Tuple> keyed = input.keyBy(0)
在这里,我们将元组分组在一个由第一和第二个字段组成的组合键上。
DataStream> input = // [...]
KeyedStream,Tuple> keyed = input.keyBy(0,1)
嵌套元组的一个注释:如果你有一个带有嵌套元组的DataStream,例如:
指定keyBy(0)将导致系统使用完成的Tuple2作为键(使用整数和浮点数作为键)。如果你想要导航到嵌套的Tuple2上,你必须使用后面介绍的字段表达式键。DataStream
你可以使用基于字符串的字段表达式来引用嵌套的字段,并定义用于group,sort,join,cogroup.
字段表达式可以很容易地选择复合类型的字段,例如Tuple和POJO类型。
在下面的实例中,我们有个WC的POJO,它有两个字段word和count,按照word来分组,我们只是将其传递给keyBy函数。
// some ordinary POJO (Plain old Java Object)
public class WC {
public String word;
public int count;
}
DataStream words = // [...]
DataStream wordCounts = words.keyBy("word").window(/*window specification*/);
字段表达式的语法:
字段表达式实例:
public static class WC {
public ComplexNestedClass complex; //nested POJO
private int count;
// getter / setter for private field (count)
public int getCount() {
return count;
}
public void setCount(int c) {
this.count = c;
}
}
public static class ComplexNestedClass {
public Integer someNumber;
public float someFloat;
public Tuple3 word;
public IntWritable hadoopCitizen;
}
这些是上面示例代码的有效字段表达式:
定义键的另一个方法是“key selector”函数。一个键选择器函数将一个元素作为输入,并返回元素的键。键可以是任何类型的,并且是由确定性计算派生出来的。
下面的示例显示了一个键选择器函数,它简单的地返回一个对象的字段。
// some ordinary POJO
public class WC {public String word; public int count;}
DataStream words = // [...]
KeyedStream kyed = words
.keyBy(new KeySelector() {
public String getKey(WC wc) { return wc.word; }
});
大多数转换都需要用户自定义函数。本节列出了如何指定它们的不同方式。
// 最基本的方法是实现所提供的接口之一:
class MyMapFunction implements MapFunction {
public Integer map(String value) { return Integer.parseInt(value); }
});
data.map(new MyMapFunction());
// 可以用一个匿名类传给一个函数
data.map(new MapFunction () {
public Integer map(String value) { return Integer.parseInt(value); }
});
// Flink also supports Java 8 Lambdas in the Java API. Please see the full Java 8 Guide.
data.filter(s -> s.startsWith("http://"));
data.reduce((i1,i2) -> i1 + i2);
// 所有需要用户定义函数的转换可以将其作为一个rich function。例如,而不是
class MyMapFunction implements MapFunction {
public Integer map(String value) { return Integer.parseInt(value); }
});
// 可以写成下面这种实现:
class MyMapFunction extends RichMapFunction {
public Integer map(String value) { return Integer.parseInt(value); }
});
// and pass the function as usual to a map transformation:
data.map(new MyMapFunction());
Rich functions can also be defined as an anonymous class:
data.map (new RichMapFunction() {
public Integer map(String value) { return Integer.parseInt(value); }
});
除了用户定义的函数(map,reduce等)之外,Rich function还提供了四个方法:open,close,getRuntimeContext,setRuntimeContext。这些对于参数化的函数,创建和终结局部状态,访问广播变量,访问诸如累加器和计数器之类的运行时信息,以及迭代的信息,都是很有用的。
Flink对可能在DataSet或DataStream中元素类型进行了一些限制。这样做的原因是系统分析这些类型来决定有效的执行策略。
有六种不同类别的数据类型:
Tuple是包含有不同类型的固定数量的字段的复合类型。Java API提供了从Tuple1到Tuple25的类。Tuple的每个字段都可以是任意的Flink类型,包括further Tuple,结果是嵌套的元组。可以使用字段的名称作为Tuple直接访问Tuple的字段tuple.f4,或使用通用的getter方法 tuple.getField(int position)。字段索引从0开始。注意,这与Scala的Tuple形成了对比,但是它更符合Java的一般索引。
DataStream> wordCounts = env.fromElements(
new Tuple2("hello", 1),
new Tuple2("world", 2));
wordCounts.map(new MapFunction, Integer>() {
@Override
public Integer map(Tuple2 value) throws Exception {
return value.f1;
}
});
wordCounts.keyBy(0); // also valid .keyBy("f0")
Java和Scala类被Flink视为一种特殊的POJO数据类型,如果它们满足以下要求:
Flink分析了POJO类型的结构,它学习了一个POJO字段。因此,POJO类型比一般类型更容易使用。此外,Flink可以比一般类型更有效的处理POJO
下面的示例展示了一个具有两个public字段的简单POJO:
public class WordWithCount {
public String word;
public int count;
public WordWithCount() {}
public WordWithCount(String word, int count) {
this.word = word;
this.count = count;
}
}
DataStream wordCounts = env.fromElements(
new WordWithCount("hello", 1),
new WordWithCount("world", 2));
wordCounts.keyBy("word"); // key by field expression "word"
Flink支持所有的Java和Scala的基本类型,如Integer,String,Double等
4.General Class Types
Flink支持大多数Java和Scala类(API和自定义)。限制适用于包含不能序列化的字段的类,比如文件指针,IO流,或其他本地资源。遵循JavaBean的约定的类通常工作的很好。
所有没有被确定为POJO类型的class都是由Flink作为一般类型处理。Flink将这些数据类型视为黑盒子,无法访问他们的内容(例如,有效的排序)。一般类型使用Kryo进行序列化和反序列化。
Value类型可以手动描述它们的序列化和反序列化。它们没有使用通用的序列化框架,而是通过实现org.apache.flinktypes.Value接口的write和read方法来提供定制操作。使用Value类型是合理的,因为一般的序列化是非常低效的。例如,一个数据类型实现了作为数组元素的稀疏向量。知道数组大部分为零,可以为非零元素使用特殊的编码,而一般的序列化则只需要编写所有的数组元素。org.apache.flinktyps.CopyableValue接口以类型的方式支持手动的内存克隆逻辑。
Flink带着与基本数据类型对应的预定义Value类型(ByteValue,ShortValue,IntValue,LongValue,FloatValue,DoubleValue,StringValue,CharValue,BooleanValue).这些Value类型充当基本数据类型可变变体。他们的值可以被修改,允许程序员重用对象并垃圾收集器中释放压力。
你可以使用实现了org.apache.hadoop.Writable接口的类型。在write和readFields方法中定义序列化逻辑用于序列化。
可以使用特殊类型,包括Scala的Either,Option,Try。Java API 也有Either自己的自定义实现。与Scala的Either类似,它代表一种两种可能的类型的值,Left和Right。对于需要输出两种不同类型记录的错误处理或操作,Either是有用的。
注意:这部分只与Java有关。
Java编译器在编译之后会抛出很多泛型类型的信息。这在Java中称为类型擦除。这意味着在运行时,对象的实例不再知道它的泛型类型。例如,DataStream
Flink在准备执行程序的时候需要类型信息(当程序的主要方法被调用时)。Flink Java API试图重构以各种方式抛出的类型信息,并将其显示地存储在数据集合操作符中。你可以通过DataStream.getType()来检索类型。这个方法返回一个TypeInfomation实例,这是Flink的内部方式来表示类型。
类型推断有其局限性,在某些情况下需要程序员的“合作”。例如,创建数据集的方法的示例,ExecutionEnvironment,fromCollection(),你可以传递一个参数描述类型,还有个泛型函数MapFunction可能需要额外的类型信息。
可以通过输入格式和函数来实现ResultTypeQueryable接口,从而明确地告诉API 他们的返回类型。函数调用的输入类型通常可以通过前一个操作的结果类型来推断。
累加器是一个简单的构造,有一个添加操作( add operation)和最终积累的结果( final accumulated result),在作业结束后可用。
最简单的累加器是一个计数器(counter),可以使用Accumulator.add(V value)方法来增加它。在job结束时,Flink将sum(merge)所有的部分结果,并将结果发送给客户端。在调试过程中,累加器是非常有用的,或者如果你想要了解更多关于你的数据信息。
Flink目前有以下内置的累加器,每个都实现了累加器接口:
如何使用累加器:
首先,在自定义的转换函数中创建一个累加器对象(例如:counter)
private IntCounter numLines = new IntCounter();
其次,注册累加器对象,通常在rich function中的open()方法。还可以定义累加器名称
getRuntimeContext().addAccumulator("num-lines", this.numLines);
现在,可以在operator 函数的任何地方使用累加器,包括open()和close()方法
this.numLines.add(1);
整个结果将存储在JobExecutionResult对象中,该对象是从执行环境的execute()方法返回的(当前这仅在等待作业执行完成时才有效)
myJobExecutionResult.getAccumulatorResult("num-lines")
所有累加器在每个Job中共享一个命名空间。因此,在一个JOb的不同operator函数中使用相同的累加器。Flink将在内部合并所有相同名称的累加器。
关于累加器和迭代器的注释:
目前累加器的结果仅在整个作业结束后才可用。我们还计划在下一次迭代中使前一次迭代的可用结果。 您可以使用聚合器来计算每次迭代统计信息,并根据此类统计信息确定迭代的终止。
自定义累加器: