【Flink】Basic API的核心概念

目录

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替代。

1.DataSet and DataStream

Flink有特殊的类DataSet和DataStream来表示程序中的数据。你可以将它们认为包含副本的不可变的数据集合。DataSet数据集是有界的,而DataStream数据元素是无界的。

这些集合在某些关键的方面与常规的Java集合不同。首先,它们是不可变的,这意味着一旦创建了它们,就不能添加或删除元素。也不能简单的检查内部的元素。一个集合最初是通过在Flink程序中添加一个source来创建的,而新的集合则是通过诸如map,filter等API方法来转换它们的。

2.Anatomy of a Flink Program(Flink程序剖析)

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<String> text = env.readTextFile("file:///path/to/file");

这将为你返回一个DataStream,然后你可以应用转换来创建新的派生DataStream。你可以使用转换函数调用DataStream的转换方法。例如,map转换:

DataStream<String> input = ...;
DataStream<Integer> parsed = input.map(new MapFunction<String, Integer>() {
    @Override
    public Integer map(String value) {
        return Integer.parseInt(value);
    }
});

这将通过将原始集合中的每个字符串转换为整数来创建新的DataStream.

一旦有了一个最终结果的DataStream,就可以通过创建一个sink来讲其写入外部系统。这些只是创建一个sink的实例方法:

writeAsText(String path)
print()

只要你指定了需要触发执行的完整程序,通过调用StreamExecutionEnvironment的execute()方法来执行,依靠ExecutionEnvironment的类型,将在本地机器触发执行,或者在集群上提交你的程序。execute()方法返回一个JobExecutionResult,它包含执行时间和累加器结果。

3.Lazy Evaluation(延迟执行)

所有的Flink程序都是延迟执行的。当程序的main方法被执行时,加载数据和转换不会直接发生。相反,每个操作都被创建并添加到程序的计划中。当执行环境中的execute()显示地触发执行时,才开始执行实际的操作。程序在本地执行还是集群上执行取决于执行环境的类型。

延迟执行可以让你创建复杂的程序,Flink作为一个整体计划单元执行

4.Specifying Keys(key的定义)

一些转换(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替代。

1.Define keys for Tuples(元组键)

最简单的情况是在元组的一个或多个字段上对元组进行分组:

元组被分组在第一个字段(整数类型)

DataStream<Tuple3<Integer,String,Long>> input = // [...]
KeyedStream<Tuple3<Integer,String,Long>,Tuple> keyed = input.keyBy(0)

在这里,我们将元组分组在一个由第一和第二个字段组成的组合键上。

DataStream<Tuple3<Integer,String,Long>> input = // [...]
KeyedStream<Tuple3<Integer,String,Long>,Tuple> keyed = input.keyBy(0,1)

嵌套元组的一个注释:如果你有一个带有嵌套元组的DataStream,例如:

指定keyBy(0)将导致系统使用完成的Tuple2作为键(使用整数和浮点数作为键)。如果你想要导航到嵌套的Tuple2上,你必须使用后面介绍的字段表达式键。DataStream<Tuple3<Tuple2<Integer, Float>,String,Long>> ds;

2.Define keys using Field Expressions(字段表达式键)

你可以使用基于字符串的字段表达式来引用嵌套的字段,并定义用于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<WC> words = // [...]
DataStream<WC> wordCounts = words.keyBy("word").window(/*window specification*/);

字段表达式的语法:

  1. 根据字段名选择POJO字段。例如,"user"指的是POJO类型的“user”字段
  2. 通过字段名或0偏移量开始的索引来选择Tuple类型的字段。例如,“f0” 和 “5” 指的是Java Tuple类型的第一个和第六个字段
  3. 你可以在POJO和Tuple中选择嵌套的字段。例如,“user.zip” 指的是一个POJO的“zip”字段,该字段存储在POJO类型的“user”字段中。对POJO和Tuple的任意嵌套和混合都是支持的。如,“f1.user.zip” or “user.f3.1.zip”
  4. 你可以使用同配置表达式“*”选择完整的类型。这也适用于非Tuple和POJO类型的类型

字段表达式实例:

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<Long, Long, String> word;
  public IntWritable hadoopCitizen;
}

这些是上面示例代码的有效字段表达式:

  • “count”:WC类中的count字段
  • “complex”:递归的选择POJO类型ComplexNestedClass类的复杂字段
  • “complex.word.f2”:选择嵌套Tuple3的最后一个字段
  • “complex.hadoopCitizen”:选择Hadoop IntWritable类型

3.Define keys using Key Selector Functions(Key选择器函数)

定义键的另一个方法是“key selector”函数。一个键选择器函数将一个元素作为输入,并返回元素的键。键可以是任何类型的,并且是由确定性计算派生出来的。

下面的示例显示了一个键选择器函数,它简单的地返回一个对象的字段。

// some ordinary POJO
public class WC {public String word; public int count;}
DataStream<WC> words = // [...]
KeyedStream<WC> kyed = words
  .keyBy(new KeySelector<WC, String>() {
     public String getKey(WC wc) { return wc.word; }
   });

5.Specifying Transformation Functions(转换函数)

大多数转换都需要用户自定义函数。本节列出了如何指定它们的不同方式。

1.实现接口

// 最基本的方法是实现所提供的接口之一:
class MyMapFunction implements MapFunction<String, Integer> {
  public Integer map(String value) { return Integer.parseInt(value); }
});
data.map(new MyMapFunction());

2.匿名内部类

// 可以用一个匿名类传给一个函数
data.map(new MapFunction<String, Integer> () {
  public Integer map(String value) { return Integer.parseInt(value); }
});

3.Java 8 Lambdas

// 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);

4.Rich functions

// 所有需要用户定义函数的转换可以将其作为一个rich function。例如,而不是
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); }
});
// 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<String, Integer>() {
  public Integer map(String value) { return Integer.parseInt(value); }
});

除了用户定义的函数(map,reduce等)之外,Rich function还提供了四个方法:open,close,getRuntimeContext,setRuntimeContext。这些对于参数化的函数,创建和终结局部状态,访问广播变量,访问诸如累加器和计数器之类的运行时信息,以及迭代的信息,都是很有用的。

6.Flink数据类型

Flink对可能在DataSet或DataStream中元素类型进行了一些限制。这样做的原因是系统分析这些类型来决定有效的执行策略。

有六种不同类别的数据类型:

  1. Java Tuples and Scala Case Classes
  2. Java POJOs
  3. Primitive Types(基本类型)
  4. Regular Classes(普通的class类型)
  5. Values(Flink自带的一种对应基本类型一种高效序列化类型)
  6. Hadoop Writables
  7. Special Types

1.Tuples and Case Classes

Tuple是包含有不同类型的固定数量的字段的复合类型。Java API提供了从Tuple1到Tuple25的类。Tuple的每个字段都可以是任意的Flink类型,包括further Tuple,结果是嵌套的元组。可以使用字段的名称作为Tuple直接访问Tuple的字段tuple.f4,或使用通用的getter方法 tuple.getField(int position)。字段索引从0开始。注意,这与Scala的Tuple形成了对比,但是它更符合Java的一般索引。

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")

2.POJOs

Java和Scala类被Flink视为一种特殊的POJO数据类型,如果它们满足以下要求:

  • class必须是public
  • 必须有一个public的无参构造函数
  • 所有的字段要么是public,要么必须通过getter和setter方法访问
  • 字段类型必须由Flink支持。目前Flink使用avro来序列化任意对象(如date)

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<WordWithCount> wordCounts = env.fromElements(
    new WordWithCount("hello", 1),
    new WordWithCount("world", 2));
wordCounts.keyBy("word"); // key by field expression "word"

3.Primitive Types(基本类型)

Flink支持所有的Java和Scala的基本类型,如Integer,String,Double等

4.General Class Types

Flink支持大多数Java和Scala类(API和自定义)。限制适用于包含不能序列化的字段的类,比如文件指针,IO流,或其他本地资源。遵循JavaBean的约定的类通常工作的很好。

所有没有被确定为POJO类型的class都是由Flink作为一般类型处理。Flink将这些数据类型视为黑盒子,无法访问他们的内容(例如,有效的排序)。一般类型使用Kryo进行序列化和反序列化。

4.Values

Value类型可以手动描述它们的序列化和反序列化。它们没有使用通用的序列化框架,而是通过实现org.apache.flinktypes.Value接口的write和read方法来提供定制操作。使用Value类型是合理的,因为一般的序列化是非常低效的。例如,一个数据类型实现了作为数组元素的稀疏向量。知道数组大部分为零,可以为非零元素使用特殊的编码,而一般的序列化则只需要编写所有的数组元素。org.apache.flinktyps.CopyableValue接口以类型的方式支持手动的内存克隆逻辑。

Flink带着与基本数据类型对应的预定义Value类型(ByteValue,ShortValue,IntValue,LongValue,FloatValue,DoubleValue,StringValue,CharValue,BooleanValue).这些Value类型充当基本数据类型可变变体。他们的值可以被修改,允许程序员重用对象并垃圾收集器中释放压力。

5.Hadoop Writables

你可以使用实现了org.apache.hadoop.Writable接口的类型。在write和readFields方法中定义序列化逻辑用于序列化。

6.Special Types(特殊类型)

可以使用特殊类型,包括Scala的Either,Option,Try。Java API 也有Either自己的自定义实现。与Scala的Either类似,它代表一种两种可能的类型的值,Left和Right。对于需要输出两种不同类型记录的错误处理或操作,Either是有用的。

7.Type Erasure & Type Inference(类型擦除和类型推断)

注意:这部分只与Java有关。

Java编译器在编译之后会抛出很多泛型类型的信息。这在Java中称为类型擦除。这意味着在运行时,对象的实例不再知道它的泛型类型。例如,DataStream<String>和DataStream<Long>对于JVM来说是一样的。

Flink在准备执行程序的时候需要类型信息(当程序的主要方法被调用时)。Flink Java API试图重构以各种方式抛出的类型信息,并将其显示地存储在数据集合操作符中。你可以通过DataStream.getType()来检索类型。这个方法返回一个TypeInfomation实例,这是Flink的内部方式来表示类型。

类型推断有其局限性,在某些情况下需要程序员的“合作”。例如,创建数据集的方法的示例,ExecutionEnvironment,fromCollection(),你可以传递一个参数描述类型,还有个泛型函数MapFunction<I,O>可能需要额外的类型信息。

可以通过输入格式和函数来实现ResultTypeQueryable接口,从而明确地告诉API 他们的返回类型。函数调用的输入类型通常可以通过前一个操作的结果类型来推断。

7.Accumulators & Counters(累加器和计数器)

累加器是一个简单的构造,有一个添加操作( add operation)和最终积累的结果( final accumulated result),在作业结束后可用。

最简单的累加器是一个计数器(counter),可以使用Accumulator.add(V value)方法来增加它。在job结束时,Flink将sum(merge)所有的部分结果,并将结果发送给客户端。在调试过程中,累加器是非常有用的,或者如果你想要了解更多关于你的数据信息。

Flink目前有以下内置的累加器,每个都实现了累加器接口:

  • IntCounterLongCounter and DoubleCounter
  • Histogram: 一个用于离散数量的容器的直方图实现。在内部,它只是一个从整数到整数的映射。你可以使用它来计算值的分布,例如,一个单词计数程序的每一行单词的分布。

如何使用累加器:

首先,在自定义的转换函数中创建一个累加器对象(例如: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将在内部合并所有相同名称的累加器。

关于累加器和迭代器的注释:

目前累加器的结果仅在整个作业结束后才可用。我们还计划在下一次迭代中使前一次迭代的可用结果。 您可以使用聚合器来计算每次迭代统计信息,并根据此类统计信息确定迭代的终止。

自定义累加器:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值