第3章 序列化与压缩 3.1序列化

来源:互联网 发布:游戏环境检测软件 编辑:程序博客网 时间:2024/05/19 03:44

《Hadoop技术内幕:深入解析Hadoop Common和HDFS架构设计与实现原理》第3章序列化与压缩,本章涉及了org.apache.hadoop.io包下最重要的两部分内容:序列化和压缩。本节为大家介绍序列化。

第3章 序列化与压缩

传统的计算机系统通过I/O操作与外界进行交流,Hadoop的I/O由传统的I/O系统发展而来,但又有些不同,Hadoop需要处理P、T级别的数据,所以在org.apache.hadoop.io包中包含了一些面向海量数据处理的基本输入输出工具,本章会对其中的序列化和压缩进行研究。

3.1 序列化

对象的序列化(Serialization)用于将对象编码成一个字节流,以及从字节流中重新构建对象。“将一个对象编码成一个字节流”称为序列化该对象(Serializing);相反的处理过程称为反序列化(Deserializing)。

序列化有三种主要的用途:

作为一种持久化格式:一个对象被序列化以后,它的编码可以被存储到磁盘上,供以后反序列化用。

作为一种通信数据格式:序列化结果可以从一个正在运行的虚拟机,通过网络被传递到另一个虚拟机上。

作为一种拷贝、克隆(clone)机制:将对象序列化到内存的缓存区中,然后通过反序列化,可以得到一个对已存对象进行深拷贝的新对象。

在分布式数据处理中,主要使用上面提到的前两种功能:数据持久化和通信数据格式。

在分析Hadoop的序列化机制前,先介绍一下Java内建的序列化机制。

3.1.1 Java内建序列化机制

Java序列化机制将对象转换为连续的byte数据,这些数据可以在日后还原为原先的对象状态,该机制还能自动处理不同操作系统上的差异,在Windows系统上序列化的Java对象,可以在UNIX系统上被重建出来,不需要担心不同机器上的数据表示方法,也不需要担心字节排列次序,如大端(big endian)、小端(little endian)或其他细节。

在Java中,使一个类的实例可被序列化非常简单,只需要在类声明中加入implements Serializable即可。Serializable接口是一个标志,不具有任何成员函数,其定义如下:

  1. public interface Serializable {  

Serializable接口没有任何方法,所以不需要对类进行修改,Block类通过声明它实现了Serializable接口,立即可以获得Java提供的序列化功能。代码如下:

  1. public class Block implements Writable, Comparable<Block>, Serializable 

由于序列化主要应用在与I/O相关的一些操作上,其实现是通过一对输入/输出流来实现的。如果想对某个对象执行序列化动作,可以在某种OutputStream对象(后面还会讨论Java的流)的基础上创建一个对象流ObjectOutputStream对象,然后调用writeObject()就可达到目的。

writeObject()方法负责写入实现了Serializable接口对象的状态信息,输出数据将被送至该OutputStream。多个对象的序列化可以在ObjectOutputStream对象上多次调用writeObject(),分别写入这些对象。下面是序列化一个Block对象的例子:

  1. Block block1=new Block(7806259420524417791L, 39447755L, 56736651L);  
  2. Block block2=new Block(5547099594945187683L, 67108864L, 56736828L);  
  3. ……  
  4. ByteArrayOutputStream out=new ByteArrayOutputStream();  
  5.  
  6. //在ByteArrayOutputStream的基础上创建ObjectOutputStream  
  7. ObjectOutputStream objOut=new ObjectOutputStream(out);  
  8.  
  9. //对block进行序列化  
  10. objOut.writeObject(block1);  

对于Java基本类型的序列化,ObjectOutputStream提供了writeBoolean()、writeByte()等方法。

输入过程类似,将InputStream包装在ObjectInputStream中并调用readObject(),该方法返回一个指向向上转型后的Object的引用,通过向下转型,就可以得到正确结果。读取对象时,必须要小心地跟踪存储的对象的数量、顺序以及它们的类型。

Java的序列化机制非常“聪明”,JavaDoc中对ObjectOutputStream的writeObject()方法的说明是:“……这个对象的类、类签名、类的所有非暂态和非静态成员的值,以及它所有的父类都要被写入”,序列化机制会自动访问对象的父类,以保证对象内容的一致性。同时,序列化机制不仅存储对象在内存中的原始数据,还会追踪通过该对象可以到达的其他对象的内部数据,并描述所有这些对象是如何被链接起来的。对于复杂的情形,Java序列化机制也能应付自如:在输出objectA和objectB时,不会重复保存对象的序列化结果(如objectC,即objectC只被序列化一次);对于循环引用的对象,序列化也不会陷入死循环(如图3-1右图的情形)。

但是,序列化以后的对象在尺寸上有点过于充实了,以Block类为例,它只包含3个长整数,但是它的序列化结果竟然有112字节,而BlockMetaDataInfo其实只多了一个long型的成员变量,输出结果已经膨胀到190字节。包含3个长整数的Block对象的序列化结果如下:

  1. AC ED 00 05 73 72 00 1C  6F 72 67 2E 68 61 64 6F    ....sr.. org.hado  
  2. 6F 70 69 6E 74 65 72 6E  61 6C 2E 73 65 72 2E 42    opintern al.ser.B  
  3. 6C 6F 63 6B E7 80 E3 D3  A6 B6 22 53 02 00 03 4A    lock.... .."S...J  
  4. 00 07 62 6C 6F 63 6B 49  64 4A 00 0F 67 65 6E 65    ..blockI dJ..gene  
  5. 72 61 74 69 6F 6E 53 74  61 6D 70 4A 00 08 6E 75    rationSt ampJ..nu  
  6. 6D 42 79 74 65 73 78 70  6C 55 67 95 68 E7 92 FF    mBytesxp lUg.h...  
  7. 00 00 00 00 03 61 BB 8B  00 00 00 00 02 59 EC CB    .....a.. .....Y.. 

仔细看Block的输出会发现,序列化的结果中包含了大量与类相关的信息。Java的序列过程在《Java Object Serialization Specification》中规范,以Block为例,其结果的前两个字节是魔数(Magic Number)“AC ED”;后续两个字节是序列化格式的版本号,现在使用的版本号是5;接下来是类的描述信息,包括类的版本ID、是否实现writeObject()和readObject()方法等信息,对于拥有超类的类(如BlockMetaDataInfo),超类的信息也会递归地被保存下来;这些信息都写入OutputStream对象后,接下来才是对象的数据。在这个过程中,序列化输出中保存了大量的附加信息,导致序列化结果膨胀,对于需要保存和处理大规模数据的Hadoop来说,需要一个新的序列化机制。

由于篇幅的关系,不再详细讨论Java的序列化机制,有兴趣的读者可以参考《Java Object Serialization Specification》。

3.1.2 Hadoop序列化机制

和Java序列化机制不同(在对象流ObjectOutputStream对象上调用writeObject()方法),Hadoop的序列化机制通过调用对象的write()方法(它带有一个类型为DataOutput的参数),将对象序列化到流中。反序列化的过程也是类似,通过对象的readFields(),从流中读取数据。值得一提的是,Java序列化机制中,反序列化过程会不断地创建新的对象,但在Hadoop的序列化机制的反序列化过程中,用户可以复用对象:如,在Block的某个对象上反复调用readFields(),可以在同一个对象上得到多个反序列化的结果,而不是多个反序列化的结果对象(对象被复用了),这减少了Java对象的分配和回收,提高了应用的效率。

  1. public static void main(String[] args) {  
  2.   try {  
  3.      Block block1=new Block(7806259420524417791L, 39447755L, 56736651L);  
  4.      ……  
  5.      ByteArrayOutputStream bout = new ByteArrayOutputStream();  
  6.      DataOutputStream dout=new DataOutputStream(bout);  
  7.      block1.write(dout); //序列化对象到输出流dout中  
  8.      dout.close();  
  9.      System.out.println(……);  
  10.      SerializationExample.print16(out.toByteArray(), bout.size());  
  11.   }  
  12.   ……  

由于Block对象序列化时只输出了3个长整数, block1的序列化结果一共有24字节,如下所示。和Java的序列化机制的输出结果对比,Hadoop的序列化结果紧凑而且快速。

  1. AC ED 00 05 73 72 00 28  6F 72 67 2E 68 61 64 6F    ....sr.( org.hado  
  2. 6F 70 69 6E 74 65 72 6E                             opintern  

3.1.3 Hadoop序列化机制的特征

对于处理大规模数据的Hadoop平台,其序列化机制需要具有如下特征:

紧凑:由于带宽是Hadoop集群中最稀缺的资源,一个紧凑的序列化机制可以充分利用数据中心的带宽。

快速:在进程间通信(包括MapReduce过程中涉及的数据交互)时会大量使用序列化机制,因此,必须尽量减少序列化和反序列化的开销。

可扩展:随着系统的发展,系统间通信的协议会升级,类的定义会发生变化,序列化机制需要支持这些升级和变化。

互操作:可以支持不同开发语言间的通信,如C++和Java间的通信。这样的通信,可以通过文件(需要精心设计文件的格式)或者后面介绍的IPC机制实现。

Java的序列化机制虽然强大,却不符合上面这些要求。Java Serialization将每个对象的类名写入输出流中,这导致了Java序列化对象需要占用比原对象更多的存储空间。同时,为了减少数据量,同一个类的对象的序列化结果只输出一份元数据,并通过某种形式的引用,来共享元数据。引用导致对序列化后的流进行处理的时候,需要保持一些状态。想象如下一种场景,在一个上百G的文件中,反序列化某个对象,需要访问文件中前面的某一个元数据,这将导致这个文件不能切割,并通过MapReduce来处理。同时,Java序列化会不断地创建新的对象,对于MapReduce应用来说,不能重用对象,在已有对象上进行反序列化操作,而是不断创建反序列化的各种类型记录,这会带来大量的系统开销。

3.1.4 Hadoop Writable机制

为了支持以上这些特性,Hadoop引入org.apache.hadoop.io.Writable接口,作为所有可序列化对象必须实现的接口,其类图如图3-2所示。

Writable机制紧凑、快速(但不容易扩展到Java以外的语言,如C、Python等)。和java.io.Serializable不同,Writable接口不是一个说明性接口,它包含两个方法:

  1. public interface Writable {  
  2.   /**  
  3.    * 输出(序列化)对象到流中  
  4.    * @param out DataOuput流,序列化的结果保存在流中  
  5.    * @throws IOException  
  6.    */  
  7.   void write(DataOutput out) throws IOException;  
  8.  
  9.  /**  
  10.    * 从流中读取(反序列化)对象  
  11.    * 为了效率,请尽可能复用现有的对象  
  12.    * @param in DataInput流,从该流中读取数据  
  13.    * @throws IOException  
  14.    */  
  15.   void readFields(DataInput in) throws IOException;  
  16. }  

Writable.write()方法用于将对象状态写入二进制的DataOutput中,反序列化的过程由readFields()从DataInput流中读取状态完成。下面是一个例子:

  1. public class Block implements Writable, Comparable<Block>, Serializable {  
  2.   ……  
  3.   private long blockId;  
  4.   private long numBytes;  
  5.   private long generationStamp;  
  6.   ……  
  7.   public void write(DataOutput out) throws IOException {  
  8.      out.writeLong(blockId);  
  9.      out.writeLong(numBytes);  
  10.      out.writeLong(generationStamp);  
  11.   }  
  12.  
  13.  public void readFields(DataInput in) throws IOException {  
  14.      this.blockId = in.readLong();  
  15.      this.numBytes = in.readLong();  
  16.      this.generationStamp = in.readLong();  
  17.      if (numBytes < 0) {  
  18.         throw new IOException("Unexpected block size: " + numBytes);  
  19.      }  
  20.   }  
  21.   ……  
  22. }  

这个例子使用的是前面分析Java序列化机制的Block类,Block实现了Writable接口,即需要实现write()方法和readFields()方法,这两个方法的实现都很简单:Block有三个成员变量,write()方法简单地把这三个变量写入流中,而readFields()则从流中依次读入这些数据,并做必要的检查。

Hadoop序列化机制中还包括另外几个重要接口:WritableComparable、RawComparator和WritableComparator。

WritableComparable,顾名思义,它提供类型比较的能力,这对MapReduce至关重要。该接口继承自Writable接口和Comparable接口,其中Comparable用于进行类型比较。ByteWritable、IntWritable、DoubleWritable等Java基本类型对应的Writable类型,都继承自WritableComparable。

效率在Hadoop中非常重要,因此HadoopI/O包中提供了具有高效比较能力的RawComparator接口。RawComparator和WritableComparable类图如图3-3所示。

 

RawComparator接口允许执行者比较流中读取的未被反序列化为对象的记录,从而省去了创建对象的所有开销。其中,compare()比较时需要的两个参数所对应的记录位于字节数组b1和b2的指定开始位置s1和s1,记录长度为l1和l2,代码如下:

  1. public interface RawComparator<T>extends  
  2. Comparator<T> {  
  3.   public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2);  

以IntWritable为例,它的RawComparator实现中(WritableComparator是一个辅助类,实现了RawComparator接口),compare()方法通过readInt()直接在字节数组中读入需要比较的两个整数,然后输出Comparable接口要求的比较结果。值得注意的是,该过程中compare()方法避免使用IntWritable对象,从而避免了不必要的对象分配。相关代码如下:

  1. public static class Comparator extends WritableComparator {  
  2.   ……  
  3.   public int compare(byte[] b1, int s1, int l1,  
  4.                      byte[] b2, int s2, int l2) {  
  5.      int thisValue = readInt(b1, s1);  
  6.      int thatValue = readInt(b2, s2);  
  7.      return (thisValue<thatValue ? -1 : (thisValue==thatValue ? 0 : 1));  
  8.   }  
  9.   ……  

WritableComparator是RawComparator对WritableComparable类的一个通用实现。它提供两个主要功能。首先,提供了一个RawComparator的compare()默认实现,该实现从数据流中反序列化要进行比较的对象,然后调用对象的compare()方法进行比较(这些对象都是Comparable的)。其次,它充当了RawComparator实例的一个工厂方法,例如,可以通过如下代码获得IntWritable的RawComparator:

  1. RawComparator<IntWritable>comparator=  
  2.   WritableComparator.get(IntWritable.class);  

3.1.5 典型的Writable类详解(1)

Hadoop将很多Writable类归入org.apache.hadoop.io包中,类图如图3-4所示。

在这些类中,比较重要的有Java基本类、Text、Writable集合、ObjectWritable等,本节重点介绍Java基本类和ObjectWritable的实现。

1. Java基本类型的Writable封装

目前Java基本类型对应的Writable封装如表3-1所示。所有这些Writable类都继承自WritableComparable。也就是说,它们是可比较的。同时,它们都有get()和set()方法,用于获得和设置封装的值。

表3-1 Java基本类型对应的Writable封装

在表3-1中,对整型(int和long)进行编码的时候,有固定长度格式(IntWritable和LongWritable)和可变长度格式(VIntWritable和VLongWritable)两种选择。固定长度格式的整型,序列化后的数据是定长的,而可变长度格式则使用一种比较灵活的编码方式,对于数值比较小的整型,它们往往比较节省空间。同时,由于VIntWritable和VLongWritable的编码规则是一样的,所以VIntWritable的输出可以用VLongWritable读入。下面以VIntWritable为例,说明Writable的Java基本类封装实现。代码如下:

  1. public class VIntWritable implements WritableComparable {  
  2.   private int value;  
  3.   ……  
  4.   // 设置VIntWritable的值  
  5.   public void set(int value) { this.value = value; }  
  6.  
  7.  // 获取VIntWritable的值  
  8.   public int get() { return value; }  
  9.  
  10.  public void readFields(DataInput in) throws IOException {  
  11.      value = WritableUtils.read VInt(in);  
  12.   }  
  13.  
  14.  public void write(DataOutput out) throws IOException {  
  15.      WritableUtils.writeVInt(out, value);  
  16.   }  
  17.   ……  
  18. }  

首先,每个Java基本类型的Writable封装,其类的内部都包含一个对应基本类型的成员变量value,get()和set()方法就是用来对该变量进行取值/赋值操作的。而Writable接口要求的readFields()和write()方法,VIntWritable则是通过调用Writable工具类中提供的readVInt()和writeVInt()读/写数据。方法readVInt()和writeVInt()的实现也只是简单调用了readVLong()和writeVLong(),所以,通过writeVInt()写的数据自然可以通过readVLong()读入。

writeVLong ()方法实现了对整型数值的变长编码,它的编码规则如下:

如果输入的整数大于或等于–112同时小于或等于127,那么编码需要1字节;否则,序列化结果的第一个字节,保存了输入整数的符号和后续编码的字节数。符号和后续字节数依据下面的编码规则(又一个规则):

如果是正数,则编码值范围落在–113和–120间(闭区间),后续字节数可以通过–(v+112)计算。

如果是负数,则编码值范围落在–121和–128间(闭区间),后续字节数可以通过–(v+120)计算。

3.1.5 典型的Writable类详解(2)

后续编码将高位在前,写入输入的整数(除去前面全0字节)。代码如下:

  1. public final class WritableUtils  {  
  2.   public stati cvoid writeVInt(DataOutput stream, int i) throws IOException  
  3.   {  
  4.      writeVLong(stream, i);  
  5.   }  
  6.  
  7.  /**  
  8.    * @param stream保存系列化结果输出流  
  9.    * @param i 被序列化的整数  
  10.    * @throws java.io.IOException  
  11.    */  
  12.   public static void writeVLong(DataOutput stream, long i) throws……  
  13.   {  
  14.      //处于[-112, 127]的整数  
  15.      if (i >= -112 && i <= 127) {  
  16.         stream.writeByte((byte)i);  
  17.         return;  
  18.      }  
  19.      //计算情况2的第一个字节  
  20.      int len = -112;  
  21.      if (i < 0) {  
  22.         i ^= -1L;  
  23.         len = -120;  
  24.      }  
  25.  
  26.     long tmp = i;  
  27.      while (tmp != 0) {  
  28.         tmptmp = tmp >> 8;  
  29.         len--;  
  30.      }  
  31.  
  32.     stream.writeByte((byte)len);  
  33.  
  34.     len = (len < -120) ? -(len + 120) : -(len + 112);  
  35.  
  36.     //输出后续字节  
  37.      for (int idx = len; idx != 0; idx--) {  
  38.         int shiftbits = (idx - 1) * 8;  
  39.         long mask = 0xFFL << shiftbits;  
  40.         stream.writeByte((byte)((i & mask) >> shiftbits));  
  41.      }  
  42.   }  
  43. }  

注意 本书附带代码对writeVLong()的输出结果做了一些分析,有兴趣的读者可以运行org.hadoopinternal.ser.VLongShow,分析一些典型整数的writeVLong()输出结果。

2. ObjectWritable类的实现

针对Java基本类型、字符串、枚举、Writable、空值、Writable的其他子类,ObjectWritable提供了一个封装,适用于字段需要使用多种类型。ObjectWritable可应用于Hadoop远程过程调用(将在第4章介绍)中参数的序列化和反序列化;ObjectWritable的另一个典型应用是在需要序列化不同类型的对象到某一个字段,如在一个SequenceFile的值中保存不同类型的对象(如LongWritable值或Text值)时,可以将该值声明为ObjectWritable。

ObjectWritable的实现比较冗长,需要根据可能被封装在ObjectWritable中的各种对象进行不同的处理。ObjectWritable有三个成员变量,包括被封装的对象实例instance、该对象运行时类的Class对象和Configuration对象。

ObjectWritable的write方法调用的是静态方法ObjectWritable.writeObject(),该方法可以往DataOutput接口中写入各种Java对象。

writeObject()方法先输出对象的类名(通过对象对应的Class 对象的getName()方法获得),然后根据传入对象的类型,分情况系列化对象到输出流中,也就是说,对象通过该方法输出对象的类名,对象序列化结果对到输出流中。在ObjectWritable.writeObject()的逻辑中,需要分别处理null、Java数组、字符串String、Java基本类型、枚举和Writable的子类6种情况,由于类的继承,处理Writable时,序列化的结果包含对象类名,对象实际类名和对象序列化结果三部分。

3.1.5 典型的Writable类详解(3)

为什么需要对象实际类名呢?根据Java的单根继承规则,ObjectWritable中传入的declaredClass,可以是传入instance对象对应的类的类对象,也可以是instance对象的父类的类对象。但是,在序列化和反序列化的时候,往往不能使用父类的序列化方法(如write方法)来序列化子类对象,所以,在序列化结果中必须记住对象实际类名。相关代码如下:

  1. public class ObjectWritable implements Writable, Configurable {  
  2.   private Class declaredClass;//保存于ObjectWritable的对象对应的类对象  
  3.   private Object instance;//被保留的对象  
  4.   private Configuration conf;  
  5.  
  6.  public ObjectWritable() {}  
  7.  
  8.  public ObjectWritable(Object instance) {  
  9.      set(instance);  
  10.   }  
  11.  
  12.  public ObjectWritable(Class declaredClass, Object instance) {  
  13.      this.declaredClass = declaredClass;  
  14.      this.instance = instance;  
  15.   }  
  16.   ……  
  17.   public void readFields(DataInput in) throws IOException {  
  18.      readObject(in, this, this.conf);  
  19.   }  
  20.  
  21.  public void write(DataOutput out) throws IOException {  
  22.      writeObject(out, instance, declaredClass, conf);  
  23.   }  
  24.   ……  
  25.   public static void writeObject(DataOutput out, Object instance,  
  26.         Class declaredClass,Configuration conf) throws……{  
  27.  
  28.     if (instance == null) {//空  
  29.         instance = new NullInstance(declaredClass, conf);  
  30.         declaredClass = Writable.class;  
  31.      }  
  32.  
  33.     // 写出declaredClass的规范名  
  34.      UTF8.writeString(out, declaredClass.getName());  
  35.  
  36.     if (declaredClass.isArray()) {//数组  
  37.         ……  
  38.      } else if (declaredClass == String.class) {//字符串  
  39.         ……  
  40.      } else if (declaredClass.isPrimitive()) {//基本类型  
  41.         if (declaredClass == Boolean.TYPE) {        //boolean  
  42.            out.writeBoolean(((Boolean)instance).booleanValue());  
  43.         } else if (declaredClass == Character.TYPE) { //char  
  44.            ……  
  45.         }  
  46.      } else if (declaredClass.isEnum()) {//枚举类型  
  47.         ……  
  48.      } else if (Writable.class.isAssignableFrom(declaredClass)) {  
  49.         //Writable的子类  
  50.         UTF8.writeString(out, instance.getClass().getName());  
  51.         ((Writable)instance).write(out);  
  52.      } else {  
  53.         ……  
  54.   }  
  55.  
  56.  public static Object readObject(DataInput in,  
  57.                  ObjectWritable objectWritable, Configuration conf){  
  58.      ……  
  59.      Class instanceClass = null;  
  60.      ……  
  61.      Writable writable = WritableFactories.newInstance(instanceClass,  
  62.            conf);  
  63.      writable.readFields(in);  
  64.      instance = writable;  
  65.      ……  
  66.   }  
  67. }  

和输出对应,ObjectWritable的readFields()方法调用的是静态方法ObjectWritable.readObject (),该方法的实现和writeObject()类似,唯一值得研究的是Writable对象处理部分,readObject ()方法依赖于WritableFactories类。WritableFactories类允许非公有的Writable子类定义一个对象工厂,由该工厂创建Writable对象,如在上面的readObject ()代码中,通过WritableFactories的静态方法newInstance(),可以创建类型为instanceClass的Writable子对象。相关代码如下:

  1. public class WritableFactories {  
  2.   //保存了类型和WritableFactory工厂的对应关系  
  3.   private static final HashMap<Class, WritableFactory>CLASS_TO_FACTORY 
  4.      = new HashMap<Class, WritableFactory>();  
  5.   ……  
  6.   public static Writable newInstance(Class<? extends Writable> c,  
  7.                                      Configuration conf) {  
  8.      WritableFactory factory = WritableFactories.getFactory(c);  
  9.      if (factory != null) {  
  10.         Writable result = factory.newInstance();  
  11.         if (result instanceof Configurable) {  
  12.            ((Configurable) result).setConf(conf);  
  13.         }  
  14.         return result;  
  15.      } else {  
  16.         //采用传统的反射工具ReflectionUtils,创建对象  
  17.         return ReflectionUtils.newInstance(c, conf);  
  18.      }  
  19.   }  

3.1.5 典型的Writable类详解(4)

WritableFactories.newInstance()方法根据输入的类型查找对应的WritableFactory工厂对象,然后调用该对象的newInstance()创建对象,如果该对象是可配置的,newInstance()还会通过对象的setConf()方法配置对象。

WritableFactories提供注册机制,使得这些Writable子类可以将该工厂登记到WritableFactories的静态成员变量CLASS_TO_FACTORY中。下面是一个典型的WritableFactory工厂实现,来自于HDFS的数据块Block。其中,WritableFactories.setFactory()需要两个参数,分别是注册类对应的类对象和能够构造注册类的WritableFactory接口的实现,在下面的代码里,WritableFactory的实现是一个匿名类,其newInstance()方法会创建一个新的Block对象。

  1. public class Block implements Writable, Comparable<Block> {  
  2.   static {  
  3.      WritableFactories.setFactory  
  4.         (Block.class,//类对象  
  5.         new WritableFactory() {//对应类的WritableFactory实现  
  6.            public Writable newInstance() { return new Block(); }  
  7.         });  
  8.   }  
  9.   ……  

ObjectWritable作为一种通用机制,相当浪费资源,它需要为每一个输出写入封装类型的名字。如果类型的数量不是很多,而且可以事先知道,则可以使用一个静态类型数组来提高效率,并使用数组索引作为类型的序列化引用。GenericWritable就是因为这个目的被引入org.apache.hadoop.io包中,由于篇幅关系,不再详细介绍,有兴趣的读者可以继续分析GenericWritable的源代码。

3.1.6 Hadoop序列化框架

大部分的MapReduce程序都使用Writable键–值对作为输入和输出,但这并不是Hadoop的API指定的,其他序列化机制也能和Hadoop配合,并应用于MapReduce中。

目前,除了前面介绍过的Java序列化机制和Hadoop使用的Writable机制,还流行其他序列化框架,如Hadoop Avro、Apache Thrift和Google Protocol Buffer。

Avro是一个数据序列化系统,用于支持大批量数据交换的应用。它的主要特点有:支持二进制序列化方式,可以便捷、快速地处理大量数据;动态语言友好,Avro提供的机制使动态语言可以方便地处理 Avro数据。

Thrift是一个可伸缩的、跨语言的服务开发框架,由Facebook贡献给开源社区,是Facebook的核心框架之一。基于Thrift的跨平台能力封装的Hadoop文件系统Thrift API(参考contrib的thriftfs模块),提供了不同开发语言开发的系统访问HDFS的能力。

Google Protocol Buffer是Google内部的混合语言数据标准,提供了一种轻便高效的结构化数据存储格式。目前,Protocol Buffers提供了C++、Java、Python三种语言的API,广泛应用于Google内部的通信协议、数据存储等领域中。

Hadoop提供了一个简单的序列化框架API,用于集成各种序列化实现,该框架由Serialization实现(在org.apache.hadoop.io.serializer包中)。

Serialization是一个接口,使用抽象工厂的设计模式,提供了一系列和序列化相关并相互依赖对象的接口。通过Serialization应用可以获得类型的Serializer实例,即将一个对象转换为一个字节流的实现实例;Deserializer实例和Serializer实例相反,它用于将字节流转为一个对象。很明显,Serializer和Deserializer相互依赖,所以必须通过抽象工厂Serialization,才能获得对应的实现。相关代码如下:

  1. public interface Serialization<T> {  
  2.  
  3.   //客户端用于判断序列化实现是否支持该类对象  
  4.   boolean accept(Class<?> c);  
  5.  
  6.  //获得用于序列化对象的Serializer实现  
  7.   Serializer<T> getSerializer(Class<T> c);  
  8.  
  9.  //获得用于反序列化对象的Deserializer实现  
  10.   Deserializer<T> getDeserializer(Class<T> c);  
  11. }  

如果需要使用Serializer来执行序列化,一般需要通过open()方法打开Serializer,open()方法传入一个底层的流对象,然后就可以使用serialize()方法序列化对象到底层的流中。最后序列化结束时,通过close()方法关闭Serializer。Serializer接口的相关代码如下:

  1. public interface Serializer<T> {  
  2.   //为输出(序列化)对象做准备  
  3.   void open(OutputStream out) throws IOException;  
  4.  
  5.  //将对象序列化到底层的流中  
  6.   void serialize(T t) throws IOException;  
  7.  
  8.  //序列化结束,清理  
  9.   void close() throws IOException;  
  10. }  

Hadoop目前支持两个Serialization实现,分别是支持Writable机制的WritableSerialization和支持Java序列化的JavaSerialization。通过JavaSerialization可以在MapReduce程序中方便地使用标准的Java类型,如int或String,但如同前面所分析的,Java的Object Serialization不如Hadoop的序列化机制有效,非特殊情况不要轻易尝试。


原创粉丝点击