Flink程序会在分布式的集合上进行各类转化操作(如,filter,map,update state,join,group,window,aggregate)。集合是由数据源构造出来的(如:文件,kafak topic,或者本地内存中的集合等)。结果会返回给sink,sink可能代表着写入文件或者标准输出。Flink程序可以在各种上下文中运行,如standalone,内置在其他程序中。程序执行可以发生在本地JVM,或者集群中的多个机器中。
根据你的数据源是否有界,你可以选择使用Dataset API来写一个批处理程序或者使用DataStream API来写一个流处理程序。这一片文档会介绍这两种API都会使用到的概念。
DataSet 与 DataStream
Flink使用DataSet 与 DataStream代表程序中的数据。你可以将他们看做一个可以包含重复元素的不可变集合。DataSet的数据时有限的,而DataSteam的数据可以是无限的。
这些集合和普通的java集合在某些方面有不同。首先,他们是不可变的,意味着一旦它们被创建,不能再添加或移除元素。并且也不可以直接检查内部的元素。
在Flink程序中通过添加source生成集合,并且可以通过map,filter等API可以生成新的集合。
Flink程序剖析
Flink程序看起来像是一个转化集合的普通程序。每个程序都包括下面的部分:
- 获取执行环境 execution environment
- 加载/创建数据
- 在数据上定义转化
- 确定计算结果输出到何处
- 触发程序计算
以下代码均为java代码
我们将会对每个步骤提供一个代码示例。所有DataSet的核心类可以在 org.apache.flink.api.java 找到,而DataStream的 API 可以在 org.apache.flink.streaming.api中找到。
StreamExecutionEnvironment是所有Flink程序的基础。你可以通过下面的方式获取:
getExecutionEnvironment()
createLocalEnvironment()
createRemoteEnvironment(String host,int port,String... jarFiles)
一般来说,你只需要使用getExecutionEnvironment()即可,因为这个方法会根据上下文选择合适的类:如果你在IDE中执行或者作为一个普通的java程序,那么这个方法会创建一个本地环境,你的程序会在本地机器运行。如果你将程序打成jar包,并通过命令行提交任务,Flink集群会执行你jar包的main方法,getExecutionEnvironment返回的执行环境是集群环境。
执行环境提供了多种方法从文件中读取:一行行的读取,以csv的格式读取,或者使用自定义的input format读取文件。你可以如下方式,一行行的读取内容:
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream text = env.readTextFile("file:///path/to/file");
上述步骤会生成一个DataStream,你可以在这上面应用转化来创建新的DataStream。
下面的几行直接略过,太简单,就是大概介绍了下每个步骤代码怎么写,给了个示例。
Lazy Evaluation
所有的flink程序都是懒执行的:当执行程序的main方法时,数据加载与转化并不会立即发生。而是创建每个操符并加入程序计划plan中。当调用execute()方法时,所有的操作符才会真正执行计算。程序会在本地执行还是在集群执行,取决于执行环境的类型。
lazy evaluation允许用户构造精细的程序,flink会将他们看做一个整体计划来执行。
指定key
一些转化操作(如join,coGroup,keyBy,groupBy)要求在数据上定义一个key。另一些转化(reduce,groupReduce,aggregate,window)允许数据在key上聚合数据。
DataStream这样定义key:
DataStream<...> windowed = input.
.keyBy()
.window();
Flink的数据模型不是基于key-value对的。因此,你不必一定将数据包裹为key-value的形式。key是“虚拟的”:它被定义为作用在实际数据上的函数,指导grouping操作符如何定义key。
为tuple定义key
最简单的场景是根据tuple的field来分组:
DataStream input = //
KeyedStream,Tuple> keyed = input.keyBy(0)
tuple根据第一个field来分组(Integer类型)
DataStream input = //
KeyedStream,Tuple> keyed = input.keyBy(0,1)
上面的示例中,我们使用一个组合key来对tuple分组,组合key由第一个和第二个field组成。
如果在DataStream的数据是嵌套结构,如:
DataStream,String,Long>> ds;
定义 keyBy(0)会导致系统使用整个 Tuple2 作为key。如果你想要使用嵌套结构内的field,可以参照下面的内容。
使用field表达式定义key
你可以使用基于字符串的field表达式来引用嵌套结构内的field来定义key,用于group,sort,join,coGroup等。field表达式使得选取复杂(嵌套)结构如tuple,POJO中的field更容易了。
下面的例子,我们有一个WC POJO,它有两个field,word与count。想要根据word来分组,我们可以将field的名字传递给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*/);
field表达式语法:
- 通过field name,选择 POJO 的field。如 “user” 代表POJO类的userfield
- 通过field name或者从0开始的索引,选择 tuple 的field。如 “f0”与“5”分别代表java tuple的第一个和第6个field
- 你可以选择POJO 与 tuple中的嵌套结构中的field。
- 可以使用“*”通配符,选择整个数据作为key。这对于那些不是tuple也不是POJO的数据类型很有用
使用key选择器函数来定义key
另一个选择key的方式是使用key选择器函数。它将数据作为入参,返回该数据的key。key可以是任意类型并且源于确定的计算。
下面的例子展示了使用key选择器函数,返回对象中的field作为key:
// some ordinary POJO
public class WC {public String word; public int count;}
DataStream words = // [...]
KeyedStream keyed = 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); }
});
Java8 Lambda表达式
flink的java api也支持java 8 的Lambda表达式:
data.filter(s -> s.startsWith("http://"));
data.reduce((i1,i2) -> i1 + i2);
Rich function
所有要求自定义函数的转化操作,都可以使用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); }
};
然后将函数作为参数,传递给map转化方法中:
data.map(new MyMapFunction());
Rich function 也可以定义成一个匿名类:
data.map (new RichMapFunction() {
public Integer map(String value) { return Integer.parseInt(value); }
});
除了用户需要重写的方法(map,reduce等)外,Rich function还有四个方法:open,close,getRuntimeContext与setRuntimeContext。当需要传递参数给这些函数,或者创建以及确定local state,访问广播变量 broadcast variable,访问运行时信息如accumulator,counter以及获取iteration的信息等时,很有用处。
支持的数据类型
Flink对于什么样的数据类型可以在DataSet或DataStream上处理有一定限制。这样做的原因是,flink系统会分析数据类型,以便选择一个高效的执行策略。
Flink支持如6种不同类型的数据:(原文确实是6种)
- Java Tuple 与 Scala Case Class
2.Java POJO - 基本类型
- Regular Class (比POJO 更一般的类)
- Values (实现了相关接口的类型)
- Hadoop Writable
- 特殊类型
Tuple 与 Case Class
Tuple(元组)是一个包含固定数量,且类型多样的field的组合类型。Java API提供了从 Tuple1 到 Tuple25 这25中tuple。每一个tuple的field都可以是一个Flink支持的类型,包括tuple,这就会形成嵌套的tuple。tuple的field可以使用field name直接访问,如: tuple.f4,或者使用更通用的get方法,如: tuple.getFiled(int position)。field的索引从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")
POJO
如果Java/Scala的类符合如下要求,则在flink中,他们会被Flink看做是一个特殊的POJO 数据类型:
- calss必须是pulish的
- 必须含有无参构造函数
- 所有的field必须要么是public要么有public才get/set方法。对于名为 foo 的field,它的get/set方法必须命名为getFoo() 与 setFoo() (注:这就要求即便是boolean类型,其get/set方法不能再遵循java的习俗,使用isXXX的命名方式了)
- field的类型必须是flink支持的类型。目前,Flink使用Avro系列化任意对象(如Date)(注:不太明确这个的意思,是说POJO使用Avro序列化?还是说使用Avro序列化那些java提供的非基本类型,如Date,因为Date不符合这里说的POJO的特点,起码field不是pulic的且没有public的get/set方法)
Flink会分析POJO的结构,也就是说,flink会知道POJO的field有哪些。因此,POJO类型比普通的类更容易使用。除此之外,Flink处理POJO类型比普通的类更高效。
下面给出了一个示例,一个简单的包含两个public field的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。
更普通的类 General Class Type
Flink支持大多数Java/Scala的类(API自带的类或自定义的类)。对于这些类的限制包括:不能被序列化的field,如:file pointer,I/O stream,或其他本地资源(native resource)。遵循java Bean 传统的类一般来说都可以很好的使用。
所有没有符合POJO标准的类会被Flink任务是普通类。Flink将这些类看做是一个黑盒子,不能获取他们的内容(如,为了高效的排序,应该要获取到他们的content)。这些普通类 General type使用 Kryo 框架来进行序列化/反序列化。
Values
Value类型自己就描述了如何进行序列化/反序列化。不同于要经过一个通用的序列化框架来进行序列化操作,Value类型通过实现org.apache.flinktypes.Value接口的 read 与 write方法来定义如何实现序列化/反序列化操作。当使用通用序列化框架会产生很不高效的情况是,可以使用Value类型。如:一个包含稀疏矩阵的array作为field的类。可能array中大部分都是0,因此可以自定义编码方式来处理这么多的0,而通用序列化框架会将整个array的元素都进行序列化。
同理,org.apache.flinktypes.CopyableValue接口支持自定义clone的逻辑。
Flink为基本类型自带了对应的Value接口实现(ByteValue,ShortValue, IntValue, LongValue, FloatValue, DoubleValue, StringValue, CharValue, BooleanValue)。这些Value类型可以看做是基本类型变体为值是可变的:他们的值可以被修改,允许程序重用对象并且减轻垃圾回收的压力。
Hadoop Writable
你可以使用实现了org.apache.hadoop.Writable接口的类型。序列化的逻辑定义在write()中,readFields()方法用来反序列化(注:感觉原文这里写错了,因此翻译的时候纠正了过来,原文写的是readFields方法用来serialization)。
Special Type 特殊类型
你可以使用一些特殊类型,包括Scala的Either,Option与Try。Java API也实现了自己的 Either。与Scala的Either类似,它代表着一个可能是两种类型之一的值,Left或者Right。Either在处理错误或者需要输出两种不同类型的结果时很有用。
类型擦除与类型推断
注意:这一部分只存在使用java api场景中
Java编译器会在编译后抛弃掉大部分的泛型信息。这一现象被称作java中的类型擦除。这意味着,在运行时期,一个对象的实例不再清楚的知道它的泛型类型是什么了。如:DataStream
当Flink准备执行程序时(程序的main方法被调用时),会要求类型信息。Flink Java API会根据各种方式获取类型信息(如根据上游operator的结果的类型,来判断当前operator的input的类型)并进行重构,然后将其存储在数据集与操作符中。你可以使用DataStream.getType()获取它们。这个方法会返回一个TypeInformation实例,TypeInformation是flink内部表示类型的一个类。
类型推断有它自身的限制,有时候需要编程人员的协助。如:使用ExecutionEnvironment.fromCollection()方法从集合中创建数据集时,你可以传入一个变量,用于说明数据的类型。另一些含有泛型的方法,如MapFunction也有可能需要额外的类型信息。(注:这一部分会在后面的 Java Lambda Expressions 部分中有详细说明)
可以让input format(自定义数据源的接口)与function(转化操作符需要的函数接口)实现ResultTypeQueryable接口,确切的告知API它们的返回类型。对于operator来说,所有input的类型一般都可以通过上游的operator的输出结果的类型来推断。
Accumulator 与 Counter
Accumulator 是一个由 add 操作符与 final accumulated result组成的简单的结构。它可以在job结束后获取到信息。
最简单的Acumulator是 counter:你可以使用 Accumulator.add(V value)来增加value的值。job结束后,Flink会将所有的result累加起来,并发送结果给client。Accumulator在调试时或者你想要快速发现数据更多信息时很有用。
目前,Flink有如下内置的 累加器。每一个都实现了 Acumulator 接口。
- IntCounter,LongCounter与DoubleCounter:下面会有一个示例介绍如何使用
- Histogram:histogram直方图内部含有一个从Integer到Integer的映射。你可以使用它来计算离散的值,如:在字数统计程序中,计算每行的字数的分布。
如何使用累加器:
首先,你必须要在你想要使用的自定义转化function内创建一个accumulator对象
private IntCounter numLines = new IntCounter();
第二步,你需要注册这个累加器对象,通常你可以在rich function的open方法中进行注册。这一步,你需要定义累加器的名字
getRuntimeContext().addAccumulator("num-lines", this.numLines);
现在,你可以在任何地方使用这个累加器了,包括open和close方法(注:当然,仅限这个自定转化function内)
this.numLines.add(1);
全部的结果,会保存在 JobExecutionResult 对象中,这个对象由 executionEnvironment 调用execute()方法返回(目前,仅当 execution 会阻塞直到job结束的情况下才可用)
myJobExecutionResult.getAccumulatorResult("num-lines")
同一个job中同一个名字的累加器会共享这个名称。因此若你想要在不同的function中定义一个一样的累加器,只要将他们的name设置为一样即可。Flink会在内部将name相同名称的累加器的值进行merge。
关于accumulator和iteration的注释:目前accumulator的结果仅可在job结束后才可以获取。我们计划允许下次iteration能使用之前的iteration的结果。你可以使用 Aggregator 来计算每次迭代统计信息,并根据此类统计信息确定迭代的终止。(注:这一段没怎么懂)
自定义累加器:
你可以通过实现 Accumulator 接口来实现自己的累加器。如果你觉得你的累加器应该被推广到flink,可以创建一个pull request。
你可以选择实现 Accumulator 或者 SimpleAccumulator。
Accumulator