Hadoop源码分析(一):序列化

625 阅读7分钟

序列化

所谓序列化(serialization),是指将结构化对象转换为字节流,以便在网络上传输或持久化到磁盘。反序列化(deserialization) 是指将字节流转回结构化对象的逆过程。

序列化在分布式数据处理的两大领域经常出现:进程通信和永久存储

在Hadoop中,系统中多个节点上进程间的通信是通过“远程过程调用”(remote procedure call,RPC)实现的,RPC协议将消息序列化成二进制流随后发到远程节点,远程节点连着将二进制流反序列化为原始消息。通常情况,RPC序列化格式如下:

  • 紧凑:紧凑的格式能使我们充分利用网络带宽(它是数据中心中最稀缺的资源)
  • 快速:进程间通信形成了分布式系统的骨架,所以需要尽量减少序列化和反序列化的性能开销,这是最基本的。
  • 可拓展:协议为了满足新的需求而不断变化,所以在控制客户端和服务器的过程中。需要直接引进相应的协议。例如,需要能够在方法调用的过程中增添新的参数,并且新的服务器需要能够接受例子老客户端的老格式的消息(无新增的参数)。

初始数据永久存储时,为它选择数据格式需要来自序列化框架的不同需求。毕竟,RPC的存活时间不到1秒钟,然而永久存储的数据可能会写到磁盘若干年后才会被读取。由此,数据永久存储所期望的4个RPC序列化属性非常重要。我们希望存储格式比较紧凑(进而高效实用存储空间)、快速(进而读写数据的额外开销比较小)、可拓展(进而可以透明地读取老格式的数据)且可以互操作(进而可以使用不同的语言读写永久存储的数据)。

Hadoop使用自己的序列化格式Writable,它格式紧凑,速度快,但很难用Java以外的语言进行拓展或使用。因为Writable是hadoop的核心(大多数MapReduce程序都会为键和值使用它),所以在接下来的三个小节中,我们要进行深入探讨,然后再从总体上看看序列化框架和Avro,后者是一个客服了Writable少许局限性的序列化系统。

Writable接口

Writable接口定义了两个方法:一个将其状态写到DataOutput 二进制流,另一个从DataInput 二进制流读取其状态:

import org.apache.hadoop.io.Writable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

public class WordCount implements Writable {
    public void write(DataOutput dataOutput) throws IOException { }

    public void readFields(DataInput dataInput) throws IOException { }
}

让我们通过一个特殊的Writable类来看看它的具体用途。我们将使用IntWritable来封装一个Java int。我们可以新建一个并使用set()方法来设置它的值:

    IntWritable intWritable = new IntWritable();
    intWritable.set(155);

为了检查IntWritable的序列化形式,我们在java.io.DataOutoytStream(Java.io.DataOutput的一个实现)中加入了一个帮助函数来封装java.io.ByteArrayOutputStream,以便在序列化流中捕捉字节:

    static byte[] serialize(Writable writable) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        DataOutputStream dataOutputStream = new DataOutputStream(out);
        writable.write(dataOutputStream);
        dataOutputStream.close();
        return out.toByteArray();
    }
    
     public static void main(String[] args) throws IOException {
        IntWritable intWritable = new IntWritable();
        intWritable.set(155);
        byte[] serialize = serialize(intWritable);
        System.out.println(serialize.length);
    }

WritableComparable和Comparator

IntWritable实现了WritableComparable接口,该接口继承自Writable和Java.lang.Comparable接口:

public class IntWritable implements WritableComparable<IntWritable> {}

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

对于MapReduce来说,类型的比较是非常重要的,因为中间有个基于键的排序阶段。Hadoop提供的一个优化接口是继承自Java Comparator的RawComparator接口:

public interface RawComparator<T> extends Comparator<T> {

  /**
   * Compare two objects in binary.
   * b1[s1:l1] is the first object, and b2[s2:l2] is the second object.
   * 
   * @param b1 The first byte array.
   * @param s1 The position index in b1. The object under comparison's starting index.
   * @param l1 The length of the object in b1.
   * @param b2 The second byte array.
   * @param s2 The position index in b2. The object under comparison's starting index.
   * @param l2 The length of the object under comparison in b2.
   * @return An integer result of the comparison.
   */
  public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2);
}

该接口允许实现直接比较数据流中的记录,无需先把数据流反序列化为对象,这样便避免新建对象的额外开销

例如:我们根据IntWritable 接口实现的comparator实现了compare()方法,该方法可以从每个字节数组b1和b2中读取给定起始位置(s1和s2)以及长度(l1和l2)的一个整数而直接进行比较。

WritableComparator是对继承自WritableComparable类的RawComparator类的一个通用实现。它提供两个主要功能。第一,它提供了对原始compare()方法的一个默认实现,该方法能够将反序列将在流中进行比较的对象,并调用对象的compare()方法。第二:它充当的是RawComparator实例的工厂(已注册Writable的实现)。例如,为了获得IntWritable的comparator,我们直接如下调用:

        WritableComparator comparator = WritableComparator.get(IntWritable.class);
        // 这个comparator可用于相比较两个IntWritable对象
        IntWritable intWritable1 = new IntWritable(40);
        IntWritable intWritable2 = new IntWritable(50);

        System.out.println(comparator.compare(intWritable1, intWritable2));

为什么不使用Java Object Serialization?

Java有自己的序列化机制,成为Java Object Serialization(通常简称为“Java Serialization”),该机制与编程语言紧密相关,所以我们很自然会问为什么不在Hadoop中使用该机制。针对这个问题。Doug Cutting是这样解释的:

“为什么开始设计Hadoop的时候我不用Java Serialization?因为它看起来太复杂,而我认为需要有一个至精至简的机制,可以用于精确控制对象的读和写,因为这个机制是Hadoop核心。使用Java Serialization后,虽然可以获得一些控制权,但用起来非常纠结”

不用RMI也出于类似的考虑。高效、高性能的进程间通信是Hadoop的关键,我觉得我们需要精确控制连接、延迟和缓冲的处理方式,然而RMI对此无能为力。

问题在于Java Serialization不满足先前列出的序列化格式标准:精简、快速、可拓展性、可以互操作。

Java Serialization并不精简:每个对象写到数据流时,它都要写入其类名--->实现Java.io.Serializable或Java.io.Externalization接口的类确实如此。同一个类后续的实例只引用第一次出现的句柄,这占5个字节。引用句柄不太适用于随机访问,因为被引用的类可能出现在先前数据流的任何位置----也就是说,需要在数据流中存储状态。更糟的是,句柄引用会对序列化数据流中的排序记录造成巨大的破坏,因为一个特定的类的第一个记录是不同的,必须当做特殊情况区别对待。

压根不把类名写到数据流,则可以避免所有这些问题,Writable接口采取的正是这种做法。这需要假设客户端知道会收到什么类型。其结果是,这个格式比Java Serialization格式更精简,同事又支持随机存取和排序,因为流中的每一条记录均独立于其他记录(所以数据量是无状态的)。

Java Serialization是序列化图对象的通用机制,所以它有序列化和反序列化开销。更有甚至,它有一些从数据流中反序列化对象时,反序列化程序需要为每个对象新建一个实例。另一刚面,Writable对象可以重用。例如,对于MapReduce作业(主要对只有几个类型的几十亿个对象进行序列化和反序列),不需要为新建对象分配空间而得到的存储节省是非常客观的。

至于拓展性,Java Serialization支持演化而来的新类型,但是难以使用。不支持Writable类型,程序要需要自行管理。

原则上讲,其他编程语言能够理解Java Serialization流协议(由Java Serialization定义),但事实上,其他语言的实现并不多,所以只有Java实现。Writable的情况也不列外