MySQL 的 binlog 和 redo log 写入原理

binlog 的写入流程

事务执行过程中,binlog 首先会被写到 binlog cache 中;事务提交的时候,再讲binlog cache 写到 binlog 文件中。一个事务的 binlog 是原子的,无论多大都需要保证完整性。

系统为每个客户端线程分配一个 binlog cache,其大小由 binlog_cache_size 控制。如果binlog cache 超过阀值,就会临时持久化到磁盘。当事务提交的时候,再将 binlog cache 中完整的事务持久化到磁盘中,并清空 binlog cache。

从上面可以看出,每个客户端线程都有自己独立的 binlog cache,但是会共享一份 binlog files。

上面的 write 是指把binlog cache 写到文件系统的 page cache,并没有写入到磁盘中,因此速度较快。

fsync 是实际的写盘操作,占用磁盘的 IOPS。

write 和 fsync 的写入时机,是由sync_binlog 控制的:

1、sync_binlog=0:每次事务提交都只 write,不 fsync;

2、sync_binlog=1:每次事务提交都会fsync;

3、sync_binlog=N(N>1):每次提交事务都会 write,累计N 个后再执行 fsync。

在出现 IO 瓶颈的情况下,可以考虑将 sync_binlog 设置成一个大的值。比较常见的是将 N设置为 100~1000。但是存在的风险是,当主机异常重启时会丢失 N 个最近提交的事务 binlog。

redo log 写入流程

前面介绍过了 redo log 的写入首先会写入 redo log cache,其详细的状态如下所示:


redo log 对应上面的 3 种状态分别是:1、在 MySQL 应用的 redo log buffer 中;2、write 到文件系统的 page cache 中,但是没有进行实际的写盘操作(fsync);3、执行 fsync 之后,写盘结束。

InnoDB 有一个后台线程,每个 1 秒钟 就会将 redo log buffer 中的日志,调用 write 写入到 文件系统的 page cache 中,然后再调用 fsync 持久化到磁盘中。redo log buffer 是共享的,因此一些正在执行中的事务的 redo log 也有可能被持久化到磁盘中。

通常我们说的 MySQL 的 “双1” 操作,指的是 sync_binlog = 1 AND innodb_flush_log_at_trx_commit = 1 。innodb_flush_log_at_trx_commit 设置成 1 表示 redo log 在 prepare 阶段就需要持久化一次,那么 “双1” 配置 每个事务提交的时候都会刷盘 2 次,一次是 binlog,一次是 redo log。

为了控制 redo log 的写入策略,innodb_flush_log_at_trx_commit 会有下面 3 中取值:1、0:每次提交事务只写在 redo log buffer 中;2、1:每次提交事务持久化到磁盘;3、2:每次提交事务写到 文件系统的 page cache 中。

redo log 实际的触发 fsync 操作写盘包含以下几个场景:1、后台每隔 1 秒钟的线程轮询;2、innodb_flush_log_at_trx_commit 设置成 1 时,事务提交时触发;3、innodb_log_buffer_size 是设置 redo log 大小的参数,当 redo log buffer 达到 innodb_log_buffer_size / 2 时,也会触发一次 fsync。

组提交

MySQL 为了优化磁盘持久化的开销,会有一个 组提交(group commit)的机制。首先我们介绍一下 日志逻辑序列号(log sequence number,LSN),它是用来对应每个 redo log 写入点的递增序列号。每次写入长度为 length 的 redo log,LSN 就会加上 length。

下面是 3 个并发事务(trx1、trx2、trx3)在 prepare 阶段都写完了 redo log buffer,然后组提交持久化的过程。其中 3 个事务对应的 LSN 分别是:50、120、160。


1、trx1 是最先到达的,会被选为组 leader;

2、当 trx1 准备提交的时候,组里面已经有 3 个事务了,此时 LSN 变成了 160;

3、trx1 写盘结束后,LSN 小于 160 的区块都已经被持久化;

4、此时 trx2 和 trx3 就可以直接返回了。

组提交优化

从上面描述可以看出,一次组提交里面的组员越多,节约磁盘 IOPS 的效果越好。在并发场景下,一个事务写完 redo log 之后,fsnyc 越晚调用,组员可能越多,其节约IOPS 的效果越好。binlog 和 redo log 的执行过程简图如下所示:


上面 binlog 的写入是 1 个步骤,事实上 binlog 的写入也是分成 2 步的:1、先将 binlog 从 binlog cache 写入磁盘的 page cache;2、然后再调用 fsync 持久化。为了让组提交的效果更好,把 redo log 做 fsync 的时间调整到了 binlog write 之后,如下所示:


从上图的流程可以看出,binlog 的写盘也可以组提交了。当上面执行 binlog: fsync 时,可以将需要写盘的 binlog 一起写入(事务完整 binlog),这样也可以减少一部分 IOPS 的开销。

通常情况下 redo log prepare: fsync 阶段执行的时间较短,此时可能binlog 的组提交可能没有 redo log 的组提交效果那么好。此时可以通过下面 2 个参数来提升 binlog 组提交的效率:

1、binlog_group_commit_sync_delay:表示延迟多少微秒后,再执行 fsync;

2、binlog_group_commit_sync_no_delay_count:表示累计多少次后,在调用 fsync;

上面 2 个参数是 或 的关系,满足其中一个就可以触发 fsync。

WAL 机制可以减少磁盘的写入次数,主要得益于下面 2 个方面:

1、redo log 和 binlog 都是顺序写,比磁盘的随机写要快;2、组提交机制可以大幅度减少磁盘的IOPS 消耗。

总结

当MySQL 出现了 IO 上面的性能问题,可以考虑下面的优化策略。

1、设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count。可以使用故意等待来减少,binlog 的写盘次数,没有数据丢失的风险,但是会有客户端响应变慢的风险。

2、设置 sync_binlog 设置为 100~1000 之间的某个值。这样做存在的风险是可能造成 binlog 丢失。

3、设置 innodb_flush_log_at_trx_commit = 2,可能会丢数据。


参考:《极客时间:MySQL实战》、《高性能MySQL》