组提交的作用
为了解决两阶段提交性能问题,MYSQL引入了binlog组提交机制,就是当有多个事务提交时,会将多个刷盘操作合并成一个,从而减少磁盘 I/O 的次数。
- 在没有开启binlog时
redo log的刷盘操作将会是最终影响MySQL TPS的瓶颈所在。为了缓解这一问题,MySQL使用了组提交,将多个刷盘操作合并成一个,如果说10个事务依次排队刷盘的时间成本是10,那么将这10个事务一次性一起刷盘的时间成本则近似于1。
- 当开启binlog时
为了保证redo log和binlog的数据一致性,MySQL使用了二阶段提交,由binlog作为事务的协调者。而引入二阶段提交 使得binlog又成为了性能瓶颈,先前的redo log 组提交也成了摆设。为了再次缓解这一问题,MySQL增加了binlog的组提交,目的同样是将binlog的多个刷盘操作合并成一个,结合redo log本身已经实现的组提交,分为三个阶段(Flush 阶段、Sync 阶段、Commit 阶段)完成binlog 组提交,最大化每次刷盘的收益,弱化磁盘瓶颈,提高性能。
- flush 阶段:由事务组的Leader 对 rodo log 做一次 write + fsync,即一次将同组事务的 redolog 刷盘。多个事务按进入的顺序将 binlog 从 cache 写入文件,调用 write不会调用 fsync,所以binlog不会刷盘。binlog 缓存在操作系统的文件系统中。
- sync 阶段:对 binlog 文件做 fsync 操作(多个事务的 binlog 合并一次刷盘);
- commit 阶段:各个事务按顺序做 InnoDB commit 操作;
其实上面这三个阶段中每个阶段都有一个队列来完成,而每个队列有锁来保护,从而保证了事务写入的顺序,第一个进入队列的事务会成为 leader,leader领导所在队列的所有事务,全权负责整队的操作,完成后通知队内其他事务操作结束。
对每个阶段引入了队列后,锁就只针对每个队列进行保护,不再锁住提交事务的整个过程,可以看的出来,锁粒度减小了,这样就使得多个阶段可以并发执行,从而提升效率。
有 binlog 组提交,那有 redo log 组提交吗?
在MYSQL5.7版本之前是没有的,从该版本就开始有了。即在prepare 阶段不再让事务各自执行 redo log 刷盘操作,而是推迟到组提交的 flush 阶段,也就是说 prepare 阶段融合在了flush 阶段。
这个改进是将 redo log 的刷盘延迟到了 flush 阶段之中,sync 阶段之前。通过延迟写 redo log 的方式,为 redolog 做了一次组写入,这样 binlog 和 redo log 都进行了优化。 下面,在“双 1” 配置中介绍每个阶段的过程。
组提交概述
第一阶段(prepare阶段):
持有prepare_commit_mutex,并且write/fsync redo log到磁盘,设置为prepared状态,完成后就释放prepare_commit_mutex,binlog不作任何操作。
**第二个阶段(commit阶段):**这里拆分成了三步,每一步的任务分配给一个专门的线程处理:
-
Flush Stage(写入binlog缓存)
① 持有Lock_log mutex [leader持有,follower等待]
② 获取队列中的一组binlog(队列中的所有事务)
③ 写入binlog缓存
-
Sync Stage(将binlog落盘)
①释放Lock_log mutex,持有Lock_sync mutex[leader持有,follower等待]
②将一组binlog落盘(fsync动作,最耗时,假设sync_binlog为1)。
-
Commit Stage(InnoDB commit,清楚undo信息)
①释放Lock_sync mutex,持有Lock_commit mutex[leader持有,follower等待]
② 遍历队列中的事务,逐一进行InnoDB commit
③ 释放Lock_commit mutex
每个Stage都有自己的队列,队列中的第一个事务称为leader,其他事务称为follower,leader控制着follower的行为。每个队列各自有mutex保护,**队列之间是顺序的。**只有flush完成后,才能进入到sync阶段的队列中;sync完成后,才能进入到commit阶段的队列中。
但是这三个阶段的作业是可以同时并发执行的,即当一组事务在进行commit阶段时,其他新事务可以进行flush阶段,实现了真正意义上的组提交,大幅度降低磁盘的IOPS消耗。
但是这三个阶段的作业是可以同时并发执行的,即当一组事务在进行commit阶段时,其他新事务可以进行flush阶段,实现了真正意义上的组提交,大幅度降低磁盘的IOPS消耗。
但是这三个阶段的作业是可以同时并发执行的,即当一组事务在进行commit阶段时,其他新事务可以进行flush阶段,实现了真正意义上的组提交,大幅度降低磁盘的IOPS消耗。
第一个阶段是1个队列,假设有6个事务,t1作为leader进入了队列,领导了t2 t3, t4领导了t5 t6。
t1现在要write/fsync redo log到磁盘,设置为prepared状态,首先要获取到prepare_commit_mutex。
t1获取到prepare_commit_mutex执行完成后就释放prepare_commit_mutex,然后进入下一个队列。
此时t4获取到了prepare_commit_mutex,执行write/fsync redo log到磁盘。
以此类推。
针对组提交为什么比两阶段提交加锁性能更好,简单做个总结:
1.组提交虽然在每个队列中仍然保留了prepare_commit_mutex锁,但是锁的粒度变小了,变成了原来两阶段提交的1/4,所以锁的争用性也会大大降低;
2.组提交是批量刷盘,相比之前的单条记录都要刷盘,能大幅度降低磁盘的IO消耗。
有个疑问,为什么组提交使用了队列还要使用锁来保证顺序性呢?
有个疑问,为什么组提交使用了队列还要使用锁来保证顺序性呢?
有个疑问,为什么组提交使用了队列还要使用锁来保证顺序性呢?
组提交之flush阶段
在MySQL中每个阶段都有一个队列,每个队列都有一把锁保护,第一个进入队列的事务会成为leader,leader领导所在队列的所有事务,全权负责整队的操作,完成后通知队内其他事务操作结束。
第一个事务会成为 flush 阶段的 Leader,此时后面到来的事务都是 Follower。
接着,获取队列中的事务组,由绿色事务组的 Leader 对 rodo log 做一次 write + fsync,即一次将同组事务的 redolog 刷盘。
完成了 prepare 阶段后,将绿色这一组事务执行过程中产生的 binlog 写入 binlog 文件(调用 write,不会调用 fsync,所以不会刷盘,binlog 缓存在操作系统的文件系统中)。此时只是写入文件系统的缓冲,并不能保证数据库崩溃时binlog不丢失。
从上面这个过程,可以知道 flush 阶段队列的作用是用于支撑 redo log 的组提交。
如果在这一步完成后数据库崩溃,由于 binlog 中没有该组事务的记录,所以 MySQL 会在重启后回滚该组事务。
组提交之sync阶段
绿色这一组事务的 binlog 写入到 binlog 文件后,并不会马上执行刷盘的操作,而是会等待一段时间,这个等待的时长由 Binlog_group_commit_sync_delay 参数控制,目的是为了组合更多事务的 binlog,然后再一起刷盘,如下过程:
不过,在等待的过程中,如果事务的数量提前达到了 Binlog_group_commit_sync_no_delay_count 参数设置的值,就不用继续等待了,就马上将 binlog 刷盘,如下图:
从上面的过程,可以知道 sync 阶段队列的作用是用于支持 binlog 的组提交。
如果想提升 binlog 组提交的效果,可以通过设置下面这两个参数来实现:
binlog_group_commit_sync_delay= N。
表示在等待 N 微妙后,直接调用 fsync,将处于文件系统中 page cache 中的 binlog 刷盘,也就是将「 binlog 文件」持久化到磁盘。
binlog_group_commit_sync_no_delay_count = N。
表示如果队列中的事务数达到 N 个,就忽视binlog_group_commit_sync_delay 的设置,直接调用 fsync,将处于文件系统中 page cache 中的 binlog 刷盘,也就是将「 binlog 文件」持久化到磁盘。。
如果在这一步完成后数据库崩溃,由于 binlog 中已经有了事务记录,MySQL会在重启后通过 redo log 刷盘的数据继续进行事务的提交。
组提交之commit 阶段
- 首先获取队列中的事务组
- 依次将Redo log中已经prepare的事务在引擎层提交(图中InnoDB Commit)
- Commit阶段不用刷盘,如上所述,Flush阶段中的Redo log刷盘已经足够保证数据库崩溃时的数据安全了
- Commit阶段队列的作用是承接Sync阶段的事务,完成最后的引擎提交,使得Sync可以尽早的处理下一组事务,最大化组提交的效率
组提交的优化
通常我们说MySQL的“双1”配置,指的就是sync_binlog和innodb_flush_log_at_trx_commit都设置成1。
也就是说,一个事务完整提交前,需要等待两次刷盘,一次是redolog(prepare阶段),一次是binlog。
这时候,你可能有一个疑问,如果从MySQL看到的TPS是每秒两万的话,每秒就会写四万次磁盘;
但是,用工具测试出来,磁盘能力也就两万左右,怎么能实现两万的TPS?
解释这个问题,就要用到组提交(group commit)机制了。
3个并发事务(trx1,trx2,trx3)在prepare阶段,都写完redologbuffer,准备持久化到磁盘的过程,对应的LSN分别是50、120和160;
这里引入了日志逻辑序列号(log sequence number,LSN)的概念;LSN是单调递增的(有序),用来对应redolog的一个个写入点。
每次写入长度为length的redolog,LSN的值就会加上length。LSN也会写到InnoDB的数据页中,来确保数据页不会被多次执行重复的redolog。
trx1是第一个到达的,会被选为这组的leader;
等trx1要开始写盘的时候,发现这个组里面已经有了3个事务,最大的LSN=160,这时候trx1的LSN也变成了160;
trx1去写盘的时候,带的就是LSN=160,因此等trx1返回时,所有LSN小于等于160的redolog,都已经被持久化到磁盘;
这时候trx2和trx3就可以直接返回了;
所以,一次组提交里面,组员越多,节约磁盘IOPS的效果越好;在并发更新场景下,第一个事务写完redolog buffer以后,接下来这个fsync越晚调用,组员可能越多,节约IOPS的效果就越好;
为了让一次fsync带的组员更多,MySQL有一个很有趣的优化:拖时间——对于单个事务的提交,不急着立即fsync而是等一等;
在介绍两阶段提交的时候,有下面的这一张图:
图中,"写binlog"是一个动作,但MySQL针对组提交机制做了优化,实际上,写binlog是分成两步的:
先把binlog从binlogcache中写到磁盘上的binlog文件;
调用fsync持久化;
MySQL为了让组提交的效果更好,把redolog做fsync的时间拖到了步骤1之后;也就是说,上面的图实际变成了这样:
这么一来,binlog也可以组提交了。
在执行图中第4步把binlog fsync到磁盘时,如果有多个事务的binlog已经write写完了,也是一起持久化的,这样也可以减少IOPS的消耗;
现在你就能理解了,WAL机制主要得益于两个方面:
redolog和binlog都是顺序写,磁盘的顺序写比随机写速度要快;
充分利用组提交机制,可以大幅度降低磁盘的IOPS消耗;
组提交之LSN
执行命令:show engine innodb status
可以看到以下信息:
Log sequence number: 当前系统最大的LSN号
log flushed up to:当前已经写入redo日志文件的LSN
pages flushed up to:已经将更改写入脏页的lsn号
Last checkpoint at: 是系统最后一次刷新buffer pool脏中页数据到磁盘的checkpoint。
LSN表示事务写入到日志的字节总量。是1个全局单调有序递增的序列号,假设现在的LSN的值是300,有3个事务,分别需要写入10/20/30,
那么事务1先对LSN执行+10的操作,然后对当前redolog对应的偏移量写入10个字节即可。
LSN不仅只存在于重做日志中,在每个数据页头部也会有对应的LSN号,该LSN记录当前页最后一次修改的LSN号,用于在recovery时对比重做日志LSN号决定是否对该页进行恢复数据,这样可以加速重启恢复。前面说的checkpoint也是有LSN号记录的,LSN号串联起一个事务开始到恢复的过程。
图解LSN:
组提交总结
如果出现 MySQL 磁盘 I/O 很高的现象,我们可以通过控制以下参数,来 “延迟” binlog 和 redo log 刷盘的时机,从而降低磁盘 I/O 的频率:
设置组提交的两个参数:binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 参数,延迟 binlog 刷盘的时机,从而减少 binlog 的刷盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。