关闭

java实现deflate算法

717人阅读 评论(0) 收藏 举报
分类:
转自:http://www.weixingon.com/s/java+deflate算法
想想一下,当你需要处理500TB的数据的时候,你最先要做的是存储下来。你是选择源文件存储呢?还是处理压缩再存储?很显然,压缩编码处理是必须的。一段刚刚捕获的60分钟原始视屏可能达到2G,经过压缩处理可以减至500MB左右,一张单反照片可能有5MB,经过压缩之后只有400KB,而质量不会发生明显的损失。

hadoop面临的情况也是一样的,大量的数据需要存储在磁盘或者内存中,进行压缩是一种经济的方法。对数据文件进行压缩,可以有效减少存储文件所需的空间,并加快数据在网络上或者到磁盘上的传输速度。在Hadoop中,压缩应用于文件存储、Map阶段到Reduce阶段的数据交换(需要打开相关的选项)等情景。

数据压缩的方式非常多,不同特点的数据有不同的数据压缩方式:如对声音和图像等特殊数据的压缩,就可以采用有损的压缩方法,允许压缩过程中损失一定的信息,换取比较大的压缩比;而对音乐数据的压缩,由于数据有自己比较特殊的编码方式,因此也可以采用一些针对这些特殊编码的专用数据压缩算法。

hadoop使用的压缩工具主要有:

压缩格式工具算法扩展名多文件可分割性DEFLATE无DEFLATE.deflate不不GZIPgzipDEFLATE.gzp不不ZIPzipDEFLATE.zip是是,在文件范围内BZIP2bzip2BZIP2.bz2不是LZOlzopLZO.lzo不是

hadoop使用的编码器:

考察压缩工具的性能,主要从时间和空间考虑,一个压缩工具,压缩同样的文件,花费的时间越短,空间越少,压缩率越大,我们就会越喜欢他。另外还要考虑的就是可不可以分割文件,在hadoop中不能够分割文件是个不好的消息。因为hadoop处理数据进行计算的时候,需要将大量的大文件拆分,分割就很重要了。

哪个性能好呢?(下面表格数据引自IBM)

压缩算法原始文件大小压缩文件大小压缩速度解压速度gzip8.3GB1.8GB17.5MB/s58MB/sbzip28.3GB1.1GB2.4MB/s9.5MB/sLZO-bset8.3GB2GB4MB/s60.6MB/sLZO8.3GB2.9GB49.3MB/s74.6MB/s

这还不是全部,hadoop通过压缩流,也就是将文件写进压缩流里面进行数据读写,性能如何呢?

以下引自xuxm2007。

CompressionCodec对流进行压缩和解压缩

CompressionCodec有两个方法可以用于轻松地压缩或解压缩数据。要想对正在被写入一个输出流的数据进行压缩,我们可以使用createOutputStream(OutputStreamout)方法创建一个CompressionOutputStream(未压缩的数据将被写到此),将其以压缩格式写入底层的流。相反,要想对从输入流读取而来的数据进行解压缩,则调用createInputStream(InputStreamin)函数,从而获得一个CompressionInputStream,,从而从底层的流读取未压缩的数据。CompressionOutputStream和CompressionInputStream类似干java.util.zip.DeflaterOutputStream和java.util.zip.DeflaterInputStream,前两者还可以提供重置其底层压缩和解压缩功能,当把数据流中的section压缩为单独的块时,这比较重要。比如SequenceFile。

下例中说明了如何使用API来压缩从标谁输入读取的数据及如何将它写到标准输出:

publicclassStreamCompressor

{

publicstaticvoidmain(String[]args)throwsException

{

StringcodecClassname=args[0];

Class<?>codecClass=Class.forName(codecClassname);//通过名称找对应的编码/解码器

Configurationconf=newConfiguration;

CompressionCodeccodec=(CompressionCodec)ReflectionUtils.newInstance(codecClass,conf);

//通过编码/解码器创建对应的输出流

CompressionOutputStreamout=codec.createOutputStream(System.out);

//压缩

IOUtils.copyBytes(System.in,out,4096,false);

out.finish;

}

}

用CompressionCodecFactory方法来推断CompressionCodecs

在阅读一个压缩文件时,我们通常可以从其扩展名来推断出它的编码/解码器。以.gz结尾的文件可以用GzipCodec来阅读,如此类推。每个压缩格式的扩展名如第一个表格;

CompressionCodecFactory提供了getCodec方法,从而将文件扩展名映射到相应的CompressionCodec。此方法接受一个Path对象。下面的例子显示了一个应用程序,此程序便使用这个功能来解压缩文件。

publicclassFileDecompressor{

publicstaticvoidmain(String[]args)throwsException{

Stringuri=args[0];

Configurationconf=newConfiguration;

FileSystemfs=FileSystem.get(URI.create(uri),conf);

PathinputPath=newPath(uri);

CompressionCodecFactoryfactory=newCompressionCodecFactory(conf);

CompressionCodeccodec=factory.getCodec(inputPath);

if(codec==null){

System.err.println("Nocodecfoundfor"+uri);

System.exit(1);

}

StringoutputUri=

CompressionCodecFactory.removeSuffix(uri,codec.getDefaultExtension);

InputStreamin=null;

OutputStreamout=null;

try{

in=codec.createInputStream(fs.open(inputPath));

out=fs.create(newPath(outputUri));

IOUtils.copyBytes(in,out,conf);

}finally{

IOUtils.closeStream(in);

IOUtils.closeStream(out);

编码/解码器一旦找到,就会被用来去掉文件名后缀生成输出文件名(通过CompressionCodecFactory的静态方法removeSuffix()来实现)。这样,如下调用程序便把一个名为file.gz的文件解压缩为file文件:

%hadoopFileDecompressorfile.gz

CompressionCodecFactory从io.compression.codecs配置属性定义的列表中找到编码/解码器。默认情况下,这个列表列出了Hadoop提供的所有编码/解码器(见表4-3),如果你有一个希望要注册的编码/解码器(如外部托管的LZO编码/解码器)你可以改变这个列表。每个编码/解码器知道它的默认文件扩展名,从而使CompressionCodecFactory可以通过搜索这个列表来找到一个给定的扩展名相匹配的编码/解码器(如果有的话)。

属性名类型默认值描述io.compression.codecs逗号分隔的类名

org.apache.hadoop.io.compress.DefaultCodec,

org.apache.hadoop.io.compress.GzipCodec,

org.apache.hadoop.io.compress.Bzip2Codec

用于压缩/解压的CompressionCodec列表

本地库

考虑到性能,最好使用一个本地库(nativelibrary)来压缩和解压。例如,在一个测试中,使用本地gzip压缩库减少了解压时间50%,压缩时间大约减少了10%(与内置的Java实现相比较)。表4-4展示了Java和本地提供的每个压缩格式的实现。井不是所有的格式都有本地实现(例如bzip2压缩),而另一些则仅有本地实现(例如LZO)。

压缩格式Java实现本地实现DEFLATE是是gzip是是bzip2是否LZO否是

Hadoop带有预置的32位和64位Linux的本地压缩库,位于库/本地目录。对于其他平台,需要自己编译库,具体请参见Hadoop的维基百科http://wiki.apache.org/hadoop/NativeHadoop。

本地库通过Java系统属性java.library.path来使用。Hadoop的脚本在bin目录中已经设置好这个属性,但如果不使用该脚本,则需要在应用中设置属性。

默认情况下,Hadoop会在它运行的平台上查找本地库,如果发现就自动加载。这意味着不必更改任何配置设置就可以使用本地库。在某些情况下,可能希望禁用本地库,比如在调试压缩相关问题的时候。为此,将属性hadoop.native.lib设置为false,即可确保内置的Java等同内置实现被使用(如果它们可用的话)。

CodecPool(压缩解码池)

如果要用本地库在应用中大量执行压缩解压任务,可以考虑使用CodecPool,从而重用压缩程序和解压缩程序,节约创建这些对象的开销。

下例所用的API只创建了一个很简单的压缩程序,因此不必使用这个池。此应用程序使用一个压缩池程序来压缩从标准输入读入然后将其写入标准愉出的数据:

publicclassPooledStreamCompressor

Class<?>codecClass=Class.forName(codecClassname);

Configurationconf=newConfiguration;

CompressionCodeccodec=(CompressionCodec)ReflectionUtils.newInstance(codecClass,conf);

Compressorcompressor=null;

try{

compressor=CodecPool.getCompressor(codec);//从缓冲池中为指定的CompressionCodec检索到一个Compressor实例

CompressionOutputStreamout=codec.createOutputStream(System.out,compressor);

}finally

{

CodecPool.returnCompressor(compressor);

我们从缓冲池中为指定的CompressionCodec检索到一个Compressor实例,codec的重载方法createOutputStream中使用的便是它。通过使用finally块,我们便可确保此压缩程序会被返回缓冲池,即使在复制数据流之间的字节期间抛出了一个IOException。

压缩和输入分割

在考虑如何压缩那些将由MapReduce处理的数据时,考虑压缩格式是否支持分割是很重要的。考虑存储在HDFS中的未压缩的文件,其大小为1GB.HDFS块的大小为64MB,所以文件将被存储为16块,将此文件用作输入的MapReduce会创建16个输入分片(split,也称为"分块"),每个分片都被作为一个独立map任务的输入单独进行处理。

现在假设,该文件是一个gzip格式的压缩文件,压缩后的大小为1GB。和前面一样,HDFS将此文件存储为16块。然而,针对每一块创建一个分块是没有用的因为不可能从gzip数据流中的任意点开始读取,map任务也不可能独立于其分块只读取一个分块中的数据。gZlp格式使用DEFLATE来存储压缩过的数据,DEFLATE将数据作为一系列压缩过的块进行存储。问题是,每块的开始没有指定用户在数据流中任意点定位到下一个块的起始位置,而是其自身与数据流同步。因此,gzip不支持分割(块)机制。在这种情况下,MapReduce不分割gzip格式的文件,因为它知道输入的是gzip格式(通过文件扩展名得知),而gzip压缩机制不支持分割机制。这样是以牺牲本地化为代价:一个map任务将处理16个HDFS块,大都不是map的本地数据。与此同时,因为map任务少,所以作业分割的粒度不够细,从而导致运行时间变长。

在我们假设的例子中,如果是一个LZO格式的文件,我们会碰到同样的问题,因为基本压缩格式不为reader提供方法使其与流同步。但是,bzip2格式的压缩文件确实提供了块与块之间的同步标记(一个48位的π近似值)因此它支持分割机制。对于文件的收集,这些问题会稍有不同。ZIP是存档格式,因此t可以将多个文件合并为一个ZIP文件。每个文单独压缩,所有文档的存储位置存储在ZIP文件的尾部。这个属性表明ZlP文件支持文件边界处分割,每个分片中包括ZIP压缩文件中的一个或多个文件。

ZIP格式文件的结构如下图:ZIP文件结构请查看wiki

在MapReduce中使用压缩

如前所述,如果输入的文件是压缩过的.那么在被MapReduce读取时,它们会被自动解压,根据文件扩展名来决定应该使用哪一个压缩解码器。如果要压缩MapReduce作业的输出.请在作业配置文件中将mapred.output.compress属性设置为true,将mapred.output.compression.codec属性设置为自己打算使用的压缩编码/解码器的类名。

publicclassMaxTemperatureWithCompression{

publicstaticvoidmain(String[]args)throwsException{

if(args.length!=2){

System.err.println("Usage:MaxTemperatureWithCompression<inputpath>"

+"<outputpath>");

System.exit(-1);

}

Jobjob=newJob;

job.setJarByClass(MaxTemperature.class);

FileInputFormat.addInputPath(job,newPath(args[0]));

FileOutputFormat.setOutputPath(job,newPath(args[1]));

job.setOutputKeyClass(Text.class);

job.setOutputValueClass(IntWritable.class);

FileOutputFormat.setCompressOutput(job,true);

FileOutputFormat.setOutputCompressorClass(job,GzipCodec.class);

job.setMapperClass(MaxTemperatureMapper.class);

job.setCombinerClass(MaxTemperatureReducer.class);

job.setReducerClass(MaxTemperatureReducer.class);

System.exit(job.waitForCompletion(true)?0:1);

我们使用压缩过的输入来运行此应用程序(其实不必像它一样使用和输入相同的格式压缩输出),如下所示:

%hadoopMaxTemperatureWithCompressioninput/ncdc/sample.txt.gz

output

量生终输出的每部分都是压缩过的.在本例中,只有一部分:

%gunzip-coutput/part-OOOOO.gz

1949111

195022

如果为输出使用了一系列文件,可以设置mapred.output.compresson.type属性来控制压缩类型。默认为RECORD,它压缩单独的记录。将它改为BLOCK,可以压缩一组记录,由于有更好的压缩比,所以推荐使用。

map作业输出结果的压缩

即使MapReduce应用使用非压缩的数据来读取和写入,我们也可以受益于压缩map阶段的中阔输出。因为map作业的输出会被写入磁盘并通过网络传输到reducer节点,所以如果使用LZO之类的快速压缩,能得到更好的性能,因为传输的数据量大大减少了.表4-5显示了启用map输出压缩和设置压缩格式的配置属性.

下面几行代码用于在map作业中启用gzip格式来压缩输出结果:

Configurationconf=newConfiguration;

conf.setBoolean("mapred.compress.map.output",true);

conf.setClass("mapred.map.output.compression.codec",GzipCodec.class,

CompressionCodec.class);

Jobjob=newJob(conf);

旧的API要这样配置

conf.setCompressMapOutput(true);

conf.setMapOutputCompressorClass(GzipCodec.class);

压缩就到此为止了。总之编码和解码在hadoop有着关键的作用。

Charles于P.P

版权说明:

本文由CharlesDong原创,本人支持开源以及免费有益的传播,反对商业化谋利。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:215682次
    • 积分:3994
    • 等级:
    • 排名:第7834名
    • 原创:89篇
    • 转载:548篇
    • 译文:13篇
    • 评论:7条
    最新评论