hadoop中的lzo和snappy

澄清一些相关概念。

Lempel-Ziv是一种编码方式(类比霍夫曼编码),特点是字典小速度快:http://zh.wikipedia.org/wiki/LZW
在这个理论的基础上实现了很多压缩算法,包括lzo/lzf/lz4/lzw/snappy等等。snappy特殊,不是lz前缀。
http://en.wikibooks.org/wiki/Data_Compression/Dictionary_compression

这里只考虑lzo和snappy,这两个用的比较广泛。
它们的共同特点是压缩比小、压缩/解压快(更注重解压速度)、不耗CPU、会占用一些内存。snappy相对更快。

LZO

http://www.oberhumer.com/opensource/lzo/

LZO的全称是Lempel-Ziv-Oberhumer。Oberhumer就是这家公司的名字,澳大利亚的一家公司,专注于数据压缩/解压/加密/解密等。创始人Markus,他还参与开发了著名的压缩软件UPX,一般用于压缩可执行文件,加壳/脱壳等等,对Crack有兴趣的同学应该都知道。

用ANSI C写的。只是一套压缩/解压算法的库,可以在自己的程序中调用。lzo其实是很多算法的统称,包括lzo1a、lzo1b、lzo1x、lzo2a等等。作者说这是因为一些历史原因(support unlimited backward compatibility)。可以粗略分为lzo1和lzo2,同一个类别下的算法互相兼容。比如lzo1x可以解压lzo1a生成的数据。但lzo1和lzo2不兼容。二者区别:

Category LZO1 algorithms: compressed data format is strictly byte aligned
Category LZO2 algorithms: uses bit-shifting, slower decompression // lzo2的解压比lzo1慢

lzo2算法似乎很少使用。作者投入的也不多。
注意不要把这里的lzo2和版本号里的2.x搞混了,lzo2只是表示算法上的变化。截止2014-12-24,lzo最新的版本是2.08。
同一种算法(例如lzo1x)还可以根据压缩比分为lzo1x-1/lzo1x-999等等。lzo1x-1压缩比小速度快,lzo1x-999压缩比大速度慢。

作者推荐一般情况下都使用lzo1x:

LZO1X seems to be best choice in many cases, so:

  • when going for speed use LZO1X-1
  • when generating pre-compressed data use LZO1X-999
  • if you have little memory available for compression use LZO1X-1(11)
    or LZO1X-1(12)

如何在自己的程序中使用lzo压缩:

Let’s assume you want to compress some data with LZO1X-1:
A) compression

  * include <lzo/lzo1x.h>
    call lzo_init()
    compress your data with lzo1x_1_compress()
  * link your application with the LZO library

B) decompression

  * include <lzo/lzo1x.h>
    call lzo_init()
    decompress your data with lzo1x_decompress()
  * link your application with the LZO library

lzo的编译是标准的3步(先export CFLAGS=-m64):./configure -enable-shared、make、make install。会生成liblzo2.so等一堆文件,默认是在/usr/local/lib下。

如果configure时自己指定了prefix,可以在/etc/ld.so.conf.d/目录下新建lzo.conf文件,只需写入lzo库文件的路径,然后运行/sbin/ldconfig -v使配置生效。

如果是debian系统,可以直接sudo apt-get install liblzo-dev2,相关的lib会放在/usr/lib64下(跟debian的版本有关,不确定)

hadoop@inspur218:~/hadoop-2.2.0/sbin$ ls -lh /usr/lib*/liblzo*
-rw-r--r-- 1 root root 219K 11月  6 2009 /usr/lib64/liblzo2.a
-rw-r--r-- 1 root root  786 11月  6 2009 /usr/lib64/liblzo2.la
lrwxrwxrwx 1 root root   16  6月 12 2012 /usr/lib64/liblzo2.so -> liblzo2.so.2.0.0
lrwxrwxrwx 1 root root   16  6月 12 2012 /usr/lib64/liblzo2.so.2 -> liblzo2.so.2.0.0
-rw-r--r-- 1 root root 129K 11月  6 2009 /usr/lib64/liblzo2.so.2.0.0
-rw-r--r-- 1 root root 219K 11月  6 2009 /usr/lib/liblzo2.a
-rw-r--r-- 1 root root  786 11月  6 2009 /usr/lib/liblzo2.la
lrwxrwxrwx 1 root root   16  6月 12 2012 /usr/lib/liblzo2.so -> liblzo2.so.2.0.0
lrwxrwxrwx 1 root root   16  6月 12 2012 /usr/lib/liblzo2.so.2 -> liblzo2.so.2.0.0
-rw-r--r-- 1 root root 129K 11月  6 2009 /usr/lib/liblzo2.so.2.0.0

lzo以GPL协议开源,不能用于商业用途、具有传染性(用了lzo的软件也必须以GPL协议开源)。
而Hadoop是Apache协议,商业友好,不强制开源。与GPL协议冲突,所以hadoop不能直接使用lzo,但可以对lzo提供支持。只是用户要自己安装lzo的相关lib和jar。

lzop

基于lzo的库编写的命令行工具。用于在shell中压缩/解压。类似gzip。只不过lzop采用lzo压缩,gzip采用DEFLATE压缩。
lzop会生成以lzo结尾的文件,但其实这个lzo文件里不光是数据,还有一些元数据,比如原始的文件名、修改时间、权限等等。

lzop stores the original file name, mode and time stamp in the compressed file.
These can be used when decompressing the file with the -d option.
This is useful when the compressed file name was truncated or when the time stamp was not preserved after a file transfer.

lzop preserves the ownership, mode and time stamp of files when compressing.
When decompressing lzop restores the mode and time stamp if present in the compressed files.
See the options -n, -N, –no-mode and –no-time for more information.

没有找到lzop生成的文件格式,但是可以参考gzip生成的文件格式:

“gzip” is often also used to refer to the gzip file format, which is:
1.a 10-byte header, containing a magic number (1f 8b), a version number and a timestamp
2.optional extra headers, such as the original file name,
3.a body, containing a DEFLATE-compressed payload
4.an 8-byte footer, containing a CRC-32 checksum and the length of the original uncompressed data

估计lzop也差不多。
通俗的说,lzop就是使用了lzo的第三方程序(虽然是同一个作者)。lzop生成的文件格式就是对纯lzo数据块的打包。
lzop也以GPL协议开源。

hadoop-lzo

之前说过hadoop不能直接使用lzo,但可以对lzo提供支持,就是通过hadoop-lzo实现的。(其实hadoop提供了接口可以支持任意压缩算法,倒不是特意针对lzo的)

hadoop-lzo的前身是hadoop-gpl-compression,本来是在google code上的,后来迁移到github。都是以GPL协议开源(因为使用了lzo)。

github上有两个hadoop-lzo提到的比较多:https://github.com/kevinweil/hadoop-lzohttps://github.com/twitter/hadoop-lzo 。前者是从twitter的hadoop-lzo fork而来。其实kevinweil本来就是twitter的员工吧,twitter hadoop-lzo早期就是他维护的,不过从2011年3月以后就没有commit了。而且他fork之后也没有任何commit,感觉就是随手fork一下就放那不管了,结果很多文章给出的链接都是他的。。。如果按那个去配置lzo就会有很多坑,而且只能用于老版本的hadoop。

这里以twitter的hadoop-lzo为准。

编译的过程省略,基本就是maven clean package。hadoop-lzo可以分为两部分:

  1. native lib。编译后生成libgplcompression的一堆文件。这些lib依赖于liblzo2.so等一堆文件,编译的时候如果没有相关lib会报错。使用的时候也必须要有lzo的lib。
    hadoop@inspur116:~/jxy/hadoop-2.2.0/test/hadoop-2-2-0-prepare/hadoop-lzo/target$ ls -lh native/Linux-amd64-64/lib
    总用量 184K
    -rw-r--r-- 1 hadoop netease 102K 12月  9 20:14 libgplcompression.a
    -rw-r--r-- 1 hadoop netease 1.2K 12月  9 20:14 libgplcompression.la
    lrwxrwxrwx 1 hadoop netease   26 12月  9 20:14 libgplcompression.so -> libgplcompression.so.0.0.0
    lrwxrwxrwx 1 hadoop netease   26 12月  9 20:14 libgplcompression.so.0 -> libgplcompression.so.0.0.0
    -rwxr-xr-x 1 hadoop netease  67K 12月  9 20:14 libgplcompression.so.0.0.0
  2. hadoop-lzo的jar文件。这个jar文件包括了在hadoop里使用lzo的相关接口和工具,比如LzoCodec,LzoCompressor等。其实这些java类都是通过JNI调用libgplcompression实现相关功能的。还有mapreduce中会用到的LzoTextInputFormat、LzoLineRecordReader等类。
    hadoop@inspur116:~/jxy/hadoop-2.2.0/test/hadoop-2-2-0-prepare/hadoop-lzo/target$ ls -lh *.jar
    -rw-r--r-- 1 hadoop netease 170K 12月  9 20:14 hadoop-lzo-0.4.20-SNAPSHOT.jar
    -rw-r--r-- 1 hadoop netease 181K 12月  9 20:14 hadoop-lzo-0.4.20-SNAPSHOT-javadoc.jar
    -rw-r--r-- 1 hadoop netease  50K 12月  9 20:14 hadoop-lzo-0.4.20-SNAPSHOT-sources.jar

把这些libgplcompression和jar拷到hadoop对应的目录,再修改core-site.xml中相关配置,即可在hadoop中使用lzo。具体不展开了。

调用关系:hadoop -> hadoop lzo的jar -> libgplcompression -> liblzo2

在hadoop中使用lzo的几种方式

  1. 直接在hdfs上读写lzo文件。可以使用LzoCodec/LzopCodec创建input stream和output stream。直接fs -text也可以。
  2. mapreduce的输入是lzo。在有同名的index文件存在的情况下,map可以自动分片,一个lzo文件可以分成多块,由多个map处理。注意必须将InputFormat设置为LzoTextInputFormat才能分片。如果是默认的TextInputFormat,程序也能正常运行,但一个lzo文件只能由一个map处理,就算有index文件也没用。
  3. lzo压缩MR的中间结果和最终结果。注意中间结果最好用LzoCodec,别用LzopCodec。否则可能会出错。这两种Codec的区别见下面。

hadoop-lzo使用的那种算法?

前面说过,lzo其实包含多种压缩算法。那hadoop-lzo使用的是什么?

默认是lzo1x,见LzoCodec:

// 可以通过这两个配置项配置具体用那种算法
public static final String LZO_COMPRESSOR_KEY = "io.compression.codec.lzo.compressor";
public static final String LZO_DECOMPRESSOR_KEY = "io.compression.codec.lzo.decompressor";

// 只有压缩时才需要指定压缩比,所以是lzo1x-1,对应C函数lzo1x_1_compress()
static LzoCompressor.CompressionStrategy getCompressionStrategy(Configuration conf) {
    assert conf != null : "Configuration cannot be null!";
    return LzoCompressor.CompressionStrategy.valueOf(
          conf.get(LZO_COMPRESSOR_KEY,
            LzoCompressor.CompressionStrategy.LZO1X_1.name()));
  }

  // 解压时不用指定压缩比,所以是lzo1x,对应C函数lzo1x_decompress() 
static LzoDecompressor.CompressionStrategy getDecompressionStrategy(Configuration conf) {
    assert conf != null : "Configuration cannot be null!";
    return LzoDecompressor.CompressionStrategy.valueOf(
          conf.get(LZO_DECOMPRESSOR_KEY,
            LzoDecompressor.CompressionStrategy.LZO1X.name()));
  }

LzopCodec和LzoCodec

  1. 基本上就是lzop文件格式和纯lzo数据块的区别。LzopCodec输出的包含一些元数据(MAGIC_NUMBER/VERSION之类的),LzoCodec输出的则纯粹是压缩后的数据,没有其他信息。
  2. LzopCodec是继承LzoCodec的。
  3. LzopCodec输出的文件后缀是lzo,LzoCodec输出的文件后缀是lzo_deflate。这是由2者的getDefaultExtension方法决定的。
  4. LzopCodec输出的文件可以用lzop程序解压。lzo_deflate不行。
  5. lzo和lzo_deflate使用上差别不大。二者都可以直接fs -text,也都可以作为MR的输入。但lzo_delate无法创建index,也就是无法分片。
  6. 两种Codec都可以用作压缩map中间结果,也都可以用作压缩最终输出。但使用某些自定义Writable类时,如果用LzopCodec压缩中间结果,反序列化会出错,有同事碰到过,具体原因不太清楚。而且相对而言LzoCodec更快一些,更适合压缩中间结果。所以为了避免不必要的麻烦,一般用LzoCodec压缩中间结果,LzopCodec压缩最终结果

LzoIndexer

hadoop-lzo提供的工具,用来给lzo文件创建索引的。其实lzo压缩算法本身是没有这种东西的。算是hadoop-lzo的发明创造吧。

有了index文件,就可以分多个map处理了(必须是LzoTextInputFormat)。所以处理lzo文件很慢时,记得看下有没有index。

有两种使用方式:

// 单机
hadoop jar lib/hadoop-lzo-0.4.19-SNAPSHOT.jar com.hadoop.compression.lzo.LzoIndexer
// mapreduce
hadoop jar lib/hadoop-lzo-0.4.19-SNAPSHOT.jar com.hadoop.compression.lzo.DistributedLzoIndexer

部署hadoop时的是否一定要单独安装lzo?

目前我们在部署hadoop的时候,都是事先在节点上sudo apt-get install liblzo-dev2的。

仔细想想的话,其实我们可以将liblzo2.so等文件放到$HADOOP_HOME/lib/native里,随hadoop一起分发。我觉得这样也是可以的,虽然没测试过。
在libgplcompression调用liblzo2时,只要能找到就可以了,是系统安装的还是hadoop自带的,都无所谓。
我试过在没装lzo的机器上读取hdfs上的lzo文件,liblzo是从其他机器拷过来的,是可行的。

这样有2个问题:1.这个liblzo只有hadoop能用,不能给系统的其他应用;2.不同机器/系统/内核编译出的lzo可能不通用。

一点疑惑

以前一直疑惑既然项目名是hadoop-lzo,是为了给hadoop添加lzo支持,为啥编译出来的lib叫gpl compression,根本看不出来是跟lzo相关的。

后来知道了它的前身是hadoop-gpl-compression项目,去google code上看了下,这个项目本来是雄心勃勃,要把各种GPL的算法都加入进来的,各种GPL的算法都可以通过libgplcompression使用。结果最后只实现了lzo。。。
迁移到github后,干脆直接改名叫hadoop-lzo了,估计也不会添加其他算法了。。。

所以,虽然生成的lib叫做libgplcompression.*,但其实只是为lzo服务。

Snappy

http://code.google.com/p/snappy/

Snappy以BSD协议开源(限制非常少,接近于public domain),跟Apache协议不冲突,所以hadoop很早就提供支持了。cloudera从cdh3u1版本开始支持,apache官方版本从0.23开始提供支持:https://issues.apache.org/jira/browse/HADOOP-7206

在hadoop本身开始支持snappy之前,曾有一个hadoop-snappy项目(http://code.google.com/p/hadoop-snappy/),类似hadoop-lzo,以“外挂”的形式支持snappy。早就停止开发了。但网上很多文章还是用的这个项目,注意别被误导。

hadoop使用snappy时,跟lzo类似,分为native lib和java两部分。在编译hadoop本身的native lib时,默认是不加入snappy支持的,需要加两个参数:

// 如果snappy安装到默认位置,应该是不需要指定snappy.prefix的
$M2_HOME/bin/mvn package -Pdist,native -DskipTests -Dtar -Drequire.snappy -Dsnappy.prefix=$SNAPPY_DIR

这样生成的libhadoop.so才能使用snappy。在分发tar包时,也要把libsnappy.*放入$HADOOP_HOME/lib/native(或者在每个节点上单独安装相关的包,只要系统能找到libsnappy就可以了)。

调用关系:hadoop -> libhadoop -> libsnappy

在hadoop中使用Snappy时,跟使用lzo类似,hadoop本身就提供了SnappyCodec,可以创建input stream、output stream。也可以用作MR的输入、输出、或者压缩中间结果。

但有几个问题要注意:

  1. Snappy是unsplittable的,意味着一个snappy压缩的文件只能被一个map处理。Snappy也没有类似LzoIndexer的工具。所以Snappy并不适合作为MR的输入。
  2. Snappy的解压速度相对lzo更快,更适合压缩MR的中间结果。
  3. Snappy文件是没有内部结构的,是纯粹的数据,更类似于lzo_deflate而不是lzo。
  4. Snappy非常适合hbase。在hbase中建表时可以指定COMPRESSION=>’snappy’属性。但前提是编译libhadoop.so时开启了snappy支持。

P.S:无论snappy还是lzo,压缩、解压都是在本地进行的。例如在客户端读写hdfs上的snappy文件时,datanode节点可以没有libsnappy,但客户端必须要有libsnappy。换言之,如果MR用snappy压缩map中间结果,只要datanode上有libsnappy就可以了,客户端不需要。lzo同理。

P.S.2:曾经试过直接替换DN的libhadoop.so,结果不只DN挂掉,诡异的是RM也挂了。但直接替换libsnappy似乎不会有问题。即插即用。

P.S.3:理论上客户端-text一个lzo文件时需要客户端有libgplcompression和liblzo。结果测试时发现删掉$HADOOP_HOME/lib/native/libgplcompression.*后仍能正确读取lzo文件。

难道系统其它地方有libgplcompression?debug了半天,看了hadoop-lzo一点代码才明白是怎么回事,hadoop-lzo编译生成的jar里有个native目录,libgplcompression相关的so文件都放在里面。

看GPLNativeCodeLoader类:

// 初始化这个类
static {
    try {
      //优先从jar文件里加载lib,可以用JVM属性com.hadoop.compression.lzo.use.libpath控制
      if (!useBinariesOnLibPath()) {
        // 这里其实是将jar文件解压
        File unpackedFile = unpackBinaries();
        if (unpackedFile != null) { // the file was successfully unpacked
          String path = unpackedFile.getAbsolutePath();
          // 加载解压出来的libgplcompression
          System.load(path);
          LOG.info("Loaded native gpl library from the embedded binaries");
        } else { // fall back
          System.loadLibrary(LIBRARY_NAME);
          LOG.info("Loaded native gpl library from the library path");
        }
      }
      // 否则就尝试从java.library.path里加载
      else {
        System.loadLibrary(LIBRARY_NAME);
        LOG.info("Loaded native gpl library from the library path");
      }
      nativeLibraryLoaded = true;
    } catch (Throwable t) {
      LOG.error("Could not load native gpl library", t);
      nativeLibraryLoaded = false;
    }
 }

private static File unpackBinaries() {
    // locate the binaries inside the jar
    String fileName = System.mapLibraryName(LIBRARY_NAME);    // fileName是libgplcompression.so
    String directory = getDirectoryLocation();       // directory是/native/Linux-amd64-64/lib,这个路径其实是相对classpath的路径

    // use the current defining classloader to load the resource
    // 注意这里的路径其实是相对classpath的路径
    InputStream is =GPLNativeCodeLoader.class.getResourceAsStream(directory + "/" + fileName);
    // 省略了一些代码
    // write the file
    byte[] buffer = new byte[8192];
    OutputStream os = null;
    try {
      // prepare the unpacked file location
      File unpackedFile = File.createTempFile("unpacked-", "-" + fileName);
      // ensure the file gets cleaned up
      unpackedFile.deleteOnExit();

      os = new FileOutputStream(unpackedFile);
      int read = 0;
      while ((read = is.read(buffer)) != -1) {
        os.write(buffer, 0, read);
      }
      // set the execution permission
      unpackedFile.setExecutable(true, false);
      LOG.debug("temporary unpacked path: " + unpackedFile);
      // return the file
      return unpackedFile;
    } catch (IOException e) {
      LOG.error("could not unpack the binaries", e);
      return null;
    } finally {
      try { is.close(); } catch (IOException ignore) {}
      if (os != null) {
        try { os.close(); } catch (IOException ignore) {}
      }
    }}

注意,如果是从jar文件里加载的libgplcompression(也可以说是从classpath里加载的),会打印日志:

15/02/11 16:43:45 INFO lzo.GPLNativeCodeLoader: Loaded native gpl library from the embedded binaries

如果是从java.library.path加载的libgplcompression,日志不同:

15/02/11 16:50:21 INFO lzo.GPLNativeCodeLoader: Loaded native gpl library from the library path

如果只有libgplcompression没有liblzo,会出错

15/02/11 15:08:29 INFO GPLNativeCodeLoader: Loaded native gpl library from the embedded binaries
15/02/11 15:08:29 WARN LzoCompressor: java.lang.UnsatisfiedLinkError: Cannot load liblzo2.so.2 (liblzo2.so.2: cannot open shared object file: No such file or directory)!
15/02/11 15:08:29 ERROR LzoCodec: Failed to load/initialize native-lzo library