《Hadoop权威指南》学习笔记(6)——Hadoop的I/O操作(2)

序列化

这一部分的主要内容是序列化。

概念

所谓序列化是指将结构化对象转化为字节流以便在网络上串数或写到磁盘进行永久存储的过程。
相应的,既然有序列化,就一定有反序列化。
反序列化是指将字节流转回结构化对象的逆过程。
序列化用于分布式数据处理的两大领域:进程间通信和永久存储。
在 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接口,该接口继承自Writablejava.io.Comparable接口:

package org.apache.hadoop.io;
public interface WritableComparable<T> extends Writable, Comparable<T>{
}

因为存在基于键的排序阶段,所以对MapReduce来说类型比较很重要。
Hadoop提供一个继承自Java ComparatorRawComparator优化接口:

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()两个方法用于读取或存储封装的值。
在这里插入图片描述
其中,VIntWritableVLongWritable是变长格式,适合在数值在值域空间中分布不均匀的情况下使用,这样可以更节省空间。

2、Text类型

Text 是针对 UTF-8序列的Writable类,一般可以认为它是java.lang.StringWritable等价。所以下面主要说明一下 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

NullWritableWritable 的特殊类型,它的序列化长度为 0.它并不从数据流中读取数据或写入数据,而是充当占位符,在不需要使用键或值得序列化地址时,可将键或值声明为NullWritable
它是一个不可变的单实例类型,通过调用NullWritable.get()方法获取该实例。

5、ObjectWritable 和 GenericWritable

ObjectWritable 是对 Java 基本类型的一个通用封装。
当一个字段中包含多个类型时,ObjectWritable 很有用。如:若SequenceFile中的值包含多个类型,就可以将值声明为 ObjectWritable 类型,并将每个类型封装在一个ObjectWritable中。
但是,每次序列化都写封装类型的名称是很浪费空间的。若封装的类型数量较少且提前知道,那么可以通过使用静态类型数组,并使用对序列化后的类型的引用加入位置索引来提高性能,GenericWritable就是用的这种方式,所以要在继承的子类中指定所支持的类型。

6、Writable集合类

org.apache.hadoop.io包中共有6个Writable集合类,分别是ArrayWritableArrayPrimitiveWritableTwoDArrayWritableMapWritableSortedMapWritableEnumWritable
ArrayWritableTwoDArrayWritable是对Writable的数组和二维数组的实现,数组中的元素类型要在构造函数中指定:

ArrayWritable writable = new ArrayWritable(Text.class);

ArrayWritableTwoDArrayWritable都有get()set()toArray()方法,其中,toArray()方法用来新建对应数组的一个“浅拷贝”。

MapWritableSortedMapWritable分别实现了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字节表示的RawComparatorFirstComparator类是用于比较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,常用作二进制数据的永久存储格式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值