InnoDB页压缩技术
Ⅰ、想起一个报错
1.1 创建表报错
(root@localhost) [(none)]> create tablespace ger_space add datafile 'ger_space.ibd' file_block_size=8192;
Query OK, 0 rows affected (0.02 sec)
(root@localhost) [(none)]> create database test;
Query OK, 1 row affected (0.01 sec)
(root@localhost) [(none)]> create table test_ger(a int) tablespace=ger_space;
(root@localhost) [(none)]> use test;
Database changed
(root@localhost) [test]> create table test_ger(a int) tablespace=ger_space;
ERROR 1478 (HY000): InnoDB: Tablespace `ger_space` uses block size 8192 and cannot contain a table with physical page size 16384
2.1 解决
(root@localhost) [test]> create table test_ger(a int) tablespace=ger_space row_format=compressed, key_block_size=8;
Query OK, 0 rows affected (0.14 sec)
amazing!!!(~﹃~)~zZ
这里用到一个压缩page的技术,本章我们讨论一下InnoDB的页压缩
Ⅱ、传统压缩方式(from 5.5)
2.1 原理——伙伴算法
- 磁盘中存放压缩页(row_format=compressed),假设key_block_size=8K,Buffer Pool的页大小是16K
- 向Free List中申请空闲的页,如果没有空闲页,则向LRU List申请页,如果LRU满了,则找LRU中最后的一个页,如果最后的页是脏页,则做flush操作,最后得到一个空白的页(16K)
- 该16k的空白页,就给8K的压缩页使用,这样就多出一个8K的空间 ,该空间会移到8K的Free List中去
- 如果有一个4K的压缩页,就把8K的Free list中的空白页给他用,然后多余的4K的空间移到4K的Free List中去
注意点:
- 这种压缩是基于页的,每个表的页大小可以不同,压缩算法是L777
- 当用户获取数据时,如果压缩的页没有在Innodb_Buffer_Pool缓冲池里,那么会从磁盘加载进去,并且会在Innodb_Buffer_Pool缓冲池里开辟一个新的未压缩的16KB的数据页来解压缩,为了减少磁盘I/O以及对页的解压操作(更快地查询),在缓冲池里同时存在着被压缩的和未压缩的页
- 为了给其他需要的数据页腾出空间,缓冲池里会把未压缩的数据页踢出去,而保留压缩的页在内存中,如果未压缩的页在一段时间内没有被访问,那么会直接刷入磁盘中,因此缓冲池中可能有压缩和未压缩的页,也可能只有压缩页
- 压缩页保留的原因是为了在更新数据的时候,将redo添加到压缩页的空闲部分,如果要刷回磁盘,可以直接将该压缩页刷回去,如果该页被写满,则做一次reorganize操作(在此之前也要做解压),真的写满了才做分裂
缺点:
- 压缩页占用了Buffer Pool的空间,对于热点数据来说,相当于内存小了,可能造成性能下降(热点空间变小)
- 所以在开启了压缩后,Buffer Pool的空间要相应增大
- 如果启用压缩后节省的磁盘IO能够抵消掉Buffer Pool空间变小所带来的性能下降,那整体性能还是会上涨,所以Buffer Pool要尽可能大
2.2 玩两手
直接创建
(root@localhost) [test]> create table comps_test(a int) row_format=compressed, key_block_size=4;
Query OK, 0 rows affected (0.04 sec)
对已存在的表启用压缩,并且页大小为4k,
alter table xxxxx
engine=innodb
row_format=compressed,key_block_size=4
可以设置为1 2 4 8 16
操作须知:
指定row_format=compressed,则可忽略key_block_size的值,这时使用默认innodb页的一半,即8kb
指定key_block_size的值,则可忽略row_format=compressed,会自动启用压缩
0代表默认压缩页的值,Innodb页的一半
key_block_size的值只能小于等于innodb page size,若指定了一个大于innodb page size的值,mysql会忽略这个值然后产生一个警告,这时key_block_size的值是Innodb页的一半
若设置了innodb_strict_mode=ON,那么指定一个不合法的key_block_size的值是返回报错
tips:
虽然SQL语法中写的是row_format=compressed,但是压缩是针对页的,而不是记录,即读页的时候解压,写页的时候压缩,并不会在读取或写入单个记录(row)时就进行解压或压缩操作
2.3 细说key_block_size
- key_block_size的可选项是1k,2k,4k,8k,16k(是页大小,不是比例)
- 不是将原来innodb_page_size页大小的数据压缩成key_block_size的页大小,因为有些数据可能不能压缩,或者压缩不到那么小
- 压缩是将原来的页的数据通过压缩算法压缩到一定的大小,然后用key_block_size大小的页去存放
- 比如原来的innodb_page_size大小是16K,现在的key_block_size设置为8K,某表的数据大小是24k,原先使用2个16k的页存放,压缩后数据从24k变为18k,由于现在的key_block_size=8k,所以需要3个8K的页存放压缩后的18k数据,多余的空间可以留给下次插入或者更新
- 压缩比和设置的key_block_size没有关系,压缩比看数据本身和算法,key_block_size仅仅是设置存放压缩数据的页大小
tips:
不解压也能插入数据,通过在剩余空间直接存放redo log,然后页空间存放满后,再解压,利用redo log更新完成后,最后再压缩存放(此时就没有redo log 了)以此来减少解压和压缩的次数
2.4 重点须知
并不是key_block_size越小,压缩比越高,只是页的大小发生了修改
压缩过程,16k的页压8k,先判断能不能压,可以就存为8k,压完大于8k,比如12k,这时候就会存为两个8k
16k压到8k成功率在80%~90%,但是再压就不能保证了
2.5 查看缓冲池中的压缩页
(root@localhost) [(none)]> SELECT
-> table_name,
-> space,
-> page_number,
-> index_name,
-> compressed,
-> compressed_size
-> FROM
-> information_schema.innodb_buffer_page_lru
-> WHERE
-> compressed = 'yes'
-> LIMIT 5;
+------------+-------+-------------+------------+------------+-----------------+
| table_name | space | page_number | index_name | compressed | compressed_size |
+------------+-------+-------------+------------+------------+-----------------+
| NULL | 168 | 502 | NULL | YES | 4096 |
| NULL | 168 | 505 | NULL | YES | 4096 |
| NULL | 168 | 508 | NULL | YES | 4096 |
| NULL | 168 | 510 | NULL | YES | 4096 |
| NULL | 168 | 513 | NULL | YES | 4096 |
+------------+-------+-------------+------------+------------+-----------------+
5 rows in set (0.00 sec)
(root@localhost) [(none)]> SELECT
-> table_id, name, space, row_format, zip_page_size
-> FROM
-> information_schema.INNODB_SYS_TABLES
-> WHERE
-> space = 168;
+----------+----------------+-------+------------+---------------+
| table_id | name | space | row_format | zip_page_size |
+----------+----------------+-------+------------+---------------+
| 173 | sbtest/sbtest1 | 168 | Compressed | 4096 |
+----------+----------------+-------+------------+---------------+
1 row in set (0.00 sec)
(root@localhost) [(none)]> show create table sbtest.sbtest1\G
*************************** 1. row ***************************
Table: sbtest1
Create Table: CREATE TABLE `sbtest1` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`k` int(10) unsigned NOT NULL DEFAULT '0',
`c` char(120) NOT NULL DEFAULT '',
`pad` char(60) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k_1` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=10000001 DEFAULT CHARSET=latin1 MAX_ROWS=1000000 ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4
1 row in set (0.00 sec)
一路摸下来,果然是个压缩表
再看下压缩页在bp中的存储
(root@localhost) [(none)]> show engine innodb status\G
...
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 137428992
Dictionary memory allocated 127372
Buffer pool size 8191
Free buffers 7759
Database pages 632
Old database pages 253
Modified db pages 0
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 597, created 35, written 42
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No buffer pool page gets since the last printout
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 632, unzip_LRU len: 4 压缩页在bp中的长度是4
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
...
2.6 查看压缩比
查看压缩比,看information_schema.innodb_cmp表
这个表里面的数据是累加的,是全局信息,没法对应到某一张表,查它之前先查另一张表来清空此表
select * from information_schema.innodb_cmp_reset;
把innodb_cmp表中的数据复制过来,并清空innodb_cmp,此处不展示结果
玩起来了
(root@localhost) [emp]> create table emp_comp like emp;
Query OK, 0 rows affected (0.26 sec)
(root@localhost) [emp]> alter table emp_comp row_format=compressed,key_block_size=4;
Query OK, 0 rows affected (0.23 sec)
Records: 0 Duplicates: 0 Warnings: 0
(root@localhost) [emp]> show create table emp_comp\G
*************************** 1. row ***************************
Table: emp_comp
Create Table: CREATE TABLE `emp_comp` (
`emp_no` int(11) NOT NULL,
`birth_date` date NOT NULL,
`first_name` varchar(14) NOT NULL,
`last_name` varchar(16) NOT NULL,
`gender` enum('M','F') NOT NULL,
`hire_date` date NOT NULL,
PRIMARY KEY (`emp_no`),
KEY `ix_firstname` (`first_name`),
KEY `ix_3` (`emp_no`,`first_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4
1 row in set (0.04 sec)
(root@localhost) [emp]> insert into emp_comp select * from emp;
Query OK, 300024 rows affected (23.13 sec)
Records: 300024 Duplicates: 0 Warnings: 0
看压缩比咯
(root@localhost) [emp]> select * from information_schema.innodb_cmp;
+-----------+--------------+-----------------+---------------+----------------+-----------------+
| page_size | compress_ops | compress_ops_ok | compress_time | uncompress_ops | uncompress_time |
+-----------+--------------+-----------------+---------------+----------------+-----------------+
| 1024 | 0 | 0 | 0 | 0 | 0 |
| 2048 | 0 | 0 | 0 | 0 | 0 |
| 4096 | 34296 | 27184 | 9 | 7743 | 0 |
| 8192 | 0 | 0 | 0 | 0 | 0 |
| 16384 | 0 | 0 | 0 | 0 | 0 |
+-----------+--------------+-----------------+---------------+----------------+-----------------+
5 rows in set (0.69 sec)
(root@localhost) [emp]> select 27184/34296; # compress_ops_ok/compress_ops
+-------------+
| 27184/34296 |
+-------------+
| 0.7926 |
+-------------+
1 row in set (0.11 sec)
压缩比为79.26%
看下物理存储
[root@VM_0_5_centos emp]# ll -h *.ibd
-rw-r----- 1 mysql mysql 40M Feb 27 19:01 emp.ibd
-rw-r----- 1 mysql mysql 20M Feb 27 19:36 emp_comp.ibd
能看出来表空间小了很多,但是不是79.26%,这里有时间需要理解一下
打开一个参数,可以看到每个表的压缩情况,此参数默认不开,会影响性能
(root@localhost) [emp]> set global innodb_cmp_per_index_enabled=1;
Query OK, 0 rows affected (0.09 sec)
重复上面的测试,这次压到2k,过程省略只看结果
(root@localhost) [emp]> select * from information_schema.innodb_cmp;
+-----------+--------------+-----------------+---------------+----------------+-----------------+
| page_size | compress_ops | compress_ops_ok | compress_time | uncompress_ops | uncompress_time |
+-----------+--------------+-----------------+---------------+----------------+-----------------+
| 1024 | 0 | 0 | 0 | 0 | 0 |
| 2048 | 68793 | 52455 | 12 | 18353 | 1 |
| 4096 | 0 | 0 | 0 | 0 | 0 |
| 8192 | 0 | 0 | 0 | 0 | 0 |
| 16384 | 0 | 0 | 0 | 0 | 0 |
+-----------+--------------+-----------------+---------------+----------------+-----------------+
5 rows in set (0.00 sec)
(root@localhost) [emp]> select * from information_schema.innodb_cmp_per_index;
+---------------+------------+--------------+--------------+-----------------+---------------+----------------+-----------------+
| database_name | table_name | index_name | compress_ops | compress_ops_ok | compress_time | uncompress_ops | uncompress_time |
+---------------+------------+--------------+--------------+-----------------+---------------+----------------+-----------------+
| emp | emp_comp | PRIMARY | 34676 | 23729 | 4 | 11053 | 0 |
| emp | emp_comp | ix_firstname | 20958 | 18349 | 5 | 4384 | 0 |
| emp | emp_comp | ix_3 | 13159 | 10377 | 2 | 2916 | 0 |
+---------------+------------+--------------+--------------+-----------------+---------------+----------------+-----------------+
3 rows in set (0.00 sec)
(root@localhost) [emp]> select 52455/68793;
+-------------+
| 52455/68793 |
+-------------+
| 0.7625 |
+-------------+
1 row in set (0.06 sec)
(root@localhost) [emp]> select (23729+18349+10377)/(34676+20958+13159);
+-----------------------------------------+
| (23729+18349+10377)/(34676+20958+13159) |
+-----------------------------------------+
| 0.7625 |
+-----------------------------------------+
1 row in set (0.00 sec)
可以直接看到emp.emp_comp这个表的压缩比(innodb索引即数据)
当只有一个表的时候innodb_cmp等于innodb_cmp_per_index
2.6 压缩后的存储和性能
一个问题,压缩时将key_block_size设置为innodb_page_size有没有意义
答:有意义,key_block_size的设置不影响压缩本身(只和数据本身及压缩算法有关),只是确定压缩后的数据存放的页大小,varchar、text等数据类型这么压效果还是很明显的
压缩后的存储和性能呢?
github放图好麻烦,大概弄个差不多的图将就下
结论:
- inodb_page_size=16k的数据设置key_block_size=16是可以压缩的,且效果比较明显
- 并不是key_block_size设置的越小,压缩率就越高,上图中8K和4K的压缩率几乎一样
- 在启用压缩后,16K和8K的插入性能要好于原来未压缩的插入性能,所以启用了压缩,性能不一定会变差
- 在I/O Bound(IO密集型)的业务场景下,减少I/O操作的次数对性能提升比较明显
- key_block_size的设置的值(经验值)通常为innodb_page_size的1/2
Ⅲ、transparent page compression(from 5.7)
3.1 先玩
(root@localhost) [test]> create table trans_test1(a int) compression='zlib';
Query OK, 0 rows affected, 1 warning (0.04 sec)
(root@localhost) [test]> create table trans_test2(a int) compression='lz4';
Query OK, 0 rows affected, 1 warning (0.06 sec)
(root@localhost) [test]> alter table trans_test1 compression='lz4';
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
alter table会很快,因为它不会真的改,只是下次压缩的时候才会用,不会重新压,如果非要马上生效则需要optimize
(root@localhost) [test]> optimize table trans_test1;
+------------------+----------+----------+-------------------------------------------------------------------+
| Table | Op | Msg_type | Msg_text |
+------------------+----------+----------+-------------------------------------------------------------------+
| test.trans_test1 | optimize | note | Table does not support optimize, doing recreate + analyze instead |
| test.trans_test1 | optimize | status | OK |
+------------------+----------+----------+-------------------------------------------------------------------+
2 rows in set, 1 warning (0.23 sec)
(root@localhost) [test]> show warnings;
+---------+------+---------------------------------------------------------------------------------------------+
| Level | Code | Message |
+---------+------+---------------------------------------------------------------------------------------------+
| Warning | 138 | Punch hole is not supported by the file system. Compression disabled for 'test/trans_test1' |
+---------+------+---------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
错误日志中有下面这段:
2017-04-22T19:20:14.168298+8:00 0 [Note] InnoDB: PUNCH HOLE support not available
换言之,5.7现在还不能用这个功能,二进制包编译的时候没把PUNCH HOLE编译进去
怎么解决?
自己编译源码,带上PUNCH HOLE,或者用percona
MySQL8.0的时候就完美availabe,可以去看看,亲测
create table a ( a int ) compression='lz4';
Query OK, 0 rows affected (0.03 sec)
tips:
5.7.19二进制包已经把做进去了,超赞
经测试,貌似ext3不支持punch hole
lz4和zlib什么区别?
lz4更快,zlib压缩比更高
通常选择lz4,更快,也能压到一半,够用了,hadoop平台很多数据库默认就用lz4
看表空间的文件系统相关内容:
(root@localhost) [(none)]> SELECT
-> space, page_size, fs_block_size, file_size, allocated_size
-> FROM
-> information_schema.INNODB_SYS_TABLESPACES
-> LIMIT 5;
+-------+-----------+---------------+-----------+----------------+
| space | page_size | fs_block_size | file_size | allocated_size |
+-------+-----------+---------------+-----------+----------------+
| 2 | 16384 | 4096 | 98304 | 102400 |
| 3 | 16384 | 4096 | 98304 | 102400 |
| 4 | 16384 | 4096 | 9437184 | 9453568 |
| 5 | 16384 | 4096 | 114688 | 118784 |
| 6 | 16384 | 4096 | 147456 | 151552 |
+-------+-----------+---------------+-----------+----------------+
5 rows in set (0.00 sec)
表空间id page大小 文件系统块大小 文件大小 文件实际分配大小
3.2 再看原理(Hole Punch Size)
细心的朋友会发现,以上整个过程并未指定页大小
这里是是使用了文件系统(filesystem)层中稀疏文件的特性,来达到压缩的目的(文件系统空洞)
1、文件的大小与占用空间:
[root@VM_0_5_centos ~]# dd of=spare-file bs=1k seek=5120 count=0 #创建数据全为0的临时文件
0+0 records in
0+0 records out
0 bytes (0 B) copied, 4.1441e-05 s, 0.0 kB/s
[root@VM_0_5_centos ~]# ls -lh spare-file
-rw-r--r-- 1 root root 5.0M Feb 28 10:53 spare-file #文件大小5M
[root@VM_0_5_centos ~]# du --block-size=1 spare-file
0 spare-file #文件占用空间0M
文件中数据连续为0的部分不占用磁盘空间
2、文件系统的空洞特性 :
- 一个16k的页,前面数据占了4k,后面填0,被填充的12K的空间,可以提供给后序的插入,更新等使用
- 从innodb的角度看还是16K的页大小,只是文件系统知道该页只需要4K就能够存储(对innodb是透明的)
- SpaceID和PageNumber的读取方式没有改变(细节由文件系统屏蔽)
fopen(f,o_direct|o_punch_hole)
fwrite(f,page) 这时候这个page在磁盘上就是4k的大小
3、TPC过程如下:
对一个页写入磁盘之前先在内存进行压缩,压缩完,前面一部分是压缩的数据,后面填0,再调用fwrite,如果开了punch hole,这时候在磁盘上就实现了压缩
压缩大小是根据文件系统的块大小进行对齐,默认4k,所以16k只能压缩成4,8,16,如果压缩完是6k,那就占用8k
4、新老压缩算法对比:
-
TPC是调用文件系统层的punch hole,只是写入之前先压缩填0,简洁高效;老的压缩需要指定key_block_size,老算法数据在bp中会占两个空间,一种是压缩的版本,一种是非压缩的版本,更新一个page,两种page都要更新,需要额外开销,较复杂,所以性能时好时坏
-
TPC的情况下disk每个页大小16k,实际上可能只有4k,8k或者12k,管理依然根据16k来管理
-
TPC的情况下,在bp里面一个disk page对应一个16k的page,因为在第一次读取的时候从磁盘上读(space,page_no),读完解压,解压完肯定是16k的,在bp里面就只占用一个空间
3.3 性能相关问题
官方测试:
122G,ssd跑ext4
老算法压缩后,storage减小40%,QPS下降20%,还能接受,1w3降到1w出头,最重要的还是sql的性能
新算法,storge减小40%不到,QPS涨到了1w8,提升了30%,读一个块的大小,变小,节省了io操作的时间,cpu的时间在第一次读取的时候做一次解压,写入的时候做一次压缩,并不是每写一条记录压缩解压。
装载测试:
老算法导入速度慢了50%,新算法和不压缩差不多