【华为云MySQL技术专栏】MySQL 双写缓冲区(Doublewrite Buffer)机制解析
1、概述
MySQL中的Doublewrite Buffer是InnoDB存储引擎的关键机制,用于防止因部分写(partial page write)导致的数据损坏,它是MySQL保证数据完整性和可靠性的一个关键组件。
2、背景与问题
Linux操作系统的文件系统页(OS Page)大小通常为4KB,而部署在Linux系统上的数据库引擎因为需要与底层文件系统进行高效交互,所以大多数据库系统都会将数据页大小设置为操作系统页大小的整数倍。
MySQL的InnoDB存储引擎的数据页(Page)默认大小为16KB,这意味着InnoDB在执行单次数据页刷盘操作时,实际上需要向磁盘写入4个连续的文件系统页。如图所示:
图1:InnoDB数据页落盘
这种设计虽然在性能上有所优化,但也带来了原子写入的挑战——在部分写场景下,如果系统在写入过程中发生故障,可能导致只有部分文件系统页被成功写入,从而造成数据页损坏。这正是Doublewrite Buffer机制要解决的核心问题。
-
数据写不完整:InnoDB数据页默认16KB,而文件系统/磁盘块通常为4KB,若在写入过程中发生崩溃(如断电),可能导致只有部分块写入磁盘,导致页面不完整。

图2:部分写
-
数据读写失败:因为部分写而导致页面数据不一致,访问该页面会出错,进一步说,也会影响redo log的应用,导致其应用失败。
3、Doublewrite Buffer 核心机制
3.1. 结构
Doublewrite Buffer的出现就是为了解决上面的这种情况,给InnoDB存储引擎提供了数据页的可靠性。
Doublewrite Buffer是内存+磁盘的结构,包括内存结构和磁盘结构两个部分:
-
内存结构:
Doublewrite Buffer内存结构由128个页构成,大小是2MB。这些页在内存中以Doublewrite Buffer的形式存在。 -
磁盘结构:
Doublewrite Buffer磁盘结构在系统表空间上是128个页(2个区,extend1和extend2),大小也是2MB。这些页在磁盘上以Doublewrite File的形式存在。
3.2. 原理
Doublewrite Buffer的原理是,在把数据页写到数据文件之前,InnoDB先把它们写到系统共享表空间的Doublewrite Buffe区域内,在Doublewrite Buffer刷盘完成后,InnoDB才会将数据页写入到各自的数据文件位置。
如果在写数据文件的过程中发生意外崩溃,InnoDB会在恢复时先检查数据文件的数据页是否损坏,若数据文件的数据页损坏再从Doublewrite Buffer中获取完整的page部分进行恢复。
图3:Doublewrite Buffer原理
如上图所示,当有数据页要刷盘时:
-
页数据先通过
memcpy函数拷贝至内存中的Doublewrite Buffer中。 -
Doublewrite Buffer的内存里的数据页,会fsync刷到Doublewrite Buffer的磁盘上,分两次写入磁盘共享表空间中(连续存储,顺序写,性能很高)。 -
Doublewrite Buffer的内存里的数据页,再刷到数据页对应的表空间ibd文件上(离散写)。
工作流程图如下:
图4:Doublewrite工作流程
3.3. 崩溃恢复
因为在两次写操作中,数据文件和Doublewrite Buffer文件中至少有一份数据是正确无误的。所以如果操作系统在将页写入数据文件的过程中发生了崩溃,在恢复过程中,MySQL的InnoDB存储引擎可以从共享表空间中的Doublewrite Buffer文件中找到损坏的数据页的一个副本,将其复制到表空间文件,再应用redo日志。
以下为通过校验和来确认数据文件是否损坏的详细条件判断分支:
-
如果数据文件正确,则直接从数据文件中拉取原始数据,再根据redo log得出正确的目标数据。 -
如果数据文件错误了,则将 Doublewrite Buffer中的数据重新写入数据文件,然后再应用后续redo log,得出正确的目标数据。 -
如果数据文件和
Doublewrite Buffer文件都错误了,则说明MySQL出现了严重错误,直接crash退出。
bool dblwr::recv::Pages::dblwr_recover_page(page_no_t dblwr_page_no,
fil_space_t *space,
page_no_t page_no,
byte *page) noexcept {
... ...
/* 从数据文件中读取的数据页是否损坏? */
BlockReporter data_file_page(true, buffer.begin(), page_size,
fsp_is_checksum_disabled(space->id));
if (data_file_page.is_corrupted()) {
ib::info(ER_IB_MSG_DBLWR_1315) << "Database page corruption or"
<< " a failed file read of page " << page_id
<< ". Trying to recover it from the"
<< " doublewrite file.";
dberr_t dblwr_err;
const bool dblwr_corrupted =
is_dblwr_page_corrupted(page, space, page_no, &dblwr_err);
if (dblwr_corrupted) {
... ...
ib::fatal(UT_LOCATION_HERE, ER_IB_MSG_DBLWR_1306);
}
} else {
bool data_page_zeroes = buf_page_is_zeroes(buffer.begin(), page_size);
bool dblwr_zeroes = buf_page_is_zeroes(page, page_size);
dberr_t dblwr_err;
const bool dblwr_corrupted =
is_dblwr_page_corrupted(page, space, page_no, &dblwr_err);
if (data_page_zeroes && !dblwr_zeroes && !dblwr_corrupted) {
/* 数据页面为全0页面,而Doublewrite Buffer中存有有效页面 */
} else {
/* 数据页面完好,不需要从Doublewrite Buffer中恢复 */
return false;
}
}
... ...
/* 从Doublewrite Buffer中读取完好有效页面写入到数据文件的目标位置 */
err = fil_io(write_request, true, page_id, page_size, 0, page_size.physical(),
const_cast<byte *>(page), nullptr);
ut_a(err == DB_SUCCESS || err == DB_TABLESPACE_DELETED);
ib::info(ER_IB_MSG_DBLWR_1308)
<< "Recovered page " << page_id << " from the doublewrite buffer.";
return true;
}
4、关键设计考量
4.1. 性能影响
写Doublewrite Buffer会导致系统有更多的fsync操作,而硬盘的fsync性能,会降低MySQL的整体性能,但是两次写并不会使性能降低到一次写的50%,这主要是因为:
-
顺序写入优势:
Doublewrite Buffer是一个连续的存储空间,所以硬盘再写数据的时候是顺序写,而不是随机写,这样性能更高,总体开销可控。 -
批量处理:将数据从
Doublewrite Buffer写入到真正的segment中的时候,内存缓冲区累积多个脏页后批量写入,减少I/O次数。
综上,Doublewrite Buffer性能开销,通常带来至少10%-25%的性能影响。
4.2. 配置与管理
-
启用参数:
innodb_doublewrite(默认ON)。 -
禁用场景:在具备原子写能力的存储(如ZFS、BBWC RAID)中可关闭以提升性能,但需谨慎评估风险。
-
多文件支持:MySQL 8.0+支持
innodb_doublewrite_files和innodb_doublewrite_dir,允许分散存储提升并发性。
4.3. 例外情况
-
临时表空间:
CREATE TEMPORARY TABLE生成的临时表数据不经过Doublewrite,因其重启后自动清理,无需做崩溃恢复处理。 -
非InnoDB引擎:如MyISAM不受此机制影响。
4.4. 相关参数及状态变量
以下是一些与Doublewrite Buffer相关的参数及其含义:
mysql> show variables like 'innodb_doublewrite%';
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| innodb_doublewrite | ON |
| innodb_doublewrite_batch_size | 0 |
| innodb_doublewrite_dir | |
| innodb_doublewrite_files | 2 |
| innodb_doublewrite_pages | 4 |
+-------------------------------+-------+
5 rows in set (0.01 sec)
-
innodb_doublewrite:这个参数用于启用或禁用双写缓冲区。设置为ON时启用,设置为OFF时禁用,默认值为ON。 -
innodb_doublewrite_files:(在MySQL 8.0.20中引入)这个参数定义了多少个双写文件被使用。默认值为2。 -
innodb_doublewrite_dir:(在MySQL 8.0.20中引入)这个参数制定了存储双写缓冲文件的目录的路径。默认为空字符串,表示将文件存在哪个数据目录中。 -
innodb_doublewrite_batch_size:(在MySQL 8.0.20中引入)这个参数定义了每次批处理操作写入的字节数。默认值为0,表示InnoDB会选择最佳的批量大小。 -
innodb_doublewrite_pages:(在MySQL 8.0.20中引入)这个参数定义了每个线程在双写缓冲区中可以处理的最大页数。默认值为innodb_write_io_threads参数的值,即4。
状态变量及其含义:
mysql> show status like 'Innodb_dblwr%';
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| Innodb_dblwr_pages_written | 65 |
| Innodb_dblwr_writes | 19 |
+----------------------------+-------+
2 rows in set (0.00 sec)
-
Innodb_dblwr_pages_written:已写入Doublewrite缓冲区的页数。 -
Innodb_dblwr_writes:执行Doublewrite的操作数。
5、总结
Doublewrite Buffer机制是MySQL成为高可靠数据库系统的基石之一,不仅可以实现数据安全保障:通过冗余写入确保崩溃恢复时有完整副本可用,避免部分写失效;还可以做到性能权衡:以额外顺序写为代价,换取数据可靠性,适合多数生产场景。通过这一机制,InnoDB可以在保障数据完整性的同时,最小化性能影响。
- 点赞
- 收藏
- 关注作者
评论(0)