上一篇blog从DML语句的执行流程开始到两种日志以及两阶段提交,详细介绍了MySQL是如何具备crash-safe能力【只要 redo log 和 binlog 保证持久化到磁盘,就能确保 MySQL 异常重启后,数据可以恢复】的,以及WAL机制,由前文可能会产生疑问,WAL 机制是减少磁盘写,可是每次提交事务都要写 redo log 和 binlog,这磁盘读写次数也没变少呀?其实WAL 机制主要得益于两个方面:
- redo log 和 binlog 都是顺序写,磁盘的顺序写比随机写速度要快;
- 组提交机制,可以大幅度降低磁盘的 IOPS 消耗(IOPS (Input/Output Per Second)即每秒的输入输出量(或读写次数),是衡量磁盘性能的主要指标之一。IOPS是指单位时间内系统能处理的I/O请求数量)
这篇文章更加深入的对两种日志进行深入介绍,详细说明其写入机制以及设置策略【组提交策略】,看看MySQL是如何安全的实现WAL机制的。本篇文章学习自《极客时间45MySQL45讲》
BinLog写入机制
事务执行过程中,**先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件【文件系统缓存page cache】**中。一个事务的 binlog 是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。这就涉及到了 binlog cache 的保存问题。
系统给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size
用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。事务提交的时候,执行器把 binlog cache 里的完整事务写入到 binlog 中,并清空 binlog cache
- 每个线程有自己 binlog cache,但是共用同一份 binlog 文件。
- write,指的就是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,所以速度比较快【binlog file on FS cache】。
- fsync,才是将数据持久化到磁盘的操作【binlog file on disk】
一般情况下,我们认为 fsync 才占磁盘的 IOPS。write 和 fsync 的时机,是由参数 sync_binlog
控制的:
- sync_binlog=0 的时候,表示每次提交事务都只 write,不 fsync;实际的业务场景中,考虑到丢失日志量的可控性,一般不建议将这个参数设成 0
- sync_binlog=1 的时候,表示每次提交事务都会执行 fsync;
- sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。比较常见的是将其设置为 100~1000 中的某个数值。但是,将 sync_binlog 设置为 N,对应的风险是:如果主机发生异常重启,会丢失最近 N 个事务的 binlog 日志,主机都crash了,文件系统缓存里的日志当然也丢了,没法落盘
可以理解为sync_binlog等于几表明每几个事务发生一次fsync,因此,在出现 IO 瓶颈的场景里,将 sync_binlog 设置成一个比较大的值,可以提升性能。因为binlog完整事务写入binlog cache的需求可能因为binlog cache没有那么大的内存不能得到满足,所以所有的策略都是从write起步,而redolog没有这个限制,所以甚至可以控制redolog buffer到write之间的粒度。
为什么 binlog cache 是每个线程自己维护的,而 redo log buffer 是全局共用的?
MySQL 这么设计的主要原因是,binlog 是不能“被打断的”:
- 一个事务的 binlog 必须连续写,因此要整个事务完成后,再一起写到文件里。而 redo log 并没有这个要求,中间有生成的日志可以写到 redo log buffer 中。redo log buffer 中的内容还能“搭便车”,其他事务提交的时候可以被一起写到磁盘中
- binlog存储是以statement或者row格式存储的,而redo log是以page页格式存储的。page格式,天生就是共有的,而row格式,只跟当前事务相关,statement记录的是更新的SQL,但是要写上下文,因此不能中断,要不同步到从库无法恢复一样的数据内容
基于以上原因,binglog cache不能共有。
为什么binlog要一次性写入
因为事务是原子的,且要保证一致性,所以,将事务写入日志,也必须是一次性完整写入的,因为一个线程只能同时有一个事务在执行。由于这个设定,所以每当执行一个begin/start transaction的时候,就会默认提交上一个事务;这样如果一个事务的binlog被拆开的时候,在备库执行就会被当做多个事务分段自行,这样破坏了原子性,是有问题的
RedoLog写入机制
事务在执行过程中,生成的 redo log 是要先写到 redo log buffer 的。redo log buffer 里面的内容并非每次生成后都要直接持久化到磁盘呢?如果事务执行期间 MySQL 发生异常重启,那这部分日志就丢了。由于事务并没有提交,所以这时日志丢了也不会有损失,所以丢日志其实和crash-safe机制没有冲突。
redo log 可能存在三种状态,这三种状态是依次写入的:
- 事务先写到 redo log buffer 中,物理上是在 MySQL 进程内存中,就是图中的红色部分;日志写到 redo log buffer 是很快的
- 写到磁盘 (write),但是没有持久化(fsync),物理上是在文件系统的 page cache 里面,也就是图中的黄色部分【redo log file on FS cache】,wirte 到 page cache 也很快
- 持久化到磁盘,对应的是 hard disk,也就是图中的绿色部分【binlog file on disk】,fsync到磁盘的速度就慢多了
为了控制 redo log 的写入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit
参数,它有三种可能取值:
- 设置为 0 的时候,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ,不管是主机掉电还是MySQL异常重启,都有丢数据的风险,风险高
- 设置为 2 的时候,表示每次事务提交时都只是把 redo log 写到 page cache,写入文件系统的page cache,主机掉电后会丢数据,但是MySQL异常重启不会丢数据,风险较低,写入比较快
- 设置为 1 的时候,表示每次事务提交时都将 redo log 直接持久化到磁盘,非常安全,但慢。
InnoDB 有一个后台线程,每隔 1 秒,就会把 redo log buffer 中的日志,调用 write 写到文件系统的 page cache,然后调用 fsync 持久化到磁盘。可以理解为innodb_flush_log_at_trx_commit三个设置对应每次事务写入到不同级别,而每隔一秒就会从最不稳定状态直接刷到最稳定状态
什么情况下未提交事务的 redo log 会写盘
实际上总共有三种常见场景会让一个没有提交事务的 redo log 写盘,当然写盘指的是写到文件系统缓存或持久化到磁盘:
- 事务执行中间过程的 redo log 也是直接写在 redo log buffer 中的,这些 redo log 也会被后台线程一起持久化到磁盘。也就是说,一个没有提交的事务的 redo log,也是可能已经持久化到磁盘的。【redo log file on disk】
- redo log buffer 占用的空间即将达到
innodb_log_buffer_size
一半的时候,后台线程会主动写盘。注意,由于这个事务并没有提交,所以这个写盘动作只是 write,而没有调用 fsync,也就是只留在了文件系统的 page cache【redo log file on FS cache】。 - 并行的事务提交的时候会顺带将这个事务的 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 来恢复的【校验binlog完整即提交】。每秒一次后台轮询刷盘,再加上崩溃恢复这个逻辑,InnoDB 就认为 redo log 在 commit 的时候就不需要 fsync 了,只会 write 到文件系统的 page cache 中就够了
- 被刷盘以后这个事务就彻底完成了,文件缓存中有这个commit标识,当Mysql崩溃时恢复很方便
- 如果主机crash了,只要binlog写成功了,无论redo有没有commit,其实整个事务都已经是成功的了,因为可以根据redo log和bin log中的xid确认事务的最终状态
通常我们说 MySQL 的“双 1”配置,指的就是 sync_binlog
和 innodb_flush_log_at_trx_commit
都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是 redo log(prepare 阶段),一次是 binlog。
日志组提交策略
通常我们说 MySQL 的“双 1”配置,指的就是 sync_binlog
和 innodb_flush_log_at_trx_commit
都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是 redo log(prepare 阶段),一次是 binlog。这意味着从 MySQL 看到的 TPS 是每秒两万的话,每秒就会写四万次磁盘。但测试表明磁盘能力也就两万左右,怎么能实现两万的 TPS?解释这个问题,就要用到组提交(group commit)机制了
LSN概念
LSN表示日志逻辑序列号(log sequence number,LSN)。LSN 是单调递增的,用来对应 redo log 的一个个写入点。每次写入长度为 length 的 redo log, LSN 的值就会加上 length。LSN 也会写到 InnoDB 的数据页中,来确保数据页不会被多次执行重复的 redo log
组提交策略
三个并发事务 (trx1, trx2, trx3) 在 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 的效果就越好
提升MySQL的IO性能
结合以上的日志写入策略和延迟刷盘尽量组里事务多一些来谈谈MySQL的IO性能
日志落盘参数配置
如果你的 MySQL 现在出现了性能瓶颈,而且瓶颈在 IO 上,可以通过哪些方法来提升性能呢?针对这个问题,可以考虑以下三种方法:
- 设置
binlog_group_commit_sync_delay
和binlog_group_commit_sync_no_delay_count
参数,减少 binlog 的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。 - 将 sync_binlog 设置为大于 1 的值(比较常见是 100~1000)【事务都写到文件系统缓存】。这样做的风险是,主机crash掉电时会丢 binlog 日志。
- 将
innodb_flush_log_at_trx_commit
设置为 2【事务都写到文件系统缓存】。这样做的风险是,主机掉电的时候会丢数据。
不建议把 innodb_flush_log_at_trx_commit 设置成 0。因为把这个参数设置成 0,表示 redo log 只保存在内存中,这样的话 MySQL 本身异常重启也会丢数据,风险太大。而 redo log 写到文件系统的 page cache 的速度也是很快的,所以将这个参数设置成 2 跟设置成 0 其实性能差不多,但这样做 MySQL 异常重启时就不会丢数据了,相比之下风险会更小
MySQL的延迟刷盘策略
为了让一次 fsync 带的组员更多,MySQL 有一个优化策略:拖时间。之前聊到的两阶段提交长这样:
实际上,了解了两种日志的写入过程后,我们可以把这个过程更加的细化
这样做有两个好处,对redolog和binglog都加了组提交优化:
- redolog可以更好的组提交。把 redo log 做 fsync 的时间拖到了步骤 1 之后,这样redo log做fsync 和 write的中间隔了一个binlog的write,所以会累计更多的事务在文件缓存中一起刷盘
- binlog 也可以组提交了。在执行第 4 步把 binlog fsync 到磁盘时,如果有多个事务的 binlog 已经写完了,也是一起持久化的,这样也可以减少 IOPS 的消耗。不过通常情况下第 3 步执行得会很快,所以 binlog 的 write 和 fsync 间的间隔时间短,导致能集合到一起持久化的 binlog 比较少,因此 binlog 的组提交的效果通常不如 redo log 的效果那么好
这就是MySQL的拖时间组提交策略,需注意其实组提交处理的是并发的场景,单线程的话在双1设置下都是每次事务完成单个刷盘的。如果想提升 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