Flink的数据类型和序列化

Apache Flink 以一种特有的方式来处理数据类型和序列化,包括自有的类型描述器、泛型抽取和类型序列化框架,本文将描述其背后的概念和原理。

Flink的类型处理

Flink试图去推断出在分布式计算过程中交换和存储的数据的类型信息,像数据库推断表模式一样。在大多数情况下,Flink可以自己无缝地推测出所有必要的信息。拥有类型信息Flink就可以完成如下的事情了:
  1、使用POJO类型,通过引用它们的字段名称对数据进行分组、连接和聚合操作,如dataSet.keyBy("username")。类型信息允许Flink可以提前对类型进行检测(如拼写错误和类型兼容性),而不是等到运行时再出错。
  2、Flink了解的数据类型信息越多,序列化和数据类型布局模式就更好,这在Flink的内存使用范例中是非常有用的。
3、最后,它还让用户在大多数情况下不再担心序列化框架和必须注册类型
通常,数据类型的信息都是在预处理阶段需要,即在程序对DataStream和DataSet的调用之前,及执行execute()、print()、count()和collect()的任何调用之前。

常见问题

用户与Flink的数据类型处理交互最常见的问题是:
  1、注册子类型:如果函数仅描述了超类型,但是执行过程中实际上使用的是这些超类的子类型,这样的话,Flink要识别这些子类型,可能会导致一定的性能下降。对此,可以在StreamExecutionEnvironment 或者 ExecutionEnvironment中调用 .registerType(clazz) 注册子类来解决。
  2、注册自定义序列化:对于不适用于自己的序列化框架的数据类型,Flink会使用Kryo来进行序列化,并不是所有的类型都与Kryo无缝连接。如,许多Google Guava集合类型,解决方案是为出问题的数据类型注册额外的序列化类,在StreamExecutionEnvironment 或者 ExecutionEnvironment 中调用.getConfig().addDefaultKryoSerializer(clazz, serializer)来实现,额外的Kryo序列化器在许多第三方库中都存在的,可以参考(自定义序列化器)[https://ci.apache.org/projects/flink/flink-docs-release-1.3/dev/custom_serializers.html]来了解更多如何创建自定义序列化器的信息。
  3、添加类型提示:有时,当Flink用尽各种手段都无法推测出泛型信息时,用户需要传入一个类型提示,这个只适用于JAVA API,类型提示部分描述了许多类型提示的信息。
  4、手动创建一个TypeInformation:对于某些API调用来说,这可能是必须的,因为Flink无法通过Java的通用类型擦除来推断数据类型。查看创建一个TypeInformation 或者 TypeSerializer来获取更多信息。

Flink的TypeInformation类

TypeInformation类是所有类型描述类的基类,它揭示了数据类型的一些基础属性,并产生序列化器在默写特殊情况下生成类型比较器(注意,Flink中的比较器不仅仅定义一个顺序,他们还是处理key的辅助工具utility)。
本质上,Flink通过类型来做出以下区分:
  基础类型:所有的Java原生类型及它们的封装类型,包括voidStringDateBigDecimalBigInteger
  原生数组和Object数组
  组合类型:
    Flink Java Tuples(Flink Java API的一部分):最多25个字段,空字段不支持
    Scala Case classes(包括Scala tuples):最多25个字段,空字段不支持
    Row:具有任意数量字段的元组,并支持空字段
    POJO:遵循某种Bean模式的类
  辅助类型:如:Option,Either,List,Maps,...
  泛型类型:这些不会被Flink自带的序列化器进行序列化,但是可以通过Kryo来进行
POJO非常有意思,因为它们支持创建复杂数据类型并通过字段名称来定义key:dataSet.join(another).where("name").equalTo("personName"),它们在运行时是透明的,并被Flink高效地处理。

POJO类型的规则

如果一个数据类型满足如下条件的话,就被认为是一个POJO类型:
  1、class是public的或者是独立的(不是非static内部类)
  2、class有无参构造函数
  3、所有class中的非静态的、非局部字段要么是public类型,要么有public类型的getter和setter方法

创建一个TypeInformation或者TypeSerializer

为一个数据类型创建一个TypeInformation,需要根据不同的语言来进行:

Java

因为Java通常会擦除类型信息,所以你需要将数据类型传递给TypeInformation构造函数:
对于非泛型数据类型,你可以传递Class:
TypeInformation<String> info = TypeInformation.of(String.class);
对于泛型数据类型,你需要通过TypeHint来捕获数据累心:
TypeInformation<Tuple2<String,String>> info = TypeInformation.of(new TypeHint<Tuple2<String,String>>(){});
在内部,这个创建了TypeHint的匿名子类,来捕获类型的泛型信息并保持知道运行时。

Scala

在Scala中,Flink使用编译时的宏,并在其任然可用时捕获所有通用类型信息

import org.apache.flink.streaming.api.scala._

val stringInfo: TypeInformation[String] = createTypeInformation[String]

val tupleInfo: TypeInformation[(String, Double)] = createTypeInformation[(String, Double)]

你也可以在Java中以回调函数的形式来使用同样的方法。

创建一个TypeSerializer,仅需要在TypeInformation类中调用typeInfo.createSerializer(config)即可。
config参数是ExecutionConfig类型的,并保持了程序注册的自定义serializer的信息,你可以通过DataStream或者DataSet的getExecutionCondif()方法来获取ExecutionConfig。在函数内部(如:MapFunction),你可以创建一个RichFunction并调用getRuntimeContext().getExecutionConfig()来获取它。

创建一个TypeSerializer,仅需要在TypeInformation类中调用typeInfo.createSerializer(config)即可。
config参数是ExecutionConfig类型的,并保持了程序注册的自定义serializer的信息,你可以通过DataStream或者DataSet的getExecutionCondif()方法来获取ExecutionConfig。在函数内部(如:MapFunction),你可以创建一个RichFunction并调用getRuntimeContext().getExecutionConfig()来获取它。

推荐阅读更多精彩内容