序列化与压缩

序列化与压缩

Hadoop生态学习小组-liyakun.hit
2018-04-12

写这篇文章的原因是,很多人搞不清楚序列化、压缩、列式存储都是什么概念。所以,补一篇文章来科普一下,这样整体的概念更多一些,能了解各项技术所处的位置。

1. 序列化

1.1 RPC

如果不同进程之间想交换内存中的数据,需要怎么做?

这就是IPC技术,有很多种,一种比较简单易想到的做法是通过磁盘\网络IO来做:

  • 磁盘:一个进程写到指定的文件位置,另一个进程不停的去读相同的文件
  • 网络(RPC):两个进程建立socket,然后通过socket互相传输数据

对一个分布式系统来说,RPC是不可或缺的。

RPC的效率对一个分布式系统来说非常重要。

1.2 RPC效率优化

只考虑软件层面:

  • 减少要传输的文件体积
  • 加快压缩和解压的过程

像Java,Python这类的语言,天生就自带把对象序列化成文本,再把文本反序列化的工具。

无论是体积还在速度都还不错,并且还在进一步的优化之中。

但是,RPC经常会遇到一个问题:

就是两端的编程语言经常不同,比如python连接java,scala,C/C++。

于是,出现了一种技术:数据交换协议

  • 两个系统要交换数据,需要定义一种双方都明白的协议,我们称为”数据交换协议”

1.3 常见的数据交换协议

协议 实现 跨语言 性能 传输量 RPC
xml 广泛 几乎所有 很大 N
json 广泛 大量 一般 一般 N
thrift thrift 大量 Y
protobuf protobuf 较少 N
avro Apache Avro 较少 Y
  • 首先明确一点,所有数据交换协议都是可以跨语言的,尽管有多跨的多,有的少。

  • 然后说一下不同的地方:

    • xml,json是单纯的数据交换协议,所以无论是发送端,还是接受端都需要在内存中对数据进行加工,带来的结果是性能很差。
    • thrift\protobuf\avro为各种语言提供了类库,可以把它们的内存对象直接序列化为文本流,在接收端再把这些文本流重新组装成内存中的对象结构,效率会高非常多
      • thrift支持语言多,有RPC类库,支持向前\后兼容,不支持动态修改schema
      • pb支持5种语言,无RPC类库,支持向前\后兼容,不支持动态修改schema
      • avro支持4种语言,有RPC类库,支持动态修改schema,只支持Avro自己的序列化格式,目前应用少

1.4 序列化示例-pickle

下面以python自带的序列化、反序列工具pickle来简单了解一下:

1
2
3
4
5
import pickle
obj = {"a": 1, "b": 2, "c": 3}

# 将 obj 持久化保存到文件 tmp.txt 中
pickle.dump(obj, open("tmp.txt", "w"))
1
2
3
with open("tmp.txt") as f:
lines = f.readlines()
print str(lines)
['(dp0\n', "S'a'\n", 'p1\n', 'I1\n', "sS'c'\n", 'p2\n', 'I3\n', "sS'b'\n", 'p3\n', 'I2\n', 's.']
1
2
3
4
# 从 tmp.txt 中读取并恢复 obj 对象
obj2 = pickle.load(open("tmp.txt", "r"))

print obj2
{'a': 1, 'c': 3, 'b': 2}

2. 压缩

2.1 为什么需要压缩算法

数据量越来越大会带来两个问题:

  1. 物理存储空间不够
  2. 数据加载时磁盘IO过大,数据传输时网络IO过大,都可能成为计算的瓶颈

在简单的,平铺直叙的存储上,很难解决这两个问题

不如再封装一层,于是

压缩算法就来了(这里说的是无损压缩技术,后面说的都是无损压缩)。

2.2 压缩算法原理

压缩算法的出现是为了解决空间不足和IO的问题,

但是要付出一点成本,就是额外的计算资源的开销。

下面举几个常见的压缩例子,来看看压缩技术是如何实现的:

  • 游程编码(Run Length Encoding)

    • 用一个符号值或串长代替具有相同值的连续符号
    • 一个例子:
      • 输入串为:AAAAAAAAAAAAAAA AAAAAAAAAAAAAAA AAA
      • 压缩后为:15A 15A 3A
    • 适用场景:重复数据
  • 差分编码(Delta Encoding)

    • 又称增量编码,是通过储存差异来达到压缩的目标
    • 一个例子:
      • 输入串为:10000,10001,10002,10003,10004,10005,10006,10007
      • 压缩后为:10000 0,1,2,3,4,5,6,7
    • 适用场景:有序数据集,例如timestamp
  • 字典编码(Dictionary Encoding)

    • 提供一个字典,如果输入串中的单词命中字典,那用字典中的位置来替换这个词
    • 一个例子:
      • 输入串为:”hello world. hello new world.”
      • 压缩后为:[‘hello’,’world’] “0 1. 0 new 1”
    • 适用场景:小规模的数据集合,例如IP地址

实际工业界中使用的算法会更加复杂,这里不逐一展开。

2.3 压缩算法的评价标准

对于压缩算法来说,衡量它们的优劣,通常有如下的指标:

  • 压缩比:压缩后的文件大小占不压缩大小比例
  • 压缩速度:在相同环境下,指定时间内压缩文件的大小
  • 解压速度:大相同环境下,指定时间内解压文件的大小

在Hadoop圈,还有另一个关键指标:可分割性

Gzip是一种非常常见的压缩算法,但是它不支持可分割性,带来的后果就是:

  • 对一个Gzip压缩后依然非常大的文件进行load时,本来需要多个map同时工作,但是由于文件只能从头开始进行解压缩,于是只能使用一个map来进行工作。

包括压缩界的新秀zstd,也是不支持可分割性,那么好的算法,是不是在hadoop圈不能用了?

显然与事实不符,我们都知道zstd在hadoop圈应用还是比较多的,那么如何来解决这个问题呢?

当你在原来的层面发现问题很难时,不防想想可不可以通过增加一层来解决这个问题。

既然zstd这类的算法只能从文件开头开始进行解压缩,那么我们把一个大文件切分成一小块一小块的小文件(最好是每个文件刚刚好在压缩后对应到一个HDFS文件块),sequencefile就是这么一种技术,这里不详细的介绍了。

2.4 压缩算法Benchmarks

Compressor name| Ratio| Compression |Decompress.
–| –| –| – | –
zstd 1.3.4 -1 |2.877 |470 MB/s |1380 MB/s
zlib 1.2.11 -1 |2.743 |110 MB/s |400 MB/s
brotli 1.0.2 -0 |2.701 |410 MB/s |430 MB/s
quicklz 1.5.0 -1| 2.238 |550 MB/s |710 MB/s
lzo1x 2.09 -1 |2.108 |650 MB/s| 830 MB/s
lz4 1.8.1 | 2.101 |750 MB/s |3700 MB/s
snappy 1.1.4 |2.091 |530 MB/s |1800 MB/s
lzf 3.6 -1 |2.077 |400 MB/s| 860 MB/s

For comparison, several fast compression algorithms were tested and compared on a server running Linux Debian (Linux version 4.14.0-3-amd64), with a Core i7-6700K CPU @ 4.0GHz, using lzbench, an open-source in-memory benchmark by @inikep compiled with gcc 7.3.0, on the Silesia compression corpus.


以上这些内容算是列式存储的前奏吧,后面会介绍Parquet文件,如同上面提到的sequencefile一样,Parquet也是一种文件的组织形式