利用 WAL 技术,数据库将随机写转换成了顺序写,大大提升了数据库的性能,由此也带来了内存脏页的问题。
脏页会被后台线程自动 flush,也会由于数据页淘汰而触发 flush,而刷脏页的过程由于会占用资源,可能会让你的更新和查询语句的响应时间长一些。
当内存数据页跟磁盘数据页内容不一致的时候,我们成这个内存页为“脏页”;内存数据写入磁盘后,内存和磁盘上的数据页内容就一致了,称为“干净页”。
InnoDB引擎以页作为磁盘和内存之间交互的基本单位,数据库 I/O 操作的最小单位是页。也就是说,在数据库中,不论读一行,还是读多行,都是将这些行所在的页进行加载。
记录是按照行来存储的,但是数据库的读取并不以行为单位,否则一次读取(也就是一次 I/O 操作)只能处理一行数据,效率会非常低。
Buffer Pool 中存的就是一页一页的数据,当我们要查询的数据不在 Buffer Pool 中时,InnoDB 会将记录所在的页整个加载到 Buffer Pool 中去。
同样的,将 Buffer Pool 中的脏页刷入磁盘时,也是按照页为单位刷入磁盘的。
你从磁盘中读取一个数据页,会先从Free List中找出一个空闲缓存页的描述信息,然后将你读出的数据页中加载进缓存页中。同时将缓存页的描述信息从Free List中剔除,此外该描述信息块还会被维护进LRU链表中。
数据页被加载进Buffer Pool后你就可以对其进行变更操作了。
如果我们修改了Buffer Pool中某个缓冲页的数据,那么它就与磁盘上的页不一致了,这样的缓冲页也被称之为脏页(dirty page)。
为了性能问题,我们每次修改缓冲页后,并不着急立刻把修改刷新到磁盘上,而是在未来的某个时间点进行刷新操作。
如果有了修复发生,不是立刻刷新,那之后再刷新的时,我们怎么知道Buffer Pool中哪些页是脏页,哪些页从来没有被修改过呢?
创建一个存储脏页的 Flush list,凡是被修改过的缓冲页对应的控制块都会作为节点加入到这个链表中。
除了以上,Buffer Pool还有另外一种LRU List,整体结构如下:
在BufferPool中,内存管理如下:
后台线程会根据当时系统的繁忙程度确定刷新速率,定时从flush链表中刷新一部分页面到磁盘,即:BUF_FLUSH_LIST
有时后台线程刷新脏页的进度比较慢,导致用户准备加载一个磁盘页到Buffer Pool中时没有可用的缓冲页。此时,就会尝试查看LRU链表尾部,看是否存在可以直接释放掉的未修改缓冲页。
如果没有,则不得不将LRU链表尾部的一个脏页同步刷新到磁盘(与磁盘交互是很慢的,这会降低处理用户请求的速度),即:BUF_FLUSH_SINGLE_PAGE
后台线程会定时从LRU链表的尾部开始扫描一些页面,扫描的页面数量可以通过系统变量innodb_lru_scan_depth来指定,如果在LRU链表中发现脏页,则把它们刷新到磁盘。
控制块里会存储该缓冲页是否被修改的信息,所以在扫描LRU链表时,可以很轻松地获取到某个缓冲页是否是脏页的信息。
flush脏页虽然是常态,但是出现以下这两种情况,都是会明显影响性能的:
InnoDB 会在后台刷脏页,而刷脏页的过程是要将内存页写入磁盘。所以,无论是你的查询语句在需要内存的时候可能要求淘汰一个脏页,还是由于刷脏页的逻辑会占用 IO 资源并可能影响到了你的更新语句,都可能是造成你从业务端感知到 MySQL“抖”了一下的原因。
要尽量避免这种情况,你就要合理地设置 innodb_io_capacity 的值,并且平时要多关注脏页比例,不要让它经常接近 75%。
一旦一个查询请求需要在执行过程中先 flush 掉一个脏页时,这个查询就可能要比平时慢了。
而 MySQL 中的一个机制,可能让你的查询会更慢:在准备刷一个脏页的时候,如果这个数据页旁边的数据页刚好是脏页,就会把这个“邻居”也带着一起刷掉;而且这个把“邻居”拖下水的逻辑还可以继续蔓延,也就是对于每个邻居数据页,如果跟它相邻的数据页也还是脏页的话,也会被放到一起刷。
在 InnoDB 中,innodb_flush_neighbors 参数就是用来控制这个行为的,值为 1 的时候会有上述的“连坐”机制,值为 0 时表示不找邻居,自己刷自己的。
找“邻居”这个优化在机械硬盘时代是很有意义的,可以减少很多随机 IO。机械硬盘的随机 IOPS 一般只有几百,相同的逻辑操作减少随机 IO 就意味着系统性能的大幅度提升。
而如果使用的是 SSD 这类 IOPS 比较高的设备的话,建议你把 innodb_flush_neighbors 的值设置成 0。因为这时候 IOPS 往往不是瓶颈,而“只刷自己”,就能更快地执行完必要的刷脏页操作,减少 SQL 语句响应时间。
在 MySQL 8.0 中,innodb_flush_neighbors 参数的默认值已经是 0 了。