Hadoop I/O

在Hadoop集群中,数据在网络上传输,保证数据完整性的通常做法使用checksum,比如常用的 CRC-32 (cyclic redundancy check)。

Hadoop上支持的文件压缩格式有:gzip  ZIP  bzip2  LZO。例如在UNIX上可以使用命令:gzip -1 file,会生成file.gz,但是原来的file就没有了。压缩算法都要在执行速度和压缩比上做一个权衡,-1表示只注意速度,-9表示只注重压缩比。

CompressionOutputStream和CompressionInputStream很类似于java.util.zip.DeflaterOutputStream 和 java.util.zip.DeflaterInputStream。

Interface CompressionCodec{
  CompressionInputStream createInputStream(InputStream in);      //解压
  CompressionOutputStream createOutputStream(OutputStream out);    //压缩

  ……

} 

实现了CompressionCodec接口的类有:BZip2Codec, DefaultCodec, GzipCodec, SnappyCodec

package io;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;

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 org.apache.hadoop.io.compress.CompressionOutputStream;
import org.apache.hadoop.io.compress.GzipCodec;

public class FileCmpress {

    public static void main(String[] args) throws IOException{
        //解压示例codec.createInputStream
        String uri=args[0];
        Configuration conf=new Configuration();
        FileSystem fs=FileSystem.get(URI.create(uri),conf);
        Path inputPath=new Path(uri);
        CompressionCodecFactory factory=new CompressionCodecFactory(conf);
        CompressionCodec codec=factory.getCodec(inputPath);        //根据文件名的后缀来选择生成哪种类型的CompressionCodec
        if(codec==null){
            System.err.println("No codec found for "+uri);
            System.exit(1);
        }
        String outputUri=CompressionCodecFactory.removeSuffix(uri, codec.getDefaultExtension());
        InputStream in=null;
        OutputStream out=null;
        try{
            in=codec.createInputStream(fs.open(inputPath));        //对输入流进行解压
            out=fs.create(new Path(outputUri));
            IOUtils.copyBytes(in, out, conf);
        }finally{
            IOUtils.closeStream(in);
            IOUtils.closeStream(out);
        }
        //压缩示例codec.createOutputStream
        CompressionOutputStream outStream=null;
        Path op2=new Path("2.gz");
        try{
            in=fs.open(new Path(outputUri));        //打开原始文件
            GzipCodec gzipCodec=new GzipCodec();    //创建gzip压缩实例
            gzipCodec.setConf(conf);                //给CompressionCodec设置Configuration
            outStream=gzipCodec.createOutputStream(fs.create(op2));        //打开输出文件(最终的压缩文件)
            IOUtils.copyBytes(in, outStream, 4096,false);        //从输入流向输出流拷贝,GzipCodec负责对输出流进行压缩
        }finally{
            IOUtils.closeStream(in);
            IOUtils.closeStream(out);
        }
        
    }
}

注意文件经过压缩之后再送给MapReduce时显然就不能再split了,还好gzip和LZO格式的文件(hadoop通过查看文件名后缀)默认就是不支持split的,而bzip2可以被split,ZIP文件则不能作为MapReduce的输入文件。

可以让Reduce的输出文件被压缩。

conf.setBoolean("mapred.output.compress", true);    //支持输出被压缩
conf.setClass("mapred.output.compression.codec", GzipCodec.class,CompressionCodec.class);    //指定采用的压缩算法

让Map的输出被压缩:

conf.setCompressMapOutput(true);
conf.setMapOutputCompressorClass(GzipCodec.class);

Java中的基本数据类型在Hadoop中都有对应的Writable类型,Hadoop中还有几中Writable Collections。ArrayWriatble和TwoDArrayWriatble分别是适用于一维数组和二维数组的Wriatble类型,当然数组中元素必须是相同的类型。

ArrayWritable writable = new ArrayWritable(Text.class);

但是ArrayWritable还是不要用了,Hadoop中没有实现ArrayWritable的空构造函数,而作为实现了Writable的类,是必须提供空参数构造函数的----在调用readFields(DataInput in)之前要先调用空构造函数来建立实例,所以每当我使用ArrayWritable作为Mapper的输出类型时就会报错。

MapWriatble实现了java.util.Map<Wriatble,Writable>接口;而SortedMapWritable则实现了java.util.Map<WriatbleComparable,Writable>接口。

MapWritable src = new MapWritable();
src.put(new IntWritable(1), new Text("cat"));
src.put(new VIntWritable(2), new LongWritable(163));

用户自定义Writable类型

package basic;

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

import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.WritableComparable;

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);
    }
}

SequenceFile

HDFS和MapReduce都是为处理大文件设计的,所以把诸多小文件打包在一个SequenceFile中着实是一个提高效率的好方法。

写入SequenceFile的key或value数据类型没有必要是Writable类型,只要是可以被Serialization序列化和反序列化的类型就可以了。

用SequenceFile.createWriter()创建SequenceFile.Writer,把一个文本文件写入一个二进制文件。代码如下

package basic;

import java.io.IOException;
import java.net.URI;

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;

public class SequenceFileWriteDemo {
    private static final String[] DATA = { "One, two, buckle my shoe",
            "Three, four, shut the door", "Five, six, pick up sticks",
            "Seven, eight, lay them straight", "Nine, ten, a big fat hen" };

    public static void main(String[] args) throws IOException {
        String uri = args[0];
        Configuration conf = new Configuration();
        FileSystem fs = FileSystem.get(URI.create(uri), conf);
        Path path = new Path(uri);
        IntWritable key = new IntWritable();
        Text value = new Text();
        SequenceFile.Writer writer = null;
        try {
            writer = SequenceFile.createWriter(fs, conf, path, key.getClass(),
                    value.getClass());

            for (int i = 0; i < 100; i++) {
                key.set(100 - i);
                value.set(DATA[i % DATA.length]);
                System.out.printf("[%s]\t%s\t%s\n", writer.getLength(), key,
                        value);
                writer.append(key, value);    //采用append方式
            }
        } finally {
            IOUtils.closeStream(writer);
        }
    }
}

可以想像上面代码中的DATA是从小文件中读取的,我们从多个小文件中读取文本,转换字节流append到一个大的二进制文件中,既实现了多个小文件的合并,也实现了压缩(二进制文件比文本文件小)。

下面演示读取一个SequenceFile

package basic;

import java.io.IOException;
import java.net.URI;

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.SequenceFile;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.util.ReflectionUtils;

public class SequenceFileReadDemo {
    public static void main(String[] args) throws IOException {
        String uri = args[0];
        Configuration conf = new Configuration();
        FileSystem fs = FileSystem.get(URI.create(uri), conf);
        Path path = new Path(uri);

        SequenceFile.Reader reader = null;
        try {
            reader = new SequenceFile.Reader(fs, 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() ? "*" : "";    //SequenceFile中都有sync标记
                System.out.printf("[%s%s]\t%s\t%s\n", position, syncSeen, key,
                        value);
                position = reader.getPosition(); //下一条record开始的位置
            }
        } finally {
            IOUtils.closeStream(reader);
        }
    }
}

SequenceFile.Writer会在流中写入sync pointer,由于某些原因,SequenceFile.Reader可能会找不到record的边界,sync pointer就是用来标记record边界的,但它并不是在每个record的后面都做一个标记,而隔几个做一个。sync pointer不会超过文件长度的1%。

SequenceFile同样支持随机读写:reader.seek(360);

而reader.sync(360)则是定位到360之后的第1个sync pointer处。

在命令行显示一个SequenceFile的内容不能用cat,而要用-text(表示要用文本的形式显示二进制文件)

$ hadoop fs -text myfile.seq

SequenceFile文件的最开始是Header部分,其中包含了record的key-value的数据类型、sync标记所采用的字符、有关压缩的细节、用户自定义的metadata。SequenceFile内置的压缩方式有两种:RecordCompression和Block Compression。

SequenceFile的文件结构:

二进制文件具有清晰严谨的文件结构,读写速度自然要比文本文件快,SequenceFile更容易和Hadoop的基本数据类型(IntWritable,FloatWriatble等)进行交互。

下面的代码展示如何把一个文本文件转换为SequenceFile,job.setOutputFormatClass(SequenceFileOutputFormat.class);就可以了。

 1 package basic;
 2 
 3 import org.apache.hadoop.conf.Configuration;
 4 import org.apache.hadoop.conf.Configured;
 5 import org.apache.hadoop.fs.FileSystem;
 6 import org.apache.hadoop.fs.Path;
 7 import org.apache.hadoop.io.LongWritable;
 8 import org.apache.hadoop.io.SequenceFile.CompressionType;
 9 import org.apache.hadoop.io.Text;
10 import org.apache.hadoop.io.compress.GzipCodec;
11 import org.apache.hadoop.mapreduce.Job;
12 import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
13 import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
14 import org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat;
15 import org.apache.hadoop.util.Tool;
16 import org.apache.hadoop.util.ToolRunner;
17 
18 public class ToSeqFile extends Configured implements Tool {
19 
20     @Override
21     public int run(String[] arg0) throws Exception {
22         Job job = new Job();
23         job.setJarByClass(getClass());
24         Configuration conf=getConf();
25         FileSystem fs = FileSystem.get(conf);
26     
27         FileInputFormat.setInputPaths(job, "/user/orisun/input/rdata");
28         Path outDir=new Path("/user/orisun/output");
29         fs.delete(outDir,true);
30         FileOutputFormat.setOutputPath(job, outDir);
31         
32         job.setNumReduceTasks(0);
33         job.setOutputKeyClass(LongWritable.class);
34         job.setOutputValueClass(Text.class);
35         //设置OutputFormat为SequenceFileOutputFormat
36         job.setOutputFormatClass(SequenceFileOutputFormat.class);
37         //允许压缩
38         SequenceFileOutputFormat.setCompressOutput(job, true);
39         //压缩算法为gzip
40         SequenceFileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);
41         //压缩模式为BLOCK
42         SequenceFileOutputFormat.setOutputCompressionType(job, CompressionType.BLOCK);
43 
44 
45         return job.waitForCompletion(true)?0:1;
46     }
47 
48     public static void main(String[] args) throws Exception {
49         int res = ToolRunner.run(new Configuration(), new ToSeqFile(), args);
50         System.exit(res);
51     }
52 }

代码中没有设置FileInputFormat,所以采用默认的TextInputFormat。代码中没有设置MapperClass,默认情况下map把从InputFormat中得到的key-value原样输出(新版本的Hadoop中已经没有了IndentityMapper这个类,所以我没有说“默认的Mapper就是IndentityMapper”)。

 MapFile

MapFile是排序后的SequenceFile,并且它会额外生成一个index文件提供按key的查找。

读写MapFile与读写SequenceFile非常类似,只需要换成MapFie.Reader和MapFile.Writer就可以了。在命令行显示MapFile的文件内容同样要用-text。

与SequenceFile不同的是:由于MapFile需要按key排序,所以它的key必须是WritableComparable类型的。

MapFile会生成2个文件,一个名为data,一个名为index。data中的内容就是按key排序后的SequenceFile中的内容

$ hadoop fs -text numbers.map/data | head

1  One, two, buckle my shoe
2  Three, four, shut the door
3  Five, six, pick up sticks
4  Seven, eight, lay them straight
5  Nine, ten, a big fat hen
6  One, two, buckle my shoe
7  Three, four, shut the door

$ hadoop fs -text numbers.map/index
1  128
129  6079
257  12054
385  18030
第1列就是data文件中的key值,第2列是key在data文件中的offset。我们看到并不是所有的key都记录在了index文件中,而是隔128个才记录一个(这个间隔可以在io.map.index.interval属性中设置,或直接在代码中通过MapFile.Writer实例的setIndexInterval()函数来设置)。

下面我们来看一下index文件是怎么发挥作用的。

data文件中存放的每条record都是一个key-value对,我们可以根据下面的函数由key值获取value。

public Writable get(WritableComparable key, Writable val) throws IOException

如果指定的key不存在,则不会给value赋值。

假如我们要检索的key值是300。首先MapFile.Reader把index文件读入内存,在index文件中进行二分查找,找到等于或第一个小于300的key-value对:257--12054,然后在data文件中从12054的位置开始查找,直到找到300或第一个大于300的key。可见整个查找的过程需要在内存中做一次二分查找,然后作一次文件扫描,且扫描文件的行数不会超过128行(如果io.map.index.interval属性值为128话)。

当index文件很大时,全部读到内存中也是不现实的。当然你会说我们可以调大io.map.index.interval的值,但那样的话需要重新生成MapFile。在index文件已经生成的情况下我们可以设置io.map.index.skip的值,设为1就表示index中的记录每隔一行才被载入内存。

使用MapFile.fix把一个SequenceFile转换成MapFile:

 1 package basic;
 2 
 3 import java.net.URI;
 4 
 5 import org.apache.hadoop.conf.Configuration;
 6 import org.apache.hadoop.fs.FileSystem;
 7 import org.apache.hadoop.fs.Path;
 8 import org.apache.hadoop.io.MapFile;
 9 import org.apache.hadoop.io.SequenceFile;
10 
11 public class MapFileFixer {
12     @SuppressWarnings({ "rawtypes", "unchecked" })
13     public static void main(String[] args) throws Exception {
14     String mapUri = args[0];
15     Configuration conf = new Configuration();
16     FileSystem fs = FileSystem.get(URI.create(mapUri), conf);        //MapFile(由data和index组成的目录)也可以自成一个FileSystem
17     Path map = new Path(mapUri);
18     Path mapData = new Path(map, MapFile.DATA_FILE_NAME);
19     //通过SequenceFile.Reader来获取SequenceFile的key和value类型
20     SequenceFile.Reader reader = new SequenceFile.Reader(fs, mapData, conf);
21     Class keyClass = reader.getKeyClass();
22     Class valueClass = reader.getValueClass();
23     reader.close();
24 
25     //使用MapFile.fix把一个SequenceFile转换成MapFile
26     long entries = MapFile.fix(fs, map, keyClass, valueClass, false, conf);
27     System.out.printf("Created MapFile %s with %d entries\n", map, entries);
28 
29     }
30 }

程序开始前先新建一个目录假如取名为map

$ hadoop fs -mkdir map

把一个sequencefile放到map目录下,并重命名为data(注意必须重命名为data)

$ hadoop fs -put myseqfile map/data

最后运行程序

$ hadoop jar dm.jar basic.MapFileFixer map

结果:data文件中的record按key值重新排序,并在map目录下生成了index文件。

posted @ 2012-08-24 10:13  高性能golang  阅读(4701)  评论(0编辑  收藏  举报