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 的压测图:
image.png
该图测试的是 TPS(每秒事务数),实际 QPS 过万。虽然这张图不能代表实际生产环境 Buffer Pool 的性能,但是也反映出 Buffer Pool 对 MySQL 性能的巨大影响。

在实际生产环境中,根据数据的容量,物理机的性能,Buffer Pool 设置的越大,MySQL 的性能越好。

Buffer Pool 状态

第一种方式:使用 show engine innodb status\G 命令查看 Buffer Pool 状态。

  1. mysql> show engine innodb status\G
  2. -- ---------------省略其他输出-----------------
  3. ---BUFFER POOL 0
  4. Buffer pool size 16383 -- Buffer Pool中有多少个页
  5. Free buffers 16357 -- Buffer Pool中有多少个空白页(Free List),线上可能看到为0
  6. Database pages 41 -- Buffer Pool中使用了多少页(LRU List
  7. Old database pages 0 -- old pages(见3.4
  8. Modified db pages 0 -- 脏页
  9. Pending reads 0
  10. Pending writes: LRU 0, flush list 0, single page 0
  11. Pages made young 0, not young 0
  12. 0.00 youngs/s, 0.00 non-youngs/s -- young表示old-->new的状态
  13. Pages read 41, created 0, written 20
  14. 0.00 reads/s, 0.00 creates/s, 0.00 writes/s
  15. No buffer pool page gets since the last printout
  16. Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
  17. LRU len: 41, unzip_LRU len: 0 I/O sum[0]:cur[0], unzip sum[0]:cur[0]
  18. -- ---------------省略其他输出-----------------

第二种方式:使用元数据表 information_schema.INNODB_BUFFER_POOL_STATS 查看 Buffer Pool 状态。

  1. mysql> select * from information_schema.INNODB_BUFFER_POOL_STATS\G
  2. *************************** 1. row ***************************
  3. POOL_ID: 0
  4. POOL_SIZE: 16383 -- Buffer Pool中有多少个页
  5. FREE_BUFFERS: 16357 -- Buffer Pool中有多少个空白页(Free List),线上可能看到为0
  6. DATABASE_PAGES: 41 -- Buffer Pool中使用了多少页(LRU List
  7. OLD_DATABASE_PAGES: 0 -- old pages (见3.4
  8. MODIFIED_DATABASE_PAGES: 0 -- 脏页
  9. PENDING_DECOMPRESS: 0
  10. PENDING_READS: 0
  11. PENDING_FLUSH_LRU: 0
  12. PENDING_FLUSH_LIST: 0
  13. PAGES_MADE_YOUNG: 0
  14. PAGES_NOT_MADE_YOUNG: 0
  15. PAGES_MADE_YOUNG_RATE: 0
  16. PAGES_MADE_NOT_YOUNG_RATE: 0
  17. NUMBER_PAGES_READ: 41
  18. NUMBER_PAGES_CREATED: 0
  19. NUMBER_PAGES_WRITTEN: 20
  20. PAGES_READ_RATE: 0
  21. PAGES_CREATE_RATE: 0
  22. PAGES_WRITTEN_RATE: 0
  23. NUMBER_PAGES_GET: 1041
  24. HIT_RATE: 0
  25. YOUNG_MAKE_PER_THOUSAND_GETS: 0
  26. NOT_YOUNG_MAKE_PER_THOUSAND_GETS: 0
  27. NUMBER_PAGES_READ_AHEAD: 0
  28. NUMBER_READ_AHEAD_EVICTED: 0
  29. READ_AHEAD_RATE: 0
  30. READ_AHEAD_EVICTED_RATE: 0
  31. LRU_IO_TOTAL: 0
  32. LRU_IO_CURRENT: 0
  33. UNCOMPRESS_TOTAL: 0
  34. UNCOMPRESS_CURRENT: 0
  35. -- ---------------省略其他输出-----------------
  36. mysql> select * from information_schema.INNODB_BUFFER_PAGE_LRU limit 1\G
  37. *************************** 1. row ***************************
  38. POOL_ID: 0
  39. LRU_POSITION: 0
  40. SPACE: 0 -- space id 表空间号
  41. PAGE_NUMBER: 7 -- 对应的页号
  42. PAGE_TYPE: SYSTEM
  43. FLUSH_TYPE: 1
  44. FIX_COUNT: 0
  45. IS_HASHED: NO
  46. NEWEST_MODIFICATION: 4005630175 -- 该页最近一次(最新)被修改的LSN
  47. OLDEST_MODIFICATION: 0 -- 该页在Buffer Pool中第一次被修改的LSN值,FLushList是根据该值进行排序的
  48. -- 该值越小,表示该页应该最先被刷新
  49. ACCESS_TIME: 729305074
  50. TABLE_NAME: NULL
  51. INDEX_NAME: NULL
  52. NUMBER_RECORDS: 0
  53. DATA_SIZE: 0
  54. COMPRESSED_SIZE: 0
  55. COMPRESSED: NO
  56. IO_FIX: IO_NONE
  57. IS_OLD: NO
  58. FREE_PAGE_CLOCK: 0
  59. 1 row in set (0.01 sec)
  60. -----------------省略其他输出-----------------

Buffer Pool 在线调整

从 MySQL 5.7 开始,可以在线修改 innodb_buffer_pool_size

  1. mysql> show variables like "%innodb_buffer_pool_size%"; +-------------------------+------------+
  2. | Variable_name | Value |
  3. +-------------------------+------------+
  4. | innodb_buffer_pool_size | 1073741824 |
  5. +-------------------------+------------+
  6. 1 row in set (0.00 sec)
  7. mysql> set global innodb_buffer_pool_size=2*1024*1024*1024; -- 扩大
  8. Query OK, 0 rows affected (0.00 sec)
  9. mysql> show variables like "%innodb_buffer_pool_size%"; +-------------------------+------------+
  10. | Variable_name | Value |
  11. +-------------------------+------------+
  12. | innodb_buffer_pool_size | 2147483648 |
  13. +-------------------------+------------+
  14. 1 row in set (0.00 sec)
  15. mysql> set global innodb_buffer_pool_size=1*1024*1024*1024; -- 缩小,没修改的页被丢弃,修改的需要刷回磁盘
  16. Query OK, 0 rows affected (0.00 sec)
  17. mysql> show variables like "%innodb_buffer_pool_size%"; +-------------------------+------------+
  18. | Variable_name | Value |
  19. +-------------------------+------------+
  20. | innodb_buffer_pool_size | 1073741824 |
  21. +-------------------------+------------+
  22. 1 row in set (0.01 sec)

扩大 Buffer Pool 容量比较方便,直接扩大就可以了,但是缩小 Buffer Pool 容量就相对比较复杂了,对于没有修改的页,直接丢弃,对于修改过的脏页数据,则需要将脏页数据刷回磁盘。

在 MySQL 5.7 之前的版本,修改 innodb_buffer_pool_size,需要重启 MySQL。

Buffer Pool 原理

Buffer Pool 的组成

image.png

  • 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 语句,开销较大。

  1. SELECT
  2. pool_id,
  3. lru_position,
  4. space,
  5. page_number,
  6. table_name,
  7. oldest_modification,
  8. newest_modification
  9. FROM
  10. information_schema.INNODB_BUFFER_PAGE_LRU
  11. WHERE
  12. oldest_modification <> 0
  13. AND oldest_modification <> newest_modification; -- 如果没有脏页,结果集为空

Midpoint LRU 算法

传统的 LRU 算法是,当该页被第一次读取时,将该页放到 LRU List 的头部,最为最近访问的元素,这里又分两种情况:

  1. 页已经在缓冲池里,那就只做“移至”LRU头部的动作,而没有页被淘汰。
  2. 页不在缓冲池里,除了做“放入”LRU头部的动作,还要做“淘汰”LRU尾部页的动作。

InnoDB 缓冲池(Buffer Pool) - 图3
如上图,假如管理缓冲池的LRU长度为10,缓冲了页号为1,3,5…,40,7的页。

假如,接下来要访问的数据在页号为4的页中:
InnoDB 缓冲池(Buffer Pool) - 图4

  • 页号为4的页,本来就在缓冲池里。
  • 把页号为4的页,放到 LRU 的头部即可,没有页被淘汰。

为了减少数据移动,LRU 一般用链表实现。

假如,再接下来要访问的数据在页号为50的页中:
InnoDB 缓冲池(Buffer Pool) - 图5

  • 页号为50的页,原来不在缓冲池里。
  • 把页号为50的页,放到 LRU 头部,同时淘汰尾部页号为7的页。

MySQL 没有直接使用传统的 LRU 算法,而是对它做了改变,使用了 Midpoint LRU 算法。

具体实现方法是:

  • 将 LRU 分为两个部分:新生代(new sublist)和老生代(old sublist)。
  • 新老生代收尾相连,即:新生代的尾(tail)连接着老生代的头(head)。
  • innodb_old_blocks_pct 参数用来控制 mid point 的位置,默认是 37 ,即 3/8 的位置。

image.png
Midpoint LRU 算法执行过程:

  • 当该页被第一次读取时,将该页先放在 mid point 的位置(因为无法保证一定是活跃)。
  • 当改业被第二次读取时,才将改页放入到 new sublist 的首部。
  • 当不断有新的页被加入到 LRU List 中,则老的数据会慢慢从 new sublist 挤到 old sublist 中,直到最后淘汰出 LRU List,如果被淘汰的页是脏页,则需要先刷回磁盘,在把 LRU List 的空余空间给新查询出的页使用。

那么为什么 MySQL 需要使用 Midpoint LRU 算法呢?

因为这里存在两个问题:

  1. 预读失效
  2. 缓冲池污染

先来看看什么是预读?

磁盘读写,并不是按需读取,而是按页读取,一次至少读一页数据(一般是 16K),如果未来要读取的数据就在页中,就能够省去后续的磁盘 IO,提高效率。

预读为什么有效?

数据访问,通常都遵循“集中读写”的原则,使用一些数据,大概率会使用附近的数据,这就是所谓的“局部性原理”,它表明提前加载是有效的,确实能够减少磁盘 IO。

这说明 InnoDB 会根据预读机制将一些“可能要访问的页”提前加载到 Buffer Pool 中,避免未来多次磁盘 IO 操作。

再来看看什么是预读失效?

由于预读(Read-Ahead),提前把页放入了缓冲池,但最终 MySQL 并没有从页中读取数据,称为预读失效。

Buffer Pool 的 Midpoint LRU 算法是如何解决预读失效问题的?
InnoDB 缓冲池(Buffer Pool) - 图7
假如有一个页号为50的新页被预读加入缓冲池中:

  • 50只会从老生代头部插入,老生代尾部(也是整体尾部)的页会被淘汰掉。
  • 假设50这一页不会被真正读取,即预读失败,它将比新生代的数据更早淘汰出缓冲池。

image.png
假如50这一页立刻被读取到,例如 SQL 访问了页内的行 row 数据:

  • 它会被立刻加入到新生代的头部。
  • 新生代的页会被挤到老生代,此时并不会有页面被真正淘汰。

这样改进版的 Midpoint LRU 算法就能很好的解决预读失效的问题。

但是改进版的 Midpoint LRU 算法仍然解决不了缓冲池污染的问题。

什么是 MySQL 缓冲池污染?

当某一个 SQL 语句,要批量扫描大量数据时,可能导致把缓冲池的所有页都替换出去,导致大量热数据被换出,MySQL 性能急剧下降,这种情况叫缓冲池污染。

例如,有一个数据量较大的用户表,当执行:select * from user

执行该条语句,会读取大量的页,且很可能每页都会被读取多次(该页可能被读取了两次以上,引文一页有多条数据,读取一条记录,就需要读一次页)结果就导致大量的页被放到 new sublist 中(尽管其实每条记录只会被读取一次),而真正的热数据被淘汰掉。

怎么解决 MySQL 缓冲池污染的问题呢?

MySQL 缓冲池引入了一个“老生代停留时间窗口”的机制:

  • 假设 T=老生代停留时间窗口。
  • 插入老生代头部的页,即使立刻被访问,并不会立刻放入新生代头部。
  • 只有满足“被访问”并且“在老生代停留时间”大于 T 的页才会被放入到新生代的头部。

通过配置 innodb_old_blocks_time 参数实现。

  1. mysql> show variables like "%innodb_old_blocks_time%";
  2. +------------------------+-------+
  3. | Variable_name | Value |
  4. +------------------------+-------+
  5. | innodb_old_blocks_time | 1000 | -- 设置为1s
  6. +------------------------+-------+
  7. 1 row in set (0.00 sec)
  • 当该页被第一次读取时,将该页放在 mid point 位置,但是随后无论你读多少次,我在这 innodb_old_blocks_time 的时间内都不管(都视作只读取了一次),等这个时间过去了,如果该页还是被读取了,我才把这个页放到 new sublist 的首部。
  • 通常 select * 扫描操作不会高于1秒,一个页很快就被扫完了。

InnoDB 缓冲池(Buffer Pool) - 图9
假如批量数据扫描,有51,52,53,54,55等五个页面将要依次被访问。
InnoDB 缓冲池(Buffer Pool) - 图10
如果没有“老生代停留时间窗口”的策略,这些批量被访问的页面,会换出大量热数据。
InnoDB 缓冲池(Buffer Pool) - 图11
加入“老生代停留时间窗口”策略后,短时间内被大量加载的页,并不会立刻插入新生代头部,而是优先淘汰那些,短期内仅仅访问了一次的页。
InnoDB 缓冲池(Buffer Pool) - 图12
而只有在老生代呆的时间足够久,停留时间大于T,才会被插入新生代头部。

Buffer Pool 预热

在 MySQL 5.6 之前,MySQL 启动后,Buffer Pool 中页的数据是空的,需要大量的时间才能把磁盘中的页读入到内存中,导致启动后的一段时间性能很差。

在 MySQL 5.6 以后,可以在正常停机的时候 dump 出 Buffer Pool 的数据(space,page number),然后在启动的时候 Load 进 Buffer Pool。该功能可以让 MySQL 启动时自动预热,无需人工干预。

关于 Buffer Pool 预热的一些参数及描述:

  1. mysql> show variables like "%innodb_buffer_pool%";
  2. +-------------------------------------+----------------+
  3. | Variable_name | Value |
  4. +-------------------------------------+----------------+
  5. | innodb_buffer_pool_chunk_size | 134217728 |
  6. | innodb_buffer_pool_dump_at_shutdown | ON | -- 在停机时dumpbuffer pool中的(space,page
  7. | innodb_buffer_pool_dump_now | OFF | -- set 一下,表示现在就从buffer pooldump
  8. | innodb_buffer_pool_dump_pct | 40 | -- dump的百分比,是每个buffer pool文件,而不是整体
  9. | innodb_buffer_pool_filename | ib_buffer_pool | -- dump出的文件的名字
  10. | innodb_buffer_pool_instances | 8 |
  11. | innodb_buffer_pool_load_abort | OFF |
  12. | innodb_buffer_pool_load_at_startup | ON | -- 启动时加载dump的文件,恢复到buffer pool
  13. | innodb_buffer_pool_load_now | OFF | -- set一下,表示现在加载 dump的文件
  14. | innodb_buffer_pool_size | 1073741824 |
  15. +-------------------------------------+----------------+
  16. 10 rows in set (0.00 sec)

查看 dump 出来的文件内容:

  1. shell> head ib_buffer_pool # dump出来的文件
  2. 120,3
  3. 120,2
  4. 120,1
  5. 120,0
  6. 106,4539
  7. 106,4538
  8. 106,4537
  9. 106,4536
  10. 106,4535
  11. 106,4534

测试:

  1. # mysql 启动
  2. sehll> cat error.log
  3. ## ---------------省略其他输出-----------------
  4. 2016-01-10T20:17:31.263901+08:00 0 [Note] InnoDB: Loading buffer pool(s) from /data/mysql_data/5.7/ib_buffer_pool
  5. ## ---------------省略其他输出-----------------
  6. 2016-01-10T20:17:31.365768+08:00 0 [Note] InnoDB: Buffer pool(s) load completed at 160110 20:17:31 # 速度还是很快的
  7. # mysql 停机
  8. shell> cat error.log
  9. ## ---------------省略其他输出-----------------
  10. 2016-01-11T08:47:20.067308+08:00 0 [Note] InnoDB: Dumping buffer pool(s) to /data/mysql_data/5.7/ib_buffer_pool
  11. 2016-01-11T08:47:20.067730+08:00 0 [Note] InnoDB: Buffer pool(s) dump completed at 160111 8:47:20
  12. ## ---------------省略其他输出-----------------
  13. # 查看当前buffer pool中的数据的条数
  14. shell> wc -l ib_buffer_pool
  15. 524 ib_buffer_pool
  16. mysql> set global innodb_buffer_pool_dump_now=1;
  17. Query OK, 0 rows affected (0.00 sec)
  18. mysql> show status like 'Innodb_buffer_pool_dump_status';
  19. +--------------------------------+--------------------------------------------------+
  20. | Variable_name | Value |
  21. +--------------------------------+--------------------------------------------------+
  22. | Innodb_buffer_pool_dump_status | Buffer pool(s) dump completed at 160111 9:04:21 |
  23. +--------------------------------+--------------------------------------------------+
  24. 1 row in set (0.00 sec)
  25. -- 已经完成
  26. mysql> wc -l ib_buffer_pool
  27. 524 ib_buffer_pool # 发现和原来一致,并没有减少为原来的40,似乎和innodb_buffer_pool_dump_pct设置的不符合
  28. -- 由于innodb_buffer_pool_dump_pct是针对每个buffer pool的百分比,应该是由于我的数据太小,没有达到该比例(40%),所以这里全部dump出来了
  29. -- 测试的时候可以设置小一点
  30. mysql> set global innodb_buffer_pool_dump_pct=2; -- 设置成2%
  31. Query OK, 0 rows affected (0.00 sec)
  32. mysql> set global innodb_buffer_pool_dump_now=1;
  33. Query OK, 0 rows affected (0.00 sec)
  34. mysql> show status like 'Innodb_buffer_pool_dump_status';
  35. +--------------------------------+--------------------------------------------------+
  36. | Variable_name | Value |
  37. +--------------------------------+--------------------------------------------------+
  38. | Innodb_buffer_pool_load_status | Buffer pool(s) load completed at 160111 9:05:38 |
  39. +--------------------------------+--------------------------------------------------+
  40. 1 row in set (0.00 sec)
  41. shell> wc -l ib_buffer_pool # dump前
  42. 524 ib_buffer_pool
  43. shell> wc -l ib_buffer_pool # dump后
  44. 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 的表结构如下所示:

  1. mysql> desc information_schema.INNODB_BUFFER_PAGE_LRU;
  2. +---------------------+---------------------+------+-----+---------+-------+
  3. | Field | Type | Null | Key | Default | Extra |
  4. +---------------------+---------------------+------+-----+---------+-------+
  5. | POOL_ID | bigint(21) unsigned | NO | | 0 | |
  6. | LRU_POSITION | bigint(21) unsigned | NO | | 0 | |
  7. | SPACE | bigint(21) unsigned | NO | | 0 | |
  8. | PAGE_NUMBER | bigint(21) unsigned | NO | | 0 | |
  9. | PAGE_TYPE | varchar(64) | YES | | NULL | |
  10. | FLUSH_TYPE | bigint(21) unsigned | NO | | 0 | |
  11. | FIX_COUNT | bigint(21) unsigned | NO | | 0 | |
  12. | IS_HASHED | varchar(3) | YES | | NULL | |
  13. | NEWEST_MODIFICATION | bigint(21) unsigned | NO | | 0 | |
  14. | OLDEST_MODIFICATION | bigint(21) unsigned | NO | | 0 | |
  15. | ACCESS_TIME | bigint(21) unsigned | NO | | 0 | |
  16. | TABLE_NAME | varchar(1024) | YES | | NULL | |
  17. | INDEX_NAME | varchar(1024) | YES | | NULL | |
  18. | NUMBER_RECORDS | bigint(21) unsigned | NO | | 0 | |
  19. | DATA_SIZE | bigint(21) unsigned | NO | | 0 | |
  20. | COMPRESSED_SIZE | bigint(21) unsigned | NO | | 0 | | -- 压缩页的大小
  21. | COMPRESSED | varchar(3) | YES | | NULL | | -- 该页是否被压缩
  22. | IO_FIX | varchar(64) | YES | | NULL | |
  23. | IS_OLD | varchar(3) | YES | | NULL | |
  24. | FREE_PAGE_CLOCK | bigint(21) unsigned | NO | | 0 | |
  25. +---------------------+---------------------+------+-----+---------+-------+
  26. 20 rows in set (0.00 sec)
  • COMPRESSED_SIZE:表示压缩页的大小。
  • COMPRESSED:表示该页是否被压缩。

查看压缩页的数据:

  1. mysql> select
  2. -> table_name, space, page_number,
  3. -> index_name, compressed, compressed_size
  4. -> from
  5. -> information_schema.INNODB_BUFFER_PAGE_LRU
  6. -> where
  7. -> compressed = 'yes' limit 10;
  8. +------------+-------+-------------+------------+------------+-----------------+
  9. | table_name | space | page_number | index_name | compressed | compressed_size |
  10. +------------+-------+-------------+------------+------------+-----------------+
  11. | NULL | 104 | 2669 | NULL | YES | 4096 |
  12. | NULL | 104 | 2671 | NULL | YES | 4096 |
  13. | NULL | 104 | 2674 | NULL | YES | 4096 |
  14. | NULL | 104 | 2677 | NULL | YES | 4096 |
  15. | NULL | 104 | 2679 | NULL | YES | 4096 |
  16. | NULL | 104 | 2682 | NULL | YES | 4096 |
  17. | NULL | 104 | 2685 | NULL | YES | 4096 |
  18. | NULL | 104 | 2687 | NULL | YES | 4096 |
  19. | NULL | 104 | 2686 | NULL | YES | 4096 |
  20. | NULL | 104 | 2684 | NULL | YES | 4096 |
  21. +------------+-------+-------------+------------+------------+-----------------+
  22. 10 rows in set (0.04 sec)
  23. mysql> select
  24. -> table_id, name, space, row_format, zip_page_size
  25. -> from
  26. -> information_schema.INNODB_SYS_TABLES
  27. -> where
  28. -> space = 104;
  29. +----------+----------------------------+-------+------------+---------------+
  30. | table_id | name | space | row_format | zip_page_size |
  31. +----------+----------------------------+-------+------------+---------------+
  32. | 104 | employees/employee_comps_1 | 104 | Compressed | 4096 |
  33. +----------+----------------------------+-------+------------+---------------+
  34. 1 row in set (0.00 sec)
  35. mysql> show create table employees.employee_comps_1\G
  36. *************************** 1. row ***************************
  37. Table: employee_comps_1
  38. Create Table: CREATE TABLE `employee_comps_1` (
  39. `emp_no` int(11) NOT NULL,
  40. `birth_date` date NOT NULL,
  41. `first_name` varchar(14) NOT NULL,
  42. `last_name` varchar(16) NOT NULL,
  43. `gender` enum('M','F') NOT NULL,
  44. `hire_date` date NOT NULL,
  45. PRIMARY KEY (`emp_no`)
  46. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4 -- 之前确实是指定压缩
  47. 1 row in set (0.00 sec)

压缩页在磁盘中是通过 key_block_size 来设置页的大小的,那它在内存中是如何存储的呢?

在 Buffer Pool 中,压缩页是存放在 unzip_LRU 中的,通过 show engine innodb status 命令可以查看 unzip_LRU 的长度:

  1. mysql> show engine innodb status\G
  2. -- -----------省略其他输出-------------
  3. ---BUFFER POOL 0
  4. Buffer pool size 16383
  5. Free buffers 15540
  6. Database pages 651
  7. Old database pages 237
  8. Modified db pages 0
  9. Pending reads 0
  10. Pending writes: LRU 0, flush list 0, single page 0
  11. Pages made young 0, not young 0
  12. 0.00 youngs/s, 0.00 non-youngs/s
  13. Pages read 589, created 62, written 124
  14. 0.00 reads/s, 0.00 creates/s, 0.00 writes/s
  15. No buffer pool page gets since the last printout
  16. Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
  17. LRU len: 651, unzip_LRU len: 382 -- 压缩页LRU的长度在 buffer pool 1 中的长度是382
  18. I/O sum[0]:cur[0], unzip sum[0]:cur[0]
  19. -- -----------省略其他输出-------------

假设我们创建了一张压缩表(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 中去。

image.png
通过使用“伙伴算法”可以实现不同大小的页可以在同一个 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 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。