基本API概念(Basic API Concepts)—— For Java
-----------------------------------
Flink程序是在分布式数据集上(collection)实现Transformation(如filtering, mapping, updating state, joining, grouping, defining windows, aggregating等)的一般程序。数据集最初由数据源创建(如通过读取文件,Kafka,或来自本地数据集)。运算结果通过sink返回,例如将数据写到(分布式)文件中,或是写到标准输出中(如中断命令行等处)。Flink程序可在多种情况下运行,不论是独立运行(standalone)或是以嵌入式的形式运行在其他程序中。Flink程序可以执行在本地JVM上,也可以执行在许多设备的集群上。
根据不同类型的数据源(如有限/无限数据源),你可以选择编写一个批处理程序或是一个流处理程序,其中前者使用DataSet API,而后者使用DataStream API。本文将介绍两种API共有的基本概念,若想获得有关使用某种API编写程序的详细信息,请见Streaming Guide和Batch Guide。
注意:在本文中,我们会使用StreamingExecutionEnvironment以及DataStream API作为如何使用API的实例,而它们的使用概念与DataSet API是完全一样的,仅仅是替代为ExecutionEnvironment和DataSet API
一、与Flink连接
为了使用Flink编写程序,你需要包含与你的工程中的编程语言相关的Flink类库。最简单的方法就是使用为Java或Scala编写的Quickstart脚本。它们从一个模板(Maven Archetype)创建了一个空白工程,并为你部署好一切。若想要手动创建工程,你可以用如下命令来使用archetype并创建一个工程:
mvn archetype:generate
-DarchetypeGroupId=org.apache.flink
-DarchetypeArtifactId=flink-quickstart-java
-DarchetypeVersion=1.0.3
archetype运行的是稳定的发布版本以及之前的版本(-SNAPSHOT版本)。
如果你想将Flink添加到一个已存在的Maven工程,通过在你的工程的pom.xml中添加以下Entry到dependencies块中:
<!-- Use this dependency if you are using the DataStream API --> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-streaming-java_2.10</artifactId> <version>1.0.3</version> </dependency> <!-- Use this dependency if you are using the DataSet API --> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-java</artifactId> <version>1.0.3</version> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-clients_2.10</artifactId> <version>1.0.3</version> </dependency>
1.1 Scala 依赖版本
因为Scala 2.10的binary与Scala 2.11的binary不兼容,我们必须提供多个artifact来支持两个Scala版本。
从Scala的0.10版本线开始,我们为2.10和2.11交叉编译(cross-build)了所有的Flink模块。如果你想要用Scala 2.11在Flink上运行你的程序,你需要在你的dependencies块上的artifactId值中添加"_2.11"后缀
如果你像用Scala 2.11编译,请查看编译指导。
1.2 Hadoop依赖版本
如果你与Hadoop一起使用Flink,依赖的版本可能会随着在Flink中使用的Hadoop版本(具体为HDFS版本)的不同而变化。请参考下载页面的可用版本列表,以及如何与自定义版本的Hadoop连接的指导。
若你想连接到最新的SNAPSHOT版本的代码,请查看有关Nightly-build的指导
flink-client依赖仅在本地调用Flink程序时是必要的(如standalone运行它来测试或debug)。如果仅要以JAR文件形式导出Flink程序并将之运行在集群上,你可以跳过这个依赖
二、DataSet和DataStream
Flink使用DataSet和DataStream的特殊类来表示程序中的数据。你可以将它们看作不可变(immutable)且内容可重复的数据的集合。在DataSet的情况下,数据是有限的,而在DataStream中,element的数量可以是无限的。
上述的数据集合与Java中的集合有着一些区别。首先,它是不可变的,即当它被创建后就无法被添加或是删除了。其次,你也无法简单地查看其中的element。
Flink程序通过添加一个数据源来创建一个集合,并且这些集合在经过利用诸如map,filter的API方法的transforming后,会产生新的集合。
三、Flink程序剖析
Flink程序类似对数据集合进行转换的普通程序,每个程序包括以下几个基本部分
1. 获得一个execution environment
2. 载入/创建初始数据
3. 确定对数据的transformation
4. 确定计算结果的去向
5. 触发程序执行
我们不会给出每个步骤的详细描述,请参考相应区域获取进一步信息。注意所有Java DataSet API的核心类都可以在org.apache.flink.api.java中找到,而Java DataStream API的核心类都可以在org.apahce.flink.streaming.api中找到。
StreamExecutionEnvironment是Flink程序的基础,你可以通过以下静态方法获得其实例:
· getExecutionEnvironment()
· createLocalEnvironment()
· CreateRemoteEnvironment(Strting host, int port, String… jarFiles)
通常你仅需要使用getExecutionEnvironment()即可,因为该函数会依据上下文来做出适合的行为:如果你在IDE内执行程序或是以普通Java程序执行,它会创建一个本地的环境来在本地设备上运行你的程序;如果你从程序创建了一个JAR文件,并且通过Flink命令行来调用它,则Flink的集群管理(cluster manager)执行你的主函数,并且getExecutionEnvironment()会返回在集群上执行你的程序的执行环境
关于确定数据源,执行环境有数个方法来使用多种函数从文件中读取:你可以仅仅按行读取、作为CSV文件读取、或是使用完全自定义的数据输入格式读取。你可以使用如下代码实现以顺序读取行的方式来读取文本文件:
1 final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 2 DataStream<String> text = env.readTextFile("file:///path/to/file");
上述方法会为你提供一个DataStream,你可以对其使用transformation来创建新的衍生DataStream。你可以通过调用一些方法来对DataStream应用transformation函数,例如,一个map transformation应当如下所示:
1 DataStream<String> input = ...; 2 DataStream<Integer> parsed = input.map(new MapFunction<String, Integer>() { 3 @Override 4 public Integer map(String value) { 5 return Integer.parseInt(value); 6 } 7 });
上述方法会通过转换源数据集合中的每个字符串为整数来创建一个新的DataStream。
一旦你拥有了一个包含最终结果的DataStream,你可以通过创建一个sink来将它写到外部系统中去。以下是一些创建sink的例子:
· writeAsText(String path)
· print()
execute()方法会返回一个JobExecutionResult实例,它会包含执行时间和蓄积的结果。
关于流式(Streaming)数据source和sink的信息以及更多DataStream支持的transformation的详细信息,请参考Streaming Guide
有关批式(batch)数据source和sink以及更多DataSet支持的transformation的详细信息,请参考Batch Guide
三、懒执行(Lazy Evaluation)
所有Flink程序都是懒执行的:当程序主函数被执行时,载入数据和transformation不会直接发送,反之,每个Operator将被创建并加入程序的运行计划中。Operation在明确触发调用在执行环境上的execute()函数时,其执行内容才算是开始执行。程序是本地执行还是集群执行是由执行环境决定的。
在你编写复杂的程序后,懒执行可以使Flink将其作为一个整体而有规划的执行单元(holistically planned unit)来执行。
四、明确Key
有些transformation(join,coGroup,keyBy,groupBy)需要在element集合上定义一个key。其他的transformation(Reduce,GroupReduce,Aggregate,Windows)则可以先令数据根据key分组,再应用这些transformation。
一个DataSet按如下代码进行Group操作
1 DataSet<...> input = // [...] 2 DataSet<...> reduced = input.groupBy(/*define key here*/).reduceGroup(/*do something*/);
我们使用如下代码在DataStream上定义key
1 DataSet<...> input = // [...] 2 DataSet<...> reduced = input.groupBy(/*define key here*/).reduceGroup(/*do something*/);
Flink的数据模型并不是基于key-value对的,因此,你不需要将数据集的类型打包到key和value中。Key是"虚拟(virtual)"的:它们定义为在实际数据上的函数来知道grouping operator。
注意:在本文接下来的讨论中,我们使用DataStream API和keyBy。而DataSet API中你只需要将下面的例子用DataSet和groupBy代替即可。
4.1 为Tuple定义Key
最简单的用例就是为Tuple根据其一或多个域分组:
1 DataStream<Tuple3<Integer,String,Long>> input = // [...] 2 KeyedStream<Tuple3<Integer,String,Long> keyed = input.keyBy(0);
在上面代码中,tuple是按照第一个域(Integer的域)来分组的
1 DataStream<Tuple3<Integer,String,Long>> input = // [...] 2 KeyedStream<Tuple3<Integer,String,Long> keyed = input.keyBy(0,1);
上面的代码中,我们按第一个域和第二个域的组合key来分组。
当使用嵌套tuple时请注意:如果你有如下代码所示的嵌套tuple,调用keyBy(0)会导致系统使用整个Tuple2作为key(即Integer和Float的Tuple2作为key)。如果你想要“导航”到被嵌套的Tuple2的内部域,则你需要下一小节所述的field expression key
1 DataStream<Tuple3<Tuple2<Integer, Float>,String,Long>> ds;
4.2 使用Field Expression定义key
你可以使用String域的expression来引用嵌套域并定义key,用以grouping、sorting、joining或coGrouping。Field expression使得访问诸如Tuple和POJO的复合(嵌套)类型中的域变得十分容易。
在下面的例子中,POJO类wc拥有两个域"word"和"count"。若要使用域word来分组,我们仅需要将该域的名字传递给groupBy()函数即可。
1 // some ordinary POJO (Plain old Java Object) 2 public class WC { 3 public String word; 4 public int count; 5 } 6 DataStream<WC> words = // [...] 7 DataStream<WC> wordCounts = words.keyBy("word").window(/*window specification*/);
Field Expression语法:
· 通过域名字来访问POJO域。如用"user"来访问POJO类型中的user成员。
· 使用域名字或是该域的0-偏移的域索引来访问Tuple的域。如"f0"和"5"分别表示访问Java Tuple类型的第1个和第6个域。
· 你可以访问POJO类和Tuple类型中的嵌套域。如"user.zip"表示存在一个POJO类的"user"域中的POJO类的zip域。该语法可以支持POJO和Tuple的嵌套和混合,如"f1.user.zip"或"user.f3.1.zip"。
· 你可以使用通配符表达式"*"来访问全部类型,这同样适用于不是Tuple和POJO类的类型。
Field Expression举例:
1 public static class WC { 2 public ComplexNestedClass complex; //nested POJO 3 private int count; 4 // getter / setter for private field (count) 5 public int getCount() { 6 return count; 7 } 8 public void setCount(int c) { 9 this.count = c; 10 } 11 } 12 public static class ComplexNestedClass { 13 public Integer someNumber; 14 public float someFloat; 15 public Tuple3<Long, Long, String> word; 16 public IntWritable hadoopCitizen; 17 }
这些是对上面例子代码的合法的Field Expression:
· "count": wc类的count域。
· "complex": 递归访问复杂POJO类型ComplexNestedClass的所有域。
· "complex.word.f2": 访问嵌套Tuple3的最后一个域。
· "complex.hadoopCitizen": 访问Hadoop的IntWritable类型
4.3 用key Selector函数来定义key
另一个定义key的方法就是"key selector"方法。一个key selector用一个element作为输入并返回element的key。该key可以是任何类型并且可以由任意运算衍生而成。
下面的例子展示了一个简单地返回Object的域的key selector方法:
1 // some ordinary POJO 2 public class WC { 3 public String word; 4 public int count; 5 } 6 DataStream<WC> words = // [...] 7 KeyedStream<WC> kyed = words.keyBy(new KeySelector<WC, String>() { 8 public String getKey(WC wc) { 9 return wc.word; 10 } 11 });
五、明确Transformation方法
多数Transformation需要用户定义的方法,本节列举明确Tranformation的几种不同的方法。
1. 实现一个接口
最基本的方法是实现提供的接口中的一个:
class MyMapFunction implements MapFunction<String, Integer> {
public Integer map(String
value) { return Integer.parseInt(value);
}
});
data.map(new MyMapFunction());
2. 匿名类(Anonymous class)
你可以传递一个方法作为一个匿名类:
data.map(new
MapFunction<String, Integer> () {
public Integer map(String
value) { return Integer.parseInt(value);
}
});
3. Java 8 Lambda表达式
Flink Java API同样支持Java 8 Lambda表达式,详见Java 8 Guide
data.filter(s -> s.startsWith("http://"));
data.reduce((i1,i2) -> i1 + i2);
4. Rich function
所有需要用户定义方法的Transformation可以替代为一个rich函数作为参数。例如,一般情况我们如下定义:
class MyMapFunction
implements MapFunction<String,
Integer> {
public Integer map(String
value) { return Integer.parseInt(value);
}
});
而我们可以写成如下形式:
class MyMapFunction
extends RichMapFunction<String,
Integer> {
public Integer map(String
value) { return Integer.parseInt(value);
}
});
并且将方法传递给map transformation:
data.map(new MyMapFunction());
此外,Rich function还能以匿名类的方式定义:
data.map (new
RichMapFunction<String, Integer>() {
public Integer map(String
value) { return Integer.parseInt(value);
}
});
Rich function提供了用户定义的方法(map,reduce等),此外还提供了四个方法:open,close,getRuntimeContext,setRuntimeContext。这些方法在向方法传递参数(参看Passing Parameters to Funtion)、创建并确定(finalizing)本地状态、访问广播变量(参看Broadcast Variables),访问诸如累加器和计数器等运行时信息(参看Accumulators and Counters),以及迭代的信息(参看Iterations)等多方面都十分有用。
六、支持的数据类型
Flink对可能出现在DataSet或DataStream中的类型有一些约束,如此使得系统可以分析类型来决定较为高效的执行策略。数据类型分为7个不同的类别:
1. Java Tuple和Scala Case Classes
2. Java POJO类
3. 原生(primitive)类型
4. 正则类(regular class)
5. Value
6. Hadoop Writables
7. 特殊类型(Special Type)
6.1 Tuple和Case Class
Tuple是包含固定数量而多种类型的域的复合类型。Java API提供从Tuple1到Tuple25的类,一个tuple的每隔域都可以是任意的Flink类型,包括tuple类型也可以作为域的类型而形成嵌套Tuple。tuple的域可以用它的名字(像tuple.f4)来直接访问,也可以使用泛化getter方法访问(tuple.getField(int position))。域的索引数开始于0,注意这与Scala tuple的情况相反,但0-索引与Java的普遍索更加一致。
1 DataStream<Tuple2<String, Integer>> wordCounts = env.fromElements( 2 new Tuple2<String, Integer>("hello", 1), 3 new Tuple2<String, Integer>("world", 2)); 4 5 wordCounts.map(new MapFunction<Tuple2<String, Integer>, Integer>() { 6 @Override 7 public String map(Tuple2<String, Integer> value) throws Exception { 8 return value.f1; 9 } 10 }); 11 12 wordCounts.keyBy(0); // also valid .keyBy("f0")
6.2 POJO类
在Flink中,若Java和Scala的类满足一下几个要求,则将之看作是特殊POJO数据类型:
· 类必须是public的
· 它必须有一个public的无参数构造函数(默认构造函数)
· 所有与要么是public的,要么可以通过getter和setter方法访问。对名为foo的域的getter和setter必须命名为getFoo()和setFoo()。
· 域的类型必须是Flink支持的类型。此时Flink使用Avro来序列化任意Object(如Date)
Flink会分析POJO类型的结构,即了解POJO的域的信息,从而POJO类型要比一般类型易用。此外,Flink处理POJO也比处理一般类型更加效率。下面的例子展示了一个简单的拥有两个public域的POJO:
1 public class WordWithCount { 2 public String word; 3 public int count; 4 5 public WordWithCount() {} 6 7 public WordWithCount(String word, int count) { 8 this.word = word; 9 this.count = count; 10 } 11 } 12 13 DataStream<Tuple2<String, Integer>> wordCounts = env.fromElements( 14 new WordWithCount("hello", 1), 15 new WordWithCount("world", 2)); 16 17 wordCounts.keyBy("word"); // key by field expression "word"
6.3 原生类型
Flink支持所有Java和Scala原生类型,如Integer,String和Double
6.4 一般类类型
Flink支持大多数Java和Scala类(API和自定义的)。那些包含不能序列化的域(如文件指针,I/O stream,或其他原生资源(native resource))的类均处于约束之下。遵循Java Bean的约定协议的类都普遍能很好地适用。
对所有非POJO类(有关POJO要求见上),Flink都按照普通类的类型来处理。对这些数据类型,Flink将它们看作黑箱,并无法访问它们的内容(如在高效排序时)。Flink使用序列化框架Kryo来序列化/反序列化那些普通类型。
6.5 Value
Value类型人工描述它们的序列化和反序列化。它们不通过通用序列化框架,而是针对那些Operation,通过实现接口org.apache.flink.types.value接口的read和write方法来提供自定义的代码。当通用序列化非常低效时,使用Value类型就比较合理了。举例来说,有一类型实现了以数组表示的一个稀疏element向量,数组中绝大多数都是0,通用序列化仅简单地写入所有数组element,而自定义的Value类型可以使用对非0的element的特殊编码来进行序列化。
而接口org.apache.flink.types.CopyableValue以相似的方式支持人工内部clone逻辑。
Flink自带预先定义的有关基础数据类型的Value类型(ByteValue,ShortValue,IntVlaue,LongValue,FloatValue,DoubleValue,StringValue,CharValue,BooleanValue)。这些Value类型均作为基础数据类型的可变(mutable)的变种:它们的值可以被修改,且允许程序员重用Object来减轻垃圾回收的负担。
6.6 Hadoop Writable
你可以实现接口org.apache.hadoop.Writable的类型。定义在函数write()和readFields()的序列化逻辑将会用于序列化过程之中。
6.7 特殊类型
你可以使用特殊类型,包括Scala的Either, Option和Try。Java API有它自己的Either的实现,类似于Scala的Either,它代表着一个值有这两个可能的类型,即Left或Right。Either在错误处理或是需要输出两种类型的数据的Operator中比较有用。
6.8 类型擦除(Type Erasure)和类型接口
注意:本小节内容仅与Java有关
Java编译器在编译后会扔掉许多泛型信息,这在Java中称为类型擦除。意即在运行时,一个Object的实例将不再知道它的泛型。例如,在JVM中DataStream<String>和DataStream<Long>是一样的。
Flink在他准备执行程序时(当调用main方法时)需要类型信息。Flink Java API试图重构被各种方式抛弃的类型信息,并分明地存储在Data set和operator中。你可以通过DataStream.getType()获得类型,该方法返回TypeInformation类的实例,该类即为Flink内部表示类型的方法。
类型接口拥有一些缺陷,在一些情况下,它需要程序员的一些“合作”。例如,一些诸如ExecutionEnvironment.fromCollection()等从需要从collection中创建数据集的方法中,你可以传递一个描述类型的参数。但诸如MapFunction<I, O>的泛型方法也可能需要额外的类型信息。
ResultTypeQueryable接口可以由输入格式化以及告诉API确切的返回类型的方法实现。方法调用的input类型通常可以通过前一个Operation的返回类型推断。
七、执行配置
StreamExecutionEnvironment同样包含ExecutionConfig来为runtime设置job确切的配置值。
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
ExecutionConfig executionConfig = env.getConfig();
在配置选项中,有以下可选选项(粗体为默认值)
· enableClosureCleaner() / disableClosureCleaner()。Closure cleaner默认是开启的。Closure cleaner移除在Flink程序内部中不需要的匿名函数的包围类应用。当Closure cleaner关闭后,则一个匿名用户函数可能应用它的包围类,通常会导致无法序列化的问题,从而使得序列化器抛出异常。
· getParallelism() / setParallelism(int parallelism)。设置job的默认并行度。
· getNumberOfExecutionRetries() / setNumberOfExecutionRetries(int numberOfExecutionRetries)。设置失效任务重新执行的次数。“0”值则会有效地关闭fault tolerance。“-1”值表明应当使用系统默认值(定义在configuration中)。
· getExecutionRetryDelay() / setExecutionRetryDelay(long executionRetryDelay)。设置job失效后重试的延迟(毫秒)。该延迟开始于所有TaskManager上的任务都被成功停止时,且一旦延迟过后,任务就将重新开始。该参数在延迟重新执行来使得一些与超时相关的失效完全显露出来(如没有完全超时的断开连接问题)时十分有用,从而避免了重新开始后立即因为同样问题失效的情况。该参数仅在重试此时被设置为1或更多时才会生效。
· getExecutionMode() / setExecutionMode()。默认的执行模式为PIPELINED,该函数设置执行程序的执行模式。执行模式定义了数据交换是以批处理方式还是以流水线方式运行。
· enableForceKryo() / disbaleForceKryo()。Kryo默认并不是强制的。强制GenericTypeInformation使用Kryo来序列化POJO(即使我们可以将它们按POJO来分析)。在一些情况下,这种方式也是比较合适的,例如当Flink内部的序列化器无法正确处理POJO时。
· enableForceAvro() / disableForceAvro()。Avro默认并不是强制的。强制Flink的AvroTypeInformation使用Avro来替代Kryo来序列化Avro POJO。
· enableObjectReuse() / disableObejctReuse()。默认下Object在Flink中是不被重用的,开启object重用将会命令运行时重用用户Object来获得更好的性能。请记住这可能会在Operation中用户编写的方法中对此行为并不知情从而导致bug。
· enableSysoutLogging() / disableSysoutLogging()。JobManager的状态更新会默认地打印到System.out中,这个设置可以关闭该行为。
· getGlobalParameters() / setGlobalParameters()。该方法使用户可以设置一个自定义对象作为job的全局配置。由于ExecutionConfig在所有用户定义的方法中都是可以访问的,这是一个使配置全局应用与job的简单方法。
· addDefaultKryoSerializer(Class<?> type, Serializer<?> serializer)。该方法为一个给定的type注册一个Kryo序列化器的实例。
· addDefaultKryoSerializer(Class<?> type, Class<? Extends Serializer<?>> serializerClass)。该方法为一个给定的type注册一个Kryo的序列化器的类。
· registerTypeWithKryoSerilizer(Class<?> type, Serializer<?> serializer)。该函数注册一个给定的type到Kyro,并且明确了它的序列化器。通过注册一个类型到Kryo,在序列化该类型时将会高效得多。
· registKryoType(Class<?> type)。如果类型最后使用Kryo来序列化,则它将在Kryo中进行注册来保证唯一的tag(整数ID)已被写入。如果类型不用Kryo注册,则它的整个类名将会序列化到每个示例中,从而导致更高的I/O开销。
· registPojoType(Class<?> type)。注册给定type到序列化栈。如果type最终序列化为一个POJO,则该type将会注册到POJO序列化器中。如果type最终使用Kryo序列化,则他将会注册到Kryo来保证已写入了唯一的tag。如果类型不在Kryo中注册,则它的整个类名将序列化到每个实例中,从而导致更高的I/O开销。
注意,Flink的Kryo序列化器实例不能使用registKryoType()函数注册。
· disableAutoTypeRegistration()。自动类型注册默认下是开启的。自动类型注册将所有用于用户代码中的类型(包括子类型subtype)注册到Kryo和POJO序列化器中。
可以在Rich*函数中通过getRuntimeContext()访问到的RuntimeContext同样使我们可以在所有用户定义方法中访问ExecutionConfig
八、程序打包和分布式执行
如上所述,Flink可以在集群上使用remote environment来执行程序。或者程序可以打包成为JAR文件(Java Archives)来执行。程序打包是通过命令行接口执行程序的先决条件。
8.1 程序打包
为了支持从命令行或web接口执行打包后的JAR文件,程序必须使用通过StreamExecutionEnvironment.getExecutionEnvironment()获得的environment,该environment将在JAR提交到命令行或是web接口是充当集群的环境。如果Flink程序不是从上述接口中调用,则environment将会作为本地的环境。
为了打包程序,仅需要将涉及的类导出为一个JAR文件,该文件的manifest必须指向具有程序入口的类(即拥有public的main函数的类)。最简单的方式就是将main-class值放入manifest(例如:main-class: org.apache.flink.example.MyProgram)。main-class属性与JVM在通过指令"java -jar pathToTheJarFile"执行一个JAR文件时为了寻找主函数使用的属性是同一个。多数IDE提供了在导出JAR文件时自动包含该属性的功能。
8.2 通过Plan打包程序
我们还支持将程序打包成Plans。与通常的定义程序的main方法并在environment上调用execute()不同,plan打包返回的是Program Plan,它是程序的数据流图的描述。为了打包为plan,程序必须实现接口org.apache.flink.api.common.Program,定义getPlan(String …)方法。这些传递给方法的字符串是命令行参数。程序的plan可以通过environment的ExecutionEnvironment.createProgramPlan()获取到。当打包程序plan时,JAR文件的manifest必须指向实现了接口org.apache.flink.api.common.Program的类,而不是拥有main方法的类。
8.3 总结
调用程序打包功能的过程大致如下:
1. 搜索JAR的manifest来寻找main-class或program-class属性。如果发生两个属性同时存在的情况,则相比main-class属性,优先考虑program-class属性。对于两个属性都没有的情况,命令行和web接口都支持用一个参数来人工传递入口点类的名称。
2. 如果入口点类实现了接口org.apache.flink.api.common.Program,系统将调用其getPlan(String …)来获取该程序的执行plan。
3. 如果入口点函数没有实现org.apache.flink.api.common.Program接口,则系统会调用该类的main函数
九、累加器 & 计数器
累加器是用一个add operator和一个final accumulated result构造的,其中final accumulated result在job结束时就会变得可用。
最直接的累加器就是计数器,你可以通过使用Accmulator.add(v value)方法来增加它。在job的最后阶段,Flink会将所有部分结果相加(merge),并将最终结果发送到client中。累加器在debug时以及在你想要快速获得你的数据的更多信息是相当有用。
Flink当前拥有以下几种自带累加器(build-in accumulator),每种累加器都实现了接口Accumulate。
· IntCounter,LongCounter,DoubleCounter:相关使用这种Counter的例子见下面内容。
· Histogram:一个histogram的实现是为了a discrete number of bins。其内部实现仅仅是一个从Integer到Integer的映射。你可以将它用在计算数值的分布,如对于一个word count程序,它可以计算每行词数(word-per-line)的分布。
如何使用accumulator:
首先,在用户定义的transformation函数中,你要在想使用accumulator的位置创建一个accumulator的对象(此处创建一个counter)。
private IntCounter numLines = new IntCounter();
其次,你需要注册accumulator对象,通常在rich函数的open()方法中。下面的代码同样定义了累加器的名称。
getRuntimeContext().addAccumulator("num-lines", this.numLines);
现在,你便可以在任意operator方法中使用累加器了,包括在open()和close()方法中。
this.numLines.add(1);
总体的结果会存储在JobExecutionResult的对象中,这是execution environment的execute()函数返回的结果(在当前版本中,该机制仅仅在程序的执行等待job的完成结果时才有效)。
myJobExecutionResult.getAccumulatorResult("num-lines")
每个job的所有的accumulator都共享同一个namespace,因此你可以在job的不同的operator方法中使用同一个accumulator,Flink会自动合并所有同名的accumulator。
有关累加器和迭代的注意事项:现在累加器的结果仅仅在总体job结束后才可用,我们计划让前一个迭代的累加器结果也可以用于下一个迭代。你可以使用Aggregators来计算每个迭代的统计结果并根据这些统计结果来结束迭代。
自定义accumulator:
你可以通过实现Accumulator接口来自定义你自己的accumulator。如果你认为你自定义的accumulator应当被Flink收录,请自由创建pull request。
你可以选择实现接口Accumulator或是SimpleAccumulator:
· Accumulator<V, R>更加灵活:它定义了用来增加的值的类型v,以及最终结果的类型R。如一个histogram,v是一个数而R是一个histogram。
· SimpleAccumulator是增加的值和最终结果类型相同的累加器,如counter
十、并行执行
本小节描述程序的并行执行在Flink中是如何配置的。一个Flink程序包括多个任务(transformation,data source,和data sink)。一个任务分为几个并行执行实例,且每个并行实例处理任务的输入数据的一个子集。我们称一个任务的并行实例的数量为该任务的并行度(parallelism)。
Flink中任务的并行度可以设置为多个不同的level。
10.1 Operator Level
一个operator、data source、data sink的并行度可以通过调用setParallelism()来定义,例如:
1 final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 2 DataStream<String> text = [...] 3 DataStream<Tuple2<String, Integer>> wordCounts = text 4 .flatMap(new LineSplitter()) 5 .keyBy(0) 6 .timeWindow(Time.seconds(5)) 7 .sum(1).setParallelism(5); 8 9 wordCounts.print(); 10 11 env.execute("Word Count Example");
10.2 Execution Environment Level
正如anatomy-of-a-flink-program中所述,Flink程序在一个执行环境的上下文中执行。执行环境定义了所有Operator、data source,data sink的默认并行度。为了执行一个所有Operator、data source、data sink的并行度都为3的程序,我们设置执行环境的默认并行度的方式如下所示:
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(3); DataStream<String> text = [...] DataStream<Tuple2<String, Integer>> wordCounts = [...] wordCounts.print(); env.execute("Word Count Example");
10.3 Client Level
我们可以在Client向Flink提交job时设置并行度,client可以是Java或Scala程序。这种Client的一个例子便是Flink的命令行接口(CLI)。
对于CLI client,并行度可以用参数-p来确定,例如:
./bin/flink run -p 10 ../examples/*WordCount-java*.jar
在Java/Scala程序中,并行度可以被如下方式设置:
1 try { 2 PackagedProgram program = new PackagedProgram(file, args); 3 InetSocketAddress jobManagerAddress = RemoteExecutor.getInetFromHostport("localhost:6123"); 4 Configuration config = new Configuration(); 5 Client client = new Client(jobManagerAddress, config, program.getUserCodeClassLoader()); 6 7 // set the parallelism to 10 here 8 client.run(program, 10, true); 9 10 } catch (ProgramInvocationException e) { 11 e.printStackTrace(); 12 }
10.4 System Level
整个系统的默认并行度可以通过设置"./conf/flink-conf.yaml"中的parallelism.default属性。详情请见Configuration文档。
十一、Execution Plans
根据诸如数据数量(data size)或集群中设备数量等多种参数,Flink的优化器可以自动地选择你的程序的执行策略。在许多情况下,确切知道Flink如何执行你的程序会十分有用。
Plan可视化工具
Flink自带了用于可视化execution plan的工具。HTML文档中的可视化工具的内容在tools/planVisualizer.html中。它使用JSON来代表job的execution plan,并用带有执行策略完整注解的图来将之可视化。
以下代码展示了如何打印你的程序的的execution plan的JSON:
final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
...
System.out.println(env.getExecutionPlan());
若要查看execution plan的可视化结果,请遵循以下步骤:
1. 用你的浏览器打开planVisualizer.html。
2. 将JSON字符串粘贴到text域中。
3. 点击draw按钮
通过这些步骤,你便可以查看一个详细的execution plan的可视化结果:
图1 execution plan visualization
web接口
Flink提供一个web接口用来提交并执行job。该接口是JobManager用来监控的的web接口的一部分,默认都是运行在8081端口下的。通过该接口提交Job需要你设置"flink-conf-yaml"中的属性 jobmanager.web.submit.enable: true。
你可以在job执行之前确定程序的参数,Plan可视化是你可以在执行Flink的job前查看其execution plan。