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

《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. publicinterfaceSerializable{
  2. }

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

 
  1. publicclassBlockimplementsWritable,Comparable<Block>,Serializable

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

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

 
  1. Blockblock1=newBlock(7806259420524417791L,39447755L,56736651L);
  2. Blockblock2=newBlock(5547099594945187683L,67108864L,56736828L);
  3. ……
  4. ByteArrayOutputStreamout=newByteArrayOutputStream();
  5. //在ByteArrayOutputStream的基础上创建ObjectOutputStream
  6. ObjectOutputStreamobjOut=newObjectOutputStream(out);
  7. //对block进行序列化
  8. 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. ACED00057372001C6F72672E6861646F....sr..org.hado
  2. 6F70696E7465726E616C2E7365722E42opinternal.ser.B
  3. 6C6F636BE780E3D3A6B622530200034Alock......"S...J
  4. 0007626C6F636B49644A000F67656E65..blockIdJ..gene
  5. 726174696F6E5374616D704A00086E75rationStampJ..nu
  6. 6D427974657378706C55679568E792FFmBytesxplUg.h...
  7. 000000000361BB8B000000000259ECCB.....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. publicstaticvoidmain(String[]args){
  2. try{
  3. Blockblock1=newBlock(7806259420524417791L,39447755L,56736651L);
  4. ……
  5. ByteArrayOutputStreambout=newByteArrayOutputStream();
  6. DataOutputStreamdout=newDataOutputStream(bout);
  7. block1.write(dout);//序列化对象到输出流dout中
  8. dout.close();
  9. System.out.println(……);
  10. SerializationExample.print16(out.toByteArray(),bout.size());
  11. }
  12. ……
  13. }

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

 
  1. ACED0005737200286F72672E6861646F....sr.(org.hado
  2. 6F70696E7465726Eopintern

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. publicinterfaceWritable{
  2. /**
  3. *输出(序列化)对象到流中
  4. *@paramoutDataOuput流,序列化的结果保存在流中
  5. *@throwsIOException
  6. */
  7. voidwrite(DataOutputout)throwsIOException;
  8. /**
  9. *从流中读取(反序列化)对象
  10. *为了效率,请尽可能复用现有的对象
  11. *@paraminDataInput流,从该流中读取数据
  12. *@throwsIOException
  13. */
  14. voidreadFields(DataInputin)throwsIOException;
  15. }

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

 
  1. publicclassBlockimplementsWritable,Comparable<Block>,Serializable{
  2. ……
  3. privatelongblockId;
  4. privatelongnumBytes;
  5. privatelonggenerationStamp;
  6. ……
  7. publicvoidwrite(DataOutputout)throwsIOException{
  8. out.writeLong(blockId);
  9. out.writeLong(numBytes);
  10. out.writeLong(generationStamp);
  11. }
  12. publicvoidreadFields(DataInputin)throwsIOException{
  13. this.blockId=in.readLong();
  14. this.numBytes=in.readLong();
  15. this.generationStamp=in.readLong();
  16. if(numBytes<0){
  17. thrownewIOException("Unexpectedblocksize:"+numBytes);
  18. }
  19. }
  20. ……
  21. }

这个例子使用的是前面分析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. publicinterfaceRawComparator<T>extends
  2. Comparator<T>{
  3. publicintcompare(byte[]b1,ints1,intl1,byte[]b2,ints2,intl2);
  4. }

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

 
  1. publicstaticclassComparatorextendsWritableComparator{
  2. ……
  3. publicintcompare(byte[]b1,ints1,intl1,
  4. byte[]b2,ints2,intl2){
  5. intthisValue=readInt(b1,s1);
  6. intthatValue=readInt(b2,s2);
  7. return(thisValue<thatValue?-1:(thisValue==thatValue?0:1));
  8. }
  9. ……
  10. }

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. publicclassVIntWritableimplementsWritableComparable{
  2. privateintvalue;
  3. ……
  4. //设置VIntWritable的值
  5. publicvoidset(intvalue){this.value=value;}
  6. //获取VIntWritable的值
  7. publicintget(){returnvalue;}
  8. publicvoidreadFields(DataInputin)throwsIOException{
  9. value=WritableUtils.readVInt(in);
  10. }
  11. publicvoidwrite(DataOutputout)throwsIOException{
  12. WritableUtils.writeVInt(out,value);
  13. }
  14. ……
  15. }

首先,每个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. publicfinalclassWritableUtils{
  2. publicstaticvoidwriteVInt(DataOutputstream,inti)throwsIOException
  3. {
  4. writeVLong(stream,i);
  5. }
  6. /**
  7. *@paramstream保存系列化结果输出流
  8. *@parami被序列化的整数
  9. *@throwsjava.io.IOException
  10. */
  11. publicstaticvoidwriteVLong(DataOutputstream,longi)throws……
  12. {
  13. //处于[-112,127]的整数
  14. if(i>=-112&&i<=127){
  15. stream.writeByte((byte)i);
  16. return;
  17. }
  18. //计算情况2的第一个字节
  19. intlen=-112;
  20. if(i<0){
  21. i^=-1L;
  22. len=-120;
  23. }
  24. longtmp=i;
  25. while(tmp!=0){
  26. tmptmp=tmp>>8;
  27. len--;
  28. }
  29. stream.writeByte((byte)len);
  30. len=(len<-120)?-(len+120):-(len+112);
  31. //输出后续字节
  32. for(intidx=len;idx!=0;idx--){
  33. intshiftbits=(idx-1)*8;
  34. longmask=0xFFL<<shiftbits;
  35. stream.writeByte((byte)((i&mask)>>shiftbits));
  36. }
  37. }
  38. }

注意 本书附带代码对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. publicclassObjectWritableimplementsWritable,Configurable{
  2. privateClassdeclaredClass;//保存于ObjectWritable的对象对应的类对象
  3. privateObjectinstance;//被保留的对象
  4. privateConfigurationconf;
  5. publicObjectWritable(){}
  6. publicObjectWritable(Objectinstance){
  7. set(instance);
  8. }
  9. publicObjectWritable(ClassdeclaredClass,Objectinstance){
  10. this.declaredClass=declaredClass;
  11. this.instance=instance;
  12. }
  13. ……
  14. publicvoidreadFields(DataInputin)throwsIOException{
  15. readObject(in,this,this.conf);
  16. }
  17. publicvoidwrite(DataOutputout)throwsIOException{
  18. writeObject(out,instance,declaredClass,conf);
  19. }
  20. ……
  21. publicstaticvoidwriteObject(DataOutputout,Objectinstance,
  22. ClassdeclaredClass,Configurationconf)throws……{
  23. if(instance==null){//空
  24. instance=newNullInstance(declaredClass,conf);
  25. declaredClass=Writable.class;
  26. }
  27. //写出declaredClass的规范名
  28. UTF8.writeString(out,declaredClass.getName());
  29. if(declaredClass.isArray()){//数组
  30. ……
  31. }elseif(declaredClass==String.class){//字符串
  32. ……
  33. }elseif(declaredClass.isPrimitive()){//基本类型
  34. if(declaredClass==Boolean.TYPE){//boolean
  35. out.writeBoolean(((Boolean)instance).booleanValue());
  36. }elseif(declaredClass==Character.TYPE){//char
  37. ……
  38. }
  39. }elseif(declaredClass.isEnum()){//枚举类型
  40. ……
  41. }elseif(Writable.class.isAssignableFrom(declaredClass)){
  42. //Writable的子类
  43. UTF8.writeString(out,instance.getClass().getName());
  44. ((Writable)instance).write(out);
  45. }else{
  46. ……
  47. }
  48. publicstaticObjectreadObject(DataInputin,
  49. ObjectWritableobjectWritable,Configurationconf){
  50. ……
  51. ClassinstanceClass=null;
  52. ……
  53. Writablewritable=WritableFactories.newInstance(instanceClass,
  54. conf);
  55. writable.readFields(in);
  56. instance=writable;
  57. ……
  58. }
  59. }

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

 
  1. publicclassWritableFactories{
  2. //保存了类型和WritableFactory工厂的对应关系
  3. privatestaticfinalHashMap<Class,WritableFactory>CLASS_TO_FACTORY
  4. =newHashMap<Class,WritableFactory>();
  5. ……
  6. publicstaticWritablenewInstance(Class<?extendsWritable>c,
  7. Configurationconf){
  8. WritableFactoryfactory=WritableFactories.getFactory(c);
  9. if(factory!=null){
  10. Writableresult=factory.newInstance();
  11. if(resultinstanceofConfigurable){
  12. ((Configurable)result).setConf(conf);
  13. }
  14. returnresult;
  15. }else{
  16. //采用传统的反射工具ReflectionUtils,创建对象
  17. returnReflectionUtils.newInstance(c,conf);
  18. }
  19. }
  20. }

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. publicclassBlockimplementsWritable,Comparable<Block>{
  2. static{
  3. WritableFactories.setFactory
  4. (Block.class,//类对象
  5. newWritableFactory(){//对应类的WritableFactory实现
  6. publicWritablenewInstance(){returnnewBlock();}
  7. });
  8. }
  9. ……
  10. }

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. publicinterfaceSerialization<T>{
  2. //客户端用于判断序列化实现是否支持该类对象
  3. booleanaccept(Class<?>c);
  4. //获得用于序列化对象的Serializer实现
  5. Serializer<T>getSerializer(Class<T>c);
  6. //获得用于反序列化对象的Deserializer实现
  7. Deserializer<T>getDeserializer(Class<T>c);
  8. }

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

 
  1. publicinterfaceSerializer<T>{
  2. //为输出(序列化)对象做准备
  3. voidopen(OutputStreamout)throwsIOException;
  4. //将对象序列化到底层的流中
  5. voidserialize(Tt)throwsIOException;
  6. //序列化结束,清理
  7. voidclose()throwsIOException;
  8. }

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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值