Mysql中有两种比较重要的日志,一个 Mysql Server 实现的 归档日志(binlog),另外一个是 InnoDB 引擎特有的 重做日志(redo log)。
redo log
redo log 是 InnoDB 中用来实现崩溃恢复的日志。
InnoDB 会将更新操作的内容保存到内存缓冲区中,在合适的时机或不得已的情况下再把缓冲区的数据更新到磁盘中,这样可以减少磁盘的随机读写,提高更新操作的性能。但是一旦程序崩溃,缓冲区中的数据就会丢失,为了实现崩溃恢复,InnoDB引入 redo log,执行更新语句的时候不但需要把更新的数据保存到内存缓冲区,还需要把更新的数据写到redo log,这样在程序崩溃之后,能够根据redo log进行数据恢复,redo log是顺序写入的,因此能够保证更新操作的性能。这就是Mysql中经常说到的WAL技术(Write-Ahead Logging)。
redo log 又被称为 物理日志,记录的是“在某个数据页上做了什么修改”。
redo log 是固定大小的,采用循环写的方式;但是循环写是有条件的,当redo log写满以后,需要把相应的脏页写到磁盘(刷脏页),才能够对redo log进行“循环写”。
比如可以将 redo log 配置为一组 4 个文件,每个文件的大小是 1GB,那么总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。
write pos 是当前记录的位置,checkpoint 是 redo log 中还没写到磁盘的最旧更新记录的位置,write pos 和 checkpoint 之间的部分是可以 redo log 目前的可用空间,如果 write pos 追上 checkpoint,那么就需要进行刷脏页,把 checkpoint 向前推进后才能继续往 redo log 中写日志。
checkpoint机制
当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存为“脏页”。脏页是终将要写到磁盘上的,但是如何进行刷脏页才能保证性能最大化和减少数据库的崩溃恢复时间,这需要一系列的策略进行控制。因此InnoDB使用checkpoint技术来解决这个问题。
InnoDB 使用LSN(Log Sequence Number)来标记版本,LSN是8字节的数字,单位为字节。每个页有LSN,redo log 中也有LSN,checkpoint也是用LSN来表示。
InnoDB 内部有两种 checkpoint:
- Sharp Checkpoint
- Fuzzy Checkpoint
Sharp Checkpoint 是在数据库关闭时将所有脏页刷到磁盘中。Fuzzy Checkpoint 是数据库运行时采用的脏页刷盘机制。
Fuzzy Checkpoint 分为以下四种情况(分别对应不同的脏页刷盘时机):
Master Thread Checkpoint
Master Thread是InnoDB 中的一个后台线程,会以每秒或每十秒的速度异步地从缓冲区的脏页列表中刷新一定比例的页回磁盘。
FLUSH_LRU_LIST Checkpoint
InnoDB需要保证检查LRU列表中是否有足够的可用页数量,如果没有,就淘汰旧的内存页,如果要被淘汰的内存页是脏页,就需要进行把脏页写回磁盘中。
InnoDB-1.2.x 开始,用户可以通过参数innodb_lru_scan_depth
控制 LRU 列表中可用页的数量Async/Sync Flush Checkpoint
为了保证 redo log 循环使用的可用性,根据 redo log 的可用空间异步或同步地进行的脏页刷盘。
Dirty Page too much Checkpoint
当脏页太多时进行的脏页刷盘。
innodb_max_dirty_pages_pct
参数控制当缓冲区中脏页比例大于某个值时,强制进行checkpoint,刷新一部分脏页到磁盘。
InnoDB 的脏页刷盘速度参考两个因素: 脏页比例 和 磁盘能力。
innodb_io_capacity
参数是用于告诉 InnoDB 你的磁盘能力,这个值建议设置成磁盘的IOPS。
如果脏页比例一直很高,或者没有合理设置innodb_io_capacity
参数的值(设置的值比磁盘实际的IOPS小),可能会导致 InnoDB 刷盘速度慢,一直在刷脏页,跟不上数据更新的速度,导致 mysql 整体的性能下降。
脏页比例可以通过Innodb_buffer_pool_pages_dirty
/Innodb_buffer_pool_pages_total
得到的,具体的命令参考下面的代码:
1 | select VARIABLE_VALUE into @a from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_dirty';select VARIABLE_VALUE into @b from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_total';select @a/@b; |
写入机制
InnoDB 写 redo log 其实不是直接写到磁盘中的,而是先写到 redo log buffer, redo log buffer 就是一块内存,用来缓冲一次事务的 redo log,在事务提交时再把 redo log buffer 中的数据写到磁盘(事务执行过程不会“主动刷盘”,但是可能会因为其他的原因而出现“被动刷盘”,比如内存不够、其他事务提交等)。
而将 redo log buffer 写到磁盘又分是 写到 page cache 还是 持久化到磁盘 两种情况。
InnoDB 提供了 innodb_flush_log_at_trx_commit
参数来控制 redo log 的写入策略,它有三种可能取值:
- 设置为 0 的时候,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ;
- 设置为 1 的时候,表示每次事务提交时都将 redo log 直接持久化到磁盘;
- 设置为 2 的时候,表示每次事务提交时都只是把 redo log 写到 page cache。
另外,InnoDB 有一个后台线程,每隔 1 秒,就会把 redo log buffer 中的日志,调用 write 写到文件系统的 page cache,然后调用 fsync 持久化到磁盘。
另外,redo log buffer 其实是被多个事务共享的,因此除了后台线程每秒一次的轮询操作外,还有两种场景会让一个没有提交的事务的 redo log 写入到磁盘中:
- 一种是,redo log buffer 占用的空间即将达到 innodb_log_buffer_size 一半的时候,后台线程会主动写盘。注意,由于这个事务并没有提交,所以这个写盘动作只是 write,而没有调用 fsync,也就是只留在了文件系统的 page cache。
- 另一种是,并行的事务提交的时候,顺带将这个事务的 redo log buffer 持久化到磁盘。假设一个事务 A 执行到一半,已经写了一些 redo log 到 buffer 中,这时候有另外一个线程的事务 B 提交,如果 innodb_flush_log_at_trx_commit 设置的是 1,那么按照这个参数的逻辑,事务 B 要把 redo log buffer 里的日志全部持久化到磁盘。这时候,就会带上事务 A 在 redo log buffer 里的日志一起持久化到磁盘。
binlog
binlog 是 Mysql Server 用来实现备份、复制、数据恢复等功能的二进制日志。
binlog 有被称为“逻辑日志”,记录的是对mysql数据更新或潜在发生更新的SQL语句。
binlog 没有固定大小,采用追加写的方式,文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
binlog 有以下三种格式:
- Statement: 每一条修改数据的 sql 都会记录在日志中。
- Row:不记录sql语句上下文相关信息,就保存哪条记录被修改。
- Mixed: 以上两种格式的混合使用,一般的语句修改使用statment格式保存;如一些函数,statement无法完成主从复制的操作,则采用row格式保存。
写入机制
和 redo log 的写入机制类似,事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。
同样,写到 binlog 文件中又分为 写到 page cache 还是 持久化到磁盘 两种情况。
一个事务的 binlog 是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。这就涉及到了 binlog cache 的保存问题。
系统给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size
用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。
事务提交的时候,执行器把 binlog cache 里的完整事务写入到 binlog 中,并清空 binlog cache。状态如下图所示。
每个线程有自己 binlog cache,但是共用同一份 binlog 文件。
图中的 write 就是写到 page cache,fsync 就是持久化到磁盘。
write 和 fsync 的时机,是由参数 sync_binlog
控制的:
- sync_binlog=0 的时候,表示每次提交事务都只 write,不 fsync;
- sync_binlog=1 的时候,表示每次提交事务都会执行 fsync;
- sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。
因此,在出现 IO 瓶颈的场景里,将 sync_binlog 设置成一个比较大的值,可以提升性能。在实际的业务场景中,考虑到丢失日志量的可控性,一般不建议将这个参数设成 0,比较常见的是将其设置为 100~1000 中的某个数值。
但是,将 sync_binlog 设置为 N,对应的风险是:如果主机发生异常重启,会丢失最近 N 个事务的 binlog 日志。
redo log 与 binlog 的联系
两阶段提交
如上图所示,要更新某一行数据时,Mysql 执行器调用 InnoDB 引擎更新数据接口,InnoDB 将数据更新到内存,同时将更新操作记录到 redo log,此时 redo log 处于 prepare 阶段,然后告知 Mysql 执行器更新操作完成了。接着执行器生成这个操作的 binlog,并把 binlog 写到磁盘,最后调用 InnoDB 的事务提交接口,InnoDB 把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
以上,就是 Mysql 在更新数据时对 redo log 进行的两阶段提交,目的是为了保证数据的一致性。
没有两阶段提交,造成数据怎样的不一致?
- 先写 redo log 后写 binlog。 假设 redo log 写完,binlog 还没有写完时,MySQL异常重启,MySQL根据redo log 把数据恢复,但是 binlog 中没有这条数据的记录,如果之后根据 binlog 来恢复数据库时就会缺少数据,造成数据不一致。
- 先写 binlog 后写 redo log。 假设 binlog 写完,redo log 还没有写完时,MySQL异常重启,重启后由于redo log 没有该条数据的记录,所以不会恢复数据,但是binlog中有该条数据的记录,如果之后根据binlog来恢复数据库时会出现数据不一致。
为什么两阶段提交可以保证数据的一致性?
- 当在写binlog之前崩溃
重启恢复:后发现没有commit,回滚。 备份恢复:没有binlog 。 数据一致。 - 当在commit之前崩溃
重启恢复:虽没有commit,但满足prepare和binlog完整,所以重启后会自动commit。 备份恢复:有binlog。 数据一致。
如果把
innodb_flush_log_at_trx_commit
设置成 1,那么 redo log 在 prepare 阶段就要持久化一次,因为有一个崩溃恢复逻辑是要依赖于 prepare 的 redo log,再加上 binlog 来恢复的。(如果你印象有点儿模糊了,可以再回顾下第 15 篇文章中的相关内容)。
每秒一次后台轮询刷盘,再加上崩溃恢复这个逻辑,InnoDB 就认为 redo log 在 commit 的时候就不需要 fsync 了,只会 write 到文件系统的 page cache 中就够了。
通常我们说 MySQL 的“双 1”配置,指的就是
sync_binlog
和innodb_flush_log_at_trx_commit
都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是 redo log(prepare 阶段),一次是 binlog。
组提交(group commit)
组提交指的是,等多个事务的 redo log 都写到 redo log buffer 或 page cache 后再真正持久化到磁盘,能够节约磁盘的IOPS,提高性能。
MySQL 为了让组提交的效果更好,把 redo log 做 fsync 的时间拖到了 binlog write 之后 binlog fsync 之前,如下图所示:
如果你想提升 binlog 组提交的效果,可以通过设置 binlog_group_commit_sync_delay
和 binlog_group_commit_sync_no_delay_count
来实现。
binlog_group_commit_sync_delay
参数,表示延迟多少微秒后才调用 fsync;binlog_group_commit_sync_no_delay_count
参数,表示累积多少次以后才调用 fsync。
这两个条件是或的关系,也就是说只要有一个满足条件就会调用 fsync。所以,当 binlog_group_commit_sync_delay
设置为 0 的时候,binlog_group_commit_sync_no_delay_count
也无效了。
参考
Mysql实战45讲:日志系统:一条SQL更新语句是如何执行的?
Mysql实战45讲:为什么我的MySQL会“抖”一下?
Mysql实战45讲:答疑文章(一):日志和索引相关问题
Mysql实战45讲:MySQL是怎么保证数据不丢的?
InnoDB Checkpoint