MySQL 是怎么保证数据不丢的?
只要 redo log 和 binlog 保证持久化到磁盘,就能确保 MySQL 异常重启后,数据可以恢复。本文讲讲 MySQL 写入 binlog 和 redo log 的流程。
binlog 的写入机制
binlog 的写入逻辑比较简单:事务在执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件。
一个事务的 binlog 不能拆开,因此不论这个事务多大,也要确保一次性写入,这涉及到 binlog cache 的保存问题。系统给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存大小,如果超过参数大小,就要暂存到磁盘。
事务提交时,执行器把 binlog cache 里的完整事务写入 binlog 并清空 binlog cache。

上图中:
write 指的是把日志写入到文件系统的 page cache,速度较快;
fsync 指的是把数据持久化到磁盘。
write 和 fsync 的时机,由参数 sync_binlog 控制:
sync_binlog=0
,表示每次提交事务都只 write,不 fsync;sync_binlog=1
,表示每次提交事务都会 fsync;sync_binlog=N>1
,表示每次提交事务都 write,但累积 N 个事务后才 fsync。
因此,在 IO 瓶颈的场景里,一般将参数设置为较大的值。在实际业务场景中,考虑到丢失日志量的可控性,比较常见的设置为 100-1000 中的某个数值。其风险是如果主机发生异常重启,会丢失最近 N 个事务的 binlog 日志。
redo log 的写入机制
事务在执行过程中,生成的 redo log 先写到 redo log buffer。redo log buffer 里的内容,不需要每次生成后都直接持久化到磁盘,但也有可能在事务还没提交的时候被持久化到磁盘。
这个问题,需要从 redo log 可能存在的三种状态说起:

三种状态为:
存在 redo log buffer,物理上是在 MySQL 进程内存,即图中红色;
写到磁盘,但是没有持久化(fsync),物理上是在文件系统的 page cache,即图中黄色;
持久化到磁盘,对应的是 hard disk,即图中绿色。
日志写到 redo log buffer,write 到 page cache 都很快,但持久化到磁盘的速度会慢很多。
为了控制 redo log 的写入策略,InnoDB 提供 innodb_flush_log_at_trx_commit 参数:
设置为 0,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中;
设置为 1,表示每次事务提交时都将 redo log 持久化到磁盘;
设置为 2,表示每次事务提交时都只是把 redo log 写到 page cache。
InnoDB 有一个后台线程,每隔一秒会把 redo log buffer 中的日志调用 write 写到文件系统的 page cache,然后调用 fsync 持久化到磁盘。
事务执行中间过程的 redo log 也是直接写在 redo log buffer 中,这些 redo log 也会被后台线程一起持久化到磁盘,因此一个没有提交的事务的 redo log 也有可能已经被持久化到磁盘。
除了后台线程每秒一次的轮询操作外,还有两种场景会让一个没有提交的事务的 redo log 写入到磁盘:
redo log buffer 占用的空间即将达到 innodb_log_buffer_size 一半的时候,后台线程会主动写盘。这个写盘动作只是 write,而没有调用 fsync;
并行的事务提交的时候,顺带将这个事务的 redo log buffer 持久化到磁盘。假设一个事务 A 执行到一半,已经写了一些 redo log 到 buffer,这时另一个线程的事务 B 提交,如果
innodb_flush_log_at_trx_commit=1
,那么事务 B 要把 redo log buffer 里的日志全部持久化到磁盘,这时会带上事务 A 在 redo log buffer 里的日志一起持久化。
两阶段提交的时序是 redo log 先 prepare,再写 binlog,最后再把 redo log commit。如果innodb_flush_log_at_trx_commit=1
,那么 redo log 在 prepare 阶段就要持久化一次,因为有一个崩溃恢复逻辑依赖 prepare 的 redo log+binlog。每秒一次后台轮询加上崩溃恢复这个逻辑,InnoDB 会认为 redo log 在 commit 时不需要 fsync 了,只会 write 到文件系统的 page cache。
通常说 MySQL 的双 1 配置,指的就是 sync_binlog 和 innodb_flush_log_at_trx_commit 都设置为 1,即一个事务完整提交前,需要等待两次刷盘,一次是 redo log prepare,一次是 binlog。
这时,可能有一个疑问,这意味着从 MySQL 看到的 TPS 是每秒两万的话,每秒就会写四万次磁盘,但磁盘能力也就两万左右,怎么实现两万的 TPS。
解释该问题需要用到组提交机制。先介绍日志逻辑序列化(LSN)的概念。LSN 是单调递增的,用来对应 redo log 的一个个写入点,每次写入长度为 length 的 redo log,LSN 的值就会加上 length。LSN 也会写到 InnoDB 数据页,来确保数据页不会被多次执行重复的 redo log。
下图是三个并发事务在 prepare 阶段,都写完 redo log buffer 持久化到磁盘的过程,对应的 LSN 分别是 50、120 和 160:

解释该过程:
trx1 第一个到达,会被选为这个组的 leader;
当 trx1 要开始写盘,组里有三个事务,此时 LSN 变为 160;
trx1 去写盘时,带的是 LSN=160,等 trx1 返回,所有 LSN 小于等于 160 的 redo log 都已经被持久化到磁盘;
此时 trx2 和 trx3 可以直接返回了。
所以一次组提交里,组员越多,节约磁盘 IOPS 的效果越好。在并发更新场景下,第一个事务写完 redo log buffer 后,接下来 fsync 越晚调用,组员可能越多,节约 IOPS 的效果就越好。
而为了让一次 fsync 带的组员更多,MySQL 也有优化。在两阶段提交的过程,如果将 binlog 拆分,可以分为 write 和 fsync,整个两阶段提交过程实际上为:

如上,MySQL 调整了时间步骤,这样使得 binlog 也可以组提交,在执行第 4 步时,如果有多个事务的 binlog 已经写完,也是一起持久化的,这样也减少了 IOPS 的消耗。
不过通常第 3 步很快,所以第 2 步和第 4 步间隔很短,导致能集合到一起持久化的 binlog 比较少,因此 binlog 组提交效果通常不如 redo log。如果想提升 binlog 组提交效果,可以设置两个参数:
binlog_group_commit_sync_delay:表示延迟多少微秒后才调用 fsync;
binlog_group_commit_sync_no_delay_count:表示累积多少次后才调用 fsync。
这里可以总结,WAL 机制主要得益于两方面:
redo log 和 binlog 都是顺序写,比随机写速度要快;
组提交机制大幅降低磁盘的 IOPS 消耗。
最后再回答一下,如果 MySQL 出现了 IO 方面的性能瓶颈,该怎么去提升性能:
设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 参数,减少 binlog 的写盘次数;
将 sync_binlog 设置成大于 1,风险是主机掉电时会丢 binlog 日志;
将 innodb_flush_log_at_trx_commit 设为 2,风险是主机掉电会丢数据。
文章转载自:san-mu
评论