double write 处理部分写失效的问题,提升数据页的可靠性。
背景
InnoDB 中有记录(Row)被更新时,先将其在 Buffer Pool 中的 page 更新,并将这次更新记录到 Redo Log file 中,这时候 Buffer Pool 中的该 page 就是被标记为 Dirty。在适当的时候(Buffer Pool 不够、Redo 不够,系统闲置等),这些 Dirty Page 会被 Checkpoint 刷新到磁盘进行持久化操作。
存在问题
InnoDB 每个数据页的大小默认是 16KB,其数据校验也是针对这 16KB 来计算的,将数据写入到磁盘是以 Page 为单位进行操作的,而文件系统是以 4KB 为单位写入,磁盘 IO 的最小单位是 512B,因此并不能保证数据页的写入就是原子性的。
为什么不能使用 redo log 来进行恢复呢?答案是只能恢复校验完整(还没写)的页,不能恢复已损坏的页。比如某次 checkpoint 要刷入 4 个数据页,其中第一页写了 2KB,后三页还未写。那么根据 redo log 可以恢复后三页,但已经写了 2KB 的页没法恢复,因为没法知道在宕机前第一页到底写了多少。
实现策略
在应用重做日志前,用户需要一个页的副本,当写入失效发生时,先通过页的副本来还原该页,在进行重做,这就是 doublewrite。
double write 由两部分组成,一部分是内存中的 doublewrite buffer,大小为 2MB,另一部分是物理磁盘上共享表空间中连续的 128 个页,即 2 个区,大小同样为 2MB。在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是会通过 memcpy 函数将脏页先复制到内存中的 doublewrite buffer,之后通过 doublewrite buffer 再分两次,每次 1MB 顺序地写入共享表空间的物理磁盘上,然后马上调用 fsync 函数,同步磁盘。在这个过程中,因为 doublewrite 页是连续的,因此这个过程是顺序写的,开销不是很大。
当宕机发生时,有那么几种情况:
- 磁盘还未写,此时可以通过 redo log 恢复;
- 磁盘正在进行从内存到共享表空间的写,此时数据文件中的页还没开始被写入,因此也同样可以通过 redo log 恢复;
- 磁盘正在写数据文件,此时共享表空间已经写完,可以从共享表空间拷贝页的副本到数据文件实现恢复。