基本API概念
Flink程序是实现分布式集合转换(e.g. filter、map、join、group、聚合、定义窗口、更新状态)的规范化程序。集合从 source (e.g. 读取文件、kafka订阅、本地内存中集合读取...)创建;结果通过 sink 返回。Flink可以独立运行,也可以嵌入其他应用中运行;可以在本地JVM中运行,也可以在多台服务器上集群部署。
对于有界、无界两种类型的数据 source 类型,可以使用 DataSet API 编写批处理程序或使用 DataStream API 编写流处理程序来处理。
DataSet & DataStream
Flink 使用 DataSet 和 DataStream 类来表示程序中的数据,可以将它们看作可能包含重复的不可变数据集合。对于DataSet,数据是有限的;对于DataStream,数据是无限的。
这些集合同标准的 Java 集合存在一些关键的区别。首先,他们是不可变的,也就是说他们一旦被创建,就不能添加和删除元素,也不能简单的检查他们的内部元素。
在Flink程序中,集合是通过添加数据 source 来创建的,通过诸如 map(...)、filter(...) 等 API 方法对数据进行转换从而派生新的集合。
剖析一个 Flink 程序
...
延迟计算
无论在本地还是集群执行,所有 Flink 程序都是延迟执行的:当程序的 main 方法被执行时,并不是立即执行数据集合的加载和转换,而是创建每个操作并将其加入程序的执行计划。当执行环境调用 execute() 方法显示地触发执行的时候才真正执行各个操作。
指定关键字
一些转换操作(join、coGroup、keyBy、keyGroup)要求在集合上定义 key;另外一些转换操作(reduce、groupReduce、aggregate、windows)允许在应用这些转换之前将数据按键分组。
如下对 DataSet 分组:
DataSet<...> input = ... // 创建 DataSet.
DataSet<...> reduce = input
.goupBy(/* 在这里定义 Key. */)
.reduceGroup(/* 做一些处理操作 */);
如下对 DataStream 分组:
DataStream<...> input = ... // 创建 DataStream.
DataStream<...> window = input
.keyBy(/* 在这里定义 Key */)
.window(/* 创建窗口. */);
Flink 的数据模型不是基于键值对的。因此不需要将数据类型集合物理的打包到键和值模型。键的功能是指导分组算子用哪些数据来分组。
支持的数据类型
Flink 对于 DataSet 和 DataStream 中可以包含的元素做了一些限制。这么做是为了使系统能够分析类型以确定有效的执行策略。
支持7种不同的数据类型:
- Java Tuple 和 Scala Case Class。
- Java POJO。
- 基本数据类型。
- 常规的类。
- 值。
- Hadoop Writable。
- 特殊类型。
Tuple 和 Case Class
Tuple 是复合数据类型,包含固定数量的各种类型的字段。Java API 提供了从 Tuple1 ~ Tuple25 的类。Tuple 的每一个字段可以是任意的 Flink 支持的数据类型,包括 Tuple,即嵌套的 Tuple。Tuple 的字段可以通过字段名称直接访问,如 tuple.f1 直接访问第1个字段;也可以通过 getter 方法 tuple.getField(int position)。
DataStream<Tuple2<String, Integer>> wordCounts = env.fromElement(
new Tuple2<String, Integer>("Hello", 1),
new Tuple2<String, Integer>("World", 2)
)
wordCounts
.map(tuple -> tuple.f1)
.keyBy(0); // .keyBy("f0") 也可以.
POJO
Flink 将满足如下条件的 Java 类作为特殊的 POJO 数据类型处理:
- 类必须是公有的。
- 类必须有一个公有的无参构造器。
- 所有的字段要么是
pulbic的,要么可以通过getter和setter访问。 - 字段的类型必须被已注册的序列化程序所支持。
POJO 通常用 PojoTypeInfo 表示,并使用 PojoSerializer(Kryo 作为可配置的备用序列化器)序列化。例外情况是 POJO 是 Avro 类型(Avro 指定的记录)或作为“ Avro 反射类型”生成时;在这种情况下,POJO 由 AvroTypeInfo 表示,并且由 AvroSerializer 序列化。如果需要,也可以注册自己的序列化器。
Flink 分析 POJO 类型的构造,也就是,说会推断 POJO 的字段。因此,POJO 类型比常规类型更易于使用。此外,Flink 可以比一般类型更加高效的处理 POJO。
基本数据类型
Flink 支持所有 Java 的基本数据类型,如 Integer、String 、Double 等。
常规的类
Flink 支持大部分 Java 的类(API 和自定义)。除了包括无法序列化的字段的类,如文件指针、I/O流或其他本地资源。遵循 Java Beans 约定的类通常可以很好的工作。
Flink 对于所有未识别未 POJO 的类型的类(参考上面对于 POJO 的要求)都作为常规类处理。Flink 将这些数据视为黑盒,并且无法访问其内容(为了诸如高效的排序等目的)。常规类使用Kyro序列化框架进行序列化和反序列化。
值
值类型需要手动添加其序列化和反序列化过程;他们不是通过序列化框架,而是通过实现 org.apache.flinktypes.Value 接口的 read 和 write 方法来为这些操作提供自定义的序列化和反序列化过程。当通用的序列化效率非常低的时候,使用值类型是合理的。例如,使用数组实现稀疏向量;已知数组大部分元素为零,就可以对非零元素使用特殊编码,而通用序列化框架只会简单的将所有的数组元素都写入。
org.apache.flinktypes.CopyableValue 接口以类似的方式支持内部手工克隆逻辑。
/**
* Interface to be implemented by basic types that support to be copied efficiently.
*/
@Public
public interface CopyableValue<T> extends Value {...}
Flink 有与基本数据类型对应的预定义值类型,ByteValue、ShortValue、IntValue、LongValue、FloatValue、DoubleValue、StringValue、CharValue、BooleanValue。这些值类型充当基本数据类型的可变变体;它们的值可以改变,允许程序员重用对象并减轻 GC 的压力。
Hadoop Writable
可以使用实现了 org.apache.hadoop.Writable 接口类型。它们会使用 write() 和 readFields() 方法中定义的序列化逻辑。
特殊类型
可以使用特殊类型,包括 Scala 的 Either、Option 和 Try。Java API 有对 Either 的自定义实现。类似于 Scala 的 Either,它表示一个具有 Left 和 Right 两种可能类型的值。Either 可用于错误处理或需要输出两种不通类型记录的算子。
类型推断和类型擦除
类型擦除指定的 Java 编译器在编译后抛弃了大量范型信息;意味着,在程序运行时,对象的实例已经不知道它的具体范型类型。例如 DataStream<String> 和 DataStream<Integer> 的实例在运行时,对 JVM 来说是一样的。
Flink 在准备程序执行时(main 方法被调用时)需要类型信息。Flink Java API 尝试重建以各种方式丢弃的类型信息,并将其显示存储在数据集和算子中;可以通过 DataStream.getType() 获取数据数据类型。该方法返回一个 TypeInformation 的一个实例,这是 Flink 内部表示类型的方式。
类型推断有其局限性,在某些情况下需要程序员的“配合”。这方面的实例是从集合创建数据集的方法,例如ExecutionEnviroment.fromCollection()方法,可以传递一个描述类型的参数。像 Map<I, O> 这样的范型函数同样可能需要额外的类型信息。
可以通过输入格式和函数实现 ResultTypeQueryable 接口,以明确告知 API 其返回类型。被调用函数的输入类型通常可以通过先前的操作的结果类型来推断。
累加器和计数器
累加器简单地由 加法操作 和 最终累加结果 构成,可以在作业结束后使用。
最简单的累加器是一个 计数器:可以使用 Accumulator.add(V value) 方法递增它。作业结束时,Flink 会合计(合并)所有的部分结果并发送给客户端。累加器在 debug 或者想快速了解数据的时候非常有用。
Flink 目前有如下 内置累加器。它们都实现了 Accumulator 接口。
IntCounter、LongCounter和DoubleCounter。Histogram:离散数据桶的直方图实现。在内部,它只是一个从整数到整数的映射。可以用它计算值的分布,例如一个词频统计程序中每行词频的分布。
如何使用累加器?
首先必须在要使用它的用户定义转换函数中创建累加器对象。
private IntCounter numLines = new IntCounter();
其次,必须注册类加器对象,通常在富函数的 open() 方法中,这这里还可以定义名称。
getRuntimeContext().addAccumulator("num-lines", this.numLines);
你现在可以在算子函数中的任何位置使用累加器,包括 open() 和 close() 方法。
this.numLines.add(1);
总体结果将存储在 JobExecutionResult 对象中,该对象是从执行环境的 execute() 方法返回的(目前这仅在执行等待作业完成时才有效)。
myJobExecutionResult.getAccumulatorResult("num-lines");
每个作业的所有累加器共享一个命名空间,这样可以在作业的不同算子函数中使用相同的累加器。Flink 会在内部合并所有同名累加器。
注意:目前,累加器的结果只有在整个作业结束以后才可用;Flink 计划实现在下一次迭代中使用前一次迭代的结果可用;可以使用 Aggregators 计算每次迭代的统计信息,并根据这些信息确定迭代合适中止。
自定义的累加器
要实现你自己的累加器,只需编写累加器接口的实现即可。可以选择 Accumulator 或者 SimpleAccumulator。
Accumulator<V, R>最灵活:它为要递增的值定义类型 V,最最终结果定义类型 R。例如,对于 histogram,V 是数字而 R 是 histogram。SimpleAccumulator则适用于两个类型相同的情况,例如计数器。