传统的计算机系统通过I/O操作与外界进行交流, Hadoop 的I/O由传统的I/O系统发展而来,但又有些不同, Hadoop 需要处理 P、T 级别的数据,所以在org.apache.hadoop.io包中包含了一些面向海量数据处理的基本输人输出工具.本文会对其中的序列化进行研究。
序列化
对象的序列化(Serialization)用于将对象编码成一个字节流,以及从字节流中重新构建对象。"将一个对象编码成一个字节流"称为序列化该对象(SeTializing);相反的处理过程称为反序列化(Deserializing)。 序列化有三种主要的用途:
1. 作为一种持久化格式:一个对象被序列化以后,它的编码可以被存储到磁盘上,供以后反序列化用。
2. 作为一种通信数据格式:序列化结果可以从一个正在运行的虚拟机,通过网络被传递到另一个虚拟机上。
3. 作为一种拷贝、克隆(clone)机制:将对象序列化到内存的缓存区中。然后通过反序列化,可以得到一个对已存对象进行深拷贝的新对象。
在分布式数据处理中,主要使用上面提到的前两种功能:数据持久化和通信数据格式。在分析 Hadoop 的序列化机制前,先介绍一下 Java内建的序列化机。
Java内建的序列化机
Java序列化机制将对象转换为连续的byte数据,这些数据可以在日后还原为原先的对象状态,该机制还能自动处理不同操作系统上的差异,在Windows系统上序列化的 Java 对象,可以在 UNIX 系统上被重建出来,不需要担心不同机器上的数据表示方法,也不需要担心字节排列次序,如大端(big endian) 、小端(little endian )或其他细节
在 Java 中,使一个类的实例可被序列化非常简单,只需要在类声明中加入implements Serializable 接口是一个标志,不具有任何成员函数,其定义如下:
Serializable 接口是一个标志,不具有任何成员函数,其定义如下: Serializable接口没有任何方法,所以不需要对类进行修改, User 类通过声明它实现了 Serializable 接口,立即可以获得 Java 提供的序列化功能。代码如下:
由于序列化主要应用在与IO相关的一些操作上,其实现是通过一对输入/输出流来实现的。如果想对某个对象执行序列化动作,可以在某种OutputStream对象的基础上创建一个对象流 ObjectOutputStream对象,然后调用 writeObject()就可达到目的。
writeObject()方法负责写入实现了 Serializable接口对象的状态信息,输出数据将被送至该 OutputStream。多个对象的序列化可以在 ObjectOutputStream对象上多次调用writeObject(),分别写入这些对象。下面是序列化一个 User的例子:
对于Java基本类型的序列化, ObjectOutputStrean提供了 write Boolean、 write Byte等方法。
输入过程类似,将 InputStream包装在 ObjectInputStream中并调用 readObject(),该方法返回一个指向向上转型后的 Object的引用,通过向下转型,就可以得到正确结果。读取对象时于,必须要小心地跟踪存储的对象的数量、顺序以及它们的类型。
Java的序列化机制非常“聪明”, JavaDoc中对 ObjectOutputStream的 writeObject()方法的说明是:“……这个对象的类、类签名、类的所有非暂态和非静态成员的值,以及它所有的父类都要被写入”,序列化机制会自动访问对象的父类,以保证对象内容的一致性。同时,序列化机制不仅存储对象在内存中的原始数据,还会追踪通过该对象可以到达的其他对象的内部数据,并描述所有这些对象是如何被链接起来的。对于复杂的情形,Java序列化机制也能应付。
仔细看 User的输出会发现,序列化的结果中包含了大量与类相关的信息。Java的序列过程在《 Java Object Serialization Specification》中规范,以 user为例,其结果的包含魔数( Magic Number);包含序列化格式的版本号:接下来是类的描述信息,包括类的版本ID、是否实现 writeObjecto和readobject())方法等信息,对于拥有超类的类(如 user MetaDataInfo),超类的信息也会递归地被保存下来;这些信息都写入 OutputStream对象后,接下来才是对象的数据。在这个过程中,序列化输出中保存了大量的附加信息,导致序列化结果膨胀,对于需要保存和处理大规模数据的 Hadoop来说,需要一个新的序列化机制。
Hadoop序列化机制的特征
对于处理大规模数据的 Hadoop平台,其序列化机制需要具有如下特征:
1. 紧凑:由于带宽是 Hadoop集群中最稀缺的资源,一个紧凑的序列化机制可以充分利用数据中心的带宽。
2. 快速:在进程间通信(包括 MapReduce过程中涉及的数据交互)时会大量使用序列化机制,因此,必须尽量减少序列化和反序列化的开销
3. 可扩展:随着系统的发展,系统间通信的协议会升级,类的定义会发生变化,序列化机制需要支持这些升级和变化。
4. 互操作:可以支持不同开发语言间的通信,如C++和Java间的通信。这样的通信可以通过文件(需要精心设计文件的格式)或者后面介绍的IPC机制实现。
Java的序列化机制虽然强大,却不符合上面这些要求。 Java Serialization将每个对象的类名写入输出流中,这导致了Java序列化对象需要占用比原对象更多的存储空间。同时,为了减少数据量,同一个类的对象的序列化结果只输出一份元数据,并通过某种形式的引用,来共享元数据。引用导致对序列化后的流进行处理的时候,需要保持一些状态。想象如下一种场景,在一个上百G的文件中,反序列化某个对象,需要访问文件中前面的某一个元数据,这将导致这个文件不能切割,并通过 MapReduce来处理。同时,Java序列化会不断地创建新的对象,对于 MapReduce应用来说,不能重用对象,在已有对象上进行反序列化操作,而是不断创建反序列化的各种类型记录,这会带来大量的系统开销。