学习文档:《Flink 官方文档 - DataStream API - 状态与容错 - 数据类型以及序列化 - 概览》
学习笔记如下:
Flink 使用独特的方式来处理数据类型以及序列化。
因为 Flink 需要根据类型选择更有效的执行策略,所以限制了允许在 DataStream 中存在的数据类型。
以下 7 种不同的数据类型:
元组(tuples)是一个包含固定数量的不同类型的变量的组合数据结构。Flink 的 Java API 提供了 Tuple1
到 Tuple25
,其中每个字段可以是任意 Flink 支持的类型。
访问元素:可以通过类似 tuple.f4
或 tuple.getfield(int position)
方法访问指定位置的元素(其中元素下标从 0 开始)
样例:Java Tuples
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(value -> value.f0);
Java 或 Scala 的类如果完全满足以下条件,则会被 Flink 视为 POJOs 数据类型:
getter
方法和 setter
方法;例如,对于一个 foo
属性,其 getter 和 setter 必须命名为 getFoo()
和 setFoo()
POJOs 通常被 PojoTypeInfo
表示并使用 PojoSerializer
序列化。
Flink 会分析 POJO 类型的结构,即了解 POJO 的字段。因此,POJO 类型比一般类型更容易使用,且相较于一般类型,Flink 可以处理得更高效。
验证类型是否为 POJO 的方法:
org.apache.flink.types.PojoTestUtils#assertSerializedAsPojo();
样例:拥有 2 个公有字段的 POJOs 类型
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(value -> value.word);
Flink 支持 Java 和 Scala 的所有原生类型,例如 Integer、String、Double 等。
Flink 支持大多数 Java 和 Scala 类,包括来自 API 的和用户自定义的类,但是不支持包含无法序列化的字段的类(如文件指针、I / O流等)。
所有未被识别为 POJO 类型的类均会被视作通用类类型进行处理。Flink 将这些类视为黑盒,无法访问其内容。 使用序列化框架 Kryo 对通用类型进行解/序列化。
Values 类型手动定义了它的序列化和反序列化方法。它们不使用通用的序列化框架,而是实现了 org.apache.flink.types.Value
接口的 read
和 write
方法。当通用的序列化效率较低时,使用 Values 类型将会更加合适。例如,在写入稀疏元素数组时,通过对非零元素进行特殊变化,可以更高效地完成序列化和反序列化。
Flink 提供了对应于基本数据类型的预定义 Values 类型:ByteValue
、ShortValue
、IntValue
、LongValue
、FloatValue
、DoubleValue
、StringValue
、CharValue
、BooleanValue
。这些 Values 类型的值可以被改变、允许重用对象并可以减轻垃圾回收器的压力。
可以试用实现了 org.apache.hadoop.Writable
接口的类型。Flink 会使用它的 write()
和 readFields()
方法进行序列化。
Flink 也支持包括 Scala 的 Either
、Option
和 Try
的特殊类型。Flink 的 Java API 也有自定义的 Either
,与 Scala 的 Either
类似,它表示一个可能是两个类型中的任意一个。
Either
的适用场景:异常处理;Operator 需要输出两种不同类型的记录。
Java 编译器会将泛型信息擦除,在运行时,一个对象的实例将不再了解其泛型信息。例如,对于 JVM 来说,DataStream
和 DataStream
看起来是一样的。
但是,Flink 在准备应用程序的过程中(即调用 main()
方法时)是需要类型信息的。因此,Flink 的 Java API 重建了类型信息,并将类型信息明确地添加到了数据集和 Operator 中。可以通过 DataStream.getType()
方法获取实例的类型信息。
这种类型推理他页具有局限性。例子,在使用从集合创建数据集的方法 StreamExecutionEnvironment.fromCollection()
时,需要额外传递一个描述类型的参数;又如,MapFunction
这样的泛型方法可能也需要额外的类型信息。
ResultTypeQueryable
接口可以通过输入格式和函数来明确地告诉 API 它们的返回类型。通常来说,函数的输入类型通常可以根据上游操作的结果类型来判断。
Flink 推断出分布式计算过程中交换和存储的很多类型信息。在大多数情况下, Flink 可以准确地推断出所有必要的信息,从而让 Flink 可以支持如下特性:
通常来说,在 pre-flight 阶段需要有关数据类型的信息,即在调用 execute()
、print()
、count()
或 collect()
之前。
注册子类型:如果函数仅描述了超类型,但在执行过程中实际使用了这些超类型的子类型,则让 Flink 知道这些子类型会大大提高性能。因此,对每个子类型在 StreamExecutionEnvironment
上调用 .registerType(clazz)
是有意义的。
注册自定义序列化器:对自己无法处理的类型,Flink 会使用 Kryo。并非所有类型都能被 Kryo 处理,其解决方案是为导致问题的类型注册额外的序列化器,即在 StreamExecutionEnvironment
上调用 .getConfig().addDefaultKryoSerializer(clazz, serializer)
。
添加类型提示:在 Java API 中,有时当 Flink 无法推断出泛型时,用户必须传递类型提示。
手动创建类型信息:因为 Java 会擦除泛型信息,导致 Flink 无法推断数据类型。因此,对于某些 API 调用,手动创建类型信息可能是必要的。
TypeInformation
类是所有类型描述类的基类,它定义了一些基础属性,可以生成序列化程序以及比较器等。
TypeInformation 源码位置:flink-core/src/main/java/org/apache/flink/api/common/typeinfo/TypeInformation.java
在内部,Flink 对类型进行了以下区分:
void
、String
、Date
、BigDecimal
和 BigInteger
Options
、Either
、Lists
、Maps
……POJOs 支持创建复杂类型,在使用中透明、且能够被 Flink 高效处理,当前比较受欢迎。
详见:Flink|《Flink 官方文档 - 实践练习 - DataStream API 简介》学习笔记
因为 Java 会擦除泛型信息,所以需要在构造 TypeInformation
时添加类型信息。
对于非泛型,直接添加:
TypeInformation<String> info = TypeInformation.of(String.class);
对于泛型,则需要通过 TypeHint
添加:
TypeInformation<Tuple2<String, Double>> info = TypeInformation.of(new TypeHint<Tuple2<String, Double>>(){});
在 Flink 内部,将会创建一个捕获了泛型信息的泛型子类,该子类会将类型信息存储到运行时。
要创建 TypeSerializer
,直接调用 TypeInformation
的 typeInfo.createSerializer(config)
即可。其中的 config
参数是包含注册的序列化器的 ExecutionConfig
类型。可以通过调用 DataStream
的 getExecutionConfig()
方法获取它。
在函数内部,可以通过在 rich function 中使用如下方法获取它:
getRuntimeContext().getExecutionConfig()
Flink 除了通过反射重建了部分类型信息,还有根据输入类型判断是返回值类型的逻辑。但是,在部分场景下,Flink 无法重建类型信息,需要使用 type hints
样例:Flink 无法重建类型信息的样例
public class AppendOne<T> implements MapFunction<T, Tuple2<T, Long>> { public Tuple2<T, Long> map(T value) { return new Tuple2<T, Long>(value, 1L); } }
在 Flink 无法重建被擦除的泛型信息时,需要使用 Java API 的 type hints 来告诉 Flink 数据流的类型。
样例:使用 type hints 的样例
DataStream<SomeType> result = stream .map(new MyGenericNonInferrableFunction<Long, SomeType>()) .returns(SomeType.class);
在 returns
方法中,可以接受如下类型的参数:
classes
,例如 returns(SomeType.class)
,适用于无泛型的场景TypeHints
,例如 returns(new TypeHint>(){})
,适用于有泛型的场景;TypeHints
可以捕获其泛型并将其保存到运行时。Java 8 的 lambda 表达式的类型提取方法与非 lambda 表达式不同,因为 lambda 表达式并没有由继承自接口的类直接构成。
同样的,Flink 尝试找出执行了 lambda 表达式的类,并通过 Java 泛型标签确定参数类型和返回值类型。然而,并不是所有编译器都为 lambda 表达式生成了这种标签。
POJO 类型的 TypeInfo
创建了每一个字段的序列化器。其中的标准类型使用 Flink 内置的序列化器;对于其他类型, 则尝试使用 Kryo 序列化;如果 Kryo 无法处理这些类型, 可以指定使用 Avro 序列化。
样例:强制使用 Avro 序列化
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.getConfig().enableForceAvro();
样例:强制使用 Kryo 序列化
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.getConfig().enableForceKryo();
样例:添加自定义的 Kryo 序列化器
env.getConfig().addDefaultKryoSerializer(Class<?> type, Class<? extends Serializer<?>> serializerClass);
可以试用如下方法关闭 Kryo 的序列化器:
env.getConfig().disableGenericTypes();
使用场景:有时,我们可能希望保证所有类型都能被 Flink 自己的序列化器或用户自定义的序列化器高效地序列化,并避免退化到 Kryo 序列化器。
类型信息工厂(type information factory)允许用户在 Flink 类型系统中插入用户自定义的 Type Information。
首先,需要实现 org.apache.flink.api.common.typeinfo.TypeInfoFactory
来返回自定义的 type information。而后,如果在相应的类型上注释了 @org.apache.flink.api.common.typeinfo.TypeInfo
,则会在类型提取阶段调用该工厂。
类型信息工厂可以在 Java API 和 Scala API 中使用。
工厂的优先级高于 Flink 的内置类型。
样例:声明用户类型
MyTuple
并使用 factory 提供了自定义类型信息@TypeInfo(MyTupleTypeInfoFactory.class) public class MyTuple<T0, T1> { public T0 myfield0; public T1 myfield1; }
public class MyTupleTypeInfoFactory extends TypeInfoFactory<MyTuple> { @Override public TypeInformation<MyTuple> createTypeInfo(Type t, Map<String, TypeInformation<?>> genericParameters) { return new MyTupleTypeInfo(genericParameters.get("T0"), genericParameters.get("T1")); } }
方法
createTypeInfo(Type, Map
为工厂的目标类型提供了 type information。其参数提供了额外的类型信息,同时也提供了类型的泛型类型参数(generic type parameters)。>)
如果类型中包含从 Flink 函数的输入类型中派生的泛型参数,那么需要确保还实现了 org.apache.flink.api.common.typeinfo.TypeInformation#getGenericParameters
,以实现泛型参数和类型信息的双向映射。