序列化就是将一个内存对象转换成二进制串,形成网络传输或者持久化的数据流。反序列化将二进制串转换为内存对象,这样就可以直接在编程语言中读写和操作这个对象。在Java和大数据生态圈中,已有不少序列化工具,比如Java自带的序列化工具、Kryo等。一些RPC框架也提供序列化功能,比如最初用于Hadoop的Apache Avro、Facebook开发的Apache Thrift和Google开发的Protobuf,这些工具在速度和压缩比等方面与JSON相比有一定的优势。
但是Flink依然选择了重新开发了自己的序列化框架,因为序列化和反序列化将关乎整个流处理框架个方便的性能,对数据类型了解越多,可以更早地完成数据类型检查,节省数据存储空间。
所有Java和Scala基础数据类型,诸如Int、Double、Long(包括Java原生类型int和装箱后的类型Integer)、String,以及Date、BigDecimal和BigInteger。
基础类型或其他对象类型组成的数组,如String[]
。
Scala case class
case class Person(name:String,age:Int)
Java POJO
定义POJO类有一些注意事项:
(1)该类必须用public修饰。
(2)该类必须有一个public的无参数的构造函数。
(3)该类的所有非静态(non-static)、非瞬态(non-transient)字段必须是public,如果字段不是public则必须有标准的getter和setter方法,比如对于字段A a有A getA()和setA(A a)。
(4)所有子字段也必须是Flink支持的数据类型。
举例:
i)是POJO
public class Person{
private String name;
private Integer age;
public Person(){}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
ii)是POJO
public class Person{
public String name;
public Integer age;
public Person(){}
}
iii)不是POJO:name属性不是public
public class Person{
private String name;
public Integer age;
public Person(){}
}
iv)不是POJO:缺少无参构造器
public class Person{
public String name;
public Integer age;
}
判断一个类是否是POJO,可以使用下面的代码检查:
System.out.println(TypeInformation.of(Person.class).createSerializer(new ExecutionConfig()));
Tuple
Flink为Java专门准备了元组类型,最多支持到25元组。
Scala的Tuple中所有元素都不可变,Java的Tuple中的元素是可以被更改和赋值的,因此在Java中使用Tuple可以充分利用这一特性,这样可以减少垃圾回收的压力。
Flink还支持Java的ArrayList
、HashMap
和Enum
,Scala的Either
和Option
。
当以上任何一个类型均不满足时,Flink认为该数据结构是一种泛型(GenericType),使用Kryo来进行序列化和反序列化。但Kryo在有些流处理场景效率非常低,有可能造成流数据的积压。我们可以使用
env.getConfig().disableGenericTypes();
来禁用Kryo,禁用后,Flink遇到无法处理的数据类型将抛出异常,这种方法对于调试非常有效。
在Flink中,统一使用TypeInformation
类表示。TypeInformation
的一个重要的功能就是创建TypeSerializer
序列化器,为该类型的数据做序列化。每种类型都有一个对应的序列化器来进行序列化。
使用前面介绍的各类数据类型时,Flink会自动探测传入的数据类型,生成对应的TypeInformation,调用对应的序列化器,因此用户其实无需关心类型推测。比如,Flink的map函数Scala签名为:def map[R: TypeInformation](fun: T => R): DataStream[R],传入map的数据类型是T,生成的数据类型是R,Flink会推测T和R的数据类型,并使用对应的序列化器进行序列化。
以一个字符串Tuple3
类型为例,Flink首先推断出该类型,并生成对应的TypeInformation
,然后在序列化时调用对应的序列化器,将一个内存对象写入内存块。
如果传递给Flink算子的数据类型是父类,实际运行过程中使用的是子类,子类中有一些父类没有的数据结构和特性,将子类注册可以提高性能。在执行环境上调用
env.registerType(clazz);
来注册类。registerType方法的源码如下所示,其中TypeExtractor对数据类型进行推断,如果传入的类型是POJO,则可以被Flink识别和注册,否则将使用Kryo。
registerType源码:
public void registerType(Class type) {
if (type == null) {
throw new NullPointerException("Cannot register null type class.");
} else {
TypeInformation typeInfo = TypeExtractor.createTypeInfo(type);
if (typeInfo instanceof PojoTypeInfo) {
this.config.registerPojoType(type);
} else {
this.config.registerKryoType(type);
}
}
}
如果数据类型不是Flink支持的上述类型,需要对数据类型和序列化器进行注册,以便Flink能够对该数据类型进行序列化。
//使用PersonClassSerializer对Person序列化
env.getConfig().registerTypeWithKryoSerializer(Person.class,PersonClassSerializer.class);
public class PersonClassSerializer extends Serializer {
@Override
public void write(Kryo kryo, Output output, Person person) {
}
@Override
public Person read(Kryo kryo, Input input, Class aClass) {
return null;
}
}
对于Apache Thrift和Protobuf的用户,已经有人将序列化器编写好,我们可以直接拿来使用:
env.getConfig().registerTypeWithKryoSerializer(Person.class,ProtobufSerializer.class);
env.getConfig().addDefaultKryoSerializer(Person.class,TBaseSerializer.class);
Google Protobuf的pom:
com.twitter
chill-protobuf
0.7.6
com.esotericsoftware.kryo
kryo
com.google.protobuf
protobuf-java
3.7.0
Apache Thrift的pom:
org.apache.thrift
libthrift
0.11.0
javax.servlet
servlet-api
org.apache.httpcomponents
httpclient