Hadoop序列化与编码浅析

 之前在线上遇上过乱码问题,后来对这块相关的稍作了简单梳理。

       对于需要保存和处理大规模数据的Hadoop来说,每一个MapReduce任务都是对几个类型的几十亿对象进行序列化和反序列化,可以说Hadoop序列化是Hadoop的核心部分之一。

       Java内建的序列化机制,由于它在序列化时输出保存大量的附加信息,比如超类的信息也会递归地被保存下来,导致序列化结果膨胀;另外为了减少数据量,同一类后续对象实例只引用第一次的序列化结果句柄,这就会出现在一个上百G的文件中,反序列化某个对象时需要访问文件中前面的某一个记录,导致这个文件不能切割,并通过MapReduce来处理;同时Java序列化也会不断创建新的对象,对于MapReduce任务来说无法重用而是不断创建也会带来大量的系统开销。所以在Hadoop平台,Java的序列化机制并不适用,而是需要一个新的序列化机制。

一、HadoopWritable接口

       ​Hadoop定义了一个Writable接口,用于实现序列化格式,作为所有可序列化对象必须实现的接口。这个接口基于DataInputDataOutput实现了序列化协议。序列化过程由write()方法将对象状态写入二进制的DataOutput流,反序列化的过程则由readFields()方法从DataInput流中读取对象状态反序列化为Hadoop数据。

1 public interface Writable {
2   void write(DataOutput out) throws IOException;
3   void readFields(DataInput in) throws IOException;
4 }

       Hadoop中的keyvalue必须是实现了Writable接口的对象,以支持在MapReduce任务中的序列化和反序列化

​       Writable接口符合大规模数据处理的Hadoop平台上紧凑和快速的特性。其序列化产生的数据量小,可以高效使用存储空间,充分利用数据中心的带宽。还可以减少MapReduce任务的数据交互中大量序列化和反序列化的开销。

二、Hadoop自身的Writable

​       Hadoop自身已经提供多种Writable类,常见的有Java基本类型(booleanbyteshortintfloatlongdouble等)和Text等。下面是一个Writable对象序列化之后字节序列结构的演示,通过演示可以知道hadoop上的序列化做到了高效紧凑,对于hadoop影响计算效率的网络数据的传输这一重要因素,这一序列化方式加快了数据的读取和减少网络的数据传输量。

​       先定制一个工具类将序列化结果输出到字节数组输出流,这样我们就可以捕获序列化的数据流中的字节。

01 public class HadoopSerializationUtil {
02     public static byte[] serialize(Writable writable) throws IOException {
03         ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
04         DataOutputStream dataOut =  new DataOutputStream(byteOut);
05         writable.write(dataOut);
06         dataOut.close();
07         return byteOut.toByteArray();
08     }
09     public static Writable deserialize(Writable writable, byte[] bytes) throws IOException{
10         ByteArrayInputStream byteIn = new ByteArrayInputStream(bytes);
11         DataInputStream dataIn = new DataInputStream(byteIn);
12         writable.readFields(dataIn);
13         dataIn.close();
14         return writable;
15     }
16 }

Java基本类型Writable对象序列化字节结果输出:

org.apache.hadoop.io.BooleanWritable(true)

Serialize bytes: 01    length: 1
 
org.apache.hadoop.io.ByteWritable(5)
Serialize bytes: 05    length: 1
 
org.apache.hadoop.io.ShortWritable(16)
Serialize bytes: 0010    length: 2
 
org.apache.hadoop.io.IntWritable(256)
Serialize bytes: 00000100    length: 4
 
org.apache.hadoop.io.VIntWritable(127)
Serialize bytes: 7f    length: 1
 
org.apache.hadoop.io.VIntWritable(128)
Serialize bytes: 8f80    length: 2
 
org.apache.hadoop.io.FloatWritable(1.0)
Serialize bytes: 3f800000    length: 4
 
org.apache.hadoop.io.LongWritable(4096)
Serialize bytes: 0000000000001000    length: 8
 
org.apache.hadoop.io.VLongWritable(127)
Serialize bytes: 7f    length: 1
 
org.apache.hadoop.io.VLongWritable(128)
Serialize bytes: 8f80    length: 2
 
org.apache.hadoop.io.DoubleWritable(2.0)
Serialize bytes: 4000000000000000    length: 8
从结果可以看出 基本类型 Writable 对象序列化结果就等于他们的值所占的字节,比较节省空间。
​三、 Text的字节序列

       当然在我们引擎处理文档数据的MapReduce任务中,Text是一个常用的Writable对象。可以简单的认为Text类是java.lang.StringWritable类型,但是区别于String的是,Text类对于Unicode字符采用的是UTF-8编码,使用变长的14个字节对字符进行编码,对于ASCII字符只使用1个字节,而对于High ASCII和多字节字符使用24个字节表示。而String类采用的是UTF-16编码,对每个字符采用定长的16(两个字节)进行编码,对于代码点高于Basic Multilingual Plane(BMP,代码点U+0000U+FFFF)的增补字符,采用两个代理字符进行表示。Hadoop在设计时选择使用UTF-8而不是StringUTF-16就是基于上面的原因,目的也是为了节省字节长度的空间考虑。

​       Text类的字节序列表示为一个VIntWritable + UTF-8字节流,VIntWritable为整个Text的字符长度,UTF-8字节数组为真正的Text字节流。下面是一个示例:

1 ...
2 Text text = new Text("Hadoop ±");
3 String str = new String("Hadoop ±");
4 SerializeResultDisplay(text);
5 BytesDisplay(str, "UTF-8", str.getBytes("UTF-8"));
6 StringBytesDisplay(str);
7 ...

程序输出:

org.apache.hadoop.io.Text(Hadoop ±)

Serialize bytes: 094861646f6f7020c2b1    length: 10
 
String(Hadoop ±)
UTF-8  bytes   :   4861646f6f7020c2b1    length: 9
 
String(Hadoop ±)
UTF-16  bytes   :   feff004800610064006f006f0070002000b1    length: 18
从上面输出可以看出,Text的序列化结果是文本在UTF-8编码下的字节流,而多出的首个字节表示文本在UTF-8编码下占用的字节长度为9个字节。而String 采用的是UTF-16编码,占用18个字节,Hadoop设计Text采用 UTF-8 编码可以做到序列化更紧凑。

四、Hadoop中的编码

​       MapReduce任务中使用和编码相关的Writable对象,比如Text, 就特别要注意其使用的编码方式,因为序列化的字节流会采用其编码方式,另外字符串在内存中以UTF-16编码方式,在这些对象间的转换操作如果不注意编码则很容易造成乱码。

下面是一个可能会出现乱码的示例:

1 ...
2 Text character = new Text("±");
3 String chrstr = character.toString();
4 BytesDisplay(character.toString(), "UTF-8", character.getBytes());
5 BytesDisplay(chrstr, Charset.defaultCharset().name(), chrstr.getBytes());
6 ...

Text中的字节内容为UTF-8编码,UTF-8  bytes   :   c2b100    length: 3

但是字符串获得字节的getBytes()接口则是依据了目前系统的默认编码。

系统编码为ISO-8859-1时输出,ISO-8859-1  bytes   :   b1    length: 1

系统编码为GBK时输出,GBK  bytes   :   a1c0    length: 2

所以当系统编码不是UTF-8时,而在MapReduce任务的另一个地方使用UTF-8解码就会出现乱码。所以使用编码无关的接口则可以很好的避免此类的乱码问题。在这个例子中可以使用Text.getBytes()或者String.getBytes(encoding)接口来避免。

对于Hadoop文件的读取和输出,同样可能会存在编码问题。

​       在写MapReduce程序的时候,总会调用setInputFormatClass方法设置输入格式来保证输入文件按照我们想要的格式被读取,但是这个所谓的格式主要是要解决如何将数据分割分片(inputSplit),以及如何读取分片中的一个个K -V 对,这个过程中并没有编码转换。如果我们待处理的文件不是对应的编码形式时,就需要在进行mapreduce之前先对他进行转码操作。

例如读取文件为GBK编码,则在mapreduce之前进行GBK的转码防止乱码的产生:

01 public static class MyMapper extends Mapper<IntWritable, Text, Text, IntWritable>{
02         public void map(IntWritable key, Text value, Mapper<IntWritable, Text, Text, IntWritable>.Context context) throws IOException, InterruptedException{
03             try {
04                 String val = new String(value.getBytes(), "GBK");
05                 ...
06             }
07             catch (UnsupportedEncodingException e) {
08                 e.printStackTrace();
09             }
10         }
11     }

       在MapReduce中输出文件格式的时候,Hadoop提供了OutputFormat进行转换使用,通过在Job中调用setOutputFormatClass方法来设置格式化类。Hadoop提供了一些格式化类,例如TextOutputFormat但是这个格式化类对输出的编码进行了UTF-8硬编码,如果在输出是中文的情况下,就会出现乱码。解决这种情况就需要自定义一个格式化类。这个类必须是继承OutputFormat,其中的RecordWriter用于数据怎么输出到输出文件里,在编写输出格式化扩展类主要就是实现对RecordWriter的扩展。

例如输出为GBK文件格式,对keyvalue都要进行一次GBK转码。

01 public class GBKRecordWriter<K, V> extends RecordWriter<K, V> {
02     protected DataOutputStream out;
03     private static final String gbk = "GBK";
04     private static final byte[] newline;
05     private final byte[] keyValueSeparator;
06     static {
07         try {
08             newline = "n".getBytes(gbk);
09         catch (UnsupportedEncodingException uee) {
10             throw new IllegalArgumentException("no " + gbk + " encoding");
11         }
12     }
13     private void writeObject(Object o) throws IOException {     
14         out.write(o.toString().getBytes(gbk));
15     }
16     public synchronized void write(K key, V value) throws IOException {
17         writeObject(key);
18         out.write(keyValueSeparator);
19         writeObject(value);
20         out.write(newline);
21     }
22 ...
23 }

最后在MapReducemain方法里,加入job.setOutputFormatClass(GBKOutputFormat.class)来设置输出时使用当前的定制格式化类,从而正确的输出GBK文件。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值