缓存管理是 DBMS 的核心系统,用于管理数据页的访问、刷脏和驱逐;虽然操作系统本身有 page cache,但那不是专门为数据库设计的,所以大多数数据库系统都是自己来管理缓存。由于几乎所有的数据页访问都涉及到 Buffer Pool,因此 buffer pool 的并发访问控制尤为重要,可能会影响到吞吐量和响应时间,本文主要回顾一下 MySQL 的 buffer Pool 最近几个版本的发展(若有遗漏,欢迎评论补充), 感受下最近几年这一块的进步
MySQL5.5 之前
只能设置一个 buffer pool, 通过 innodb_buffer_pool_size 来控制, 刷脏由 master 线程承担,扩展性差。
MySQL 5.5
引入参数 innodb_buffer_pool_instances,将 buffer pool 拆分成多个 instance,从而减少对 buffer pool 的访问控制,这时候的刷脏还是由 Master 线程来承担。
MySQL 5.6
引入了 buffer Pool page Id 转储和导入特性,也就是说可以随时把内存中的 page no 存下来到文件里,在重启时会自动把这些 Page 加载到内存中,使内存保持 warm 状态. 此外该版本第一次引入了 page cleaner, 将 flush list/lru 上的刷脏驱逐工作转移到单独线程,减少了 master 线程的负担
MySQL 5.7
这个版本发布了一个重要特性:online buffer pool resize. 当然是否是 online 需要打一个问号,因为在 resize 的过程中需要拿很多全局大锁,在高负载场景下很容易导致实例 Hang 住 (81615)。
和之前不同,buffer pool 被分成多个 instance,每个 instance 又由多个 chunk 组成,每个 chunk 的大小受到参数 innodb_buffer_pool_chunk_size 控制,默认 128MB, buffer pool resize 都是以 chunk 为单位增加或减少的。
另外一个需要注意的点是:你配置的 Buffer Pool Size 可能比你实际使用的内存要大,尤其对于大 Bp 而言,这是因为内部做了对齐处理, buffer pool size 必须以 innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances 来做向上对齐(80350)
我们知道通常数据文件的 IO 都被设置成 O_DIRECT, 但每次修改后依然需要去做 fsync,来持久化元数据信息,而对于某些文件系统而言是没必要做 fsync 的,因此加入了新选项 O_DIRECT_NO_FSYNC,这个需求来自于 facebook. 他们也对此做了特殊处理:除非文件 size 变化,否则不做 fsync。(最近在 buglist 上对这个参数是否安全的讨论也很有意思,官方文档做了新的说明,感兴趣的可以看看 [94912:O_DIRECT_NO_FSYNC possible write hole
](https://bugs.mysql.com/bug.php?id=94912)))
再一个重要功能是终于引入了 multiple page cleaner, 可以多个后台线程并发刷脏页,提供了更好的刷脏性能,有效避免用户线程进入 single page flush。当然这还不够完美,主要有四点:
- 用户线程依然会进入 single page flush,而一旦大量线程进入,就会导致严重性能下降:超频繁的 fsync,激烈的 dblwr 竞争,线程切换等等
- 当 redo 空间不足时,用户线程也会进入 page flush,这在高负载场景下是很常见的,你会发现系统运行一段时间后,性能急剧下降。这是因为 redo 产生太快,而 page flush 又跟不上,导致 checkpoint 无法推进。那么用户线程可能就要过来做 fuzzy checkpoint 了。那时候性能基本上没法看了。
- dblwr 成为重要的单点瓶颈。如果你的服务器不支持原子写的话,必须打开 double write buffer。写入 Ibdata 一段固定区域,这里是有锁包含的,区分为两部分:single page flush 和 batch flush, 但无论如何,即使拆分了多个 page cleaner,最终扩展性还是受限于 dblwr
- 没有专用的 lru evict 线程,都是 Page cleaner 键值的。举个简单的例子,当 buffer pool 占满,同时又有很多脏页时,Page cleaner 可能忙于刷脏,而用户线程则得不到 free page,从而陷入 single page flush
如果你对上述几个问题极不满意,可以尝试 percona server, 他们向来擅长优化 Io bound 场景的性能,并且上述几个问题都解决了,尤其是 dblwr,他们做了多分区的改进。
MySQL 8.0
增加了一个功能,可以在实例宕机时,core 文件里不去掉 buffer pool, 这大大减少了 core 文件的大小。要知道,很多时候实例挂是因为文件损坏,不停的 core 重启会很快把磁盘占满,你可以通过设置参数 innodb_buffer_pool_in_core_file 来控制。
另外 8.0 最重要的一个改进就是:终于把全局大锁 buffer pool mutex 拆分了,各个链表由其专用的 mutex 保护,大大提升了访问扩展性。实际上这是由 percona 贡献给上游的,而 percona 在 5.5 版本就实现了这个特性(WL#8423: InnoDB: Remove the buffer pool mutex 以及 bug#75534)。
原来的一个大 mutex 被拆分成多个为 free_list, LRU_list, zip_free, 和 zip_hash 单独使用 mutex:
- LRU_list_mutex for the LRU_list;
- zip_free mutex for the zip_free arrays;
- zip_hash mutex for the zip_hash hash and in_zip_hash flag;
- free_list_mutex for the free_list and withdraw list.
- flush_state_mutex for init_flush, n_flush, no_flush arrays.
由于 log system 采用 lock-free 的方式重新实现,flush_order_mutex 也被移除了,带来的后果是 flush list 上部分 page 可能不是有序的,进而导致 checkpoint lsn 和以前不同,不再是某个 log record 的边界,而是可能在某个日志的中间,给崩溃恢复带来了一定的复杂度(需要回溯日志)
log_free_check 也发生了变化,当超出同步点时,用户线程不再自己去做 preflush,而是通知后台线程去做,自己在那等待 (log_request_checkpoint
), log_checkpointer 线程会去考虑log_consider_sync_flush
,这时候如果你打开了参数innodb_flush_sync
的话, 那么 flush 操作将由 page cleaner 线程来完成,此时 page cleaner 会忽略 io capacity 的限制,进入激烈刷脏
8.0 还增加了一个新的参数叫 innodb_fsync_threshold,,例如创建文件时,会设置文件 size, 如果服务器有多个运行的实例,可能会对其他正常运行的实例产生明显的冲击。为了解决这个问题,从 8.0.13 开始,引入了这个阈值,代码里在函数 os_file_set_size
注入,这个函数通常在创建或 truncate 文件之类的操作时调用,表示每写到这么多个字节时,要 fsync 一次,避免对系统产生冲击。这个补丁由 facebook 贡献给上游。
其他
当然也有些辅助结构来快速查询 buffer pool:
- adaptive hash index: 直接把叶子节点上的记录索引了,在满足某些条件时,可以直接定位到叶子节点上,无需从根节点开始扫描,减少读的 page 个数
- page hash: 每个 buffer pool instance 上都通过辅助的 page hash 来快速访问其中存储的 page,读加 s 锁,写入新 page 加 x 锁。page hash 采用分区的结构,默认为 16,有一个参数 innodb_page_hash_locks,但很遗憾,目前代码里是 debug only 的,如果你想配置这个参数,需要稍微修改下代码,把参数定义从 debug 宏下移出来
- change buffer: 当二级索引页不在时,可以把操作缓存到 ibdata 里的一个 btree(ibuf)中,下次需要读入这个 page 时,再做 merge;另外后台 master 线程会也会尝试 merge ibuf。
最后,听说官方正在努力解决 double write buffer 的瓶颈问题,期待一下.
本文作者:zhaiwx_yinfeng
阅读原文
本文为云栖社区原创内容,未经允许不得转载。