InnoDB 提供了缓冲池(Buffer Pool)机制,缓存表数据与索引数据,把磁盘上的数据加载到缓冲池中,避免每次访问都进行磁盘 IO,起到加速访问的作用。
InnoDB 的缓冲池是以页为单位存储的,且页大小和 innodb_page_size 一致。
Buffer Pool 介绍
Buffer Pool 参数
- innodb_buffer_pool_size:设置 Buffer Pool 总容量。
- 在内存允许的情况下,该值设置的越大越好,Buffer Pool 容量的大小对 MySQL 的性能影响很大。
- innodb_buffer_pool_instances:设置 Buffer Pool 实例个数。
- 所有实例的总容量还是 innodb_buffer_pool_size,只是拆分成多个实例,建议设置成 CPU 个数,设置大了也没什么伤害。
- 设置该参数的好处是什么呢?主要是因为数据库的操作首先访问的是 Buffer Pool,它是一块热点区域,多线程访问 Buffer Pool 会存在一定的竞争,把一个 Buffer Pool 拆分成多个 Buffer Pool 有利于减少竞争,将热点打散,提高并发性能。
- 正如上面所说的,该参数对 MySQL 的性能影响也较大。
- innodb_old_blocks_pct
- innodb_old_blocks_time
- 这两个参数在分析 Buffer Pool LRU 算法的时候会描述。
- innodb_buffer_pool_dump_at_shutdown
- innodb_buffer_pool_dump_now
- innodb_buffer_pool_dump_pct
- innodb_buffer_pool_filename
- innodb_buffer_pool_load_abort
- innodb_buffer_pool_load_at_startup
- innodb_buffer_pool_load_now
- 这部分参数在分析 Buffer Pool 预热的时候会描述。
Buffer Pool 性能
先来看一张 sysbench 的压测图:
该图测试的是 TPS(每秒事务数),实际 QPS 过万。虽然这张图不能代表实际生产环境 Buffer Pool 的性能,但是也反映出 Buffer Pool 对 MySQL 性能的巨大影响。
在实际生产环境中,根据数据的容量,物理机的性能,Buffer Pool 设置的越大,MySQL 的性能越好。
Buffer Pool 状态
第一种方式:使用 show engine innodb status\G
命令查看 Buffer Pool 状态。
mysql> show engine innodb status\G
-- ---------------省略其他输出-----------------
---BUFFER POOL 0
Buffer pool size 16383 -- 该Buffer Pool中有多少个页
Free buffers 16357 -- 该Buffer Pool中有多少个空白页(Free List),线上可能看到为0
Database pages 41 -- 该Buffer Pool中使用了多少页(LRU List)
Old database pages 0 -- old pages(见3.4)
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 -- young表示old-->new的状态
Pages read 41, created 0, written 20
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: 41, unzip_LRU len: 0 I/O sum[0]:cur[0], unzip sum[0]:cur[0]
-- ---------------省略其他输出-----------------
第二种方式:使用元数据表 information_schema.INNODB_BUFFER_POOL_STATS 查看 Buffer Pool 状态。
mysql> select * from information_schema.INNODB_BUFFER_POOL_STATS\G
*************************** 1. row ***************************
POOL_ID: 0
POOL_SIZE: 16383 -- 该Buffer Pool中有多少个页
FREE_BUFFERS: 16357 -- 该Buffer Pool中有多少个空白页(Free List),线上可能看到为0
DATABASE_PAGES: 41 -- 该Buffer Pool中使用了多少页(LRU List)
OLD_DATABASE_PAGES: 0 -- old pages (见3.4)
MODIFIED_DATABASE_PAGES: 0 -- 脏页
PENDING_DECOMPRESS: 0
PENDING_READS: 0
PENDING_FLUSH_LRU: 0
PENDING_FLUSH_LIST: 0
PAGES_MADE_YOUNG: 0
PAGES_NOT_MADE_YOUNG: 0
PAGES_MADE_YOUNG_RATE: 0
PAGES_MADE_NOT_YOUNG_RATE: 0
NUMBER_PAGES_READ: 41
NUMBER_PAGES_CREATED: 0
NUMBER_PAGES_WRITTEN: 20
PAGES_READ_RATE: 0
PAGES_CREATE_RATE: 0
PAGES_WRITTEN_RATE: 0
NUMBER_PAGES_GET: 1041
HIT_RATE: 0
YOUNG_MAKE_PER_THOUSAND_GETS: 0
NOT_YOUNG_MAKE_PER_THOUSAND_GETS: 0
NUMBER_PAGES_READ_AHEAD: 0
NUMBER_READ_AHEAD_EVICTED: 0
READ_AHEAD_RATE: 0
READ_AHEAD_EVICTED_RATE: 0
LRU_IO_TOTAL: 0
LRU_IO_CURRENT: 0
UNCOMPRESS_TOTAL: 0
UNCOMPRESS_CURRENT: 0
-- ---------------省略其他输出-----------------
mysql> select * from information_schema.INNODB_BUFFER_PAGE_LRU limit 1\G
*************************** 1. row ***************************
POOL_ID: 0
LRU_POSITION: 0
SPACE: 0 -- space id 表空间号
PAGE_NUMBER: 7 -- 对应的页号
PAGE_TYPE: SYSTEM
FLUSH_TYPE: 1
FIX_COUNT: 0
IS_HASHED: NO
NEWEST_MODIFICATION: 4005630175 -- 该页最近一次(最新)被修改的LSN值
OLDEST_MODIFICATION: 0 -- 该页在Buffer Pool中第一次被修改的LSN值,FLushList是根据该值进行排序的
-- 该值越小,表示该页应该最先被刷新
ACCESS_TIME: 729305074
TABLE_NAME: NULL
INDEX_NAME: NULL
NUMBER_RECORDS: 0
DATA_SIZE: 0
COMPRESSED_SIZE: 0
COMPRESSED: NO
IO_FIX: IO_NONE
IS_OLD: NO
FREE_PAGE_CLOCK: 0
1 row in set (0.01 sec)
-----------------省略其他输出-----------------
Buffer Pool 在线调整
从 MySQL 5.7 开始,可以在线修改 innodb_buffer_pool_size
。
mysql> show variables like "%innodb_buffer_pool_size%"; +-------------------------+------------+
| Variable_name | Value |
+-------------------------+------------+
| innodb_buffer_pool_size | 1073741824 |
+-------------------------+------------+
1 row in set (0.00 sec)
mysql> set global innodb_buffer_pool_size=2*1024*1024*1024; -- 扩大
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like "%innodb_buffer_pool_size%"; +-------------------------+------------+
| Variable_name | Value |
+-------------------------+------------+
| innodb_buffer_pool_size | 2147483648 |
+-------------------------+------------+
1 row in set (0.00 sec)
mysql> set global innodb_buffer_pool_size=1*1024*1024*1024; -- 缩小,没修改的页被丢弃,修改的需要刷回磁盘
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like "%innodb_buffer_pool_size%"; +-------------------------+------------+
| Variable_name | Value |
+-------------------------+------------+
| innodb_buffer_pool_size | 1073741824 |
+-------------------------+------------+
1 row in set (0.01 sec)
扩大 Buffer Pool 容量比较方便,直接扩大就可以了,但是缩小 Buffer Pool 容量就相对比较复杂了,对于没有修改的页,直接丢弃,对于修改过的脏页数据,则需要将脏页数据刷回磁盘。
在 MySQL 5.7 之前的版本,修改 innodb_buffer_pool_size
,需要重启 MySQL。
Buffer Pool 原理
Buffer Pool 的组成
- Free List:空闲页列表,Buffer Pool 刚启动时,有一个个16K的空白页,这些页就存放(链表串联)在 Free List 中。
- LRU List:当读取一个数据页的时候,就从 Free List 中取出一个页,存入数据,并将该页放入到 LRU List 中。
- Flush List:
- 当 LRU List 中的页第一次被修改了,就将该页的指针(Page Number)放入到 Flush List 中,意味着后面不管会不会再次被修改,都不会再放入 Flush List 中,因为它已经存在 Flush List 中了。
- Flush List 即脏页(数据经过修改,但是未刷入磁盘的页)列表。
- Flush List 中存放的不是一个页,而是页的指针(Page Number)。
可以通过元数据表 information_schema.INNODB_BUFFER_PAGE_LRU,实时查看 Buffer Pool 中 Flush List 的数据。注意:不要在产线上操作该 SQL 语句,开销较大。
SELECT
pool_id,
lru_position,
space,
page_number,
table_name,
oldest_modification,
newest_modification
FROM
information_schema.INNODB_BUFFER_PAGE_LRU
WHERE
oldest_modification <> 0
AND oldest_modification <> newest_modification; -- 如果没有脏页,结果集为空
Midpoint LRU 算法
传统的 LRU 算法是,当该页被第一次读取时,将该页放到 LRU List 的头部,最为最近访问的元素,这里又分两种情况:
- 页已经在缓冲池里,那就只做“移至”LRU头部的动作,而没有页被淘汰。
- 页不在缓冲池里,除了做“放入”LRU头部的动作,还要做“淘汰”LRU尾部页的动作。
如上图,假如管理缓冲池的LRU长度为10,缓冲了页号为1,3,5…,40,7的页。
假如,接下来要访问的数据在页号为4的页中:
- 页号为4的页,本来就在缓冲池里。
- 把页号为4的页,放到 LRU 的头部即可,没有页被淘汰。
为了减少数据移动,LRU 一般用链表实现。
假如,再接下来要访问的数据在页号为50的页中:
- 页号为50的页,原来不在缓冲池里。
- 把页号为50的页,放到 LRU 头部,同时淘汰尾部页号为7的页。
MySQL 没有直接使用传统的 LRU 算法,而是对它做了改变,使用了 Midpoint LRU 算法。
具体实现方法是:
- 将 LRU 分为两个部分:新生代(new sublist)和老生代(old sublist)。
- 新老生代收尾相连,即:新生代的尾(tail)连接着老生代的头(head)。
innodb_old_blocks_pct
参数用来控制 mid point 的位置,默认是 37 ,即 3/8 的位置。
Midpoint LRU 算法执行过程:
- 当该页被第一次读取时,将该页先放在 mid point 的位置(因为无法保证一定是活跃)。
- 当改业被第二次读取时,才将改页放入到 new sublist 的首部。
- 当不断有新的页被加入到 LRU List 中,则老的数据会慢慢从 new sublist 挤到 old sublist 中,直到最后淘汰出 LRU List,如果被淘汰的页是脏页,则需要先刷回磁盘,在把 LRU List 的空余空间给新查询出的页使用。
那么为什么 MySQL 需要使用 Midpoint LRU 算法呢?
因为这里存在两个问题:
- 预读失效
- 缓冲池污染
先来看看什么是预读?
磁盘读写,并不是按需读取,而是按页读取,一次至少读一页数据(一般是 16K),如果未来要读取的数据就在页中,就能够省去后续的磁盘 IO,提高效率。
预读为什么有效?
数据访问,通常都遵循“集中读写”的原则,使用一些数据,大概率会使用附近的数据,这就是所谓的“局部性原理”,它表明提前加载是有效的,确实能够减少磁盘 IO。
这说明 InnoDB 会根据预读机制将一些“可能要访问的页”提前加载到 Buffer Pool 中,避免未来多次磁盘 IO 操作。
再来看看什么是预读失效?
由于预读(Read-Ahead),提前把页放入了缓冲池,但最终 MySQL 并没有从页中读取数据,称为预读失效。
Buffer Pool 的 Midpoint LRU 算法是如何解决预读失效问题的?
假如有一个页号为50的新页被预读加入缓冲池中:
- 50只会从老生代头部插入,老生代尾部(也是整体尾部)的页会被淘汰掉。
- 假设50这一页不会被真正读取,即预读失败,它将比新生代的数据更早淘汰出缓冲池。
假如50这一页立刻被读取到,例如 SQL 访问了页内的行 row 数据:
- 它会被立刻加入到新生代的头部。
- 新生代的页会被挤到老生代,此时并不会有页面被真正淘汰。
这样改进版的 Midpoint LRU 算法就能很好的解决预读失效的问题。
但是改进版的 Midpoint LRU 算法仍然解决不了缓冲池污染的问题。
什么是 MySQL 缓冲池污染?
当某一个 SQL 语句,要批量扫描大量数据时,可能导致把缓冲池的所有页都替换出去,导致大量热数据被换出,MySQL 性能急剧下降,这种情况叫缓冲池污染。
例如,有一个数据量较大的用户表,当执行:select * from user
执行该条语句,会读取大量的页,且很可能每页都会被读取多次(该页可能被读取了两次以上,引文一页有多条数据,读取一条记录,就需要读一次页)结果就导致大量的页被放到 new sublist 中(尽管其实每条记录只会被读取一次),而真正的热数据被淘汰掉。
怎么解决 MySQL 缓冲池污染的问题呢?
MySQL 缓冲池引入了一个“老生代停留时间窗口”的机制:
- 假设 T=老生代停留时间窗口。
- 插入老生代头部的页,即使立刻被访问,并不会立刻放入新生代头部。
- 只有满足“被访问”并且“在老生代停留时间”大于 T 的页才会被放入到新生代的头部。
通过配置 innodb_old_blocks_time
参数实现。
mysql> show variables like "%innodb_old_blocks_time%";
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| innodb_old_blocks_time | 1000 | -- 设置为1s
+------------------------+-------+
1 row in set (0.00 sec)
- 当该页被第一次读取时,将该页放在 mid point 位置,但是随后无论你读多少次,我在这
innodb_old_blocks_time
的时间内都不管(都视作只读取了一次),等这个时间过去了,如果该页还是被读取了,我才把这个页放到 new sublist 的首部。 - 通常 select * 扫描操作不会高于1秒,一个页很快就被扫完了。
假如批量数据扫描,有51,52,53,54,55等五个页面将要依次被访问。
如果没有“老生代停留时间窗口”的策略,这些批量被访问的页面,会换出大量热数据。
加入“老生代停留时间窗口”策略后,短时间内被大量加载的页,并不会立刻插入新生代头部,而是优先淘汰那些,短期内仅仅访问了一次的页。
而只有在老生代呆的时间足够久,停留时间大于T,才会被插入新生代头部。
Buffer Pool 预热
在 MySQL 5.6 之前,MySQL 启动后,Buffer Pool 中页的数据是空的,需要大量的时间才能把磁盘中的页读入到内存中,导致启动后的一段时间性能很差。
在 MySQL 5.6 以后,可以在正常停机的时候 dump 出 Buffer Pool 的数据(space,page number),然后在启动的时候 Load 进 Buffer Pool。该功能可以让 MySQL 启动时自动预热,无需人工干预。
关于 Buffer Pool 预热的一些参数及描述:
mysql> show variables like "%innodb_buffer_pool%";
+-------------------------------------+----------------+
| Variable_name | Value |
+-------------------------------------+----------------+
| innodb_buffer_pool_chunk_size | 134217728 |
| innodb_buffer_pool_dump_at_shutdown | ON | -- 在停机时dump出buffer pool中的(space,page)
| innodb_buffer_pool_dump_now | OFF | -- set 一下,表示现在就从buffer pool中dump
| innodb_buffer_pool_dump_pct | 40 | -- dump的百分比,是每个buffer pool文件,而不是整体
| innodb_buffer_pool_filename | ib_buffer_pool | -- dump出的文件的名字
| innodb_buffer_pool_instances | 8 |
| innodb_buffer_pool_load_abort | OFF |
| innodb_buffer_pool_load_at_startup | ON | -- 启动时加载dump的文件,恢复到buffer pool中
| innodb_buffer_pool_load_now | OFF | -- set一下,表示现在加载 dump的文件
| innodb_buffer_pool_size | 1073741824 |
+-------------------------------------+----------------+
10 rows in set (0.00 sec)
查看 dump 出来的文件内容:
shell> head ib_buffer_pool # dump出来的文件
120,3
120,2
120,1
120,0
106,4539
106,4538
106,4537
106,4536
106,4535
106,4534
测试:
# mysql 启动
sehll> cat error.log
## ---------------省略其他输出-----------------
2016-01-10T20:17:31.263901+08:00 0 [Note] InnoDB: Loading buffer pool(s) from /data/mysql_data/5.7/ib_buffer_pool
## ---------------省略其他输出-----------------
2016-01-10T20:17:31.365768+08:00 0 [Note] InnoDB: Buffer pool(s) load completed at 160110 20:17:31 # 速度还是很快的
# mysql 停机
shell> cat error.log
## ---------------省略其他输出-----------------
2016-01-11T08:47:20.067308+08:00 0 [Note] InnoDB: Dumping buffer pool(s) to /data/mysql_data/5.7/ib_buffer_pool
2016-01-11T08:47:20.067730+08:00 0 [Note] InnoDB: Buffer pool(s) dump completed at 160111 8:47:20
## ---------------省略其他输出-----------------
# 查看当前buffer pool中的数据的条数
shell> wc -l ib_buffer_pool
524 ib_buffer_pool
mysql> set global innodb_buffer_pool_dump_now=1;
Query OK, 0 rows affected (0.00 sec)
mysql> show status like 'Innodb_buffer_pool_dump_status';
+--------------------------------+--------------------------------------------------+
| Variable_name | Value |
+--------------------------------+--------------------------------------------------+
| Innodb_buffer_pool_dump_status | Buffer pool(s) dump completed at 160111 9:04:21 |
+--------------------------------+--------------------------------------------------+
1 row in set (0.00 sec)
-- 已经完成
mysql> wc -l ib_buffer_pool
524 ib_buffer_pool # 发现和原来一致,并没有减少为原来的40,似乎和innodb_buffer_pool_dump_pct设置的不符合
-- 由于innodb_buffer_pool_dump_pct是针对每个buffer pool的百分比,应该是由于我的数据太小,没有达到该比例(40%),所以这里全部dump出来了
-- 测试的时候可以设置小一点
mysql> set global innodb_buffer_pool_dump_pct=2; -- 设置成2%
Query OK, 0 rows affected (0.00 sec)
mysql> set global innodb_buffer_pool_dump_now=1;
Query OK, 0 rows affected (0.00 sec)
mysql> show status like 'Innodb_buffer_pool_dump_status';
+--------------------------------+--------------------------------------------------+
| Variable_name | Value |
+--------------------------------+--------------------------------------------------+
| Innodb_buffer_pool_load_status | Buffer pool(s) load completed at 160111 9:05:38 |
+--------------------------------+--------------------------------------------------+
1 row in set (0.00 sec)
shell> wc -l ib_buffer_pool # dump前
524 ib_buffer_pool
shell> wc -l ib_buffer_pool # dump后
23 ib_buffer_pool
- dump 出的数据越多,启动的越慢。
- 频繁的手工 dump(set innodb_buffer_pool_dump_now = 1),会导致 Buffer Pool 中的数据越来越少,是因为设置了 innodb_buffer_pool_dump_pct。
- innodb_buffer_pool_dump_pct 该百分比(N<100)不是你当前 Buffer Pool 的总的数据(总页数)的 N%,而是每个 Buffer Pool 实例中最近使用的页的 N%(参考:innodb_buffer_pool_dump_pct)。
- 如果做了高可用,可以定期 dump,然后将该 dump 的文件传送到 Slave 上,然后直接 load(set innodb_buffer_pool_load_now = 1)(Slave上的(Space,Page)和 Master 上的大致相同)。
- 有一个疑问:如果 Slave 和 Master 上的(Space,Page)不一致,那么 load 到 Buffer Pool 中的数据是不是就是垃圾数据?
- 如果 MySQL 意外宕机了,不会 dump 出 Buffer Pool 的数据,已经存在的 ib_buffer_pool 文件还是上次正常关闭 MySQL 时 dump 出的数据,已经比较旧了,如果要避免这种问题,需要设计一个脚本,能够实现定期 dump 出 Buffer Pool 的数据。
- load now 和 dump now 都是异步在后台加载的,返回的速度很快。
Buffer Pool 与 压缩页
我们可以通过 information_schema.INNODB_BUFFER_PAGE_LRU 表来查看 Buffer Pool 中的压缩页。
information_schema.INNODB_BUFFER_PAGE_LRU 的表结构如下所示:
mysql> desc information_schema.INNODB_BUFFER_PAGE_LRU;
+---------------------+---------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------------+---------------------+------+-----+---------+-------+
| POOL_ID | bigint(21) unsigned | NO | | 0 | |
| LRU_POSITION | bigint(21) unsigned | NO | | 0 | |
| SPACE | bigint(21) unsigned | NO | | 0 | |
| PAGE_NUMBER | bigint(21) unsigned | NO | | 0 | |
| PAGE_TYPE | varchar(64) | YES | | NULL | |
| FLUSH_TYPE | bigint(21) unsigned | NO | | 0 | |
| FIX_COUNT | bigint(21) unsigned | NO | | 0 | |
| IS_HASHED | varchar(3) | YES | | NULL | |
| NEWEST_MODIFICATION | bigint(21) unsigned | NO | | 0 | |
| OLDEST_MODIFICATION | bigint(21) unsigned | NO | | 0 | |
| ACCESS_TIME | bigint(21) unsigned | NO | | 0 | |
| TABLE_NAME | varchar(1024) | YES | | NULL | |
| INDEX_NAME | varchar(1024) | YES | | NULL | |
| NUMBER_RECORDS | bigint(21) unsigned | NO | | 0 | |
| DATA_SIZE | bigint(21) unsigned | NO | | 0 | |
| COMPRESSED_SIZE | bigint(21) unsigned | NO | | 0 | | -- 压缩页的大小
| COMPRESSED | varchar(3) | YES | | NULL | | -- 该页是否被压缩
| IO_FIX | varchar(64) | YES | | NULL | |
| IS_OLD | varchar(3) | YES | | NULL | |
| FREE_PAGE_CLOCK | bigint(21) unsigned | NO | | 0 | |
+---------------------+---------------------+------+-----+---------+-------+
20 rows in set (0.00 sec)
- COMPRESSED_SIZE:表示压缩页的大小。
- COMPRESSED:表示该页是否被压缩。
查看压缩页的数据:
mysql> select
-> table_name, space, page_number,
-> index_name, compressed, compressed_size
-> from
-> information_schema.INNODB_BUFFER_PAGE_LRU
-> where
-> compressed = 'yes' limit 10;
+------------+-------+-------------+------------+------------+-----------------+
| table_name | space | page_number | index_name | compressed | compressed_size |
+------------+-------+-------------+------------+------------+-----------------+
| NULL | 104 | 2669 | NULL | YES | 4096 |
| NULL | 104 | 2671 | NULL | YES | 4096 |
| NULL | 104 | 2674 | NULL | YES | 4096 |
| NULL | 104 | 2677 | NULL | YES | 4096 |
| NULL | 104 | 2679 | NULL | YES | 4096 |
| NULL | 104 | 2682 | NULL | YES | 4096 |
| NULL | 104 | 2685 | NULL | YES | 4096 |
| NULL | 104 | 2687 | NULL | YES | 4096 |
| NULL | 104 | 2686 | NULL | YES | 4096 |
| NULL | 104 | 2684 | NULL | YES | 4096 |
+------------+-------+-------------+------------+------------+-----------------+
10 rows in set (0.04 sec)
mysql> select
-> table_id, name, space, row_format, zip_page_size
-> from
-> information_schema.INNODB_SYS_TABLES
-> where
-> space = 104;
+----------+----------------------------+-------+------------+---------------+
| table_id | name | space | row_format | zip_page_size |
+----------+----------------------------+-------+------------+---------------+
| 104 | employees/employee_comps_1 | 104 | Compressed | 4096 |
+----------+----------------------------+-------+------------+---------------+
1 row in set (0.00 sec)
mysql> show create table employees.employee_comps_1\G
*************************** 1. row ***************************
Table: employee_comps_1
Create Table: CREATE TABLE `employee_comps_1` (
`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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4 -- 之前确实是指定压缩
1 row in set (0.00 sec)
压缩页在磁盘中是通过 key_block_size 来设置页的大小的,那它在内存中是如何存储的呢?
在 Buffer Pool 中,压缩页是存放在 unzip_LRU 中的,通过 show engine innodb status
命令可以查看 unzip_LRU 的长度:
mysql> show engine innodb status\G
-- -----------省略其他输出-------------
---BUFFER POOL 0
Buffer pool size 16383
Free buffers 15540
Database pages 651
Old database pages 237
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 589, created 62, written 124
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: 651, unzip_LRU len: 382 -- 压缩页LRU的长度在 buffer pool 1 中的长度是382
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
-- -----------省略其他输出-------------
假设我们创建了一张压缩表(key_block_size = 8,即压缩页大小是8k),而 Buffer Pool 中页的大小是固定的(innodb_page_size = 16,即每页大小是16k),那它在内存中是如何分配的呢?
InnoDB 使用了“伙伴算法”的分配策略来处理压缩页的内存分配问题。举个例子来描述伙伴算法的执行过程:
- 假设 key_block_size = 8,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 中去。
通过使用“伙伴算法”可以实现不同大小的页可以在同一个 Buffer Pool 中使用,Buffer Pool 中页的大小没有变,就是固定的大小(等于 innodb_page_size)。
- 可以简单的认为 Free List 是按照页的大小来进行划分的。
- LRU List 和 Flush List 不需要按照页大小划分,都是统一的 innodb_page_size 大小。
压缩页在 Buffer Pool 中可能既存在 4K 的压缩页,又存在解压后的 16K 的页,为什么在 Buffer Pool 要把压缩页解压呢?
**
因为 InnoDB 是基于页的压缩,如果要读取某条记录,需要将页解压后才能读取。
解压后,4K 的压缩页会继续保留在 Buffer Pool 中,不会被丢弃。为什么要保留 4K 的压缩页呢?
压缩页保留的原因是为了在更新数据的时候,将 redo 添加到压缩页的空闲部分(4K 的压缩页可能没有写满,比如只写了 3K 的大小,这样就存在了 1K 的空闲空间了),如果要刷回磁盘,可以直接将该压缩页刷回去。如果该页被写满,则做一次 reorganize 操作(在此之前也要做解压),真的写满了才做分裂。
比如一个 4K 的压缩页,只写了 3K 的大小,当这时修改这个页中的一条记录,对应的解压后的页会做更新操作,而压缩页不会直接修改这条记录,因为修改压缩页意味着要执行解压操作,它会把对这个页修改操作的 redo log 写到压缩页的空闲部分。
4K 的压缩页和 16K 的解压后的页共存的弊端是什么呢?
压缩页占用了 Buffer Pool 的空间,对于热点数据来说,相当于内存小了,会造成性能下降(热点空间变小)。所以在开启了压缩后,Buffer Pool 的空间要相应增大。
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/cm2613 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。