Flink 数据类型和序列化

1. Flink 的序列化框架

1.1 Flink 的数据类型

在这里插入图片描述
Flink 支持任意的 Java 或是 Scala 类型。不需要像 Hadoop 一样去实现一个特定的接口(org.apache.hadoop.io.Writable),Flink 能够自动识别数据类型。

例如,下图中的 Person 类,是一个复合类型的一个 Pojo,在 Flink 内部是用 PojoTypeInfo 来表示,它继承自 TypeInformation,也即在 Flink 中用 TypeInformation 作为类型描述符来表示每一种要表示的数据类型
在这里插入图片描述

1.2 TypeInfomation

在这里插入图片描述
在 Flink 中每一个具体的类型都对应了一个具体的 TypeInformation 实现类,例如 BasicTypeInformation 中的 IntegerTypeInformation 和 FractionalTypeInformation 都具体的对应了一个 TypeInformation。然后还有 BasicArrayTypeInformation、CompositeType 以及一些其它类型。
每一个具体的数据类型都对应了一个 TypeInformation 的具体实现。
TypeInformation 是 Flink 类型系统的核心类。
例如,对于一个自定义的 Function 来说,需要一个类型信息来作为该函数的输入输出类型,即 TypeInfomation。该类型信息类作为一个工具来生成对应类型的序列化器 TypeSerializer,并用于执行语义检查,比如当一些字段在作为 joing 或 grouping 的键时,检查这些字段是否在该类型中存在。

1.3 Flink 的序列化过程

每一个具体的数据类型都对应一个 TypeInformation 的具体实现,每一个 TypeInformation 都会为对应的具体数据类型提供一个专属的序列化器。
TypeInformation 提供一个 createSerialize() 方法,通过这个方法就可以得到该类型的序列化器 TypeSerializer,TypeSerializer 可以对该类型进行序列化与反序列化操作。
在这里插入图片描述
大多数数据类型 Flink 可以自动生成对应的序列化器。
但对于 GenericTypeInfo 类型,Flink 会使用 Kyro 进行序列化和反序列化。其中,Tuple、Pojo 和 CaseClass 类型是复合类型,它们可能嵌套一个或者多个数据类型。在这种情况下,它们的序列化器同样是复合的。它们会将内嵌类型的序列化委托给对应类型的序列化器。
在 Flink 中,如果使用 POJO 数据类型需要遵循以下规则:

  • 类必须是 Public 的,且类有一个 public 的无参数构造函数。
  • 该类(以及所有超类)中的所有非静态 no-static、非瞬态 no-transient 字段都是 public 的(和非最终的 final)或者具有公共 getter 和 setter 方法。
  • 该类中的字段类型必须是 Flink 支持的。

当用户定义的数据类型无法识别为 POJO 类型时,必须将其作为 GenericType 处理并使用 Kryo 进行序列化。
如果 Flink 内置的数据类型和序列化方式不能满足需求,Flink 的类型信息系统也支持拓展。只需要实现 TypeInformation、TypeSerializer 和 TypeComparator 即可定制自己类型的序列化和比较大小方式,来提升数据类型在序列化和比较时的性能。
下图是一个 Tuple3 的序列化过程。
在这里插入图片描述序列化就是将数据结构或者对象转换成一个二进制串的过程,在 Java 里面可以简单地理解成一个 byte 数组。而反序列化恰恰相反,就是将序列化过程中所生成的二进制串转换成数据结构或者对象的过程。
上面的 Tuple 3 包含三个层面,一是 int 类型,一是 double 类型,还有一个是 Person。Person 包含两个字段,一是 int 型的 ID,另一个是 String 类型的 name,它在序列化操作时,会委托相应具体序列化的序列化器进行相应的序列化操作。

  • int 类型通过 IntSerializer 进行序列化操作,占用4个字节。
  • double 类型通过 DoubleSerializer 进行序列化操作,占用8个字节。
  • Person 类被当成一个 Pojo 对象,通过 PojoSerializer 进行序列化,只存储一些属性信息,占用1个字节。
  • Person 类的属性也会又其对应类型的序列化器 IntSerializer 和 StringSerializer 进行序列化操作。
    在序列化的结果中,所有的数据都是由 MemorySegment 来支持。

MemorySegment

  • MemorySegment 在 Flink 中会将对象序列化到预分配的内存块上,它代表 1 个固定长度的内存,默认大小为 32 kb。
  • MemorySegment 代表 Flink 中的一个最小的内存分配单元,相当于是 Java 的一个 byte 数组。 每条记录都会以序列化的形式存储在一个或多个 MemorySegment 中。

2. 序列化的最佳实践

2.1 常见的使用场景

  • 注册子类型:如果函数签名只描述了超类型,但是它们实际上在执行期间使用了超类型的子类型,那么让 Flink 了解这些子类型会大大提高性能。可以在 StreamExecutionEnvironment 或 ExecutionEnvironment 中调用 .registertype (clazz) 注册子类型信息。
  • 注册自定义序列化:对于不适用于自己的序列化框架的数据类型,Flink 会使用 Kryo 来进行序列化,并不是所有的类型都与 Kryo 无缝连接,具体注册方法在下文介绍。
  • 添加类型声明:有时,当 Flink 用尽各种手段都无法推测出泛型信息时,用户需要传入一个类型提示 TypeHint,这个通常只在 Java API 中需要。
  • 手动创建一个 TypeInformation:在某些 API 调用中,这可能是必需的,因为 Java 的泛型类型擦除导致 Flink 无法推断数据类型。

2.2 实践 - 类型声明

通常是用 TypeInformation.of() 方法来创建一个类型信息的对象。

  • 对于非泛型类,直接传入 class 对象即可
PojoTypeInfo<Person> typeInfo = (PojoTypeInfo<Person>) TypeInformation.of(Person.class);
  • 对于泛型类,需要通过 TypeHint 来保存泛型类型信息
final TypeInfomation<Tuple2<Integer,Integer>> resultType = TypeInformation.of(new TypeHint<Tuple2<Integer,Integer>>(){});
  • 预定义常量
    在 BasicTypeInfo 中定义了 String、Boolean、Byte、Short、Integer、Long、Float、Double、Char 等基本类型的类型声明,可以直接使用。
    同时,Flink 还提供了一个 Types 类(org.apache.flink.api.common.typeinfo.Types),直接 Types.STRING,Types.INT 来使用。
    注意:Flink 还有一个 Types 类(org.apache.flink.table.api.Types)用于 table 模块内部的类型定义信息,不要导入错误。
  • 自定义 TypeInfo 和 TypeInfoFactory
    通过自定义 TypeInfo 为任意类提供 Flink 原生内存管理(而非 Kryo),可令存储更紧凑,运行时也更高效。
    在自定义类上使用 @TypeInfo 注解,随后创建相应的 TypeInfoFactory 并覆盖 createTypeInfo() 方法。
@TypeInfo(MyTupleTypeInfoFactory.class)
public class MyTuple<T0, T1> {
	public T0 myfield0;
	public T1 myfield1;
}

public class MyTupleTypeInfoFactory extends TypeInfoFactory<MyTuple> {
	@Override
	public TypeInfomation<MyTuple> createTypeInfo(Type t, Map<String, TypeInformation<?>> genericParmeters) {
		return new MyTupleTypeInfo(genericParameters.get("T0"), genericParameters.get("T1"));
	}
}

2.3 实践 - 注册子类类型

Flink 认识父类,但不一定认识子类的一些独特特性,因此需要单独注册子类型。
StreamExecutionEnvironment 和 ExecutionEnvironment 提供 registerType() 方法用来向 Flink 注册子类信息。

final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
Env. registerType(typeClass);

在 registerType() 方法内部,会使用 TypeExtractor 来提取类型信息,获取到的类型信息属于 PojoTypeInfo 及其子类,那么需要将其注册到一起,否则统一交给 Kryo 去处理,Flink 并不过问(这种情况下性能会变差)。

2.4 实践 - Kryo 序列化

对于 Flink 无法序列化的类型(例如用户自定义类型,没有 registerType,也没有自定义 TypeInfo 和 TypeInfoFactory),默认会交给 Kryo 处理,如果 Kryo 仍然无法处理(例如 Guava、Thrift、Protobuf 等第三方库的一些类),有两种解决方案:

  • 强制使用 Avro 来代替 Kryo
env.getConfig().enableForceAvro();
  • 为 Kryo 增加自定义的 Serializer 以增强 Kryo 的功能
env.getConfig().addDefaultKryoSerializer(clazz, serializer);

注:如果希望完全禁用 Kryo(100% 使用 Flink 的序列化机制),可以通过 Kryo-env.getConfig().disableGenericTypes() 的方式完成,但注意一切无法处理的类都将导致异常,这种对于调试非常有效。

3. Flink 通讯层的序列化

Flink 的 Task 之间如果需要跨网络传输数据记录, 那么就需要将数据序列化之后写入 NetworkBufferPool,然后下层的 Task 读出之后再进行反序列化操作,最后进行逻辑处理。

为了使得记录以及事件能够被写入 Buffer,随后在消费时再从 Buffer 中读出,Flink 提供了数据记录序列化器(RecordSerializer)与反序列化器(RecordDeserializer)以及事件序列化器(EventSerializer)。

Function 发送的数据被封装成 SerializationDelegate,它将任意元素公开为 IOReadableWritable 以进行序列化,通过 setInstance() 来传入要序列化的数据。

在这里插入图片描述

  • 何时确定 Function 的输入输出类型?
    在图中序号1处,用户Jar转化为 StreamGraph 的过程中会构建一个 StreamTransformation,此时通过 TypeExtractor 工具确定 Function 的输入输出类型。TypeExtractor 类可以根据方法签名、子类信息等自动提取或恢复类型信息。
  • 何时确定 Function 的序列化/反序列化器?
    在图中序号2处,构造 StreamGraph 时,此时已经知道了 Function 的输出类型。Flink 通过 TypeInfomation 的 createSerializer() 方法获取对应类型的序列化器 TypeSerializer。在 addOperator() 的过程中执行 setSerializers() 操作,将序列化器保存在 StreamConfig 对象的 TYPE_SERIALIZER_IN_1 、 TYPE_SERIALIZER_IN_2、 TYPE_SERIALIZER_OUT_1 属性中,以便后续进行反序列化操作时,将信息提取出来。
  • 何时进行真正的触发序列化/反序列化操作?这个过程与 TypeSerializer 又是怎么联系在一起的呢?
    对于Task 和 StreamTask 两个概念,Task 是直接受 TaskManager 管理和调度的,而 Task 又会调用 StreamTask,而 StreamTask 中真正封装了算子的处理逻辑。
    在这里插入图片描述
  1. 准确的说是在 StreamTask 的 invoke 方法中。
  2. StreamTask 真正处理每一条数据是在 StreamOperator 中,将数据封装成 StreamRecord 交给算子处理。
  3. 将处理结果通过 Collector 发动给下游(在构建 Collector 时已经确定了 SerializtionDelegate)。
  4. 通过 RecordWriter 写入器将序列化后的结果写入 DataOutput,每一条记录,会使用一个 RecordSerializer 去处理。
  5. 最后真正的序列化操作交给 serializationDelegate 序列化委托器处理,调用 setInstance(record),将要序列化的数据接收进来,但其实在 new 这个序列化的委托器之前,已经拿到了对应类型的序列化器。
  6. 那么就直接调用该序列化器的序列化方法, this.serializer.serialize(this.instance, out) 来进行实际的序列化操作,实际还是通过 TypeSerializer 的 serialize() 方法完成。。
  7. 不同的序列化器会对应到不同的 TypeSerializer,例如 POJO 的序列化器对应 PoJoTypeSerializer。

以上内容是对 https://www.bilibili.com/video/av54080907/ 的学习总结。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值