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 0Buffer pool size 16383 -- 该Buffer Pool中有多少个页Free buffers 16357 -- 该Buffer Pool中有多少个空白页(Free List),线上可能看到为0Database pages 41 -- 该Buffer Pool中使用了多少页(LRU List)Old database pages 0 -- old pages(见3.4)Modified db pages 0 -- 脏页Pending reads 0Pending writes: LRU 0, flush list 0, single page 0Pages made young 0, not young 00.00 youngs/s, 0.00 non-youngs/s -- young表示old-->new的状态Pages read 41, created 0, written 200.00 reads/s, 0.00 creates/s, 0.00 writes/sNo buffer pool page gets since the last printoutPages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/sLRU 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: 0POOL_SIZE: 16383 -- 该Buffer Pool中有多少个页FREE_BUFFERS: 16357 -- 该Buffer Pool中有多少个空白页(Free List),线上可能看到为0DATABASE_PAGES: 41 -- 该Buffer Pool中使用了多少页(LRU List)OLD_DATABASE_PAGES: 0 -- old pages (见3.4)MODIFIED_DATABASE_PAGES: 0 -- 脏页PENDING_DECOMPRESS: 0PENDING_READS: 0PENDING_FLUSH_LRU: 0PENDING_FLUSH_LIST: 0PAGES_MADE_YOUNG: 0PAGES_NOT_MADE_YOUNG: 0PAGES_MADE_YOUNG_RATE: 0PAGES_MADE_NOT_YOUNG_RATE: 0NUMBER_PAGES_READ: 41NUMBER_PAGES_CREATED: 0NUMBER_PAGES_WRITTEN: 20PAGES_READ_RATE: 0PAGES_CREATE_RATE: 0PAGES_WRITTEN_RATE: 0NUMBER_PAGES_GET: 1041HIT_RATE: 0YOUNG_MAKE_PER_THOUSAND_GETS: 0NOT_YOUNG_MAKE_PER_THOUSAND_GETS: 0NUMBER_PAGES_READ_AHEAD: 0NUMBER_READ_AHEAD_EVICTED: 0READ_AHEAD_RATE: 0READ_AHEAD_EVICTED_RATE: 0LRU_IO_TOTAL: 0LRU_IO_CURRENT: 0UNCOMPRESS_TOTAL: 0UNCOMPRESS_CURRENT: 0-- ---------------省略其他输出-----------------mysql> select * from information_schema.INNODB_BUFFER_PAGE_LRU limit 1\G*************************** 1. row ***************************POOL_ID: 0LRU_POSITION: 0SPACE: 0 -- space id 表空间号PAGE_NUMBER: 7 -- 对应的页号PAGE_TYPE: SYSTEMFLUSH_TYPE: 1FIX_COUNT: 0IS_HASHED: NONEWEST_MODIFICATION: 4005630175 -- 该页最近一次(最新)被修改的LSN值OLDEST_MODIFICATION: 0 -- 该页在Buffer Pool中第一次被修改的LSN值,FLushList是根据该值进行排序的-- 该值越小,表示该页应该最先被刷新ACCESS_TIME: 729305074TABLE_NAME: NULLINDEX_NAME: NULLNUMBER_RECORDS: 0DATA_SIZE: 0COMPRESSED_SIZE: 0COMPRESSED: NOIO_FIX: IO_NONEIS_OLD: NOFREE_PAGE_CLOCK: 01 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 语句,开销较大。
SELECTpool_id,lru_position,space,page_number,table_name,oldest_modification,newest_modificationFROMinformation_schema.INNODB_BUFFER_PAGE_LRUWHEREoldest_modification <> 0AND 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,3120,2120,1120,0106,4539106,4538106,4537106,4536106,4535106,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_pool2016-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_pool524 ib_buffer_poolmysql> 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_pool524 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_poolshell> 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_1Create 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 0Buffer pool size 16383Free buffers 15540Database pages 651Old database pages 237Modified db pages 0Pending reads 0Pending writes: LRU 0, flush list 0, single page 0Pages made young 0, not young 00.00 youngs/s, 0.00 non-youngs/sPages read 589, created 62, written 1240.00 reads/s, 0.00 creates/s, 0.00 writes/sNo buffer pool page gets since the last printoutPages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/sLRU len: 651, unzip_LRU len: 382 -- 压缩页LRU的长度在 buffer pool 1 中的长度是382I/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 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
