理解 InnoDB 的 Change Buffer
背景
MySQL 版本: 8.0.23
Change Buffer 是 InnoDB 系统表空间(space id = 0) 的一个 B+ tree 索引, 它的作用是为满足指定条件下而数据 Page 不在 Buffer Pool 的二级索引操作进行缓存, 包括一开始的 INSERT 和后来加入的 UPDATE, DELETE. 聚簇索引的顺序插入,可能体现在二级索引中字段并不是顺序的, 所以存在大量的随机读取和写入, 将二级索引的数据操作顺序写入 Change Buffer 的 B+ tree, 以此达到与聚簇索引一致的顺序写入. 当我们需要读取时,会将对应的数据 Page 从磁盘读取至 Buffer Pool 并与 Change Buffer 中对应的 records 进行 merge 操作.
Change buffer 使用
参数
innodb_change_buffering: 设置缓存的操作类型, 包括
none
,all
,inserts
,deletes
,changes
,purges
.innodb_change_buffer_max_size: 设置 Change Buffer 所占 Buffer Pool 的大小, 默认 25%, 最大50%. (假如超过了阈值,会阻止进行 Change Buffer 写入,转而使用通常的写入方式,然后进行主动 merge, 即将数据 Page 读至 Buffer Pool, 并将 Change Buffer 的 records 与数据 Page 进行合并)
触发条件
除了设置上述的参数打开 Change Buffer 以外,真正使用 Change Buffer 还需要经过一些条件判断:
1 | ibool ibuf_should_try(dict_index_t *index, /*!< in: index where to insert */ |
- 设置
innodb_change_buffering
不为IBUF_USE_NONE
. - 设置
innodb_change_buffer_max_size
不为0. - 待缓存的索引不为数据字典表.
- 待缓存的索引不是聚簇索引.
- 待缓存的索引不是 Spatial Index.
- 待缓存的索引包含递减列.
- 待缓存的表上没有 flush 操作.
- 待缓存的索引包含唯一列(唯一列需要全局判断, 可以缓存删除操作, 但无法缓存插入操作).
- 设置 srv_force_recovery 不允许 ibuf merge 操作.
所以我们在打开 Change Buffer 的同时也需要判断以上的条件是否符合. 对于唯一索引和写入立即需要读取的数据并不适合打开 Change Buffer.
Change Buffer 原理
Change Buffer 的调用逻辑是当我们需要进行支持的 DML 操作时,尝试从 Buffer Pool 读取 Page 时,假如 Page 不在 Buffer Pool 中并符合上述的触发条件, 会通过ibuf_insert()
来针对不同类型的操作进行 Change Buffer 的缓存.
Change Buffer 最初的功能只有缓存 INSERT 操作,所以也作 ibuf, 代码中均使用 ibuf 代替 Change Buffer.
Change Buffer 中有几个重要的概念:
Change Buffer Record
Change Buffer 的 Page 缓存对应二级索引的 DML 操作, 使用<space_id, page_no, counter>
作为 key, 当需要查找的时, 使用<space_id, page_no>
就可以定位到具体的 record, 而 counter 作为一个递增的值,记录着 DML 的操作顺序.
Change Buffer Bitmap Page
在每个 Tablespace 中 Extent 的第二个 Page 会作为 Change Buffer 的元信息 Page, 即为 ibuf bitmap page, bitmap page 会使用 4 bits 来记录 Tablespace 中关于数据 Page 的 Change Buffer 信息, 以下方法可以计算 bitmap page no:
1 | /* #define FSP_IBUF_BITMAP_OFFSET 1 */ |
其中包括以下几个信息:
IBUF_BITMAP_FREE: 长度 2 bit, 记录该 Page 空闲空间, 使用 2个 bit 来描述空闲空间大小,以 16KB 的 page size 为例,能表示的空闲空间范围为0 (0 bytes)、1 (512 bytes)、2 (1024 bytes)、3 (2048 bytes). 注意此处2048 bytes 意为用户的累计插入 records 长度不能超过 2048 bytes, 并不单单指一次插入, 假如累积缓存的 record 长度超过了 2048bytes, 就会触发 ibuf merge 操作.
IBUF_BITMAP_BUFFERED: 长度 1 bit, 代表该 Page 上存在被缓存了的 DML 操作.
IBUF_BITMAP_IBUF: 长度 1 bit, 代表该 Page 属于 ibuf 类型, 供 AIO 线程判断.
使用函数ibuf_index_page_calc_free_from_bits()
可以计算 Page 的空闲空间:
1 | if (ibuf_code == 3) { |
在正常的 DML 操作成功后会更新对应数据 Page 的IBUF_BITMAP_BUFFERED
, IBUF_BITMAP_BUFFERED
并不是准确的记录数据 Page 的空闲空间, 最大只能记录 2kb, 所以用户在写入 record 导致 Page 的剩余空闲空间小于 2kb 之后才会更新. 而 Change Buffer 的缓存操作也通过IBUF_BITMAP_BUFFERED
最大缓存 2kb 的 records.
Change Buffer 写入
1 | static MY_ATTRIBUTE((warn_unused_result)) dberr_t |
Change Buffer 合并(ibuf merge)
有以下几个场景会触发 Change Buffer 的 merge 操作, 即将 ibuf Page 的 records 和原数据 Page 进行合并操作 (ibuf_merge_or_delete_for_page()
):
ibuf_insert_low()
中存在部分判断逻辑会导致无法使用 Change Buffer 写入,从而触发 ibuf merge.当二级索引数据 Page 从磁盘读入至 Buffer Pool 之后,会触发 merge 操作(buf_page_get_gen()).
ibuf_merge_in_background()
会在后台触发 ibuf Page 进行 merge.在 Recover 阶段会对 ibuf Page 的 Records 和数据 Page 进行 merge.
当执行 slow shutdown 时,会强制做一次全部的ibuf merge.
ibuf 的 merge 操作原理比较简单,就是根据操作类型将 records 从 ibuf Page 合并至数据 Page (ibuf_insert_to_index_page()/ibuf_set_del_mark()/ibuf_delete()
)
FAQ
- 我们讲到对于普通索引来说,Change Buffer 可以避免 Update/Delete/Insert 等修改操作的时候访问磁盘. 后续查询的时候再从磁盘中读出并 merge,对于 Delete 操作,删除一行不存在的数据,这时候 Change Buffer 如何处理?
InnoDB 的删除逻辑是先删聚簇索引, 再删除二级索引(标记删除), 所以当主键索引发现
DB_RECORD_NOT_FOUND
就会返回, 所以不会触发缓存不存在的索引数据.
总结
InnoDB 实现了 Change buffer 来优化用户在二级索引上的随机写入问题, 用户可以根据自己的需求结合 Change buffer 的一些条件来判断是否启用 Change buffer, 但需要注意的是 Change buffer 的阈值只有 2kb,假如在一个二级索引的数据 Page 写入的 record 长度超过 2kb, 就会触发 ibuf merge, 从而使后续的 ibuf 缓存条件失效, 但这也符合 IO-bound 的场景需求. 本文也介绍了 Change buffer 如何使用 Bitmap Page 跟踪数据 Page 的空闲空间.