序列化
这一部分的主要内容是序列化。
概念
所谓序列化是指将结构化对象转化为字节流以便在网络上串数或写到磁盘进行永久存储的过程。
相应的,既然有序列化,就一定有反序列化。
反序列化是指将字节流转回结构化对象的逆过程。
序列化用于分布式数据处理的两大领域:进程间通信和永久存储。
在 Hadoop 中,系统中多个节点上进程间的通信是通过“远程过程调用”(RPC)实现的。RPC 协议将消息序列化成二进制流后发送到远程节点,远程节点接着将二进制流反序列化为原始消息。
RPC 序列化格式有以下几个属性:紧凑;快速;可扩展;支持互操作。
在 Hadoop 中使用的是自己的序列化格式Writable
,它绝对紧凑、速度快、但不太容易用Java以外的语言进行扩展或使用。这是后面部分探讨的内容。
Writable 接口
Writable接口
Writable
接口定义了两个方法:一个将其状态写入DataOutput
二进制流,另一个从DataInput
二进制流读取状态:
package org.apache.hadoop.io;
import java.io.DataOutput;
import java.io.DataInput;
import java.io.IOException;
public interface Writable{
void write(DataOutput out) throws IOException;
void readFields(DataInput in) throws IOException;
}
为了检查Writable
类的序列化形式,在java.io.DataOutputStream
中加入一个帮助函数用来封装java.io.ByteArrayOutputStream
,以便在序列化流中捕捉字节(以IntWritable
类为例):
public static byte[] serialize(Writable writable) throws IOException{
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream dataOut = new DataOutputStream(out);
writable.write(dataOut);
dataOut.close();
return out.toByteArray();
}
相应的,也可以新建一个辅助方法来从一个字节数组中读取一个Writable
对象:
public static byte[] deserialize(Writable writable, byte[] bytes)
throws IOException{
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
DataInputStream dataIn = new DataInputStream(in);
writable.readFields(dataIn);
dataIn.close();
return bytes;
}
WritableComparable接口和comparator
(还是以IntWritable
为例)
IntWritable
实现原始的WritableComparable
接口,该接口继承自Writable
和java.io.Comparable
接口:
package org.apache.hadoop.io;
public interface WritableComparable<T> extends Writable, Comparable<T>{
}
因为存在基于键的排序阶段,所以对MapReduce
来说类型比较很重要。
Hadoop提供一个继承自Java Comparator
的RawComparator
优化接口:
package org.apache.hadoop.io;
import java.util.Comparator;
public interface RawComparator<T> extends Comparator<T>{
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
实例的工厂,如:
RawComparator<IntWritable> comparator = WritableComparator.get(IntWritable.class);
Writable类
Hadoop自带的org.apache.hadoop.io包中有广泛的Writable类可供选择。
1、Java基本类型的Writable封装器
Writable
类对除char
类型外所有的Java基本类型提供封装(char
类型可以存储在IntWritable
中)。所有封装包含get()
和set()
两个方法用于读取或存储封装的值。
其中,VIntWritable
和VLongWritable
是变长格式,适合在数值在值域空间中分布不均匀的情况下使用,这样可以更节省空间。
2、Text类型
Text 是针对 UTF-8序列的Writable
类,一般可以认为它是java.lang.String
的Writable
等价。所以下面主要说明一下 Text 和 String 类的区别。
①索引
对 Text 类的索引是根据编码后字节序列中的位置实现的,而非字符串中的 Unicode 字符,也不是 Java char 的编码单元(如 String)。但对于 ASCII 字符串而言,这三者是一样的。
Text.charAt()
方法返回的是一个表示 Unicode 编码位置的 int 类型值。Text.find()
方法类似于String.indexOf()
方法。
②Unicode
当使用需要多个字节来编码的字符时,Text 和 String 之间的区别就很明显了。
String 的长度是其所含 char 编码单元的个数,而 Text 对象的长度是其 UTF-8 编码的字节数。
String 的 indexOf()方法返回 char 编码单元中的索引位置,Text 类的 find() 方法返回字节偏移量。
③迭代
对 Text 类中的 Unicode 字符进行迭代是很复杂地,因为无法通过简单地增加索引值来实现该迭代。而且迭代的语法也有些模糊。
public class TextIterator{
public static void main(String[] args){
Text t = new Text("\u0041\u00DF\u6771\uD801\uDC00");
//将Text对象转换为java.io.ByteBuffer
ByteBuffer buf = ByteBuffer.wrap(t.getBytes(), 0, t.getLength());
int cp;
//利用缓冲区对Text对象反复调用bytesToCodePoint()静态方法,该方法能获取下一代码位置并返回响应地int值,最后更新缓冲区的位置
while(buf.hasRemaining() && (cp = Text.bytesToCodePoint(buf)) != -1){
System.out.println(Integer.toHexString(cp));
}
}
}
④可变性
Text 与 String 相比的另一个区别在于它是可变的,这一点与 Hadoop 中的 Writable 接口实现相似。可以通过调用set()
方法来重用 Text 实例。
要注意的是,在对 Text 实例的内容进行修改后,虽然字符串长度改变,但其字节数并不会改变,多出的部分字节仍会占位。
所以要在调用getBytes()
之前调用getLength()
,来了解字节数组中多少字符是有效的。
⑤对 String 重新排序
Text 不像 String 类那样有丰富的字符串操作 API,所以多数情况下要将 Text 对象转换成 String 对象。
3、BytesWritable
BytesWritable 是对二进制数据数组的封装,它的序列化格式为一个指定所含数据字节数的整数域(4字节),后跟数据内容本身。如序列化格式为000000020305
表示长度为 2 的字节数组包含 3 和 5 两个数值。
4、NullWritable
NullWritable
是 Writable
的特殊类型,它的序列化长度为 0.它并不从数据流中读取数据或写入数据,而是充当占位符,在不需要使用键或值得序列化地址时,可将键或值声明为NullWritable
。
它是一个不可变的单实例类型,通过调用NullWritable.get()
方法获取该实例。
5、ObjectWritable 和 GenericWritable
ObjectWritable
是对 Java 基本类型的一个通用封装。
当一个字段中包含多个类型时,ObjectWritable
很有用。如:若SequenceFile
中的值包含多个类型,就可以将值声明为 ObjectWritable
类型,并将每个类型封装在一个ObjectWritable
中。
但是,每次序列化都写封装类型的名称是很浪费空间的。若封装的类型数量较少且提前知道,那么可以通过使用静态类型数组,并使用对序列化后的类型的引用加入位置索引来提高性能,GenericWritable
就是用的这种方式,所以要在继承的子类中指定所支持的类型。
6、Writable集合类
org.apache.hadoop.io
包中共有6个Writable
集合类,分别是ArrayWritable
、ArrayPrimitiveWritable
、TwoDArrayWritable
、MapWritable
、SortedMapWritable
和EnumWritable
。
ArrayWritable
和TwoDArrayWritable
是对Writable
的数组和二维数组的实现,数组中的元素类型要在构造函数中指定:
ArrayWritable writable = new ArrayWritable(Text.class);
ArrayWritable
和TwoDArrayWritable
都有get()
、set()
和toArray()
方法,其中,toArray()方法用来新建对应数组的一个“浅拷贝”。
MapWritable
和SortedMapWritable
分别实现了java.util.Map<Writable, Writable>
和java.util.SortedMap<WritableComparable, Writable>
。每个键/值字段使用的类型是相应字段序列化形式的一部分,类型存储为单个字节。
键/值可以是不同的Writable
类,如:
MapWritable src = new MapWritable();
src.put(new IntWritable(1), new Text("cat"));
src.put(new VIntWritable(2), new LongWritable(163));
此外,还可以通过Writable
集合类来实现集合和列表。可使用MapWritable
类型来枚举集合中的元素,用NullWritable
类型枚举值,对集合的枚举类型可采用EnumSetWritable
。
对于单类型的Writable
列表,使用ArrayWritable
就可以了,但若需要将不同的Writable
类型存储在单个列表中,可以用GenericWritable
将元素封装在一个ArrayWritable
中,或者可以借鉴MapWritable
的思路写一个通用的ListWritable
。
实现定制的Writable集合
首先给出一个定制的Writable
,表示一堆字符串的实现。
import java.io.*;
import org.apache.hadoop.io.*;
public class TextPair implements WritableComparable<TextPair> {
private Text first;
private Text second;
public TextPair() {
set(new Text(), new Text());
}
public TextPair(String first, String second) {
set(new Text(first), new Text(second));
}
public TextPair(Text first, Text second) {
set(first, second);
}
public void set(Text first, Text second) {
this.first = first;
this.second = second;
}
public Text getFirst() {
return first;
}
public Text getSecond() {
return second;
}
@Override
public void write(DataOutput out) throws IOException {
first.write(out);
second.write(out);
}
@Override
public void readFields(DataInput in) throws IOException {
first.readFields(in);
second.readFields(in);
}
@Override
public int hashCode() {
return first.hashCode() * 163 + second.hashCode();
}
@Override
public boolean equals(Object o) {
if (o instanceof TextPair) {
TextPair tp = (TextPair) o;
return first.equals(tp.first) && second.equals(tp.second);
}
return false;
}
@Override
public String toString() {
return first + "\t" + second;
}
@Override
public int compareTo(TextPair tp) {
int cmp = first.compareTo(tp.first);
if (cmp != 0) {
return cmp;
}
return second.compareTo(tp.second);
}
public static class Comparator extends WritableComparator {
private static final Text.Comparator TEXT_COMPARATOR = new Text.Comparator();
public Comparator() {
super(TextPair.class);
}
@Override
public int compare(byte[] b1, int s1, int l1,
byte[] b2, int s2, int l2) {
try {
//firstL1和firstL2两个参数表示每个字节流中第一个Text字段的长度,
//两者分别由变长整数的长度(WritableUtils.decodeVIntSize())和编码值(readVInt()返回值)组成
int firstL1 = WritableUtils.decodeVIntSize(b1[s1]) + readVInt(b1, s1);
int firstL2 = WritableUtils.decodeVIntSize(b2[s2]) + readVInt(b2, s2);
int cmp = TEXT_COMPARATOR.compare(b1, s1, firstL1, b2, s2, firstL2);
if (cmp != 0) {
return cmp;
}
return TEXT_COMPARATOR.compare(b1, s1 + firstL1, l1 - firstL1,
b2, s2 + firstL2, l2 - firstL2);
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
}
static {
WritableComparator.define(TextPair.class, new Comparator());
}
public static class FirstComparator extends WritableComparator {
private static final Text.Comparator TEXT_COMPARATOR = new Text.Comparator();
public FirstComparator() {
super(TextPair.class);
}
@Override
public int compare(byte[] b1, int s1, int l1,
byte[] b2, int s2, int l2) {
try {
int firstL1 = WritableUtils.decodeVIntSize(b1[s1]) + readVInt(b1, s1);
int firstL2 = WritableUtils.decodeVIntSize(b2[s2]) + readVInt(b2, s2);
return TEXT_COMPARATOR.compare(b1, s1, firstL1, b2, s2, firstL2);
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public int compare(WritableComparable a, WritableComparable b) {
if (a instanceof TextPair && b instanceof TextPair) {
return ((TextPair) a).first.compareTo(((TextPair) b).first);
}
return super.compare(a, b);
}
}
}
其中TextPair
类是定义的存储一堆Text
对象的Writable
实现,Comparator
类是用于比较TextPair
字节表示的RawComparator
,FirstComparator
类是用于比较TextPair
对象字节表示的第一个字段的RawComparator
。
TextPair
中重写的compareTo()
方法可以用来对TextPair
对象进行比较,但是需要先将数据流反序列化为对象。这样比较复杂,所以要想要用一种方式可以通过序列化表示就能比较两个TextPair
对象,上面的Comparator
类就满足这个要求。可以看到,这个了继承了WritableComparable
类,而非RawComparator
类,因为它提供了一些较好用的方法和默认实现。
FirstComparator
类则是一个定制的comparator
,用来定义排列顺序不同于默认comparator
定义的自然排列顺序。
序列化框架
虽然大部分MapReduce
程序使用的都是Writable
类型的键/值,但这并不是MapReduce API强制要求的。所以其实可以使用任何类型,只要能有一种机制对每个类型进行类型与二进制表示的来回转换就可以。
为支持该机制,Hadoop 有一个针对可替换序列化框架的API。序列化框架用一个Serialization
实现来表示。如:WritableSerialization
类是对Writable
类型的Serialization
实现。
Serialization对象定义列从类型到Serializer实例和Deserializer实例的映射方式。
为了注册Serialization实现,要将io.serializations
属性设置为一个由逗号分隔的类名列表。它的默认值包括org.apache.hadoop.io.serializer.WritableSerialization
和 Avro 序列化及 Reflect 序列化类,这意味着只有Writable
对象和 Avro 对象才能在外部序列化和反序列化。
序列化IDL
有许多序列化框架不通过代码来定义类型,而是使用“定义接口语言”(IDL)以不依赖于具体语言的方式进行声明。这样能够有效提高互操作能力。
两个流行的序列化框架 Apache Thrift 和 Google 的 Protocol Buffers,常用作二进制数据的永久存储格式。