一、数据存储要考虑哪些方面
-
数据加载时间
Facebook数仓每天存储的数据量超过20TB,数据加载既有磁盘I/O又有网络传输,时间占用大
-
快速的数据查询
-
低的空间占用
数据压缩/数据编码
-
适合多种查询模式
如果所有人都查相同的字段,那么就可以针对这个字段做优化,如建立索引,但是不同的人在查询数据的时候,数据组合模式不同。
-
数据的写速度(我理解在大数据中不重要)
二、行式存储和列式存储
1. 行存储
001:10,Smith,Joe,40000;002:12,Jones,Mary,50000;003:11,Johnson,Cathy,44000;004:22,Jones,Bob,55000;
**优点:**适用于OLTP系统,每次都查一整行数据的,数据的写速度快
缺点:
- 在数仓中查询速度慢,因为一般仅需要查少数几列数据,但行存储会加载所有数据
- 压缩率低(我理解是不同的列存储在一起,很少会有相邻数据有重复,等后面看了数据压缩再确定)
2. 列存储
10:001,12:002,11:003,22:004;Smith:001,Jones:002,Johnson:003,Jones:004;Joe:001,Mary:002,Cathy:003,Bob:004;40000:001,50000:002,44000:003,55000:004;
2.1 两种模式:
- Column-store
每列是单独的关系,称为 Column-store,如MonetDB
优点:减少数据加载量
缺点:无法保证一行里的所有列存在同一个节点上, 这样会在重组时带来网络传输开销,过度的网络传输是MR的瓶颈。
- Column-group
把列按照不同的组合存储,称为 Column-group,如 C-store
**优点:**如果能把查询模式确定下来,那就能减少重组带来的消耗
**缺点:**数仓中查询模式一般都难以确定,所以不太可能带来上面的优点,多种组合(一个列反复和其他列组合)又会增加存储
三、RCFile的设计与实现
3.1 数据分布
-
采用行列结合的方式
切块(block) -> 切行(row group) -> 切列 (可以想一下为什么先切行,再切列)
一个表可以存储在多个block,一个block上的可以有多个row group
-
每个 row group 分成三个部分:
- sync maker,标记一个 row group 的开始,用来做 row group 分割
- row group的元数据信息,存了多少记录,每列占了多少字节,一列中的每个字段有多少字节(可以想一下为什么存这些信息)
- 实际的数据
3.2 数据压缩
在每一个row group中,元数据和数据分开压缩
元数据:采用 RLE 编码(Run Length Encoding )算法来压缩,因为一个列中有很多字段的长度是一样的
**数据:**每列单独压缩(为什么),使用 GZIP 压缩算法来达到高的压缩率
3.3 数据读取和懒解压
- 数据读取
根据元数据信息,只读取需要列的数据
例:table:(c1, c2, c3, c4)
select c1 from table where c4 = 1
只读取 c1 和 c4 到内存
-
懒解压
当一个列真正被需要的时候才解压对应的 row group(where 为 true)
如上面的sql会先解压c4,如果发现了 c4 = 1的row group,才解压 c1的列
3.3 row group size
row group szie的设计要考虑的问题:
- 存储空间占用率,大的 row group size 有更高的压缩率,Facebook使用 GZIP 压缩,当 row group size 达到 4MB 的时候,存储空间占用率下降到瓶颈;
- 读取性能,小的 row group size 在做懒解压时候性能更好
**总结:**RC File的诞生解决了当时现有存储的问题,在存储性能、查询效率上都有了很大的提升。具体对比看下面的:
http://web.cse.ohio-state.edu/hpcs/WWW/HTML/publications/papers/TR-11-4.pdf
五、ORC
5.0 RC File 的缺点
- RC File 没有类型,存的全是blob (
Blob
对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取)- 不能按类型优化
- 因为不知道存储的是什么类型,所以不能保存有用的索引信息
- 复杂类型不能被分解,都是字节流
- 基于流的编码器压缩(不确定是什么意思,我觉得可以和下面提到的压缩做对比)
- 解压时必须从 row group 的头部开始
- 懒解压慢
5.1 ORC 做的改进
提升查询效率
- 添加索引信息
降低存储占用
- 使用有类型的Writer和Reader,提供轻量级压缩技术,如dictionary encoding,bit packing,delta encoding和run length encoding,使得文件变小。
- 在轻量级压缩的基础上,可以使用通用的压缩技术,如ZLIB、snappy、LZO、LZ4、ZSTD
5.2 结构
5.2.1 Postscript
- 怎样解析其他部分的信息,如 footer 和 metadata
- 文件的版本,即最低支持那个版本的hive
- 采用的压缩格式(none,zlib,snappy)
5.2.2 Footer
- 整体文件布局
- 列的类型信息
- 行数
- 文件级别每列的统计信息
- 原始类型都记录min/max,int, double, string, decimal, date, timestamp
- 数值类型记录sum
- 从hive1.1.0开始,记录hasNull用来优化 ‘is null’ 查询
5.2.3 File Metadata
- stripe级别的列统计信息
5.3 类型信息
在真正存储的时候不会存储复杂类型,会把复杂里涉及到的列展开存储,
如下列sql创建一个这样的表,真正存储的时候会先序遍历这个树来进行存储
5.2 压缩
5.3 运行时长度编码(Run Length Encoding,RLE)
5.3.1 整数
编码规则:
-
**无符号数:**varint编码
我们存储的大部分数据都是小整数,根本占不到32位,如果用32位来存储的话,会有很多字节是0,这样太浪费了,varint编码解决的就是把整数用更少的字节来存储,编码规则是源码的每 7 个bit分成一组,然后在每组的最高有效位放置 1 或者 0,1代表下一个字节还是属于这个整数,0代表下个字节开启新的整数
如整数300,32位编码大端序是 00000000 00000000 00000001 00101100
小端序是 00101100 00000001 00000000 00000000
进行 varint 编码后就是:10101100 00000010,只需要占2个字节
解码:按照首位的1把字节拼接,去掉每一组的最高有效位,进行计算
10101100 00000010
-> 0101100 0000010(小端序)
反转 -> 0000010 0101100
-
**有符号数:**ZigZag 编码
负数不能用源码存储,需要用反码存储,反码是源码除了符号位外,其余取反加一
如 -1的源码:1000000 00000000 00000000 00000000,反码是 11111111111111111111111111111111
因为负数的最高有效位是符号为,如果直接用 varint编码的话,无法降低存储(目前理解是最少也要占 5 个字节,教程说要把负数转成 Long 再用 varint 编码,占用的就是10个字节,现在还不懂。)
ZigZag的思路是把负数映射到无符号数上,在对无符号数进行varint编码,映射算法:
(n << 1) ^ (n >> 31) (对于 32 位整数),把符号为移到最后一位 如 n = -1 -1: 11111111111111111111111111111111 n << 1: 11111111111111111111111111111110 n >> 31: 11111111111111111111111111111111 (n << 1) ^ (n >> 31) = 00000000000000000000000000001 = 1 对1进行 varint 编码就能用 1 个字节表示 解码: (n >> 1) ^ - (n & 1) 如编码之后的 n = 1 n & 1: 00000000000000000000000000001 = 1 -(n & 1): 11111111111111111111111111111111 1 >> 1: 00000000000000000000000000000 (1 >> 1) ^ -(n & 1) = 11111111111111111111111111111111
存储:
数据分成两类,一类是间隔一个小整数的序列 Run,如 1, 3, 5, 7, 9;一类是正常的整数序列 Literal
Run:
[起始数字 - 3, 差值, 结束数字],起始数字的范围是 0 ~ 127,如序列 100 - 1编码为 [0x61, 0xff, 0x64].
Literals:
序列长度取反,varint编码数字
如[2, 3, 6, 7, 11]被编码为[0xfb, 0x02, 0x03, 0x06, 0x07, 0xb]
5.3.2 Byte Run Length Encoding
把数据分成两类,一类是连续相同的字节 Run(至少三个),一类是连续不相同的字节 Literals
Runs:
存连续的字节数 - 3(不清楚为什么要 -3)用来当控制字符,0 ~ 127,如 100 个 0 的编码是 [0x61, 0x00]
Literals:
存连续的字节数取反用来当控制字符 -128 ~ -1,0x44, 0x45 的编码是 [0xfe, 0x44, 0x45]
0xfe:
反码:11111110
源码:10000010 = -2
5.3.3 Boolean Run Length Encoding
boolean类型编码把多个值放到一个 byte 存储,如 [0xff, 0x80] 表示 1 个true和7个false(1000 0000)
5.3.4 String,Char,VarChar
两种编码方式:
-
直接编码:
记录两个流:
DATA,LENGTH
如:[“Nevada”, “California”] 的 DATA 是 NevadaCalifornia,LENGTH 是 [6, 10].
-
字典编码(解决重复字符串的问题):
记录三个流:
DATA,DICTIONARY_DATA,LENGTH
如 [“Nevada”, “California”, “Nevada”, “California”, and “Florida”]
DICTIONARY_DATA 是 CaliforniaFloridaNevada”
LENGTH 是 [10, 7, 6]
DATA 是 [2, 0, 2, 0, 1].