首页 > 代码库 > 细水长流话Hadoop(1)Hadoop序列化系统

细水长流话Hadoop(1)Hadoop序列化系统

声明:个人原创,转载请注明出处。文中引用了一些网上或书里的资料,如有不妥之处请告之。

本文是我阅读Hadoop 0.20.2第二遍时写的笔记,在阅读过程中碰到很多问题,最终通过各种途径解决了大部分。Hadoop整个系统设计精良,源码值得学习分布式的同学们阅读,以后会将所有笔记一一贴出,希望能方便大家阅读Hadoop源码,少走弯路。

1 序列化核心技术... 1

2 类型对象大小比较... 3

3 Writable类型工厂... 4

4 ObjectWritable类型... 5

5 其它序列化系统... 6

5.1 Hessian 7

5.2 kryo 9

5.3 protostuff-runtime 9

5.4 msgpack 10

5.5 json/jackson/databind 11

5.6 json/flexjson 12

5.7 json/google-gson/databind 14

5.8 json/fastjson/databind 14

5.9 bson/jackson/databind 14

5.10 xml/xstream+c 15

5.11 avro、protobuf、thrift 17

 

1 序列化核心技术

0.20.2版本Hadoop中的ObjectWritable支持如下几种类型的数据格式序列化:

数据类型

示例

说明

ObjectWritable.NullInstance

ObjectWritable.NullInstance

此类型内部记录有null所属数据类型

String

String

调用UTF8.writeString写入utf8编码的字符串

java基本类型

byte,char,short,int,long,float,double,bool,

不同类型不同对待

enum

enum

写入枚举的名称,可以恢复

Writable

VIntWritable, ObjectWritable.NullInstance

实现Writable接口的数据类型

数组

数组元素必须是以上类型

逐个写入每个元素

从这个列表可以看出,当前版本的Hadoop(以后如果直接写Hadoop,均指当前版本0.20.2)序列化的核心就是Writable类型。Writable是一个接口,定义了两个接口方法,所有实现此接口的类都必须实现这两个方法,凡是实现了这个接口的类的对象,都是可以序列化的,现在来看看Writable接口的定义:

public interface Writable {

/**

* Serialize the fields of this object to <code>out</code>.

*

* @param out <code>DataOuput</code> to serialize this object into.

* @throws IOException

*/

void write(DataOutput out) throws IOException;

/**

* Deserialize the fields of this object from <code>in</code>.

*

* <p>For efficiency, implementations should attempt to re-use storage in the

* existing object where possible.</p>

*

* @param in <code>DataInput</code> to deseriablize this object from.

* @throws IOException

*/

void readFields(DataInput in) throws IOException;

}

Writabe定义的两个方法,一个用于将对象序列化字节写入输出流,一个用于将从输入流中读取序列化对象的字节流来还原对象。这就是所有序列化系统的核心:将一切类型的数据序列化,以方便保存在存储设备上,或者通过网络传输。Hadoop中实现Writable接口的类型是很多的,其中部分类型在其它地方还没用到,部分类型已过时,下面是这些类型的一个列表:

clip_image002clip_image004

图1 Hadoop中实现Writable接口的类型

2 类型对象大小比较

在图1的列表里可以看到WritableComparable<T>的接口,此接口继承了Writable和Comparable<T>接口,实现了序列化和大小比较的定义,WritableComparable<T>的接口如下:

public interface WritableComparable<T> extends Writable, Comparable<T> {

}

许多数据类型都是继承此类来提供序列化和大小比较的功能,跟大小比较相关的类型有两个:接口WritableComparable和类WritableComparator,所有实现WritableComparable接口的类型,都会实现如下代码:

/** Compares two IntWritables. */

public int compareTo(Object o) {

int thisValue = http://www.mamicode.com/this.value;

int thatValue = http://www.mamicode.com/((IntWritable)o).value;

return (thisValue<thatValue ? -1 : (thisValue=http://www.mamicode.com/=thatValue ? 0 : 1));

}

/** A Comparator optimized for IntWritable. */

public static class Comparator extends WritableComparator {

public Comparator() {

super(IntWritable.class);

}

public int compare(byte[] b1, int s1, int l1,

byte[] b2, int s2, int l2) {

int thisValue = http://www.mamicode.com/readInt(b1, s1);

int thatValue = http://www.mamicode.com/readInt(b2, s2);

return (thisValue<thatValue ? -1 : (thisValue=http://www.mamicode.com/=thatValue ? 0 : 1));

}

}

static { // register this comparator

WritableComparator.define(IntWritable.class, new Comparator());

}

第一个compareTo是Comparable接口的方法实现,然后定义了一个内部静态类。compareTo方法是基于对象值进行比较的,而这个内部静态是基于字节进行比较的,但内部静态类实际比较的方法compare还是将字节转换成int类型数字再进行比较,这种比较器可以提供对序列化的支持。

这段代码最后是一段静态初始化代码区。凡是实现了WritableComparable接口的类型,都会有compareTo方法、Comparator内部类、和如上静态初始化代码区的一行代码。这一行代码可看成是所有实现WritableComparable的注册。注册仅仅是传递了当前类的class属性,以及一个WritableComparable实现的内部类Comparator。在WritableComparator中,针对每个WritableComparable类而不是对象保存了其内部静态类Comparator。Comparator是内部静态类就意味着它不依赖外围类的实例成员和方法。所以同一类的多个对象可共享一个Comparator。

3 Writable类型工厂

Hadoop序列化系统定义了一个WritableFactory接口和WritableFactories类,实现WritableFactory接口的唯一形式就是如下一段代码:

static { // register a ctor

WritableFactories.setFactory

(BlockLocation.class,

new WritableFactory() {

public Writable newInstance() { return new BlockLocation(); }

});

}

其目的就是将所有Writable类型注册到WritableFactories里统一管理,利用WritableFactories产生注册到这里的Writable对象,我认为这种统一管理的方式主要提供管理系统可用Writable类型的方便性。如果系统规模很大,Writable类型对象四处分散在系统,那么有时不能很直观地看到一个类型是否是Writable类型。

4 ObjectWritable类型

Hadoop序列化类的核心类就是ObjectWritable类,这是一个可处理所有类型数据的集大成者数据,由于序列化写和读是逆过程,所以这里仅对写代码进行说明:

/** Write a {@link Writable}, {@link String}, primitive type, or an array of

* the preceding. */

public static void writeObject(DataOutput out, Object instance,

Class declaredClass,

Configuration conf) throws IOException {

if (instance == null) { // null

instance = new NullInstance(declaredClass, conf);

declaredClass = Writable.class;

}

UTF8.writeString(out, declaredClass.getName()); // always write declared

if (declaredClass.isArray()) { // array

int length = Array.getLength(instance);

out.writeInt(length);

for (int i = 0; i < length; i++) {

writeObject(out, Array.get(instance, i),

declaredClass.getComponentType(), conf);

}

} else if (declaredClass == String.class) { // String

UTF8.writeString(out, (String)instance);

} else if (declaredClass.isPrimitive()) { // primitive type

if (declaredClass == Boolean.TYPE) { // boolean

out.writeBoolean(((Boolean)instance).booleanValue());

} else if (declaredClass == Character.TYPE) { // char

out.writeChar(((Character)instance).charValue());

} else if (declaredClass == Byte.TYPE) { // byte

out.writeByte(((Byte)instance).byteValue());

} else if (declaredClass == Short.TYPE) { // short

out.writeShort(((Short)instance).shortValue());

} else if (declaredClass == Integer.TYPE) { // int

out.writeInt(((Integer)instance).intValue());

} else if (declaredClass == Long.TYPE) { // long

out.writeLong(((Long)instance).longValue());

} else if (declaredClass == Float.TYPE) { // float

out.writeFloat(((Float)instance).floatValue());

} else if (declaredClass == Double.TYPE) { // double

out.writeDouble(((Double)instance).doubleValue());

} else if (declaredClass == Void.TYPE) { // void

} else {

throw new IllegalArgumentException("Not a primitive: "+declaredClass);

}

} else if (declaredClass.isEnum()) { // enum

UTF8.writeString(out, ((Enum)instance).name());

} else if (Writable.class.isAssignableFrom(declaredClass)) { // Writable

UTF8.writeString(out, instance.getClass().getName());

((Writable)instance).write(out);

} else {

throw new IOException("Can‘t write: "+instance+" as "+declaredClass);

}

}

各参数的含义:

out - 序列化输出流;

instance - 此ObjectWritable要序列化的类的实例;

declaredClass - instance的类型;

conf - 配置。

以下是此方法的操作逻辑:

首先,如果instance为null,则创建一个NullInstance,以declaredClass为类型,然后给declaredClass赋值Writable.class;

其次,写入类名称(declaredClass的名称),然后根据类型不同,采取的写策略也不同:

1、如果instance是数组,则先写入数组长度,再逐个写入每个元素(同样调用writeObject);

2、如果instance是字符串String,则调用UTF8.writeString写入UTF字;

3、如果instance是基本类型,则根据情况写入具体数据,比如,如果是Boolean,则可能写入0或者1;

4、如果instance是枚举类型,则写入枚举的名称;

5、如果instance是Writable类型,则先写入instance的类名称,再调用其write序列化方法。

注意:UTF8字字符串时,先写入short类型的字符长度,再写入实际的UTF8字符生成的字节序列。所以可以利用UTF8序列化字符串。

5 其它序列化系统

一具好的序列化框架应该在时间和空间上做到极致,而且要考虑扩展性,以及新旧协议之间的兼容性问题。以下是一些常见序列化框架的性能数据(来自这个著名的评分:):

clip_image005

clip_image006

5.1 Hessian

Hessian全称为Hessian二进制Web服务协议,由Caucho开源的RPC框架:http://hessian.caucho.com,提供轻量级的Web服务框架。Hessian底层采用Http通讯,采用Servlet暴露服务,其通讯效率高于WebService和Java自带的序列化。

Hessian客户端完成一次调用处理流程是这样的:

clip_image007

从这里可以看出,对于客户端的调用请求,Hessian客户端会将请求序列化,通过Http请求将序列化数据流发送给服务器,服务器反序列化解析出请求之后开始执行相应的服务,然后通过同样的序列化和反序列化将执行结果发送给客户端。

示例代码请看参考资料,网上所有示例都只部署一个服务,要部署多个服务怎么办呢?比如如下的Web.xml配置:

<servlet>

<servlet-name>userService</servlet-name>

<servlet-class>com.caucho.hessian.server.HessianServlet</servlet-class>

<init-param>

<param-name>home-class</param-name>

<param-value>com.mhy.hessian.service.impl.UserServiceImpl</param-value>

</init-param>

<init-param>

<param-name>home-api</param-name>

<param-value>com.mhy.hessian.service.UserService</param-value>

</init-param>

</servlet>

<servlet-mapping>

<servlet-name>userService</servlet-name>

<url-pattern>/userService</url-pattern>

</servlet-mapping>

如果配置多个服务,把第一个servlet节点复制多份就行了。

参考资料:

[1] 官网

[1] Hessian解析及应用(整合Spring)

[2] Hessian入门

[4] 迷人的hessian,你需要了解

[5] Hessian Protocol - Dubbo - Alibaba Open Sesame

5.2 kryo

Kryo 是一个快速高效的Java对象图形序列化框架,主要特点是性能、高效和易用。该项目用来序列化对象到文件、数据库或者网络。

由上面的性能数据可以看出,kryo不论是速度上还是空间上,优势都非常明显。但网上貌似淘宝大牛说这个框架只有Java实现(不是问题),序列化后的字节码中不包含Field元数据,所以在新旧协议间很难做兼容性处理。

示例代码:

Kryo kryo = new Kryo();

// ...

Output output = new Output(new FileOutputStream("file.bin"));

SomeClass someObject = ...

kryo.writeObject(output, someObject);

output.close();

// ...

Input input = new Input(new FileInputStream("file.bin"));

SomeClass someObject = kryo.readObject(input, SomeClass.class);

input.close();

可以看到这是非常易用的。

参考资料:

[1] 官网

[2] Java序列化框架之Kryo

[3] Java对象序列化框架Kryo

[4] Kryo为什么比Hessian快

5.3 protostuff-runtime

Google 的protobuf是一个优秀的序列化工具,跨语言、快速、序列化后体积小。

protobuf的一个缺点是需要数据结构的预编译过程,首先要编写.proto格式的配置文件,再通过protobuf提供的工具生成各种语言响应的代码。由于java具有反射和动态代码生成的能力,这个预编译过程不是必须的,可以在代码执行时来实现。

protostuff基于Google protobuf,但是提供了更多的功能和更简易的用法。其中,protostuff-runtime实现了无需预编译对java bean进行protobuf序列化/反序列化的能力:

Schema schema = RuntimeSchema.getSchema(Foo.class);

LinkedBuffer buffer = getApplicationBuffer();

// ser

try {

byte[] protostuff = ProtostuffIOUtil.toByteArray(foo, schema, buffer);

} finally {

buffer.clear();

}

// deser

Foo f = new Foo();

ProtostuffIOUtil.mergeFrom(protostuff, f, schema);

protostuff-runtime的局限是序列化前需预先传入schema,反序列化不负责对象的创建只负责复制,因而必须提供默认构造函数。此外,protostuff还可以按照protobuf的配置序列化成json/yaml/xml等格式。

参考资料:

[1] 官网 | Wiki ProtostuffRuntime | Wiki Things you need to know

[2] java的序列化lib protostuff

5.4 msgpack

msgpack是一种基于二进制的,高效的对象序列化框架,与JSON类似,但比JSON更快更小。msgpack客户端支持的语言特别丰富,常见的语言都支持。与JSON相比,序列化和反序列化的时间少至少三分之一,生成的序列化文件体积也要少一半。官方宣传比Google Protocol Buffers还要快4倍。

clip_image009

示例代码:

// Create serialize objects.

List<String> src = http://www.mamicode.com/new ArrayList();

src.add("msgpack");

src.add("kumofs");

src.add("viver");

MessagePack msgpack = new MessagePack();

// Serialize

byte[] raw = msgpack.write(src);

// Deserialize directly using a template

List<String> dst1 = msgpack.read(raw, Templates.tList(Templates.TString));

System.out.println(dst1.get(0));

System.out.println(dst1.get(1));

System.out.println(dst1.get(2));

// Or, Deserialze to Value then convert type.

Value dynamic = msgpack.read(raw);

List<String> dst2 = new Converter(dynamic)

.read(Templates.tList(Templates.TString));

System.out.println(dst2.get(0));

System.out.println(dst2.get(1));

System.out.println(dst2.get(2));

参考文献:

[1] msgpack.org

[2] msgpack

[3] 比JSON快10倍的序列化包:msgpack

[4] 二进制数据格式MessagePack:比JSON更快更轻巧

5.5 json/jackson/databind

jackson项目包含有组件:core streaming parser/generator、Jackson Annotations和Jackson Data Processor,这个用于json的databind库就属于Jackson Data Processor。json/jackson/databind最常用的功能就是根据JSON构造POJO(Plain Old Java Object),或者反之,将POJO对象序列化到JSON字符串里。

以下是示例代码:

// Note: can use getters/setters as well; here we just use public fields directly:

public class MyValue {

public String name;

public int age;

// NOTE: if using getters/setters, can keep fields `protected` or `private`

}

 

// 声明一个com.fasterxml.jackson.databind.ObjectMapper类型的对象用于序列化和反序列化

ObjectMapper mapper = new ObjectMapper(); // create once, reuse

 

// 反序列化,从json里读取对象数据到类对象

MyValue value = mapper.readValue(new File("data.json"), MyValue.class);

// or:

value = mapper.readValue(new URL("http://some.com/api/entry.json"), MyValue.class);

// or:

value = mapper.readValue("{\"name\":\"Bob\", \"age\":13}", MyValue.class);

// 序列化,将对象信息保存到json

mapper.writeValue(new File("result.json"), myResultObject);

// or:

byte[] jsonBytes = mapper.writeValueAsBytes(myResultObject);

// or:

String jsonString = mapper.writeValueAsString(myResultObject);

可以看出,使用接口仍十分简单。jackson还支持泛型集合以及树模型。详细请看参考资料:

[1] 官网

5.6 json/flexjson

Flexjson是一个轻量级的序列化和反序列化框架,用于将Java对象序列化成JSON,或反之。不同于其它序列化系统的是:

(1)Flexjson可以控制对其序列化的对象进行深拷贝还是浅拷贝。大部分JSON序列化工具都尽量序列化对象的整个对象关系网中的所有相关对象到JSON文本,这种做法有时会带来问题,比如当你想获取服务器端的一个连接对象时,因为一般JSON序列化工具会传送整个关系网中的对象,所以这个连接对象可能会无法发送给客户端。因此序列化面向对象模型中的某个对象并通过网络传送这个对象可能会遇到麻烦;

(2)很多JSON序列化工具在每次序列化或反序列化时都要求写很多样板代码,Flexjson通过提供更高层的API,尽量减少这类样板代码。

clip_image011

以下所有代码使用的实体类都是上方这幅图中的类。用户可以控制JSON序列化的深度,比如在默认情况下进行的浅拷贝,可以看出浅拷贝就序列化直接的成员,像集合就没有被序列化,自定义对象也可能不会序列化:

代码

输出结果

public String doSomething( Object arg1, ... ) {

Person p = ...load a person...;

JSONSerializer serializer = new JSONSerializer();

return serializer.serialize( p );

}

{

"class": "Person",

"name": "William Shakespeare",

"birthday": -12802392000000,

"nickname": "Bill"

}

下面的代码增加了对phoneNumbers的序列化:

public String doSomething( Object arg1, ... ) {

Person p = ...load a person...;

return new JSONSerializer().include("phoneNumbers").serialize(p);

}

输出结果:

{

"class": "Person",

"name": "William Shakespeare",

"birthday": -12802392000000,

"nickname": "Bill"

"phoneNumbers": [

{

"class": "Phone",

"name": "cell",

"number": "555-123-4567"

},

{

"class": "Phone",

"name": "home",

"number": "555-987-6543"

},

{

"class": "Phone",

"name": "work",

"number": "555-678-3542"

}

]

}

JSONSerializer方法的include方法可接受多个参数,这样就可以选择性地序列化多个成员:

public String doSomething( Object arg1, ... ) {

Person p = ...load a person...;

return new JSONSerializer().include("phoneNumbers", "addresses").serialize(p);

}

甚至可以指定只序列化addresses.zipcode进行序列化,Flexjson可以很聪明地识别出应该如何序列化。除此之外,Flexjson可以定制化出许多种格式的json的字符串,以满足不同系统(比如EXTJS)的特殊需求。Flexjson官方资料很有参考价值,可以考虑以后将它翻译出来。具体参考资料:

[1] 官网

[2] json转换为对象,对象转换为json flexjson的灵活运用

5.7 json/google-gson/databind

Gson是Google的一个开源项目,可以将Java对象转换成JSON,也可以将JSON转换成Java对象。gson可以序列化和反序列化任意的Java对象,即使没有对象的源代码。

有很多开源项目可以在Java对象和JSON之间进行序列化和反序列化,大部分都需要你在类声明前加上Java注解,如果你没有Java源代码的话你就无法对其进行序列化。很多序列化框架也不完全支持泛型,Gson将这两条作为最重要的设计目标。

Gson的设计目标如下:

(1)提供简单的toJson()和fromJson()调用接口将Java对象转换到JSON,或者反之;

(2)可让不可更改的已经存在的对象转换为JSON;

(3)对Java泛型的广泛支持;

(4)可定制表达对象;

(5)支持任意复杂对象的序列化,包括深度继承层次和泛型这一类复杂关系的序列化。

参考资料:

[1] 官网

5.8 json/fastjson/databind

Fastjson是一个Java语言编写的高性能功能完善的JSON库。fastjson采用独创的算法,将parse的速度提升到极致,超过所有json库,包括曾经号称最快的jackson。并且还超越了google的二进制协议protocol buf。支持各种JDK类型。包括基本类型、JavaBean、Collection、Map、Enum、泛型等,支持循环引用。不需要例外额外的jar,能够直接跑在JDK上。

databind是国人开发的,博客。

5.9 bson/jackson/databind

BSON是二进制编码的JSON,bson4jackson集成到Jackson里,以使Jackson支持读和写BSON文档。因为bson4jackson被完全整合到Jackson里了,所以可使用Jackson API序列化POJOs,使之转换为BSON。BSON也是MongoDB数据库的主要数据交换格式了。

定义一个JavaBean对象Person:

public class Person {

private String _name;

public void setName(String name) {

_name = name;

}

public String getName() {

return _name;

}

}

然后使用Jackson的API进行序列化和反序列化:

import java.io.ByteArrayInputStream;

import java.io.ByteArrayOutputStream;

import com.fasterxml.jackson.databind.ObjectMapper;

import de.undercouch.bson4jackson.BsonFactory;

public class ObjectMapperSample {

public static void main(String[] args) throws Exception {

//create dummy POJO

Person bob = new Person();

bob.setName("Bob");

//serialize data

ByteArrayOutputStream baos = new ByteArrayOutputStream();

ObjectMapper mapper = new ObjectMapper(new BsonFactory());

mapper.writeValue(baos, bob);

//deserialize data

ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());

Person clone_of_bob = mapper.readValue(bais, Person.class);

assert bob.getName().equals(clone_of_bob.getName());

}

}

参考文献:

[1] Binary JSON with bson4jackson

[2] 官网

5.10 xml/xstream+c

将JSON作为中间数据的序列化工具很多,我想一方面是因为JSON简单很解析,另一方面可能是因为JSON存储数据比较紧凑,不像XML,每个元素都有开和闭两个标记,而且包含头信息、尾信息,太多的冗余信息不适合网络传输,太复杂的格式导致解析器复杂而缓慢,所以才造成JSON序列化框架这么普遍。

xstream就是一个简单的XML序列化框架。示例代码如下:

package com.hmkcode.vo;

import java.util.LinkedList;

import java.util.List;

public class Article {

private String title;

private String url;

private boolean published;

private List<String> categories;

private List<String> tags;

//getters & setters

}

XStream xs = new XStream();

// OBJECT --> XML

String xml = xs.toXML(createArticle());

// XML --> OBJECT

Article article = (Article) xs.fromXML(xml);

得到的XML如下:

clip_image013

xstream有如下特点:

(1)易用。提供高层抽象使用接口,使框架好用;

(2)绝大部分的对象不需映射就可以进行序列化;

(3)高性能。高速低内存占用一直是最重要的设计目标,因此xstream可适用于有复杂消息关系的大型复杂对象的序列化;

(4)XML干净。序列化生成的XML尽量减少不必要的信息,使得人读起来好读,并且比Java序列化系统生成的数据体积小;

(5)不需要修改对象。序列化内部字段,包括private和final的字段。支持非public和内部类,类不必有默认构造方法;

(6)完整对象图支持。对象间的重复引用关系将会保留,支持循环引用;

(7)可以从其它数据结构,而不是XML序列化和反序列化;

(8)自定义转换策略。可指定特定类型转换为XML;

(9)错误消息。如果解析XML时碰到问题,xstream会报告问题的详细信息;

(10)可选其它类型的输出格式。比如xstream现在增加对json的支持。

5.11 avro、protobuf、thrift

到目前为止最经经常被人们提及的序列化框架是avro、protobuf和thrift,不仅是因为这三个框架功能强大,性能很好,而且还因为这三个框架部署在那些世界上最成功的IT公司里。avro用在Hadoop,protobuf是Google内部使用的序列化框架,Facebook使用thrift。网上也有很多对这些系统的讨论和比较。作为最强大的框架,这三个系统都综合使用了各种序列化和反序化的技术,其内涵已相当复杂,彻底了解起来很难,因此,这里就从网上摘录一些观点和认识。

Avro和Thrift都是跨语言,基于二进制的高性能的通讯中间件. 它们都提供了数据序列化的功能和RPC服务. 总体功能上类似,但是哲学不一样. Thrift出自Facebook用于后台各个服务间的通讯,Thrift的设计强调统一的编程接口的多语言通讯框架. Avro出自Hadoop之父Doug Cutting, 在Thrift已经相当流行的情况下Avro的推出,其目标不仅是提供一套类似Thrift的通讯中间件更是要建立一个新的,标准性的云计算的数据交换和存储的Protocol。 这个和Thrift的理念不同,Thrift认为没有一个完美的方案可以解决所有问题,因此尽量保持一个Neutral框架,插入不同的实现并互相交互。而Avro偏向实用,排斥多种方案带来的 可能的混乱,主张建立一个统一的标准,并不介意采用特定的优化。Avro的创新之处在于融合了显式,declarative的Schema和高效二进制的数据表达,强调数据的自我描述,克服了以往单纯XML或二进制系统的缺陷。Avro对Schema动态加载功能,是Thrift编程接口所不具备的,符合了Hadoop上的Hive/Pig及NOSQL 等既属于ad hoc,又追求性能的应用需求。

序列化系统从XML到JSON,再从JSON到Avro/Google PBs,技术不断在发展,中间经历的时间跨度也越来越短。这篇文章甚至将序列化的历史归结为“XML -> JSON -> Protobuf&Thrift&Avro”,这篇文章是这样说的:

XML是标准信息交换格式,易懂、易扩展并被广泛支持。要实现一个Web服务,SOAP总必须被支持。

事实上SOAP有一些问题,默认使用复杂的POST命令发出调用,SOAP封装会用在request和response中。为了健壮运行和丰富功能而牺牲性能的做法是很笨拙的,特别是SOAP风格的调用很难被缓存。结果,虽然人们认为弃SOAP不用而使用HTTP的GET和POST是一种倒退,在主流Web服务里HTTP的GET和POST仍然被用来提高性能和简化编程, REST协议也可通过HTTP直接暴露出来。

然而,人们仍在寻找更轻量级的数据格式和Web服务实现方式。额外的字节需要CPU和内存,网络带宽和可用存储也有限,所有人都想尽量节约。

JSON因此得以在那些以前需要XML的场景中应用。由于有标准压缩和JavaScript的支持,考虑简单和速度,JSON是首选数据交换格式。BSON有时用来减少直接使用字符串的存储开销,但由于BSON跟Protocol Buffer有类似的问题,所以它也不很流行。

Google的Protocol Buffer通过压缩整型数据节约空间以图比BSON做的更好。为了使执行速度更快,Protocol Buffer将字段名用整数替换,这种做法是性能和可分析、可读、可扩展性的折衷,结果导致,Protocol Buffer的消息只能由指定生成的代码处理。不同版本的消息的访问方式也不同。更糟的是,length前缀方案使特别长的消息处理困难,有时甚至要牺牲性能。因此,Protocol Buffer也不是一种特别理想的序列化方案。

既然Protocol Buffer不开源,Apache Thrift就是专们为那些没呆在豪奢的Google而开发的序列化方案。Thrift中没采用益处少但很复杂的整型数字压缩方案,Thrift实现了一个完整的RPC层次使之没那么轻量级了,却使Thrift的功能更加完整。既然Protocol Buffer已成为RPC的替代方案,为什么不能创建一个语言中立的RPC方案来替代那代平台依赖的如RMI、.net remote和SUN/ONC RPC这样的RPC解决方案呢?因此Thrift是一个SOAP的彻底替代者。不幸的是,尝试为所有语言和平台提供完整的RPC功能是不好做的,很难不断推进的。

大部分情况下,Apache Avro可看作是XML的替代品。Avro既使用二进制,也使用JSON进行序列化,而且其设计也非常棒,堪称两全其美。Avro不需要预生成的代码就可进行解析处理,其架构决定了即使使用二进制进行序列化也是可以的。另外,与Thrift相反,Avro遵循“少即是多”的原则。因此,Avro是所有顶级数据格式的一个强有力竞争者。Avro唯一的问题是它有些复杂,有时比直接使用JSON要慢一些。

另一篇与之相似主题的文章:

假设你有一些数据要存储在文件里,或通过网络传输。你会发现你使用的技术经历了几次变革:

1、使用编程语言内建序列化设施,比如Java serialization、Ruby’s marshal或Python’s pickle,或者搞一个自己的格式;

2、当你意识到你已陷入只能使用一种语言这种很无脑的限制时,你就会寻求支持广泛、语言中立的数据格式,比如JSON(或者你生成1999年,就是XML);

3、然后,你又会觉得JSON太臃肿,解析慢,其不分整型符点型的存储方法也很烦人,你还想使用Unicode表示字符串,于是你创造了一个二进制的数据格式,像JSON,但是二进制的;

4、后来,你发现人们会将各种各样的域塞进对象里,使用不一致的类型,你渴望有一个模式和一些文档。或许你也在用一种静态类型语言并想从一个模式里生成模型类。你发现JSON类似的二进制仍然不够紧凑,因为你存府了域名称一遍又一遍。如果你有一个模式,你就不用再存储域名称,可以节省更多的字节。

当你达到第4层次,你的选择一般是Thrift、Protocol Buffer或者Avro。这三者都使用模式定义,提供高效、跨语言的数据序列化和Java代码生成。

参考资料:

[1] Protocol Buffers, Avro, Thrift & MessagePack