只要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,风险是主机掉电会丢数据。