Apache Flink以一种独特的方式处理数据类型和序列化,它包含自己的类型描述符、泛型类型提取和类型序列化框架。本文档描述了这些概念及其背后的基本原理。
Flink对DataStream中的元素类型有一些限制。这样做的原因是系统分析类型来确定有效的执行策略。
以下是7类数据类型:
元组是复合类型,包含固定数量的具有各种类型的字段。Java API提供了从Tuple1到Tuple25的类。元组的每个字段都可以是任意的Flink类型,包括更多的元组,从而产生嵌套元组。元组获取值有两种方式,分别是tuple.f4, 或者使用tuple.getField(int position),字段索引从0开始。注意,这与Scala元组是相反的,但它与Java的通用索引更一致。
package com.flink.datastream;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
/**
* @author DeveloperZJQ
* @since 2022-5-30
*/
public class TupleDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<Tuple2<String, Integer>> wordCounts = env.fromElements(
new Tuple2<>("hello", 1),
new Tuple2<>("world", 2));
SingleOutputStreamOperator<Integer> map = wordCounts.map((MapFunction<Tuple2<String, Integer>, Integer>) value -> value.f1);
KeyedStream<Tuple2<String, Integer>, String> keyBy = wordCounts.keyBy(one -> one.f0);
map.print();
keyBy.print();
env.execute();
}
}
如果Java和Scala类满足以下要求,Flink会将它们视为特殊的POJO数据类型:
pojo通常用PojoTypeInfo表示,并用PojoSerializer序列化(使用Kryo作为可配置的回退)。例外情况是pojo实际上是Avro类型(Avro Specific Records)或作为“Avro Reflect types”产生。在这种情况下,POJO是由AvroTypeInfo表示的,并用AvroSerializer序列化。如果需要,您还可以注册自己的自定义序列化器;有关更多信息,请参阅序列化。
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(value -> value.word);
Flink支持所有的Java和Scala原始类型,如Integer, String和Double。
Flink支持大多数Java和Scala类(API和自定义)。限制适用于包含不能序列化字段的类,如文件指针、I/O流或其他本机资源。一般来说,遵循Java bean约定的类工作得很好。
所有未标识为POJO类型的类(请参阅上面的POJO要求)都由Flink作为通用类类型处理。Flink将这些数据类型视为黑盒,不能访问它们的内容(例如,为了高效排序)。一般类型使用序列化框架Kryo进行反/序列化。
值类型手动描述它们的序列化和反序列化。它们没有使用通用的序列化框架,而是通过实现带有读写方法的org.apache.flink.types.Value接口,为这些操作提供了自定义代码。当通用序列化效率极低时,使用Value类型是合理的。一个例子是将元素的稀疏向量实现为数组的数据类型。由于知道数组大部分为零,可以对非零元素使用特殊编码,而通用的序列化只会写入所有数组元素。
org.apache.flink.types.CopyableValue接口以类似的方式支持手动内部克隆逻辑。
Flink自带预定义的值类型,对应于基本数据类型。(ByteValue, ShortValue, IntValue, LongValue, FloatValue, DoubleValue, StringValue, CharValue, BooleanValue)这些值类型充当基本数据类型的可变变体:它们的值可以更改,从而允许程序员重用对象,减轻垃圾收集器的压力。
您可以使用实现org.apache.hadoop.Writable接口的类型。在write()和readFields()方法中定义的序列化逻辑将用于序列化。
你可以使用特殊的类型,包括Scala的Either、Option和Try。Java API有它自己的自定义的Either实现。与Scala的Either类似,它表示两种可能类型的值,Left或Right。对于错误处理或需要输出两种不同类型记录的操作符,这两种方法都很有用。
注意:本节仅与Java相关。
Java编译器在编译后会丢弃很多泛型类型信息。这在Java中称为类型擦除。这意味着在运行时,对象的实例不再知道其泛型类型。例如,DataStream和DataStream的实例在JVM上看起来是相同的。
Flink在准备程序执行时(程序的主方法被调用时)需要类型信息。Flink Java API试图重新构建以各种方式丢弃的类型信息,并显式地将其存储在数据集和操作符中。您可以通过DataStream.getType()检索类型。该方法返回TypeInformation的一个实例,这是Flink表示类型的内部方式。
类型推断有其局限性,在某些情况下需要程序员的“配合”。例如从集合中创建数据集的方法,例如StreamExecutionEnvironment.fromCollection(),您可以在其中传递一个描述类型的参数。但是像MapFunction这样的泛型函数可能需要额外的类型信息。
ResultTypeQueryable接口可以通过输入格式和函数来实现,以显式地告诉API它们的返回类型。调用函数的输入类型通常可以通过前面操作的结果类型推断出来。
Flink试图推断出在分布式计算期间交换和存储的数据类型的大量信息。可以把它想象成一个推断表模式的数据库。在大多数情况下,Flink可以自己无缝地推断出所有必要的信息。拥有类型信息可以让Flink做一些很酷的事情:
一般来说,在执行前阶段 - 即在程序调用DataStream时,以及在调用execute()、print()、count()或collect()之前,需要有关数据类型的信息。
用户需要与Flink的数据类型处理进行交互时,最常见的问题是:
类TypeInformation是所有类型描述符的基类。它揭示了类型的一些基本属性,并可以生成序列化器,在专门化中,还可以生成类型的比较器。(注意,Flink中的比较器不仅仅是定义一个顺序——它们基本上是处理键的实用程序)
在内部,Flink对类型进行了以下区分:
pojo特别有趣,因为它们支持创建复杂类型。它们对运行时也是透明的,可以通过Flink非常有效地处理。
如果满足以下条件,Flink将数据类型识别为POJO类型(并允许“by-name”字段引用):
注意,当用户定义的数据类型不能被识别为POJO类型时,必须将其处理为GenericType并使用Kryo进行序列化。
要为类型创建TypeInformation对象,请使用特定于语言的方式:
因为Java通常会擦除泛型类型信息,所以需要将类型传递给TypeInformation构造:
对于非泛型类型,你可以传递Class:
TypeInformation<String> info = TypeInformation.of(String.class);
对于泛型类型,您需要通过TypeHint“捕获”泛型类型信息:
TypeInformation<Tuple2<String, Double>> info = TypeInformation.of(new TypeHint<Tuple2<String, Double>>(){});
在内部,这创建了TypeHint的一个匿名子类,用于捕获泛型信息,并将其保存到运行时。
要创建一个TypeSerializer,只需在TypeInformation对象上调用typeInfo.createSerializer(config)。
config参数的类型为ExecutionConfig,保存有关程序注册的自定义序列化器的信息。在任何可能的地方,试着给程序传递正确的ExecutionConfig。您通常可以通过调用getExecutionConfig()从DataStream获得它。在函数内部(如MapFunction),您可以通过将函数设置为Rich function并调用getRuntimeContext(). getexecutionconfig()来获得它。
在一般情况下,Java会擦除泛型类型信息。Flink试图通过反射重构尽可能多的类型信息,使用Java保留的少量信息(主要是函数签名和子类信息)。对于函数的返回类型依赖于输入类型的情况,此逻辑还包含一些简单的类型推断:
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无法重建所有泛型类型信息。在这种情况下,用户必须通过键入提示来帮助解决。
在Flink无法重建被擦除的泛型类型信息的情况下,Java API提供了所谓的类型提示。类型提示告诉系统由函数产生的数据流或数据集的类型:
DataStream<SomeType> result = stream
.map(new MyGenericNonInferrableFunction<Long, SomeType>())
.returns(SomeType.class);
returns 语句指定生成的类型,在本例中是通过一个类。提示支持通过
Java 8 lambdas的类型提取与非lambdas的工作方式不同,因为lambdas不与扩展函数接口的实现类相关联。
目前,Flink试图找出哪个方法实现了lambda,并使用Java的泛型签名来确定参数类型和返回类型。但是,并非所有编译器都为lambdas生成这些签名。如果观察到意外行为,请使用returns方法手动指定返回类型。
PojoTypeInfo正在为POJO内的所有字段创建序列化器。标准类型如int、long、String等由Flink附带的序列化器处理。对于所有其他类型,我们回到Kryo。
如果Kryo不能处理该类型,您可以要求PojoTypeInfo使用Avro序列化POJO。要这样做,你必须代码声明
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.getConfig().enableForceAvro();
注意,Flink会使用Avro序列化器自动序列化Avro生成的pojo。
如果您想让Kryo序列化器处理整个POJO类型,设置
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.getConfig().enableForceKryo();
如果Kryo不能序列化您的POJO,您可以使用
env.getConfig().addDefaultKryoSerializer(Class<?> type, Class<? extends Serializer<?>> serializerClass);
这些方法有不同的变体。
在某些情况下,程序可能希望显式地避免使用Kryo作为泛型类型的回退。最常见的一种是希望确保通过Flink自己的序列化器或用户定义的自定义序列化器有效地序列化所有类型。
每当遇到经过Kryo的数据类型时,下面的设置将引发异常:
env.getConfig().disableGenericTypes();
类型信息工厂允许将用户定义的类型信息插入到Flink类型系统中。你必须实现org.apache.flink.api.common.typeinfo.TypeInfoFactory来返回你的自定义类型信息。如果相应的类型或使用此类型的POJO的字段已经使用@org.apache.flink.api.common.typeinfo.TypeInfo注释,则在类型提取阶段调用该工厂。
类型信息工厂可以在Java和Scala API中使用。
在类型层次结构中,向上遍历时将选择距离最近的工厂,但是内置工厂的优先级最高。工厂的优先级也高于Flink的内置类型,因此您应该知道自己在做什么。
下面的示例展示了如何将自定义类型MyTuple注释为MyTuple,并使用Java中的工厂为其提供自定义类型信息。
带注释的自定义类型:
@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"));
}
}
除了注释类型本身,这对于第三方代码来说是不可能的,你还可以像这样在有效的Flink POJO中注释这种类型的用法:
public class MyPojo {
public int id;
@TypeInfo(MyTupleTypeInfoFactory.class)
public MyTuple<Integer, String> tuple;
}
方法createTypeInfo(Type, Map
如果您的类型包含可能需要从Flink函数的输入类型派生的泛型参数,请确保还实现org.apache.flink.api.common.typeinfo.TypeInformation#getGenericParameters,用于泛型参数到类型信息的双向映射。
Apache Flink 流应用程序通常设计为无限期或长时间运行。与所有长期运行的服务一样,应用程序需要更新以适应不断变化的需求。这对于应用程序所针对的数据模式也是如此;它们随着应用程序的发展而发展。
本页概述了如何改进状态类型的数据架构。当前的限制因不同类型和状态结构(ValueState、ListState等)而异。
请注意,仅当您使用由 Flink 自己的类型序列化框架生成的状态序列化器时,此页面上的信息才相关。也就是说,在声明您的状态时,提供的状态描述符未配置为使用特定的TypeSerializer or TypeInformation,在这种情况下,Flink 会推断有关状态类型的信息:
ListStateDescriptor<MyPojoType> descriptor =
new ListStateDescriptor<>(
"state-name",
MyPojoType.class);
checkpointedState = getRuntimeContext().getListState(descriptor);
在幕后,状态模式是否可以进化取决于用于读取/写入持久状态字节的序列化程序。简而言之,注册状态的模式只有在其序列化程序正确支持的情况下才能进化。这由 Flink 的类型序列化框架生成的序列化器透明地处理(当前支持的范围在下面列出)。
如果您打算TypeSerializer为您的状态类型实现自定义并想了解如何实现序列化程序以支持状态模式演变,请参阅 自定义状态序列化。那里的文档还涵盖了有关状态序列化程序和 Flink 状态后端之间相互作用的必要内部细节,以支持状态模式演变。
要演化给定状态类型的模式,您将采取以下步骤:
迁移状态以适应变化的模式的过程是自动发生的,并且对于每个状态都是独立的。这个过程由 Flink 内部执行,首先检查状态的新序列化器是否与之前的序列化器具有不同的序列化模式;如果是这样,则使用先前的序列化程序将状态读取到对象,并使用新的序列化程序再次写回字节。
有关迁移过程的更多详细信息超出了本文档的范围;请参考 这里。
目前,模式演变仅支持 POJO 和 Avro 类型。因此,如果您关心状态的模式演变,目前建议始终将 Pojo 或 Avro 用于状态数据类型。
Flink 支持POJO 类型的进化模式,基于以下规则集:
请注意,POJO 类型状态的模式只能在使用 Flink 版本高于 1.8.0 的先前保存点恢复时进行演变。使用早于 1.8.0 的 Flink 版本进行恢复时,无法更改架构。
Flink 完全支持 Avro 类型状态的进化模式,只要模式变化被 Avro 的模式解析规则认为是兼容的。
一个限制是,当作业恢复时,用作状态类型的 Avro 生成的类不能重新定位或具有不同的命名空间。
Flink 的模式迁移有一些限制,需要确保正确性。对于需要解决这些限制并了解它们在特定用例中的安全性的用户,请考虑使用自定义序列化程序或 状态处理器 api。
无法迁移密钥的结构,因为这可能会导致不确定的行为。例如,如果将 POJO 用作键并且删除了一个字段,那么可能会突然出现多个现在相同的单独键。Flink 没有办法合并对应的值。
此外,RocksDB 状态后端依赖于二进制对象标识,而不是hashCode方法。对键的对象结构的任何更改都可能导致不确定的行为。
使用 Kryo 时,框架无法验证是否进行了任何不兼容的更改。
本节旨在为需要对其状态使用自定义序列化的用户提供指南,涵盖如何提供自定义状态序列化程序以及实现允许状态模式演变的序列化程序的指南和最佳实践。
如果你只是使用 Flink 自己的序列化器,这个页面是无关紧要的,可以忽略。
注册托管操作符或键控状态时,StateDescriptor需要指定状态名称以及有关状态类型的信息。Flink 的 类型序列化框架使用类型信息来为状态创建适当的序列化器。
也可以完全绕过这一点,让 Flink 使用您自己的自定义序列化程序来序列化托管状态,只需StateDescriptor使用您自己的TypeSerializer实现直接实例化 即可:
public class CustomTypeSerializer extends TypeSerializer<Tuple2<String, Integer>> {...};
ListStateDescriptor<Tuple2<String, Integer>> descriptor =
new ListStateDescriptor<>(
"state-name",
new CustomTypeSerializer());
checkpointedState = getRuntimeContext().getListState(descriptor);
本节解释了与状态序列化和模式演变相关的面向用户的抽象,以及 Flink 如何与这些抽象交互的必要内部细节。
从保存点恢复时,Flink 允许更改用于读取和写入先前注册状态的序列化程序,这样用户就不会被锁定在任何特定的序列化模式中。当状态恢复时,将为该状态注册一个新的序列化器(即,StateDescriptor用于访问恢复的作业中的状态的序列化器)。这个新的序列化程序可能具有与以前的序列化程序不同的模式。因此,在实现状态序列化器时,除了读/写数据的基本逻辑之外,另一个需要牢记的重要事情是将来如何更改序列化模式。
当谈到schema时,在这种情况下,该术语在指称状态类型的数据模型和状态类型的序列化二进制格式之间是可以互换的。一般来说,架构可以在以下几种情况下发生变化:
为了让新的执行获得有关写入的状态模式的信息并检测模式是否已更改,在获取操作员状态的保存点时,需要将状态序列化程序的快照与状态字节一起写入。这是抽象的 a TypeSerializerSnapshot,将在下一小节中解释。
public interface TypeSerializerSnapshot<T> {
int getCurrentVersion();
void writeSnapshot(DataOuputView out) throws IOException;
void readSnapshot(int readVersion, DataInputView in, ClassLoader userCodeClassLoader) throws IOException;
TypeSerializerSchemaCompatibility<T> resolveSchemaCompatibility(TypeSerializer<T> newSerializer);
TypeSerializer<T> restoreSerializer();
}
public abstract class TypeSerializer<T> {
// ...
public abstract TypeSerializerSnapshot<T> snapshotConfiguration();
}
序列化TypeSerializerSnapshot器是一个时间点信息,它作为状态序列化器写入模式的唯一真实来源,以及恢复与给定时间点相同的序列化器所必需的任何附加信息。作为序列化程序快照,在恢复时应该写入和读取什么的逻辑在writeSnapshot和readSnapshot方法中定义。
请注意,快照自己的写入模式也可能需要随着时间的推移而改变(例如,当您希望将有关序列化程序的更多信息添加到快照时)。为此,快照是版本化的,当前版本号在getCurrentVersion方法中定义。在恢复时,当从保存点读取序列化程序快照时,写入快照的模式版本将提供给readSnapshot方法,以便读取实现可以处理不同的版本。
在恢复时,检测新序列化程序的模式是否已更改的逻辑应在该resolveSchemaCompatibility方法中实现。当先前注册的状态在操作员的恢复执行中再次注册到新的序列化程序时,新的序列化程序会通过此方法提供给先前的序列化程序的快照。此方法返回一个TypeSerializerSchemaCompatibility表示兼容性解决方案的结果,它可以是以下之一:
最后一点细节是在需要迁移的情况下如何获得之前的序列化器。序列化器的另一个重要作用TypeSerializerSnapshot是它充当恢复先前序列化器的工厂。更具体地说,TypeSerializerSnapshot应该实现restoreSerializer方法来实例化一个序列化器实例,该实例识别前一个序列化器的模式和配置,因此可以安全地读取前一个序列化器写入的数据。
最后,本节总结了 Flink,或者更具体地说,状态后端是如何与抽象交互的。交互根据状态后端略有不同,但这与状态序列化器及其序列化器快照的实现是正交的。
TypeSerializerSnapshotFlink 提供了两个可用于典型场景的 抽象基类:SimpleTypeSerializerSnapshot和CompositeTypeSerializerSnapshot.
提供这些预定义快照作为其序列化程序快照的序列化程序必须始终具有自己的独立子类实现。这对应于不跨不同序列化程序共享快照类的最佳实践,下一节将对此进行更全面的解释。
SimpleTypeSerializerSnapshot用于没有任何状态或配置的序列化程序,本质上意味着序列化程序的序列化模式仅由序列化程序的类定义。
将SimpleTypeSerializerSnapshot 用作序列化程序的快照类时,兼容性分辨率只有两种可能的结果:
下面是一个如何使用的示例SimpleTypeSerializerSnapshot,以 FlinkIntSerializer为例:
public class IntSerializerSnapshot extends SimpleTypeSerializerSnapshot<Integer> {
public IntSerializerSnapshot() {
super(() -> IntSerializer.INSTANCE);
}
}
IntSerializer没有状态或配置。序列化格式仅由序列化程序类本身定义,并且只能由另一个IntSerializer. 因此,它适合 SimpleTypeSerializerSnapshot.
无论快照当前是正在恢复还是在快照期间写入,基础超级构造函数都SimpleTypeSerializerSnapshot需要相应序列化程序的实例。Supplier该供应商用于创建恢复序列化程序,以及类型检查以验证新序列化程序是否属于相同的预期序列化程序类。
它CompositeTypeSerializerSnapshot适用于依赖多个嵌套序列化器进行序列化的序列化器。
在进一步解释之前,我们将依赖于多个嵌套序列化器的序列化器称为此上下文中的“外部”序列化器。这方面的示例可能是MapSerializer, ListSerializer,GenericArraySerializer等MapSerializer。例如 - 键和值序列化器将是嵌套序列化器,而MapSerializer其本身是“外部”序列化器。
在这种情况下,外部序列化器的快照也应该包含嵌套序列化器的快照,以便可以独立检查嵌套序列化器的兼容性。在解决外层序列化器的兼容性问题时,需要考虑每个嵌套序列化器的兼容性。
CompositeTypeSerializerSnapshot被提供来帮助实现这些复合序列化器的快照。它处理嵌套序列化程序快照的读取和写入,以及在考虑所有嵌套序列化程序的兼容性的情况下解决最终的兼容性结果。
下面是一个如何使用的示例CompositeTypeSerializerSnapshot,以 FlinkMapSerializer为例:
public class MapSerializerSnapshot<K, V> extends CompositeTypeSerializerSnapshot<Map<K, V>, MapSerializer> {
private static final int CURRENT_VERSION = 1;
public MapSerializerSnapshot() {
super(MapSerializer.class);
}
public MapSerializerSnapshot(MapSerializer<K, V> mapSerializer) {
super(mapSerializer);
}
@Override
public int getCurrentOuterSnapshotVersion() {
return CURRENT_VERSION;
}
@Override
protected MapSerializer createOuterSerializerWithNestedSerializers(TypeSerializer<?>[] nestedSerializers) {
TypeSerializer<K> keySerializer = (TypeSerializer<K>) nestedSerializers[0];
TypeSerializer<V> valueSerializer = (TypeSerializer<V>) nestedSerializers[1];
return new MapSerializer<>(keySerializer, valueSerializer);
}
@Override
protected TypeSerializer<?>[] getNestedSerializers(MapSerializer outerSerializer) {
return new TypeSerializer<?>[] { outerSerializer.getKeySerializer(), outerSerializer.getValueSerializer() };
}
}
在将新的序列化程序快照实现为 的子类时CompositeTypeSerializerSnapshot,必须实现以下三个方法:
上面的示例是CompositeTypeSerializerSnapshot除了嵌套序列化程序的快照之外没有额外信息要被快照的情况。因此,可以预期它的外部快照版本永远不需要上升。然而,其他一些序列化程序包含一些额外的静态配置,需要与嵌套组件序列化程序一起保留。一个例子是 Flink 的 GenericArraySerializer,它包含作为配置的数组元素类型的类,除了嵌套元素序列化器。
在这些情况下,需要在 上实现另外三种方法CompositeTypeSerializerSnapshot:
默认情况下,CompositeTypeSerializerSnapshot假设没有任何外部快照信息可读取/写入,因此上述方法的默认实现为空。如果子类具有外部快照信息,则必须实现所有三个方法。
下面是一个示例,说明如何将CompositeTypeSerializerSnapshot用于具有外部快照信息的复合序列化程序快照,以 FlinkGenericArraySerializer为例:
public final class GenericArraySerializerSnapshot<C> extends CompositeTypeSerializerSnapshot<C[], GenericArraySerializer> {
private static final int CURRENT_VERSION = 1;
private Class<C> componentClass;
public GenericArraySerializerSnapshot() {
super(GenericArraySerializer.class);
}
public GenericArraySerializerSnapshot(GenericArraySerializer<C> genericArraySerializer) {
super(genericArraySerializer);
this.componentClass = genericArraySerializer.getComponentClass();
}
@Override
protected int getCurrentOuterSnapshotVersion() {
return CURRENT_VERSION;
}
@Override
protected void writeOuterSnapshot(DataOutputView out) throws IOException {
out.writeUTF(componentClass.getName());
}
@Override
protected void readOuterSnapshot(int readOuterSnapshotVersion, DataInputView in, ClassLoader userCodeClassLoader) throws IOException {
this.componentClass = InstantiationUtil.resolveClassByName(in, userCodeClassLoader);
}
@Override
protected boolean resolveOuterSchemaCompatibility(GenericArraySerializer newSerializer) {
return (this.componentClass == newSerializer.getComponentClass())
? OuterSchemaCompatibility.COMPATIBLE_AS_IS
: OuterSchemaCompatibility.INCOMPATIBLE;
}
@Override
protected GenericArraySerializer createOuterSerializerWithNestedSerializers(TypeSerializer<?>[] nestedSerializers) {
TypeSerializer<C> componentSerializer = (TypeSerializer<C>) nestedSerializers[0];
return new GenericArraySerializer<>(componentClass, componentSerializer);
}
@Override
protected TypeSerializer<?>[] getNestedSerializers(GenericArraySerializer outerSerializer) {
return new TypeSerializer<?>[] { outerSerializer.getComponentSerializer() };
}
}
在上面的代码片段中有两件重要的事情需要注意。首先,由于此 实现具有作为快照的一部分写入的外部快照信息,因此只要外部快照信息的序列化格式发生更改,就必须升级CompositeTypeSerializerSnapshot由 定义的外部快照版本。getCurrentOuterSnapshotVersion()
其次,请注意我们在编写组件类时如何避免使用 Java 序列化,只编写类名并在读回快照时动态加载它。避免 Java 序列化以编写序列化程序快照的内容通常是一个很好的做法。下一节将介绍有关此的更多详细信息。
序列化程序的快照,作为注册状态如何被序列化的唯一真实来源,用作在保存点中读取状态的入口点。为了能够恢复和访问之前的状态,之前的状态序列化器的快照必须能够被恢复。
Flink 通过首先TypeSerializerSnapshot使用其类名(与快照字节一起写入)实例化 来恢复序列化程序快照。因此,为了避免意外的类名更改或实例化失败,TypeSerializerSnapshot类应该:
由于模式兼容性检查通过序列化程序快照进行,因此让多个序列化程序返回TypeSerializerSnapshot与其快照相同的类会使 TypeSerializerSnapshot#resolveSchemaCompatibilityandTypeSerializerSnapshot#restoreSerializer()方法的实现复杂化。
这也是一个不好的关注点分离;单个序列化程序的序列化模式、配置以及如何恢复它,应该合并到它自己的专用TypeSerializerSnapshot类中。
在编写持久序列化程序快照的内容时,根本不应该使用 Java 序列化。例如,一个序列化程序需要将其目标类型的类作为其快照的一部分进行持久化。关于类的信息应该通过写入类名来持久化,而不是直接使用 Java 序列化类。读取快照时,读取类名,用于通过名称动态加载类。
这种做法可确保始终可以安全地读取序列化程序快照。在上面的示例中,如果类型类是使用 Java 序列化持久化的,则一旦类实现发生更改,快照可能不再可读,并且根据 Java 序列化细节不再是二进制兼容的。
本节是从 Flink 1.7 之前存在的序列化器和序列化器快照迁移 API 的指南。
在 Flink 1.7 之前,序列化程序快照被实现为 a TypeSerializerConfigSnapshot(现在已弃用,将来最终将被删除以完全被新TypeSerializerSnapshot接口取代)。此外,序列化程序模式兼容性检查的责任TypeSerializer在TypeSerializer#ensureCompatibility(TypeSerializerConfigSnapshot)方法中实现。
新旧抽象之间的另一个主要区别是不推荐使用的抽象TypeSerializerConfigSnapshot 没有实例化以前的序列化程序的能力。因此,在您的序列化程序仍然返回一个子类的情况下TypeSerializerConfigSnapshot作为其快照,序列化程序实例本身将始终使用 Java 序列化写入保存点,以便之前的序列化程序在恢复时可用。这是非常不可取的,因为恢复作业是否成功会受到先前序列化程序类的可用性的影响,或者一般而言,是否可以使用 Java 序列化在恢复时读回序列化程序实例。这意味着您的状态仅限于使用相同的序列化程序,并且一旦您想要升级序列化程序类或执行模式迁移,可能会出现问题。
为了适应未来并灵活地迁移您的状态序列化程序和模式,强烈建议从旧的抽象迁移。执行此操作的步骤如下:
如果您在 Flink 程序中使用了无法被 Flink 类型序列化器序列化的自定义类型,则 Flink 会退回到使用通用 Kryo 序列化器。您可以使用 Kryo 注册自己的序列化程序或序列化系统,如 Google Protobuf 或 Apache Thrift。为此,只需在ExecutionConfigFlink 程序中注册类型类和序列化程序。
final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
// register the class of the serializer as serializer for a type
env.getConfig().registerTypeWithKryoSerializer(MyCustomType.class, MyCustomSerializer.class);
// register an instance as serializer for a type
MySerializer mySerializer = new MySerializer();
env.getConfig().registerTypeWithKryoSerializer(MyCustomType.class, mySerializer);
请注意,您的自定义序列化程序必须扩展 Kryo 的 Serializer 类。对于 Google Protobuf 或 Apache Thrift,这已经为您完成了:
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// register the Google Protobuf serializer with Kryo
env.getConfig().registerTypeWithKryoSerializer(MyCustomType.class, ProtobufSerializer.class);
// register the serializer included with Apache Thrift as the standard serializer
// TBaseSerializer states it should be initialized as a default Kryo serializer
env.getConfig().addDefaultKryoSerializer(MyCustomType.class, TBaseSerializer.class);
为了使上面的示例正常工作,您需要在 Maven 项目文件 (pom.xml) 中包含必要的依赖项。在依赖项部分,为 Apache Thrift 添加以下内容:
<dependency>
<groupId>com.twittergroupId>
<artifactId>chill-thriftartifactId>
<version>0.7.6version>
<exclusions>
<exclusion>
<groupId>com.esotericsoftware.kryogroupId>
<artifactId>kryoartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.apache.thriftgroupId>
<artifactId>libthriftartifactId>
<version>0.11.0version>
<exclusions>
<exclusion>
<groupId>javax.servletgroupId>
<artifactId>servlet-apiartifactId>
exclusion>
<exclusion>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpclientartifactId>
exclusion>
exclusions>
dependency>
对于 Google Protobuf,您需要以下 Maven 依赖项:
<dependency>
<groupId>com.twittergroupId>
<artifactId>chill-protobufartifactId>
<version>0.7.6version>
<exclusions>
<exclusion>
<groupId>com.esotericsoftware.kryogroupId>
<artifactId>kryoartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>com.google.protobufgroupId>
<artifactId>protobuf-javaartifactId>
<version>3.7.0version>
dependency>
如果您为自定义类型注册 Kryo’s ,即使您的自定义类型类包含在提交的用户代码 jar 中JavaSerializer,您也可能会遇到s。ClassNotFoundException这是由于 Kryo’s 的一个已知问题JavaSerializer,它可能错误地使用了错误的类加载器。
在这种情况下,您应该改用它org.apache.flink.api.java.typeutils.runtime.kryo.JavaSerializer 来解决问题。这是JavaSerializer在 Flink 中重新实现的,确保使用用户代码类加载器。