序列化与压缩
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 | import pickle |
1 | with open("tmp.txt") as f: |
['(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 | # 从 tmp.txt 中读取并恢复 obj 对象 |
{'a': 1, 'c': 3, 'b': 2}
2. 压缩
2.1 为什么需要压缩算法
数据量越来越大会带来两个问题:
- 物理存储空间不够
- 数据加载时磁盘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也是一种文件的组织形式