Hadoop I/O详解
1、数据完整性
- Hadoop用户肯定都希望系统在存储和处理数据时,数据不会有任何丢失或损坏。但是,尽管磁盘或网络上的每个I/O操作不太可能将错误引入自己正在读写的数据,但是,如果系统需要处理的数据量大到Hadoop能够处理的极限,数据被损坏的概率还是很高的。
- 检测数据是否损坏的常见措施是,在数据第一次引入系统时计算校验和(checksum),并在数据通过一个不可靠的通道进行传输时再次计算校验和,这样就能发现数据是否损坏。如果计算所得的新校验和和原来的校验和不匹配,就认为数据已损坏。但该技术并不能修复数据——它只能检测出数据错误。(这正是不使用低端硬件的原因。具体说来,一定要使用ECC内存。)注意,校验和也是可能损坏的,不只是数据,但由于校验和比数据小得多,所以损坏的可能性非常小。
- 常用的错误检测码是CRC-32(循环冗余校验),任何大小的数据输入均计算得到一个32位的整数校验和。
HDFS的数据完整性
- HDFS会对写入的所有数据计算校验和,并在读取数据时验证校验和。
- 针对由每个 io.bytes.per.checksum指定字节的数据计算校验和。默认情况下为512个字节,由于CRC-32校验和是4个字节,所以存储校验和的额外开销低于1%
- Datanode负责在验证收到的数据后存储数据及其校验和。
- 它在收到客户端的数据或复制期间其他datanode的数据时执行这个操作
- 正在写数据的客户端将数据及其校验和发送到由一系列datanode组成的管线,管线中最 后一个datanode负责验证校验和。
- 如果datanode监测到错误,客户端便会收到一个CheckSumException异常。
- 客户端从datanode中读取数据时,也会验证校验和,将他们与datanode中存储的校验和进行比较。
- 每个datanode均持久保存有一个用于验证的校验和日志,所以它知道每个数据块的最后一次验证时间
- 客户端成功验证一个数据块后,会告诉这个datanode,datanode由此更新日志
- 每个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的解压速度比压缩速度快,但仍比其他压缩格式更慢一些。LZO
,LZ4
和Snappy
均优化压缩速度,其速度比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);
}
}
}
部分运行结果如下: