与Hadoop结合使用的常见压缩算法。
压缩格式 | 工具 | 算法 | 扩展名 | 是否可切分 |
DEFLATE | N/A | DEFLATE | .deflate | 否 |
gzip | gzip | DEFLATE | .gz | 否 |
bzip2 | bzip2 | bzip2 | .bz2 | 是 |
LZO | lzop | LZO | .lzo | 否 |
LZ4 | N/A | LZ4 | .lz4 | 否 |
Snappy | N/A | Snappy | .snappy | 否 |
所有压缩算法都需要权衡空间/时间:压缩和解压缩速度更快,其代价通常是只能节省少量的空间。上表列出的所有压缩工具都提供9个不同的选项来控制压缩时必须考虑的权衡:选项-1为优化压缩速度,-9为优化压缩空间。例如,下述命令通过最快的压缩方法创建一个名为力/的压缩文件:
%gzip -1 file
不同压缩工具有不同的压缩特性。gzip是一个通用的压缩工具,在空间/时间性能的权衡中,居于其他两个压缩方法之间。bzip2的压缩能力强于gzip,但压缩速度更慢一点。尽管bzip2的解压速度比压缩速度快,但仍比其他压缩格式要慢一些。另一方面,LZO、LZ4和snappy均优化压缩速度,其速度比gzip快一个数量级,但压缩效率稍逊一筹。Snappy和LZ4的解压缩速度比LZO高出很多。上表中的“是否可切分"列表示对应的压缩算法是否支持切分(splitable),也就是说,是否可以搜索数据流的任意位置并进一步往下读取数据。可切分压缩格式尤其适合MapReduce。
1.codec
Codec是压缩-解压缩算法的一种实现。在Hadoop中,CompressionCodec接口的实现代表一个codec。例如,GzipCodec包装了gzip的压缩和解压缩算法。下表列举了Hadoop实现的codec。
压缩算法 | 特点 | 速度和压缩比 |
DEFLATE | 标准压缩算法,没有生成它的常用命令行工具,通常都是用Gzip格式,也就是在deflate格式上添加gzip的文件头和文件尾,.deflate扩展名是hadoop定义的 | —— |
Gzip | DEFLATE为基础扩展出来的一种算法; 通用压缩工具,时间和空间消耗比较折中 | 折中 |
Bzip2 | 压缩效果最好,但是压缩和解压速度最慢; 本身的解压速度比压缩速度快 | 压缩效果最好,速度最慢 |
LZO | 比gzip速度快,但是压缩效果不好 | 压缩效果不好, 速度快 |
LZ4 | 比gzip速度快,但是压缩效果不好; 解压比LZO快 | 压缩效果不好, 速度快 |
Snappy | 压缩效果不好, 速度快 |
通过CompressionCodec对数据流进行压缩和解压缩
CompressionCodec包含两个函数,可以轻松用于压缩和解压缩数据。如果要对写人输出数据流的数据进行压缩,可用create0utputStream(0utputStreamout)方法在底层的数据流中对需要以压缩格式写人在此之前尚未压缩的数据新建一个CompressionOutputStream对象。相反,对输人数据流中读取的数据进行解压缩的时候,则调用createInputStream(InputStreamin)获取CompressionInputStream,可以通过该方法从底层数据流读取解压缩后的数据。
CompressionOutputStream和CompressionInputStream,类似于java.util.zip.DeflaterOutputStream和java.util.zip.Def1aterInputStream,只不过前两者能够重置其底层的压缩或解压缩方法,对于某些将部分数据流(sectionof
datastream)压缩为单独数据块(block)的应用,例如SequenceFile(),这个能力是非常重要的。如下范例显示了如何用API来压缩从标准输人中读取的数据并将其写到标准输出。该程序压缩从标准输入读取的数据,然后将其写到标准输出。
public class StreamCompressor{
public static void main(String[]args)throws Exception{
String codecC1assname=args[O];
Class<?> codecClass=C1ass.forName(codecC1assname);
Configuration conf=newConfiguration();
CompressionCodec codec=(CompressionCodec)Ref1ectionUti1s.newInstance(codecClass,conf);
CompressionOutputStreamout=codec.createOutputStream(System.out);
IOUti1s.copyBytes(System.in,out,4096,false);
out.finish();
}
}
该应用希望将符合compressioncodec实现的完全合格名称作为第一个命令行参数。我们使用RefiectionUtils新建一个codec实例,并由此获得在System.out上支持压缩的一个包裹方法。然后,对I0Utils对象调用copyByte()方法将输人数据复制到输出,(输出由CompressionOutputstream对象压缩)。最后,我们对Compression0utputStream对象调用finish()方法,要求压缩方法完成到压缩数据流的写操作,但不关闭这个数据流。我们可以用下面这行命令做一个测试,通过GzipCodec的StreamCompressor对象对字符串"Text”进行压缩,然后使用gunzip从标准输人中对它进行读取并解压缩操作:
echo "Text" | hadoop StreamCompressor org.apache.hadoop.io.compress.GzipCodec | gunzip Text
通过CompressionCodecFactory推断CompressionCodec
在读取一个压缩文件时,通常可以通过文件扩展名推断需要使用哪个codec.如果文件以.gz结尾,则可以用GzipCodec来读取,如此等等。前面的第一个表为每一种压缩格式列举了文件扩展名。通过使用其getCodec()方法,CompressionCodecFactory提供了一种可以将文件扩展名映射到一个CompressionCodec的方法,该方法取文件的Path对象作为参数。如下范例所示的应用便使用这个特性来对文件进行解压縮,应用根据文件扩展名选取codec解压缩文件。
public class FileDecompressor{
public static void main(String[] args)throws Exception{
String uri=args[0];
Configuration conf=Configuration();
FileSystem fs=FileSystem.get(URI.create(uri),conf);
Path inputPath=new Path(uri);
CompressionCodecFactory factory=new CompressionCodecFactory(conf);
CompressionCodec codec=factory.getCodec(inputPath);
if(codec==null){
System.err.printIn("No Codec found for"+uri);
System.exit(1);
}
String outputUri=CompressionCodecFactory.remveSuffix(uri,codec.getDefaultExtension());
InputStream in=null;
OutputStream out=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);
}
}
一旦找到对应的codec,便去除文件扩展名形成输出文件名,这是通过CompressionCodecFactory对象的静态方法removeSuffix()来实现的。按照这种方法,一个名为file.gz的文件可以通过调用该程序解压为名为file的文件:
hadoop FileDecompressor file.gz
CompressionCodecFactory 加载第二张表中除LZO之外的所有codec,同样也加载io.compression.codecs配置属性(参见下表)列表中的所有codec.在默认情况下,该属性列表是空的,你可能只有在你拥有一个希望注册的定制codec(例如外部管理的LZO codec)才需要加以修改。每个codec都知道自己默认的文件扩展名,因此CompressionC0decFactory可通过搜索注册的codec找到匹配指定文件扩展名的 codec(如果有的话)。
压缩codec的属性:
属性名称 | 类别 | 默认值 | 描述 |
io.compression.codecs | 逗号分隔的类名 | 用于压缩/解压缩额外的CompressionCodec类的列表 |
原生类库
为了提高性能,最好使用“原生”(native)类库来实现压缩和解压缩。例如,在一个测试中,使用原生gzip类库可以减少约一半的解压缩时间和约10%的压縮时间(与内置的Java实现相比)。下表说明了每种压缩格式是否有Java实现和原生类库实现。所有的格式都有原生类库实现,但是并非所有格式都有Java实现(如LZO)。
压缩代码库的实现:
压缩格式 | 是否有Java实现 | 是否有原生实现 |
DEFLATE | 是 | 是 |
gzip | 是 | 是 |
bzip2 | 是 | 否 |
LZO | 否 | 是 |
LZ4 | 否 | 是 |
Snappy | 否 | 是 |
Apache Hadoop二进制压縮包本身包含有为64位Linux构建的原生压缩二进制代码,称为libhadoop.so。对于其他平台,需要自己根据位于源文件树最顶层的BUILDING.txt指令编译代码库。可以通过Java系统的java.library.path属性指定原生代码库。etc/hadoop文件夹中的hadoop脚本可以帮你设置该属性,但如果不用这个脚本,则需要在应用中手动设置该属性。默认情况下,Hadoop会根据自身运行的平台搜索原生代码库,如果找到相应的代码库就会自动加载。这意味着,你无需为了使用原生代码库而修改任何设置。但是,在某些情况下,例如调试一个压缩相关问题时,可能需要禁用原生代码库,将属性io.native.lib.available的值设置成false即可,这可确保使用内置的Java代码库(如果有的话)。
CodecPool
如果使用的是原生代码唪并且需要在应用中执行大量压缩和解压缩操作,可以考虑使用CodecPool,它支持反复使用压缩和解压缩,以分摊创建这些对象的开销。如下范例代码显示了API函数,不过在这个程序中它只新建了一个compressor,并不需要使用压缩/解压缩池,使用压缩池对读取自标准输入的数据进行压缩,然后将其写到际准输出:
public class PooledStreamCompressor{
public static void main(String[] args)throws Exception{
String codecClassName=args(0);
Class codecClass=Class.forName(codecClassName);
Configuration conf=new Configuration();
CompressionCodec codec=(CompressionCodec )ReflectionUtils.newInstance(codecClass,conf);
Compressor compressor=null;
try{
compressor=CodecPool.getCompressor(codec);
CompressionOutputStream out=codec.createOutputStream(System.out,compressor);
IOUtils.copyBytes(System.in,out,4096,false);
out.finish();
}finally{
CodecPool.returnCompressor(compressor);
}
}
在codec的重载方法createOutputStream()中,对于指定的CompressionCodec我们从池中获取一个Compressor实例。通过使用finally数据块,我们在不同的数据流之间来回复制数据,即使出现IOException异常,也可以确保compressor可以返回池中。
2.压缩和输入分片
在考虑如何压缩将由MapReduce处理的数据时,解这些压缩格式是否支持切分(splitting)是非常重要的。以一个存储在HDFS文件系统中且压缩前大小为1GB的文件为例·如果HDFS的块大小设置为128MB,那么该文件将被存储在8个块中,把这个文件作为输人数据的MapReduce作业,将创建8个输人分片,其中每个分片作为一个单独的map任务的输人被独立处理。
现在想象一下,文件是经过gzip压缩的,且压缩后文件大小为1GB.与以前一样,HDFS将这个文件保存为8个数据块·但是,将每个数据块单独作为一个输人分片是无法实现工作的,因为无法实现从gzip压缩数据流的任意位置读取数倨,所以让map任务独立于其他任务进行数据读取是行不通的。gzip格式使用DEFLATE算法来存储压缩后的数据,而DEFLATE算法将数据存储在一系列连续的压缩块中。问题在于每个块的起始位置并没有以任何形式标记,所以读取时无法从数据流的任意当前位置前进到下一块的起始位置读取下一个数据块,从而实现与整个数照侗步。由于上还原因,gzip并不支持文件切分。
在这种情况下,MapReduce会采用正确的做法,它不会尝试切分gzip压缟文件,因为它知道输人是gizp压缩文件(通过文件扩展名看出)且gzip不支持切分。这是可行的,但牺牲了数据的本地性:一个map任务处理8个HDFS块,而其中大多数块并没有存储在执行该map任务的节点上。而且,map任务数越少,作业的粒度就较大,因而运行的时间可能会更长。
在前面假设的例子中,如果文件是通过LZO压缩的,我们会而相同的向题,因为这个压缩格式也不支持数据读取和数据流伺步·但是,在预处理LZO文件的时候使用包含在Hadoop LZO库文件中的索引工具是可能的,你可以在上面所列的Goog和GitHub网站上获得该类库。该工具构建了切分点索引,如果使用恰当的MapReduce输人格式可有效实现文件的可切分特性。另一方面,bzip2文件提供不同数据块之间的同步标识(pi的48位近似值),因而它支持切分。
2.在MapReduce中使用压缩
前面讲到通过CompressionCodecFactory来推断CompressionCodec时指出,如果输入文件是压缩的,那么在根据文件扩展名推断出相应的codec后,MapReduce会在读取文件时自动解压缩文件“要想压缩MapReduce作业的输出,应在作业配置过程中将mapreduce.output.fileoutputformat.compress属性设为true,将mapreduce.output.fileoutputformat.compress.codec属性设置为打算使用的压缩codec的类名。另一种方案是在FileOutputFormat中使用更便捷的方法设置这些属性,如下范例所示。
//最高气温的代码中加入
FiieOutputFormat.setCompressOutput(job,true);
FiieOutputFormat.setOutputCompressorClass(job,GzipCodec.class);
我们按照如下指令对压缩后的输人运行程序(输出数倨不必使用相同的压缩格式进行压缩,尽管本例中不是这样):
hadoop MaxTemperatureWithCompression input/sample.txt.gz output
最终输出的每个部分都是经过压缩的,在这里,只有一部分结果:
gunzip -c output/part-r-0000.gz
1949111
1922
如果为输出生成顺序文件(sequence file),可以设置mapreduce.output.fileoutputformat.compress.type属性来控制限制使用压缩格式。默认值是RECORD,即针对每条记录进行压縮。如果将其改为BLOCK,将针对一组记录进行压缩,这是推荐的压縮策略,因为它的压缩效率更高。
SequenceFileoutputFormat类另外还有一个静态方法putCompressionType(),可以用来便捷地设置该属性。
下表归纳溉述了用于设置MpaReduce作业输出的压縮格式的配置性。如果你的MapReduce驱动使用Tool接口,则可以通过命令行将这些属性传递给程序,这比通过程序代码来修改压縮属性更加简便。
MapReduce的压缩属性
属性名称 | 类型 | 默认值 | 描述 |
mapreduce.output.fileoutputformat.compress | boolean | false | 是否压缩输出 |
mapreduce.output.fileoutputformat.compress.codec | 类名称 | org.apache.hadoop.io.compress.DefaultCodec | map输出所用的压缩codec |
mapreduce.output.fileoutputformat.compress.type | String | RECORD | 顺序文件输出使用的压缩类型:NONE,RECORD或者BLOCK |
对map任务输出进行压缩
尽管MapReduce应用读/写的是未经压缩的数据,但如果对map阶段的中间输人进行压缩,也可以获得不少好处。由于map任务的输出需要写到磁獸并通过网络传输到reducer节点,所以通过使用LZO、LZ4或者Snappy这样的快速压缩方式,是可以获得性能提升的,因为需要传输的数据减少了。启用map任务输出压縮和设置压缩格式的配置属性下表所示。
属性名称 | 类型 | 默认值 | 描述 |
mapreduce.map.output.compress | boolean | false | 是否对map任务输出进行压缩 |
mapreduce.map.output.compress.codec | Class | org.apache.hadoop.io.compress.DefaultCodec | map输出所用的压缩codec |
下面是在作业中启用map任务输出gzip压缩格式的代码。
Configuration conf=new Configuration();
conf.setBoolean(Job.MAP_OUTPUT_COMPRESS,true);
conf.setClass(Job.MAP_OUTPUT_COMPRESS_CODEC,GzipCodec.class,CompressionCodec.class);
Job job=new Job(conf);