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原生类型及它们的封装类型,包括void
,String
,Date
,BigDecimal
和BigInteger
。
原生数组和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
对于泛型数据类型,你需要通过TypeHint
来捕获数据累心:
TypeInformation
在内部,这个创建了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()
来获取它。