Hadoop I/O详解

Hadoop I/O详解

1、数据完整性

  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上的所有数据块

5.HDFS存储着每个数据块的副本(replica),可通过复制完好的数据副本来修复损坏的数据块,进而得到一个新的、完好无损的副本。

  • 客户端在读取数据块时,如果监测到错误,就向namenode报告已损坏的数据块及其正在尝试操作的这个datanode,最后才抛出CheckSumException异常。
  • Namenode将这个已损坏的数据块的副本标记为已损坏,之后它安排这个数据块的一个副本复制到另一个datanode,这样数据块的副本因子又回到期望水平
  • 已损坏的数据块副本便被删除

6.在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类

2、压缩

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

不同的压缩工具具有不同的压缩特性。所有的压缩算法都需要权衡空间/时间,压缩和解压缩哪种速度更快,其代价是降低了压缩的效率(效率越高文件压缩后越小)

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

codec 实现了一种压缩·解压缩算法。在Hadoop中,一个对CompressionCodec接口的实现代表一个codec。例如,GzipCodec包装了gzip的压缩和解压缩算法。 Hadoop实现的codec如下
在这里插入图片描述
通过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);
		
	}
}
  • 解压缩- 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);
		}
	}
}

压缩和输入分片

  • 在考虑如何压缩将由MapReduce处理的数据时,理解这些压缩格式是否支持切分(splitting)是非常重要的。
    在这里插入图片描述
    应该使用哪种压缩格式?
    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);

3、序列化(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 类

  • Hadoop自带的org.apache.Hadoop.io包中有广泛的Writable类可供选择

在这里插入图片描述
Java基本类型Writable封装器

  • Writable类对Java基本类型提供封装,short和char除外(两者可以存储在IntWritable中)。所有的封装包含get()和set()两个方法用于读取或设置封装的值。

在这里插入图片描述
Writable类-Text类型(常用类似Java中String)

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

}

结果如下:
在这里插入图片描述
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
    如:与具体的类耦合,应用比较复杂

4、基于文件的数据结构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);
        }
    }
}

运行结果如下:(部分结果)

在这里插入图片描述

  • 顺序文件中存储的键是从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);
        }
    }
}

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值