MySQL机制之Double Write

背景

我们知道MySQL在更新数据的时候,会先写入到内存中的数据页,然后再在一定的时机时,将脏页给刷新到磁盘中。
一个数据页默认为16k,但是由于操作系统的限制,每次需要多次才能将数据页给刷新到磁盘;如果在这个过程中发生故障,导致一个数据页并没有完整的给刷新到磁盘中。我们就称为页断裂 。在这种情况下,我们并不知道会出现什么未知的现象。
为了在页断裂场景发生时,能够对损坏的数据页进行修复工作。就引入了两次写(Double Write)机制。

什么是两次写

简单来说,就是在对数据页刷盘操作之前,先将该数据页写到一块独立的物理文件位置(ibdata)中。然后再将数据页进行刷盘操作。这样在宕机时,如果出现物理文件损坏,就能够利用ibdata中的副本文件进行修复行为。

Double Write由两部分组成;一部分是内存中的double write buffer,其大小为2M。另一部分由共享表空间(ibdata)中连续的128页,即2个区组成,大小也是2M。
其刷盘流程为:

  1. 当多个数据页需要进行刷盘时,并不直接写入到磁盘的物理文件中,而是先拷贝到内存中的double write buffer中。
  2. 接着从double write buffer中分两次写入到磁盘的共享表空间中(连续存储,顺序写,性能很高),每次写1M。
  3. 等第二步完成后,再将double write buffer中的脏页数据写入到实际的各个表空间中(离散写)

如何修复

在发生故障时,如何进行修复操作?
1、如果是写doublewrite buffer本身失败,那么这些数据不会被写到磁盘,InnoDB此时会从磁盘载入原始的数据,然后通过InnoDB的事务日志来计算出正确的数据,重新 写入到doublewrite buffer。
2、如果 doublewrite buffer写成功的话,但是写磁盘失败,InnoDB就不用通过事务日志来计算了,而是直接用double write buffer的数据再写一遍。
3、在恢复的时候,InnoDB直接比较页面的checksum,如果不对的话,就从硬盘载入原始数据,再由事务日志 开始推演出正确的数据.所以InnoDB的恢复通常需要较长的时间.

5.7优化

在《MySQL运维内参》一书中,提到了在5.7中,对double write进行了优化批量刷盘。但是我在5.7.18版本中,并没有看到有那个参数innodb_doublewrite_batch_size(为MySQL 8.0.20引入)。

批量刷盘包括两种方式,分别是LRU(Least Recently Used,最近最少使用)方式和LIST方式。当Buffer Pool空间不足时,再载入新的页面就必须要将一些不怎么用到的、旧的页面淘汰出去,此时系统就会从LRU链表中找到最老的页面,进行批量刷盘,将释放的空间加入到空闲空间中去,这种情况就是LRU刷盘。当日志空间不足,或者是后台MASTER线程在定时刷盘时,不需要区分页面的新旧状态,只需要选择LSN最小的那些页面,从前到后刷一批页面到文件中,此时所用的策略就是LIST方式。

在批量刷盘的两次写中,这两种刷盘方法对应的两次写空间互不干涉。

从图中可以看出落到最终的每一个shard,其实就是一个batch,对应的参数就是innodb_doublewrite_batch_size。一个shard,有一个数组,长度为innodb_doublewrite_batch_size,与单一页面刷盘的两次写是一样的,只是这个数组只属于一个shard而已。

假设由于页面淘汰,系统要做一次批量刷盘,这次就是LRU方式的,那么此时系统就需要将当前页面加入到两次写缓存中,首先根据当前页面所在的Instance号及刷盘类型就可以找到对应的shard缓存,找到缓存后,判断当前shard是否已经满了,即是否已经达到innodb_doublewrite_batch_size的大小,如果没有达到,则将当前页面内容追加复制到当前的shard缓存中,这样当前页面的刷盘操作就完成了。这里并不像单一页面那样,先写入缓存空间中,然后写入ibdata文件的两次写空间,最后还需要立即将页面的真实内容刷入表空间,对于批量刷盘来说,只需要写入到shard缓存即可。

如果当前shard中缓存的页面个数已经达到了innodb_doublewrite_batch_size,则说明当前缓存空间已经满了,此时不得不将当前shard缓存的页面写入两次写文件中,写完之后再将两次写文件FLUSH到磁盘,最后将对应的真实页面刷盘,此时可能是随机写入了,因为对应的两次写缓存中虽然是连续的,但对应的真实页面就不会这样了。这里需要注意的一点就是,表空间页面的刷盘,是异步IO操作,此时需要等待异步IO完成,且整个shard中的页面都刷盘后,刷盘操作才可以继续向后执行,而这个shard也可以再次重新使用了,缓存中的数据也都会被清空。

需要注意的是,上面过程中写入是连续innodb_doublewrite_batch_size 个页面,所以性能会比写入多次而每次写入一个页面的情况好很多。批量刷盘的情况下,有可能每隔innodb_doublewrite_batch_size个页面的刷盘操作,就会出现一次等待操作,且等待时间长短不一定,但这也是在单一页面刷盘的基础上优化过的,做了改进。

思考

发生页断裂时,理论上通过redo就应该能够进行恢复了,为什么还需要double write?

这个就得从redo log保存的记录格式说起了。
redo日志主要采用的是物理日志和逻辑日志两种方式。
对于能够唯一确定数据存储在磁盘位置时,使用的是物理日志,通过(group_id,file_id,page_no,offset)4元组来确定位置,并记录数据页变更。在这种记录格式下,能够通过redo log进行问题修复。
但是,对于一个操作记录产生的日志跨越了多个数据页时,那么会产生多个物理页面的日志,但对于每个物理页面日志,里面记录则是逻辑信息。在这种情况下,就无法单靠redo log进行问题修复

性能损耗

如果开启了double write机制。那么每次刷盘都会多一次内存复制和持久化操作。那么会对性能造成多大的影响呢?
由于持久化刷盘过程是顺序写,性能很高。据官方介绍,可能会有10%的性能损耗,但是为了数据得到完整性,这点损耗还是很有必要的。

https://www.cnblogs.com/xuliuzai/p/10290196.html
https://mp.weixin.qq.com/s/9GHIpT_YeUoZNJ2X6cS-YQ (推荐)