Hadoop(四) I/O操作

Hadoop I/O操作

主要内容

  1. 数据完整性
  2. 压缩
  3. 序列化
  4. 基于文件的数据结构SequenceFile

一. 数据完整性

  1. Hadoop用户肯定都希望系统在存储和处理数据时,数据不会有任何丢失或损坏。但是,尽管磁盘或网络上的每个I/O操作不太可能将错误引入自己正在读写的数据,但是,如果系统需要处理的数据量大到Hadoop能够处理的极限,数据被损坏的概率还是很高的。
  2. 检测数据是否损坏的常见措施是,在数据第一次引入系统时计算校验和(checksum),并在数据通过一个不可靠的通道进行传输时再次计算校验和,这样就能发现数据是否损坏。如果计算所得的新校验和和原来的校验和不匹配,就认为数据已损坏。但该技术并不能修复数据——它只能检测出数据错误。(这正是不使用低端硬件的原因。具体说来,一定要使用ECC内存。)注意,校验和也是可能损坏的,不只是数据,但由于校验和比数据小得多,所以损坏的可能性非常小。
  3. 常用的错误检测码是CRC-32(循环冗余校验),任何大小的数据输入均计算得到一个32位的整数校验和。
HDFS的数据完整性
  1. HDFS会对写入的所有数据计算校验和,并在读取数据时验证校验和。
  • 针对由每个 io.bytes.per.checksum指定字节的数据计算校验和。默认情况下为512个字节,由于CRC-32校验和是4个字节,所以存储校验和的额外开销低于1%
  1. Datanode负责在验证收到的数据后存储数据及其校验和。
  • 它在收到客户端的数据或复制期间其他datanode的数据时执行这个操作
  • 正在写数据的客户端将数据及其校验和发送到由一系列datanode组成的管线,管线中最 后一个datanode负责验证校验和。
  • 如果datanode监测到错误,客户端便会收到一个CheckSumException异常。
  1. 客户端从datanode中读取数据时,也会验证校验和,将他们与datanode中存储的校验和进行比较。
  • 每个datanode均持久保存有一个用于验证的校验和日志,所以它知道每个数据块的最后一次验证时间
  • 客户端成功验证一个数据块后,会告诉这个datanode,datanode由此更新日志
  1. 每个datanode也会有一个后台线程中运行一个DataBlockScanner。
  • 定期验证存储在这个datanode上的所有数据块
  1. HDFS存储着每个数据块的副本(replica),可通过复制完好的数据副本来修复损坏的数据块,进而得到一个新的、完好无损的副本。
  • 客户端在读取数据块时,如果监测到错误,就向namenode报告已损坏的数据块及其正在尝试操作的这个datanode,最后才抛出CheckSumException异常。
  • Namenode将这个已损坏的数据块的副本标记为已损坏,之后它安排这个数据块的一个副本复制到另一个datanode,这样数据块的副本因子又回到期望水平
  • 已损坏的数据块副本便被删除
  1. 在FileSystem的open()之前通过设置FileSystem的setVerifyCheckSum(false)方法禁用校验和,或者命令行使用get时候添加选项-ignoreCrc或者直接使用-copyToLocal。
  • fs.setVerifyChecksum(false) fs.open(new Path("")) // 就不进行校验检查了
  • Hadoop fs –get –ignoreCrc hdfs://master:9000/a.txt
  • Hadoop fs –copyToLocal hdfs://master:9000/a.txt
LocalFileSystem&ChecksumFileSystem
  • 对于本地文件系统LocalFileSystem在写入一个filename文件时候会创建一个.filename.crc存放校验和。就像HDFS -样,文件块的大小由属性io.bytes.per.checksum控制,默认为512个字节。文件块的大小作为元数据存储在.crc文件中,所以即使文件块大小的设置已经发生变化,仍然可以正确读回文件。在读取文件时需要验证校验和,并且如果检测到错误,LocalFileSystem将抛出一个ChecksumException异常。
  • LocalFileSystem通过ChecksumFileSystem来完成自己的任务,有了这个类,向其他文件系统(无校验和系统)加入校验和就非常简单,因为ChecksumFileSystem类继承自FileSystem类

二 .压缩

  • 减少文件存储所需要的磁盘空间
  • 可以加速数据在网络和磁盘上的传输
  • 有很多种压缩格式、工具和算法,各有千秋
    Hadoop(四) I/O操作_第1张图片
    所有的压缩算法都需要权衡空间/时间:压缩和解压缩速度更快,其代价通常是只能节省少量的空间。以上图的压缩工具都提供9个不同的选项来控制压缩时必须考虑的权衡:选项 -1 为优化压缩速度, -9 为优化压缩空间。
    例如,下述命令通过最快的压缩方法创建一个名为file.gz的压缩文件。
	%gzip -1 file

不同的压缩工具具有不同的压缩特性。

  • gzip是一个通用的压缩工具,在空间/时间性能的权衡中,居于其他两个压缩方法之间。
  • bzip2 的压缩能力强于gzip,但压缩速度更慢一点。尽管bzip2的解压速度比压缩速度快,但仍比其他压缩格式更慢一些。
  • LZO,LZ4和Snappy均优化压缩速度,其速度比gzip快一个数量级,但压缩效率稍逊一筹。Snappy和LZ4的解压缩速度比LZO高出很多。

codec 实现了一种压缩·解压缩算法。在Hadoop中,一个对CompressionCodec接口的实现代表一个codec。例如,GzipCodec包装了gzip的压缩和解压缩算法。 Hadoop实现的codec如下
Hadoop(四) I/O操作_第2张图片

通过CompressionCodec对数据流进行压缩和解压缩

CompressionCodec包含两个函数,用于压缩和解压缩数据

  • createOutputStream(OutputStream out)

对写入输出流的数据进行压缩,用该方法在底层的数据流中对需要以压缩格式写入一个CompressionOutput在此之前尚未压缩的数据新建Stream对象。

  • createInputStream(InputStream in)

对输入数据流中读取的数据进行解压缩的时候,则调用获取CompressionInputStream,可通过该方法从底层数据流读取解压缩后的数据。

  • 压缩- GzipCompress 案例
public class GzipCompress {
	public static void main(String[] args) throws ClassNotFoundException, IOException {
		String codecClassName = "org.apache.hadoop.io.compress.GzipCodec";
		String localUrl = "/home/username/sogou500w";
		String hdfsUrl = "hdfs://master1:9000/sogou500w.gz";
		
		
		Class<?> codecClass = Class.forName(codecClassName);
		Configuration conf = new Configuration();
		CompressionCodec codec = (CompressionCodec) ReflectionUtils.newInstance(codecClass, conf);
		
		FileSystem fs = FileSystem.get(URI.create(hdfsUrl), conf);
		InputStream in = new BufferedInputStream(new FileInputStream(localUrl));
		OutputStream out = fs.create(new Path(hdfsUrl));
		CompressionOutputStream compressOut = codec.createOutputStream(out);
		System.out.println(compressOut);
		
		IOUtils.copyBytes(in, compressOut, 1024,true);
		
	}
}

Hadoop(四) I/O操作_第3张图片

  • 解压缩- GzipDecompress 案例
  • 通过CompressionCodecFactory推断CompressionCodec
    如果输入文件是压缩的,根据文件名推断出相应的Codec
public class GzipDecompress {
	public static void main(String[] args) throws Exception{
		String uri = args[0];
		Configuration conf = new Configuration();
		FileSystem fs = FileSystem.get(URI.create(uri), conf);
		
		Path path = new Path(uri);
		CompressionCodecFactory factory = new CompressionCodecFactory(conf);
		CompressionCodec codec = factory.getCodec(path);
		if(codec == null){
			System.out.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(path));
			out = fs.create(new Path(outputUri));
			IOUtils.copyBytes(in, out, conf);
		} finally{
			IOUtils.closeStream(in);
			IOUtils.closeStream(out);
		}
	}
}

Hadoop(四) I/O操作_第4张图片

压缩和输入分片
  • 在考虑如何压缩将由MapReduce处理的数据时,理解这些压缩格式是否支持切分(splitting)是非常重要的。
    Hadoop(四) I/O操作_第5张图片
    应该使用哪种压缩格式?
    hadoop应用处理的数据集非常大,因此需要借助于压缩。使用哪种压缩格式与待处理的文件的大小、格式和所使用的工具相关。下面有一些建议,大致是按照从高到低排列的。
  • 使用容器文件格式,例如顺序文件、Avro数据文件、ORCFiles或者Parquet文件,所有这些文件格式同时支持压缩和切分。通常最好与一个快速压缩工具联合使用,例如LZO,LZ4,或者Snappy。
  • 使用支持切分的压缩格式,例如bzip2(尽管bzip2非常慢),或者使用通过索引实现切分的压缩格式,如LZO。
  • 在应用中将文件切分成块,并使用任意一种压缩格式为每个数据块建立压缩文件(不管它是否支持切分)。这种情况下,需要合理选择数据块的大小,以确保压缩后数据块的大小近似于HDFS的大小。
  • 存储未经压缩的文件。
    对于大文件来说,不要使用不支持切分整个文件的压缩格式,因为会失去数据的本地特性,进而造成MapReduce的应用效率低下。
在MapReduce中使用压缩
  • 如果输入文件是压缩的,那么在根据文件扩展名推断出相应的codec后,MapReduce会在读取文件时自动解压缩文件

    通过CompressionCodecFactory推断CompressionCodec

  • 对MapReduce作业的输出进行压缩操作,应在作业配置过程中,将mapred.output.compress属性设为true和mapred.output.compression.codec属性设置为打算使用的压缩codec的类名

    conf.setBoolean(“mapred.output.compress”, true);
    conf.setClass (“mapred.output.compression.codec”, GzipCodec.class, CompressionCodec.class);

如以下代码:

public class WordCount {
	public static void main(String[] args)throws Exception {
		Configuration conf = new Configuration();
		Job job = new Job(conf, "WordCount");
		job.setJarByClass(WordCount.class);
		job.setMapperClass(WordMap.class);
		job.setReducerClass(WordReduce.class);
		FileInputFormat.setInputPaths(job, new Path(args[0]));
		FileOutputFormat.setOutputPath(job, new Path(args[1]));
		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(IntWritable.class);
		
		//设置MapReduce作业的输出的压缩格式
		conf.setBoolean("mapred.output.compress", true);
		conf.setClass ("mapred.output.compression.codec",  GzipCodec.class, CompressionCodec.class);
		job.waitForCompletion(true);
	}
}
在MapReduce中使用压缩
  • 如果MapReduce作业输出生成顺序文件(sequence file),可以设置mapred.output.compression.type属性来控制要使用哪种压缩格式默认值是RECORD,即针对每条纪录进行压缩。如果将其改为BLOCK,将针对一组纪录进行压缩,这是推荐的压缩策略,因为它的压缩效率更高

    SequenceFileOutputFormat.setOutputCompressionType(job, SequenceFile.CompressionType.RECORD); SequenceFileOutputFormat.setOutputCompressionType(job, SequenceFile.CompressionType.BLOCK);

  • 对map任务输出进行压缩
    尽管MapReduce被用读写的是未经压缩的数据,但如果对map阶段的中间输入进行压缩,也可以获得不少好处。由于map任务的输出需要写到磁盘并通过网络传输到reducer节点,所以如果使用LZO这样的快速压缩方式,是可以获得性能提升的,因为需要传输的数据减少了。
    下面是在作业中启用map任务输出gzip压缩格式的代码:

    conf.setBoolean(job.MAP_OUTPUT_COMPRESS, true);
    conf.setClass(job.MAP_OUTPUT_COMPRESS_CODEC, GzipCodec.class, CompressionCodec.class);

三、序列化( Serialization )

  • 所谓序列化(Serialization),是指将结构化对象转化为字节流,以便在网络上传输或写到磁盘进行永久存储

  • 反序列化(deserialization)是指将字节流转回结构化对象的逆过程

  • 序列化在分布式数据处理的两大领域经常出现

    进程间通信
    永久存储

  • Hadoop中,系统中多个节点上进程间的通信是通过“远程过程调用”(Remote Procedure Call)RPC实现的。

  • RPC协议将消息序列化成二进制流后发送到远程节点,远程节点接着将二进制流反序列化为原始消息。

  • RPC序列化格式
    数据永久存储所期望的4个RPC序列化属性非常重要

紧凑,进而高效实用存储空间
快速,进而读写数据的额外开销比较小
可扩展,进而可以透明的读取老格式的数据
互操作,进而可以使用不同的语言读写永久存储的数据

序列化 Hadoop
  • Hadoop使用自己的序列化格式Writable
    紧凑、速度快、很难用Java以外的语言进行扩展和使用
  • Writable是Hadoop的核心
    大多数mapreduce程序都会为键和值使用它
  • Avro 是克服了Writable少许局限性的序列化系统
序列化 Writable接口
  • Writable接口定义了两个方法:一个(write)将其状态写到DataOutput二进制流,
    另一个(readFields)从Datalnput二进制流读取其状态:
public interface Writable {
     	void write(DataOutput out) throws IOException;
     	void readFields(Datalnput in) throws IOException;
}
  • 让通过一个特殊的Writable类来看看它的具体用途。将使用IntWritable来封装一个Java int。可以新建一个并使用set()方法来设置它的值:
IntWritable writable=new IntWritable();
writable.set (163);
writable.get()
  • 也可以通过构造函数来新建一个整数值:
IntWritable writable=new IntWritable(163);

Writable接口-IntWritable

public class WritableTest {
    public static void main(String[] args)throws Exception {
    	//序列化的过程
		IntWritable one = new IntWritable(199);
		FileOutputStream out = new FileOutputStream("/home/zkpk/a.txt");
		DataOutput out2 = new DataOutputStream(out);
		one.write(out2); 
	
		//反序列化的过程
		IntWritable two = new IntWritable();
		FileInputStream in = new FileInputStream("/home/zkpk/a.txt");
		DataInput input = new DataInputStream(in);
		two.readFields(input); 
		System.out.println(two.get());
    }
}
public class serializable {
    public static void main(String[] args) throws IOException {
        IntWritable writable = new IntWritable(111);
        System.out.println("");
        byte[] bytes = serialize(writable);
        System.out.println("serialize length: "+bytes.length); //4,一个整数占四个字节

        Writable intWritable = deserialize(writable, bytes);
        System.out.println(writable.get());  //111
        System.out.println(intWritable);  //111
    }

    /** * 序列化 */
    public static byte[] serialize(Writable writable) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        DataOutputStream dataOutput = new DataOutputStream(out);
        writable.write(dataOutput);
        dataOutput.close();
        return out.toByteArray();
    }

    /** * 反序列化 */
    public static Writable deserialize(Writable writable ,byte[] bytes) throws IOException{
        ByteArrayInputStream in = new ByteArrayInputStream(bytes);
        DataInputStream inputStream = new DataInputStream(in);
        writable.readFields(inputStream);
        in.close();
        inputStream.close();
        return writable;
    }
}

WritableComparable和 Comparable
  • IntWritabla实现了WritableComparable接口,该接口继承自Writable和java.lang. Comparable接口:

public interface WritableComparable extends Writable,Comparable { }

  • 对MapReduce来说,类型的比较是非常重要的,因为中间有个基于键的排序阶段。
  • Hadoop提供的一个优化接口是继承自Java Comparator的RawComparator接口
public interface RawComparator<T> extends Comparator<T> {
	public int compare(byte[] bl, int s1, int l1, byte[] b2. int s2, int l2);
}
  • RawComparator接口允许其实现直接比较数据流中的记录,无须先把数据流反序列化为对象,这样便避免了新建对象的额外开销。

根据IntWritable接口实现的comparator实现了compare()方法,该方法可以从每个字节数组b1和b2中读取给定起始位置(s1和s2)以及长度(11和12)的一个整数进而直接进行比较。

  • WritableComparator是对继承自Comparator类的RawComparator接口的一个通用实现,它提供两个主要功能

第一,它提供了对原始compare()方法的一个默认实现,该方法能够反序列化将在流中进行比较的对象,并调用对象的compare()方法。

第二,它充当的是RawComparator实例的工厂

RawComparator comparator=WritableComparator.get(IntWritable. class); //工厂

这个comparator可以用于比较两个IntWritable对象比较:
IntWritable wl=new IntWritable(163);
IntWritable w2=new IntWritable(67);
System.out.println(comparator.compare(wl, w2))

序列化后直接比较:
byte[] bl = serialize(wl);
byte[] b2 = serialize(w2);
int result2 = comparator.compare(b1, 0, b1.length, b2, 0, b2.length);
System.out.println(result2);

public class RawComparatorTest {
    public static void main(String[] args) throws IOException {
        RawComparator<IntWritable> comparator = WritableComparator.get(IntWritable.class);
        IntWritable i1 = new IntWritable(31);
        IntWritable i2 = new IntWritable(3);
        int result = comparator.compare(i1, i2);
        System.out.println(result);

        byte[] b1 = serialize(i1);
        byte[] b2 = serialize(i2);
        int result2 = comparator.compare(b1, 0, b1.length, b2, 0, b2.length);
        System.out.println(result2);
    }
    public static byte[] serialize(Writable writable) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        DataOutput output = new DataOutputStream(out);
        writable.write(output);
        return out.toByteArray();
    }
}

运行结果如下:
在这里插入图片描述

Writable 类

  • Hadoop自带的org.apache.Hadoop.io包中有广泛的Writable类可供选择
    Hadoop(四) I/O操作_第6张图片
    Java基本类型Writable封装器
  • Writable类对Java基本类型提供封装,short和char除外(两者可以存储在IntWritable中)。所有的封装包含get()和set()两个方法用于读取或设置封装的值。

Hadoop(四) I/O操作_第7张图片

Writable类-Text类型
  • Text是针对UTF-8序列的Writable类。一般可以认为它等价于java.lang.String的Writable。
  • 该测试表明String和Text的不同
public class TestText {
    public static void main(String[] args) {
        Text text=new Text("hadoop");
        System.out.println(text);
        System.out.println(text.charAt(2)+"\t"+(int)'d');

        String t =new String("hadoop");
        System.out.println(t);
        System.out.println ("t.length(): "+t.length());
        System.out.println("t.getBytes().length: "+t.getBytes().length);
        System.out.println(t.charAt(2)+"\t"+(int)'d');

        System.out.println("------------");

        Text txt=new Text("object");
        System.out.println(txt);
        String txtStr =txt.toString();
        byte[] bt=txtStr.getBytes();
        for(byte b: bt){
            System.out.print(b+"\t");
        }
    }

}

结果如下:
Hadoop(四) I/O操作_第8张图片

Writable类 – NullWritable

NullWritable是Writable的一个特殊类型

  • 它的序列化长度为0
  • 它并不从数据流中读数据、也不写数据,充当占位符
  • 在MapReduce中如果不需要键和值,可以将键和值声明为NullWritable
序列化框架
  • 尽管大多数MapReduce程序使用的都是Writable类型的键和值,但这并不是MapReduce APl强制使用的。事实上,可以使用任何类型,只要能有一种机制对每个类型进行类型与二进制表示的来回转换。
  • 为了支持这一机制,Hadoop有一个针对可替换序列化框架(serialization framework)的API。一个序列化框架用一个Serialization实现(在org.apache.hadoop.io.serializer包)来表示。例如,WritableSerialization类是对Writable类型的Serialization的实现。
  • Serialization对象定义了从类型到Serializer实例(将对象转换为字节流)和Deserializer实例(将字节流转换为对象)的映射方式。
  • 将lO.serizalizations属性设置为一个由句点分隔的类名列表,即可注册Serialization实现。它的默认值是org.apache.hadoop.io.serializer.WritableSerialization,这意味着只有Writable对象才可以在外部序列化和反序列化。
  • Apache Avro(http://avro.apache.org)是一个独立于编程语言的数据序列化系统
  • 该项目由Hadoop之父Doug Cutting创建,旨在解决Hadoop中Writable类型的不足
    如:缺乏语言的可移植性
  • Java的Serializable java.io.Serializable
    如:与具体的类耦合,应用比较复杂

四.基于文件的数据结构SequenceFile

SequenceFile
  • 考虑日志文件,其中每一条日志记录是一行文本。如果想记录二进制类型,纯文本是不合适的。这种情况下,Hadoop的SequenceFile类非常合适,因为上述类提供了二进制键/值对的永久存储的数据结构。当作为日志文件的存储格式时,你可以自己选择键,比如由LongWritable类型表示的时间戳,以及值可以是Writable类型,用于表示日志记录的数量。
  • SequenceFiles同样也可以作为小文件的容器。而HDFS和MapReduce是针对大文件进行优化的,所以通过SequenceFile类型将小文件包装起来,可以获得更高效率的存储和处理。
  • SequenceFile的写操作
public class SequenceFileWriteDemo {
    public static final String[] DATA = {
            "One,two,buckle my shoe",
            "Three, four,shut the door",
            "Five,six,pick up sticks",
            "Seven,eight,lay them stright",
            "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);
            }
        } finally {
            IOUtils.closeStream(writer);
        }
    }
}

运行结果如下:(部分结果)
Hadoop(四) I/O操作_第9张图片

  • 顺序文件中存储的键是从100到1降序排列的整数。表示为IntWritable对象。值为Text对象。在将每条记录追加到SequenceFile.Writer实例末尾之前,需要使用getLength()方法来获取文件访问的当前位置。
  • 读取SequenceFile
public class SequenceFileReaderDemo {
    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);
            IntWritable key = (IntWritable) ReflectionUtils.newInstance(reader.getKeyClass(), conf);
            Text value = (Text) 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);
        }
    }
}

部分运行结果如下:
Hadoop(四) I/O操作_第10张图片

你可能感兴趣的:(hadoop)