11.Basic API Concepts之Overview

flink 1.8

Basic API ConceptsAPI的基本概念

 

Flink程序是在分布式集合上实现转换的常规程序(例如,filtering, mapping, updating state, joining, grouping, defining windows, aggregating)。集合最初是从sources 创建的(例如,从文件、kafka topics或本地内存集合中读取)。结果通过sinks返回,例如,sinks可以将数据写入(分布式)文件,或者写入标准输出(例如,命令行终端)。Flink程序在各种上下文中运行,独立运行或嵌入到其他程序中。执行可以在本地JVM中进行,也可以在许多机器的集群上进行。

根据数据源data sources的类型,即有界或无界数据源sources,您可以编写一个批处理程序或流处理程序,其中DataSet API用于批处理,DataStream API用于流处理。本指南将介绍这两种API所共有的基本概念,但有关使用每种API编写程序的具体信息,请参阅我们的Streaming GuideBatch Guide

注意:在展示如何使用这些API的实际示例时,我们将使用StreamingExecutionEnvironment和DataStream API。这和DataSet API中的概念完全相同,只需要将其替换为ExecutionEnvironment和DataSet类。

 

DataSet and DataStream

Flink使用特殊的类DataSet和DataStream来表示程序中的数据。您可以将它们看作是不可变的数据集合,可以包含重复的数据。在DataSet的情况下,数据是有限的,而对于DataStream,元素的数量可以是无界的。

这些集合在某些关键方面与常规Java集合不同。首先,它们是不可变的,这意味着一旦创建了它们,就不能添加或删除元素。您也不能简单地检查其中的元素。

集合最初是通过在Flink程序中调用source创建的,通过使用map、filter等API方法对这些source进行转换,可以派生出新的集合。

 

Anatomy of a Flink Program Flink程序的剖析

Flink程序看起来像转换数据集合transform collections的常规程序。每个程序都由相同的基本部分组成:

  1. 获得一个execution environment,
  2. 加载/创建初始数据,
  3. 指定此数据的转换,
  4. 指定放置计算结果的位置,
  5. 触发程序执行

现在,我们将对每一个步骤进行讲解,请参阅相关章节了解更多细节。注意,Java DataSet API的所有核心类都可以在org.apache.flink.api.java包中找到,而Java DataStream API的类可以在 org.apache.flink.streaming.api包中找到。

StreamExecutionEnvironment是所有Flink程序运行的基础。你可以使用它静态方法获得一个StreamExecutionEnvironment类:

getExecutionEnvironment()

createLocalEnvironment()

createRemoteEnvironment(String host, int port, String... jarFiles)

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

通常,您只需要使用getExecutionEnvironment(),因为这将根据上下文context选择创建正确的flink程序运行环境类:如果您在IDE中执行程序或作为常规Java程序来执行,它将创建一个本地运行环境,将在本地计算机上执行您的flink程序。如果您从程序中创建了一个JAR文件,并通过命令行command line调用它,Flink集群管理器将执行您的main方法,getExecutionEnvironment()方法将返回一个执行环境,用于在集群上执行您的程序。

为了指定数据源data sources,执行环境有几个方法可以使用各种方式从文件中读取数据:您可以将它们作为CSV文件逐行读取,或者使用完全定制的数据输入格式读取。要将文本文件按行读取,可以使用下面方式:

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

DataStream<String> text = env.readTextFile("file:///path/to/file");

这将为您提供一个数据流,您可以在该数据流上应用转换来创建新的派生数据流。

通过使用转换函数调用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,它包含执行时间和累加器结果。

有关流数据源data sources和接收器sink的信息,以及关于DataStream上支持的转换的更深入的信息,请参阅流指南Streaming Guide

有关批处理数据源data sources和接收器sink的信息,以及关于DataSet上支持的转换的更深入的信息,请参阅批处理指南Batch Guide

 

Lazy Evaluation延迟计算

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

延迟计算允许您构建复杂的程序,Flink将这些程序作为一个整体规划的单元来执行。

 

Specifying Keys指定Keys

一些转换(join、coGroup、keyBy、groupBy)要求在元素集合上定义一个键key。其他转换(Reduce、GroupReduce、Aggregate、Windows)允许在应用数据之前根据键key对数据进行分组。

数据集被分组为:

DataSet<...> input = // [...]
DataSet<...> reduced = input
  .groupBy(/*define key here*/)
  .reduceGroup(/*do something*/);

而key可以在DataStream上使用:

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替换即可。

Define keys for Tuples为元组定义keys

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

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

上面是对元组按第一个字段(Integer类型的字段)分组。

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

在这里,我们对元组按第一个和第二个字段组成的联合主键key进行分组。

关于嵌套元组的说明:如果您有一个嵌套元组的DataStream,例如:

DataStream<Tuple3<Tuple2<Integer, Float>,String,Long>> ds;

指定keyBy(0)将导致flink程序使用整个Tuple2作为key(Integer和Float作为key)。如果您想要“使用”嵌套的Tuple2中的字段作为key,您必须使用下面的字段表达式key。

Define keys using Field Expressions

您可以使用基于字符串的字段表达式引用嵌套字段,并定义用于grouping, sorting, joining, or coGrouping的keys。字段表达式使得在(嵌套的)组合类型(如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*/);

字段表达式语法:

  • 根据字段名选择POJO字段。例如,"user"指POJO类型的“user”字段。
  • 根据元组字段名或0-offset偏移量字段索引选择元组字段。例如,“f0”和“5”分别引用Java元组类型的第一个和第六个字段。
  • 您可以选择POJOs和元组中的嵌套字段。例如,"user.zip"指POJO的“zip”字段,其存储在POJO类型的“user”字段中。支持POJO和元组的任意嵌套和组合,例如"f1.user.zip"或"user.f3.1.zip"。
  • 您可以使用“*”通配符表达式选择全部类型。这也适用于非元组或POJO类型数据。

Field Expression Example:

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":类中的count字段WC。
  • "complex":递归地选择POJO类型ComplexNestedClass复合字段中的所有字段。
  • "complex.word.f2":选择嵌套的最后一个字段Tuple3。
  • "complex.hadoopCitizen":选择Hadoop IntWritable类型。

 

Define keys using Key Selector Functions用keyi选择器函数定义key

定义key的另一种方法是“key值选择器”函数。key值选择器函数接受单个元素作为输入,并返回该元素的key值。key可以是任何类型的,并且可以从通过计算得到。

下面的例子显示了一个key值选择器函数,它只返回对象的字段:

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

Specifying Transformation Functions

大多数转换都需要用户自己定义实现相应的类和函数。本节列出了不同的实现方式:

 

Implementing an interface实现一个接口

最基本的方法是实现其中一个接口:

class MyMapFunction implements MapFunction<String, Integer> {
  public Integer map(String value) { return Integer.parseInt(value); }
};
data.map(new MyMapFunction());

Anonymous classes匿名类

你可以将匿名类传递一个函数:

data.map(new MapFunction<String, Integer> () {
  public Integer map(String value) { return Integer.parseInt(value); }
});

Java 8 Lambdas

Flink还支持Java API中的Java 8 Lambdas。

 

data.filter(s -> s.startsWith("http://"));
data.reduce((i1,i2) -> i1 + i2);

 Rich functions

所有需要用户定义函数的转换也可以用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转换:

data.map(new MyMapFunction());

Rich函数也可以定义为一个匿名类:

data.map (new RichMapFunction<String, Integer>() {
  public Integer map(String value) { return Integer.parseInt(value); }
});

除了用户定义的函数(map、reduce等)之外,Rich函数还提供四种方法:open、close、getRuntimeContext和setRuntimeContext。这对于参数化函数(请参阅将参数传递给函数Passing Parameters to Functions)、创建和结束本地状态、访问广播变量(请参阅广播变量Broadcast Variables)以及访问运行时信息,如累加器和计数器(请参阅累加器和计数器 Accumulators and Counters),以及关于迭代的信息(请参阅迭代Iterations),都非常有用。

 

Supported Data Types

Flink对数据集或数据流中的元素类型进行了一些限制。这样做的原因是通过系统分析类型以确定有效的执行策略。

数据类型有六种:

  1. Java Tuples and Scala Case Classes Java元组和Scala案例类
  2. Java POJOs
  3. Primitive Types基本数据类型
  4. Regular Classes普通类
  5. Values
  6. Hadoop Writables
  7. Special Types特殊类型

 

Tuples and Case Classes

元组是包含固定数量的具有各种类型的字段的复合数据类型。Java API提供了从Tuple1到Tuple25的类。元组的每个字段都可以是任意的Flink类型,包括在元组中嵌套元组。可以使用字段的名称作为元组直接访问元组的字段。或者使用通用getter方法tuple.getField (int position)。字段索引从0开始。注意,这与Scala元组形成了对比,但是它更符合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")

POJOs

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

  1. 类必须是公有类public。
  2. 必须有一个公有无参构造器 (default constructor)。
  3. 所有字段要么是公共的,要么可以通过getter和setter函数访问。 例如:对于 private foo字段变量必须有getFoo()和setFoo()。
  4. 字段的类型必须是Flink支持的数据类型。目前,Flink使用Avro序列化任意对象objects (比如Date)。

Flink通过分析POJO数据类型的结构(即它了解了POJO的相关字段)。因此,POJO类型比一般类型更容易使用。此外,与其他数据类型相比,Flink可以更有效地处理POJO类型的数据。

下面的示例是一个简单的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"

 总结:

  • flink框架对POJO类型有很好的性能支持;
  • 不能使用无法序列化的类(file pointers, I/O streams, or other native resources)

 

Primitive Types基本数据类型

Flink支持所有Java和Scala基本类型,比如Integer、String和Double。

 

General Class Types

Flink支持大多数Java和Scala类(API和自定义)。除了包含不能序列化字段的类,如文件指针file pointers、I/O流或其他本地资源。通常遵循Java bean规范的类能够很好的工作。

所有未标识为POJO类型的类(请参阅上面的POJO需求)都由Flink作为通用类类型处理。Flink将这些数据类型视为黑盒,无法访问它们的内容(例如,为了提高排序效率)。使用序列化框架Kryo对常规类型进行反序列化。

 

Values

Value类型可以手动进行序列化和反序列化。它们没有使用通用的序列化框架,而是通过实现org.apache.flinktypes.value接口来重写read() 和write()方法,然后自定义序列化合反序列代码。当通用序列化效率非常低时,使用Value类型是非常好的选择。例如,将一个数据元素的稀疏向量实现为数组的数据类型。并且知道数组大部分的元素为零,就可以对非零元素使用特殊的编码,而通用序列化需要所有数组元素进行序列化。

org.apache.flinktypes.CopyableValue接口以类似的方式支持手动内部克隆逻辑。

Flink带有与基本数据类型对应的预定义值类型pre-defined Value types。(ByteValue, ShortValue,IntValue,LongValue,FloatValue,DoubleValue,StringValue,CharValue, BooleanValue)。这些Value类型充当基本数据类型的可变变体:它们的值可以被更改,允许程序员重用对象,从而减小垃圾收集器的压力。

 

Hadoop Writables

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

 

Special Types

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

 

Type Erasure & Type Inference

注意:本节只与Java相关。

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

Flink在准备执行程序时(调用程序的main方法时)需要类型信息。Flink Java API尝试重构以各种方式丢弃的类型信息,并显式地将其存储在数据集和操作算子中。您可以通过DataStream.getType()检索该类型。该方法返回一个TypeInformation实例,这是Flink表示类型的内部方法。

类型推断有其局限性,在某些情况下需要程序员的“协助”。例如,从集合创建数据集的方法,如ExecutionEnvironment.fromCollection(),您可以在其中传递描述类型的参数。但是像MapFunction<I, O>这样的泛型函数可能需要额外的类型信息。

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

 

Accumulators & Counters

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

最简单的累加器是计数器counter:您可以使用Accumulator.add(V value)方法递增它。作业结束时,Flink将汇总(合并)所有部分结果,并将结果发送给客户端client。累加器在调试期间非常有用,如果您想快速了解更多有关数据的信息,也可以使用累加器。

Flink目前有以下内置的累加器。它们中的每一个都实现了累加器Accumulator接口。

  • IntCounterLongCounter 和 DoubleCounter:下面是使用计数器的示例。
  • Histogram:针对离散数量的区间的histogram实现。在内部,它只是一个从整数到整数的映射。您可以使用它来计算值的分布,例如统计单词计数程序的每行单词的分布情况。

 

How to use accumulators:

首先,您必须在需要使用它的用户定义转换函数中创建一个accumulator对象(这里是一个计数器counter)。

private IntCounter numLines = new IntCounter();

其次,必须注册accumulator对象,通常是在rich函数的open()方法中。这里还定义了名称。

getRuntimeContext().addAccumulator("num-lines", this.numLines);

现在可以在操作算子函数的任何位置使用累加器,包括open()和close()方法中。

this.numLines.add(1);

整个结果将存储在JobExecutionResult对象中,该对象是从执行环境的execute()方法返回的(目前,只有等待作业执行完成时才有效)。

myJobExecutionResult.getAccumulatorResult("num-lines")

所有累加器对每个作业共享一个名称空间。因此,您可以在作业的不同操作算子函数中使用相同的累加器。Flink将在内部合并所有具有相同名称的累加器。

关于累加器和迭代器的说明:当前,累加器的结果只有在整个作业结束后才可用。为了让上一个迭代器的结果在下一个迭代器中可用。您可以使用聚合器Aggregators来计算每次迭代器的统计数据,并将迭代的终止条件建立在这些统计数据的基础上。

 

Custom accumulators:

要实现自己的累加器,只需实现Accumulator接口即可。如果您认为您的自定义累加器Accumulator应该与Flink一起提供,请创建一个pull请求。

您可以选择实现Accumulator 或者 SimpleAccumulator

Accumulator<V,R>是最灵活的:它为要添加的值定义了类型V,为最终结果定义了结果类型R。例如,对于histogram,V是一个数字,R是一个histogram。SimpleAccumulator适用于两种类型相同的情况,例如计数器counters。

 

累加器和计数器总结:

(1)在作业结束时,Flink将汇总(合并)所有部分结果,然后将结果发送给客户端;

(2)累加器在调试期间非常用,如果您想快速了解更多关于数据的信息,也可以使用累加器。

 

https://ci.apache.org/projects/flink/flink-docs-release-1.8/dev/api_concepts.html

https://flink.sojb.cn/dev/api_concepts.html 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值