数据库内核月报

数据库内核月报 - 2015 / 08

MySQL · 社区动态 · InnoDB Page Compression

Author: 印风

背景:Punch hole和Sparse file

Punch hole是一个需要操作系统和文件系统支持的特性,顾名思义就是在文件中打洞。这个特性的目的是为了减少数据文件的磁盘开销。比如一个大文件中有一部分数据我们是不需要的,就可以通过punch hole特性将其删除,相当于在文件中打了个洞,这个洞是不占用磁盘的。

Punch hole特性通过fallocate调用来实现,在其第二个参数指定flag FALLOC_FL_PUNCH_HOLE时,第三个参数指定需要punch hole的偏移位置,第四个参数指定punch hole的长度。当成功打洞后,以后访问到这个范围的数据都返回0。

fallocate的描述见文档。根据文档的描述,FALLOC_FL_PUNCH_HOLE 需要和另外一个flag FALLOC_FL_KEEP_SIZE 一起使用,也就是说,即使在文件中打洞,通过stat获得的文件大小也不会发生变化,你需要通过du命令来获得准确的磁盘文件大小。

目前不是所有的内核都支持该特性,在阿里环境需要使用rh6 2.6.32-358及之后的版本,另外由于对应的flag 宏没有定义,我们需要显式的定义如下两个宏:(感谢 @伯瑜 大神的指点)

#define FALLOC_FL_PUNCH_HOLE    0x02 /* de-allocates range */
#define FALLOC_FL_KEEP_SIZE     0x01 /* default is extend size */

通常情况下,偏移量和长度要求是文件系统的block size大小,否则fallocate调用可能失败并返回EINVAL。

除了上面这两个flag外,还有另外一个比较有意思的flag:FALLOC_FL_COLLAPSE_RANGE,名字和其功能比较形象,相当于在挖洞后,这个洞并没有留下,而是把后面的数据往前面移,因此被打洞的部分,在被随后访问到时,读取到的数据就不是0,而是后面的数据。

关于这个flag可以参考这篇文章了解其背景。

文件的管理使用sparse file(稀疏文件),一般支持punch hole特性的,都会支持sparse file,对于sparse file,可以参阅这篇博客,讲的非常详细。简单的重整了下博客中的配图,如下所示:

sparse file

然而需要注意一种情况,由于文件中的空洞不占磁盘空间,当磁盘接近满时,如果向空洞写入数据,就可能触发写入失败的问题,导致不可预料的问题,因此空余的磁盘空间阀值需要多预留点。

InnoDB 新压缩实现

MySQL 5.7.8版本实现了一种新的压缩方式(WL#7696),也就是所谓的Innodb Transaparent PageIO Compression,其原理很简单,就是利用punch hole + 数据压缩来实现的。其在内存中表现的是一个正常的page,只在读写到磁盘时,才进行文件压缩、解压处理。处理逻辑如下图所示:

innodb punch

首先,InnoDB增加了新的Page类型,这意味着如果使用该特性,则不能原地降级到老版本,需要对格式进行转换才能降级。

目前上游支持两种压缩算法:zllib及lz4,但我们也可以很方便的进行扩展新的算法。

被压缩的数据包含除FIL_PAGE_DATA之外的所有数据(包括tailer),但需要以block size对齐(线上环境的block size通常为4KB),这意味着即使我们把数据从16KB压缩到9KB,也需要存储12KB的数据。因此block size设小点,这样的场景将受益,可以减少空间浪费。

表定义

首先需要操作系统支持该特性,在阿里的环境里,需要装上新版内核(如上述)让系统支持punch hole。

可以通过CREATE TABLE 或ALTER TABLE 来定义压缩表:

mysql> create table sb1 (a int, b blob) compression='zlib';
Query OK, 0 rows affected (0.05 sec)

也可以选择compression=’lz4’来指定lz4压缩算法;注意对compression属性的ALTER是立刻生效的,因此在做完ALTER COMPRESSION属性操作后,需要做一次表的rebuild,例如optimize table操作,才能对已有的数据做punch hole。

compression属性存储在frm文件中,以两个字节存储字符串长度,随后存储compression属性定义字符串,这也是一个操作系统降级的风险点。

具体的使用参阅官方文档

相关代码逻辑

压缩数据

压缩数据发生在对磁盘进行IO WRITE之前:

  1. 先在内存中压缩数据,并对齐Block size,确定做punch hole的范围,同时将即将写入文件的buf地址指向新分配的压缩页地址(这里存在优化的空间,需要避免重复分配内存块)。参考函数:
    • Native AIO: AIO::reserve_slot –> os_file_compress_page
    • 同步写:os_file_pwrite –> os_file_io –> os_file_compress_page
  2. 将压缩处理过的page写入文件;
  3. 随后调用函数os_file_io_complete中,执行punch hole操作(os_file_punch_hole)。

解压数据

在从磁盘读取数据到磁盘后,首先要进行解压:参考函数:os_file_io_complete –> os_file_decompress_page

特殊处理

  1. 通过dblwr恢复的corruption的page写入时禁止压缩模式,因为此时innodb处于恢复模式,还没拿到server层存储的compression属性;
  2. row_merge_read 和row_merge_write,一般是用于排序的临时文件,无需做压缩/解压;
  3. truncate操作的文件日志禁止压缩模式。

代码整体的逻辑比较清晰,但改动点还比较多,后续我们将该特性Port到RDS版本,结合新内核来发挥数据空间节省的目的。

最后,Facebook的大神Domas写了一篇博客,认为InnoDB推出这样的压缩特性,使其正在丧失自身的优势,非常值得一读。简单的摘要下: