Hadoop的I/O操作
一、压缩
文件压缩有两大好处:
- 减少存储文件所需要的磁盘空间
- 加速数据在网络和磁盘上的传输
在Hadoop中更是如此。
Codec
Codec是压缩-解压缩算法的一种实现,在Hadoop中用CompressionCodec(接口)的实现代表一个Codec,常见的实现类有DefaultCdec(implements CompressionCodec),GzipCodec(extends DefaultCodec ),BZip2Codec,LzopCodec,Lz4Codec,SnappyCodec分别对应相应的压缩-解压算法。
1)CompressionCodec要对写入输出数据流的数据进行压缩,用到函数
CompressionOutputStream createOutputStream(OutputStream out) throws IOException;
Demo:
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.compress.CompressionCodec;
import org.apache.hadoop.io.compress.CompressionOutputStream;
import org.apache.hadoop.util.ReflectionUtils;
public class StreamCompressor {
public static void main(String[] args) throws Exception{
String codeClassName = args[0];
Configuration configuration = new Configuration();
Class<?> className = Class.forName(codeClassName);
CompressionCodec codec = (CompressionCodec)ReflectionUtils.newInstance(className, configuration);
CompressionOutputStream outputStream = codec.createOutputStream(System.out);
IOUtils.copyBytes(System.in, outputStream, 4096,false);
outputStream.flush();
}
}
用GzipCodec压缩"Text"并用gunzip解压缩,命令行中输入
echo "Text" | hadoop StreamCompressor org.apache.hadoop.io.compress.GzipCodec \| gunzip
2)CompressionCodec要对输入数据流中数据进行读取时进行解压缩,用到函数
CompressionInputStream createInputStream(InputStream in) throws IOException;
CompressionOutputStream 和 CompressionInputStream 重置底层的压缩和解压缩方法,可以将部分数据流压缩为单独的数据块(BLOCK),在下面介绍的SequenceFile的格式中有应用。
通过CompressionCodecFactory推断CompressionCodec
Demo代码:
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.compress.CompressionCodec;
import org.apache.hadoop.io.compress.CompressionCodecFactory;
import java.io.InputStream;
import java.io.OutputStream;
public class FileDecompressor {
public static void main(String[] args) throws Exception{
String uri = args[1];
Path inputPath = new Path(uri);
Configuration configuration = new Configuration();
CompressionCodecFactory factory = new CompressionCodecFactory(configuration);
//getCodec()拿到Codec
CompressionCodec compressionCodec = factory.getCodec(inputPath);
//拿到codec之后用工厂的静态方法removeSuffix()去除后缀形成输出文件名,如file.gz得到的解压缩文件名就为file
String outputUri = CompressionCodecFactory.removeSuffix(uri,compressionCodec.getDefaultExtension());
InputStream in = null;
OutputStream out = null;
FileSystem fs = FileSystem.get(configuration);
try{
in = compressionCodec.createInputStream(fs.open(inputPath));
out = fs.create(new Path(outputUri));
IOUtils.copyBytes(in, out, configuration);
}finally {
IOUtils.closeStream(out);
IOUtils.closeStream(in);
}
}
}
CodecPool
用到原生库(native,可以提高压缩与解压缩的性能)并且需要重复压缩与解压缩时可以用到CodecPool,通过返回compressor在不同数据流之间来回复制数据。
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.compress.CodecPool;
import org.apache.hadoop.io.compress.CompressionCodec;
import org.apache.hadoop.io.compress.CompressionOutputStream;
import org.apache.hadoop.io.compress.Compressor;
import org.apache.hadoop.util.ReflectionUtils;
public class PooledStreamCompressor {
public static void main(String[] args) throws Exception{
String codecClassName = args[0];
Class<?> codecClass = Class.forName(codecClassName);
Configuration conf = new Configuration();
CompressionCodec codec = (CompressionCodec)ReflectionUtils.newInstance(codecClass,conf);
Compressor compressor = null;
try{
compressor = CodecPool.getCompressor(codec);
CompressionOutputStream outputStream = codec.createOutputStream(System.out, compressor);
IOUtils.copyBytes(System.in, outputStream,4096);
outputStream.flush();
}finally {
CodecPool.returnCompressor(compressor);
}
}
}
在MapReduce中使用压缩
作业配置中将mapreduce.output.fileoutputformat.compress
属性设置为true
并将mapreduce.output.fileoutputformat.compress.codec
设置为打算压缩codec的类名
或者在代码中设置:例如用FileOutputFormat.setCompressOutput(job,true)
,以及FileOutputFormat.setCompressorClass(job,GzipCodec.class)
MapReduce的压缩属性
属性名称 | 类型 | 默认值 | 描述 |
---|---|---|---|
mapreduce.output.fileoutputformat.compress | boolean | false | 是否压缩输出 |
mapreduce.output.fileoutputformat.compress.codec | 类名称 | org.apache.hadoop.io.compress.DefaultCodec | map输出所用的codec |
mapreduce.output.fileoutputformat.compress. type | String | RECORD | 顺序文件中输出可以使用的压缩类型:NONE,RECORD或BLOCK |
map任务输出的压缩属性
属性名称 | 类型 | 默认值 | 描述 |
---|---|---|---|
mapreduce.map.output.compress | boolean | false | 是否压缩输出 |
mapreduce.map.output.compress.codec | 类名程 | org.apache.hadoop.io.compress.DefaultCodec | map输出所用的codec |
在代码中对map任务输出设置压缩类型:
conf.setBoolean(Job.MAP_OUTPUT_COMPRESS,true);
conf.setClass(Job.MAP_OUTPUT_COMPRESS_CODEC, GzipCodec.class,CompressionCodec.class);
Job job = Job.getInstance(conf);
二、序列化
序列化是指将结构化的对象转化为字节流以便在网络上传输或者写到磁盘进行永久存储的过程。
反序列化是指将字节流转回结构化对象的逆过程。
序列化作用于:进程间通信和永久存储
在Hadoop中,多个节点之间的通信通过RPC(“远程过程调用”,remote procedure call)实现的,RPC协议将消息序列化为二进制流后发送给到远程节点,远程节点再把二进制流反序列化成原始消息。RPC具有的格式:
- 紧凑:充分利用网络带宽(数据中心最稀缺的资源)
- 快速:进程间形成分布式系统骨架,减少序列化和反序列化的性能开销是最基本的。
- 可扩展:为了满足需求,协议会不断变化。所以在客户端与服务器段需要直接引进相应协议。例如,需要在方法调用的过程中添加新参数,并且新的服务器需要能够接受来自老客户端的老格式的消息(没有新增的参数)。
- 支持互操作:对于某些系统,希望支持不同语言编写的客户端与服务器交互,所以需要设计一种特定的格式。
Writable接口
Hadoop自己所有的序列化接口是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;
}
利用JUnit4测试Writable接口:
package com.hadoop.guide.serialization;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Writable;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.matchers.JUnitMatchers.*;
import org.apache.hadoop.util.StringUtils;
import org.junit.Test;
import java.io.*;
public class MyWritable {
@Test
public void writableTest() throws Exception{
System.setProperty("hadoop.home.dir","d:\\winutil\\");
IntWritable intWritable = new IntWritable(163);
byte[] bytes = serializ(intWritable);
assertThat(bytes.length, is(4));
String hexStr = StringUtils.byteToHexString(bytes);
assertThat(hexStr, is("000000a3"));
IntWritable newWritable = new IntWritable();
byte[] bytes1 = deserialize(newWritable,bytes);
assertThat(newWritable.get(), is(163));
}
public static byte[] serializ(Writable writable) throws IOException{
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream outputStream = new DataOutputStream(out);
writable.write(outputStream);
outputStream.close();
return out.toByteArray();
}
public static byte[] deserialize(Writable writable, byte[] bytes) throws IOException{
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
DataInputStream inputStream = new DataInputStream(in);
writable.readFields(inputStream);
inputStream.close();
return bytes;
}
}
WrtiableComparable接口
继承Writable和java.lang.Comparable接口
package org.apache.hadoop.io;
public interface WritableComparable<T> extends Writable, Comparable<T> {
}
Hadoop提供优化接口RawComparator继承Java Comparator
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实现的原始compara()方法比较每个字节数组b1和b2,读取从开始索引s1,s2长度为l1和l2的两个整数进行直接比较。
WritableCompartor是继承自WritableComparable类的RawCompartor类的通用实现:
public class WritableComparator implements RawComparator, Configurable {
/*codes...*/
}
如IntWritable,它是继承WritableComparable接口的类,其中IntWritable的Compartor是继承WritableComparator类的,而WritableComparator实现了RawComparator接口。因此WritableCompartor有两个功能:
-
1:提供了对原始compare()方法的一个默认实现,该方法能够反序列化将在流中进行比较对象,并调用对象的compare()方法。
@Override public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) { try { buffer.reset(b1, s1, l1); // parse key1,buffer是DataInputBuffer类 key1.readFields(buffer); //key1和key2都是WritableComparable类 buffer.reset(b2, s2, l2); // parse key2 key2.readFields(buffer); buffer.reset(null, 0, 0); // clean up reference } catch (IOException e) { throw new RuntimeException(e); } return compare(key1, key2); // compare them } //调用对象比较 public int compare(WritableComparable a, WritableComparable b) { return a.compareTo(b); }
-
2:充当RawComparator实例的工厂通过调用WritableComparator的静态方法get()返回对应的comparator:
public static WritableComparator get( Class<? extends WritableComparable> c, Configuration conf) { WritableComparator comparator = comparators.get(c); if (comparator == null) { // force the static initializers to run forceInit(c); // look to see if it is defined now comparator = comparators.get(c); // if not, use the generic one if (comparator == null) { comparator = new WritableComparator(c, conf, true); } } // Newly passed Configuration objects should be used. ReflectionUtils.setConf(comparator, conf); return comparator; }
例如:把163和67比较其对象大小和字节流大小:
@Test
public void comparableTest() throws IOException{
RawComparator<IntWritable> comparator = WritableComparator.get(IntWritable.class);
IntWritable a = new IntWritable(163);
IntWritable b = new IntWritable(67);
assertThat(((WritableComparator) comparator).compare(a,b), Matchers.greaterThan(0));
//序列化表示
byte[] aBytes = serializ(a);
byte[] bBytes = serializ(b);
assertThat(comparator.compare(aBytes,0,aBytes.length, bBytes,0,bBytes.length), Matchers.greaterThan(0));
}
Writable类
IntWritable和VIntWritable与LongWritable和VLongWritable,区别在于VIntWritable和VLongWritable是变长格式,序列化大小分别是1~ 5和1~ 9(字节),有两个优点:
- VIntWritable和VLongWritable可以相互转化,因为二者编码实际是一致的。
- 一般来说变长格式更加省空间,更灵活。
Text类
一般来说可以与java.lang.String等价,但Text与Java String的区别在于Text是针对UTF-8序列的Writable类。
对Text类的索引是根据编码后字节序列中的位置实现的。在ASCII字符串中Text类的索引,Unicode字符和Java char的编码单元(如String)的索引位置的概念一致。
@Test
public void TextTest() throws IOException{
String str = "hadoop";
Text t = new Text(str);
assertThat(t.getLength(),is(6));
assertThat(t.getBytes().length,is(6));
//charAt()返回值不同
assertThat(t.charAt(2),is((int)'d')); //返回索引位置字母的编码,int
assertThat(str.charAt(2),is('d')); //String返回索引位置的字母,char
assertThat("Out of bounds",t.charAt(10),is(-1)); //越界会返回-1
//Text的find()与String的indexOf()
assertThat("Text: Find a substring",t.find("do"),is(2)); //返回字节偏移量
assertThat("Text: Find first 'o'",t.find("o",4),is(4));
assertThat("String: Find a substring",str.indexOf("do"),is(2)); //返回char编码单元中的索引位置
assertThat("String: Find first 'o'",str.indexOf("o",4),is(4));
}
而在需要多个字节来编码的字符时,Text与String之间就有显著区别。
Text遍历
当要遍历Text时,因为Text的索引不同于Java中的,所以不能简单的通过索引++来遍历,因此需要特殊的遍历技巧:
import org.apache.hadoop.io.Text;
import java.nio.ByteBuffer;
public class TextIterator {
public static void main(String[] args) {
Text text = new Text("\u0041\u00DF\u6771\uD801\uDC00");
ByteBuffer buf = ByteBuffer.wrap(text.getBytes(),0, text.getLength());
int cp = 0;
while(buf.hasRemaining() && (cp = Text.bytesToCodePoint(buf)) != -1){
System.out.println(Integer.toHexString(cp));
}
}
}
BytesWritable
对二进制数组封装,序列化格式位一个指定所含数据字节数的整数域(4字节),后面跟数据本身。
例如,长度2的字节数组包含值3,5,则它序列化为一个4字节的整数00000002和两个字节值03,05。
用例代码:
@Test
public void BytesWritableTest() throws Exception{
byte[] bytes = {3, 5};
BytesWritable bytesWritable = new BytesWritable(bytes);
byte[] serBytes = serializ(bytesWritable);
assertThat(StringUtils.byteToHexString(serBytes), is("000000020305"));
}
NullWritable
一般当占位符用,序列化长度为0,可以通过NullWritable.get()获得实例。例如,在Reducer中输出的键值对类型键可以为NullWritable,这样便可以不输出键只输出值。
ObjectWritable 和 GenericWritable
当有多个对象类型需要组成一个数组时可以用到。例如,SequenceFile的键是多种类型时。
Writable集合类
基于文件的数据结构
对于基于MapReduce的数据处理,将二进制数据大对象(blob)单独存放在各自的文件中不适合实现可扩展性,因此Hadoop需要一个更高层的容器。
关于SequenceFile
应用场景:
1)在日志文件中,每一行代表一条日志记录。纯文本不适合记录二进制类型的数据,因此SequenceFile为二进制键-值对提供一个持久数据结构。
2)作为小文件的容器,HDFS和MapReduce是对大文件进行优化,所以通过SequenceFile将小文件打包包装起来,存储和处理起来更高效。
SequenceFile的写操作
Demo代码:
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.SequenceFile;
import org.apache.hadoop.io.Text;
import java.net.URI;
public class SequenceFileWriteDemo {
static final String[] DATA = {
"line 1",
"line 2",
"line 3",
"line 4",
"line 5"
};
public static void main(String[] args) throws Exception{
System.setProperty("hadoop.home.dir", "D:\\winutil\\");
String uri = "output/sequencefile/number";
Configuration conf = new Configuration();
FileSystem fileSystem = FileSystem.get(new URI(uri), conf);
Path path = new Path(uri);
IntWritable key = new IntWritable();
Text value = new Text();
SequenceFile.Writer writer = null;
try {
writer = SequenceFile.createWriter(fileSystem,conf,path,key.getClass(),value.getClass());
for(int i = 0 ; i < 30; i++){
key.set(30 - i);
value.set(DATA[i % DATA.length]);
System.out.printf("[%s]\t%s\t%s\n",writer.getLength(),key,value);
writer.append(key,value);
}
}finally {
IOUtils.closeStream(writer);
}
}
}
输出:
[128] 100 line 1
[155] 99 line 2
[182] 98 line 3
[209] 97 line 4
[236] 96 line 5
[263] 95 line 1
...
[2686] 6 line 5
[2713] 5 line 1
[2740] 4 line 2
[2767] 3 line 3
[2794] 2 line 4
[2821] 1 line 5
SequenceFile的读操作
读操作Demo代码:用*来标记同步标识位置
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.*;
import org.apache.hadoop.util.ReflectionUtils;
import java.net.URI;
public class SequenceFileReadDemo {
public static void main(String[] args) throws Exception{
System.setProperty("hadoop.home.dir", "D:\\winutil\\");
String uri = "output/sequencefile/number";
Configuration conf = new Configuration();
FileSystem fileSystem = FileSystem.get(new URI(uri), conf);
Path path = new Path(uri);
SequenceFile.Reader reader = null;
try {
reader = new SequenceFile.Reader(fileSystem,path,conf);
Writable key = (Writable)ReflectionUtils.newInstance(reader.getKeyClass(),conf);
Writable value = (Writable)ReflectionUtils.newInstance(reader.getValueClass(),conf);
long position = reader.getPosition();
while(reader.next(key,value)){
String syncSeen = reader.syncSeen() ? "*" : "";
System.out.printf("[%s%s]\t%s\t%s\n", position,syncSeen,key,value);
position = reader.getPosition();
}
}finally {
IOUtils.closeStream(reader);
}
}
}
输出:
[128] 100 line 1
[155] 99 line 2
[182] 98 line 3
[209] 97 line 4
[236] 96 line 5
[263] 95 line 1
...
[1964] 32 line 4
[1991] 31 line 5
[2018*] 30 line 1
[2065] 29 line 2
[2092] 28 line 3
...
[2659] 7 line 4
[2686] 6 line 5
[2713] 5 line 1
[2740] 4 line 2
[2767] 3 line 3
[2794] 2 line 4
[2821] 1 line 5
SequenceFile的格式:首先顺序文件是由头文件和随后的一条或多条记录组成的。顺序文件前三字段为SEQ(顺序文件代码),其后一个字节表示顺序文件的版本号。文件头还包括键值类型,压缩细节,用户自定义的元数据以及同步标识。
1)记录压缩:同步标识不用每个记录后都有,因为同步标识的额外开销要求小于1%,特别是比较短的记录。记录压缩与未压缩的区别在于Value是否压缩。下图中以每个两个记录一个同步标识为例。
记录未压缩时的Record结构如下:
记录压缩时的结构:
2)数据块压缩:每个数据块前都要有一个同步标识
压缩数据块结构如下:由一个值指示数据块中字节数的字段与4个压缩字段(键长,键,值长,值)组成。
关于MapFile
MapFile是已经排过序的SequenceFile,它有索引,所以可以按键查找。索引自身就是一个SequenceFile,包含了map中的小部分键(默认每隔128个键)。索引可以加载到内存中,因此可以提供对主数据文件的快速查找。主数据文件也是一个SequenceFile,包含了所有map条目。MapFile也提供读写与SequenceFile非常类似的接口。但是当使用MapFile.Writer进行写操作时,map条目必须按照顺序添加,否则会抛出IOExecption。