数据库内核月报 - 2015 / 08-MySQL · 社区动态 · InnoDB Page Compression

背景: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,可以参阅这篇博客,讲的非常详细。简单的重整了下博客中的配图,如下所示:

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

InnoDB 新压缩实现

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

首先,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推出这样的压缩特性,使其正在丧失自身的优势,非常值得一读。简单的摘要下:

  • 无法完美压缩:例如9KB的数据可能需要12kb来存储,取决于block size;
  • 无法压缩Buffer pool, 这是和传统innodb压缩相比,以前的压缩方式可以在内存中只存放压缩页拷贝 (然而也有可能同时存在压缩和解压页),因此用户可能需要去购买iops更高的设备,而oracle正好也卖这些….
  • punch hole 可能产生的文件碎片化,底层的文件管理更加复杂;
  • 对innodb文件做punch hole可能带来的后果是,使得每个文件的page变成一个独立的segment,文件系统需要单独的journal和metadata来管理。另外也有可能有性能问题:可能比non-sparse的写操作昂贵五倍 (这依赖于具体的内核);
  • 删除一个拥有几百万个段管理对象的数据文件带来的开销会非常昂贵。
时间: 2024-09-21 08:11:01

数据库内核月报 - 2015 / 08-MySQL · 社区动态 · InnoDB Page Compression的相关文章

MySQL内核月报 2015.01-MySQL · 捉虫动态· InnoDB自增列重复值问题

问题重现 先从问题入手,重现下这个bug 这里我们关闭mysql,再启动mysql,然后再插入一条数据 我们看到插入了(2,2),而如果我没有重启,插入同样数据我们得到的应该是(4,2). 上面的测试反映了mysqld重启后,InnoDB存储引擎的表自增id可能出现重复利用的情况. 自增id重复利用在某些场景下会出现问题.依然用上面的例子,假设t1有个历史表t1_history用来存t1表的历史数据,那么mysqld重启前,ti_history中可能已经有了(2,2)这条数据,而重启后我们又插入

MySQL内核月报 2015.03-MySQL · 捉虫动态· pid file丢失问题分析

现象 mysql5.5,通过命令show variables like '%pid_file%'; 可以查到pid文件位置,例如/home/mysql/xx.pid.但发现在此目录下找不到此pid文件. 背景知识 mysql pid文件记录的是当前mysqld进程的pid. 通过mysqld_safe启动mysqld时,mysqld_safe会检查PID文件,未指定PID文件时,pid文件默认名为$DATADIR/`hostname`.pid pid文件不存在,不做处理 文件存在,且pid已占用

MySQL内核月报 2015.01-MySQL · 捉虫动态· 设置 gtid_purged 破坏AUTO_POSITION复制协议

bug描述 Oracle 最新发布的版本 5.6.22 中有这样一个关于GTID的bugfix,在主备场景下,如果我们在主库上 SET GLOBAL GTID_PURGED = "some_gtid_set",并且 some_gtid_set 中包含了备库还没复制的事务,这个时候如果备库接上主库的话,预期结果是主库返回错误,IO线程挂掉的,但是实际上,在这种场景下主库并不报错,只是默默的把自己 binlog 中包含的gtid事务发给备库.这个bug的造成的结果是看起来复制正常,没有错误

MySQL内核月报 2015.01-MySQL · 捉虫动态· replicate filter 和 GTID 一起使用的问题

问题描述 当单个 MySQL 实例的数据增长到很多的时候,就会考虑通过库或者表级别的拆分,把当前实例的数据分散到多个实例上去,假设原实例为A,想把其中的5个库(db1/db2/db3/db4/db5)拆分到5个实例(B1/B2/B3/B4/B5)上去. 拆分过程一般会这样做,先把A的相应库的数据导出,然后导入到对应的B实例上,但是在这个导出导入过程中,A库的数据还是在持续更新的,所以还需在导入完后,在所有的B实例和A实例间建立复制关系,拉取缺失的数据,在业务不繁忙的时候将业务切换到各个B实例.

MySQL内核月报 2015.02-MySQL · 捉虫动态· 变量修改导致binlog错误

背景 MySQL 5.6.6 版本新加了这样一个参数--log_bin_use_v1_row_events,这个参数用来控制binlog中Rows_log_event的格式,如果这个值为1的话,就用v1版的Rows_log_event格式(即5.6.6之前的),默认是0,用新的v2版本的格式,更详细看官方文档.这个参数一般保持默认即可,但是当我们需要搭 5.6->5.5 这要的主备的时候,就需要把主库的这个值改为1,不然5.5的备库不能正确解析Rows_log_event.最近在使用这个参数的时

MySQL内核月报 2015.01-MySQL · 捉虫动态· mysql client crash一例

背景 客户使用mysqldump导出一张表,然后使用mysql -e 'source test.dmp'的过程中client进程crash,爆出内存的segment fault错误,导致无法导入数据. 问题定位 test.dmp文件大概50G左右,查看了一下文件的前几行内容,发现: 问题定位到第一行出现了不正常warning的信息,是由于客户使用mysqldump命令的时候,重定向了stderr.即: mysqldump ...>/test.dmp 2>&1 导致error或者warn

阿里数据库内核月报:2015年08月

# 01 MySQL · 社区动态 · InnoDB Page Compression # 02 PgSQL · 答疑解惑 · RDS中的PostgreSQL备库延迟原因分析 # 03 MySQL · 社区动态 · MySQL5.6.26 Release Note解读 # 04 PgSQL · 捉虫动态 · 执行大SQL语句提示无效的内存申请大小 # 05 MySQL · 社区动态 · MariaDB InnoDB表空间碎片整理 # 06 PgSQL · 答疑解惑 · 归档进程cp命令的core

阿里数据库内核月报:2015年07月

# 01 MySQL · 引擎特性 · Innodb change buffer介绍 # 02 MySQL · TokuDB · TokuDB Checkpoint机制 # 03 PgSQL · 特性分析 · 时间线解析 # 04 PgSQL · 功能分析 · PostGIS 在 O2O应用中的优势 # 05 MySQL · 引擎特性 · InnoDB index lock前世今生 # 06 MySQL · 社区动态 · MySQL内存分配支持NUMA # 07 MySQL · 答疑解惑 · 外

阿里数据库内核月报:2015年06月

# 01 MySQL · 引擎特性 · InnoDB 崩溃恢复过程 # 02 MySQL · 捉虫动态 · 唯一键约束失效 # 03 MySQL · 捉虫动态 · ALTER IGNORE TABLE导致主备不一致 # 04 MySQL · 答疑解惑 · MySQL Sort 分页 # 05 MySQL · 答疑解惑 · binlog event 中的 error code # 06 PgSQL · 功能分析 · Listen/Notify 功能 # 07 MySQL · 捉虫动态 · 任性的