注册 登录
  • 欢迎访问"运维那点事",推荐使用Google浏览器访问,可以扫码关注本站的"微信公众号"。
  • 如果您觉得本站对你有帮助,那么可以扫码捐助以帮助本站更好地发展。

MySQL Group Commit

MySQL 彭东稳 6692次浏览 已收录 6个评论

一、背景

在关系型数据库中,为了满足ACID中的D(持久性)属性,也就是说事务提交并且成功返回给客户端之后,必须保证该事务的所有修改都持久化了,无论是在数据库程序崩溃的情况下或者是数据库所在的服务器发生宕机或者断电的情况下,都必须保证数据不能丢失。这就要求数据库在事务提交过程中调用fsync或fdatasync将数据持久化到磁盘。fsync是一个昂贵的系统调用,对于普通的磁盘,每秒只能完成几百次的fsync操作。很明显,fsync将会限制每秒钟提交的事务数,成为关系型数据库的瓶颈。

对于MySQL而言,这种情况变得更加糟糕。在开启binlog的情况下,为了保证主从之间数据的一致性,MySQL使用了事务的两阶段提交协议。在这种情况下,为了满足数据的持久化需求,一个完整事务的提交最多会导致3次fsync操作。为了提高MySQL在开启binlog的情况下单位时间内的事务提交数,就必须减少每个事务提交过程中导致的fsync的调用次数。所以,MySQL从5.6版本开始加入了group commit技术(MariaDB 5.3版本开始引入)。

MySQL组提交(group commit)是MySQL处理日志的一种优化方式,主要为了解决写日志时频繁刷磁盘的问题。组提交核心思想是多个并发的需要提交的事务之间共享一个fsync操作来进行数据持久化,将fsync操作的开销平摊到多个并发的事务上去。例如,有10个并发的事务需要提交,我们可以通过让这10个事务共享一个fsync操作进行持久化,这相比于每个事务需要自己执行一次fsync来进行持久化,性能上得到了明显提升。组提交伴随着MySQL的发展不断优化,从最初只支持redo log组提交,到目前MySQL 5.6官方版本同时支持redo log和binlog组提交。组提交的实现大大提高了MySQL的事务处理性能,下文将以Innodb存储引擎为例,详细介绍组提交在各个阶段的实现原理。

二、事务两阶段提交

MySQL在开启binlog的情况下,因为MySQL是通过binlog进行复制的,为了保证数据在主库和从库之间的一致性,会使用事务的两阶段提交协议。同时,为了保证数据的安全性,我们还需要设置参数innodb_flush_logs_at_trx_commit=1以及参数sync_binlog=1,前者保证了事务在InnoDB存储引擎内的修改持久化到了磁盘(对于InnoDB来说是重做日志的持久化),后者保证了该事务在binlog中的修改持久化到了磁盘。下面来看一下MySQL内部的两阶段提交过程。

– 自动为每个事务分配一个唯一的ID(XID)。

– COMMIT会被自动的分成Prepare和Commit两个阶段。

– Binlog会被当做事务协调者(Transaction Coordinator),Binlog Event会被当做协调者日志。

想了解2PC,可以参考文档:https://en.wikipedia.org/wiki/Two-phase_commit_protocol

Binlog在2PC中充当了事务的协调者(Transaction Coordinator)。由Binlog来通知InnoDB引擎来执行prepare,commit或者rollback的步骤。事务提交的整个过程如下:

MySQL Group Commit

以上的图片中可以看到,事务的提交主要分为两个主要步骤:

1. 准备阶段(Storage Engine(InnoDB) Transaction Prepare Phase)

此时SQL已经成功执行,并生成xid信息及redo和undo的内存日志。然后调用prepare方法完成第一阶段,papare方法实际上什么也没做,将事务状态设为TRX_PREPARED,并将redo log刷磁盘。

2. 提交阶段(Storage Engine(InnoDB)Commit Phase)

2.1 记录协调者日志,即Binlog日志。

如果事务涉及的所有存储引擎的prepare都执行成功,则调用TC_LOG_BINLOG::log_xid方法将SQL语句写到binlog(write()将binary log内存日志数据写入文件系统缓存,fsync()将binary log文件系统缓存日志数据永久写入磁盘)。此时,事务已经铁定要提交了。否则,调用ha_rollback_trans方法回滚事务,而SQL语句实际上也不会写到binlog。

2.2 告诉引擎做commit。

最后,调用引擎的commit完成事务的提交。会清除undo信息,刷redo日志,将事务设为TRX_NOT_STARTED状态。

PS:记录Binlog是在InnoDB引擎Prepare(即Redo Log写入磁盘)之后,这点至关重要。

由上面的二阶段提交流程可以看出,一旦步骤2中的操作完成,就确保了事务的提交,即使在执行步骤3时数据库发送了宕机。此外需要注意的是,每个步骤都需要进行一次fsync操作才能保证上下两层数据的一致性。步骤2的fsync参数由sync_binlog=1控制,步骤3的fsync由参数innodb_flush_log_at_trx_commit=1控制,俗称“双1”,是保证CrashSafe的根本。

事务的两阶段提交协议保证了无论在任何情况下,事务要么同时存在于存储引擎和binlog中,要么两个里面都不存在,这就保证了主库与从库之间数据的一致性。如果数据库系统发生崩溃,当数据库系统重新启动时会进行崩溃恢复操作,存储引擎中处于prepare状态的事务会去查询该事务是否也同时存在于binlog中,如果存在就在存储引擎内部提交该事务(因为此时从库可能已经获取了对应的binlog内容),如果binlog中没有该事务,就回滚该事务。例如:当崩溃发生在第一步和第二步之间时,明显处于prepare状态的事务还没来得及写入到binlog中,所以该事务会在存储引擎内部进行回滚,这样该事务在存储引擎和binlog中都不会存在;当崩溃发生在第二步和第三步之间时,处于prepare状态的事务存在于binlog中,那么该事务会在存储引擎内部进行提交,这样该事务就同时存在于存储引擎和binlog中。

为了保证数据的安全性,以上列出的3个步骤都需要调用fsync将数据持久化到磁盘。由于在引擎内部prepare好的事务可以通过binlog恢复,所以通常情况下第三个fsync是可以省略的。

另外,MySQL内部两阶段提交需要开启innodb_support_xa=true,默认开启。这个参数就是支持分布式事务两段式事务提交。redo和binlog数据一致性就是靠这个两段式提交来完成的,如果关闭会造成事务数据的丢失。

三、Redo Log Group Commit

MySQL没有开启Binary log的情况下?

若事务为非只读事务,则每次事务提交时需要进行一次fsync操作,以此确保重做日志都已经写入磁盘。当数据库发生宕机时,可以通过重做日志进行恢复。InnoDB存储引擎通过redo和undo日志可以safe crash recovery数据库,当数据库crash recovery时,通过redo日志将所有已经在存储引擎内部提交的事务应用重做日志恢复,所有已经prepare但是没有commit的事务将会应用undo logrollback。然后客户端连接时就能看到已经提交的数据存在数据库内,未提交被回滚地数据需要重新执行。

虽然固态硬盘的出现提高了磁盘的性能,然而磁盘的fsync性能是有限的,为了提高磁盘fsync的效率,当前数据库都提供了group commit的功能,即一次fsync可以刷新确保多个事务日志被写入磁盘文件,对于Innodb存储引擎来说,事务提交时会进行两个阶段的操作:

1)修改内存中事务对应的值,并且将日志写入重做日志缓冲。

2)调用fsync将确保日志都从重做日志缓冲写入到了磁盘。

步骤2相对于步骤1是一个较慢的过程,这是因为存储引擎需要与磁盘打交道。但当有事务进行这个过程时,其他事务可以进行步骤1的操作,正在提交的事务完成提交操作后,再次进行步骤2时,可以将多个事务的重做日志通过一次fsync刷新到磁盘,这样就大大地减少了磁盘的压力,从而提高了数据库的整体性能。对于写入或更新较为频繁的操作,group commit的效果尤为明显。

group commit?

WAL(Write-Ahead-Logging)是实现事务持久性的一个常用技术,基本原理是在提交事务时,为了避免磁盘页面的随机写,只需要保证事务的redo log写入磁盘即可,这样可以通过redo log的顺序写代替页面的随机写,并且可以保证事务的持久性,提高了数据库系统的性能。虽然WAL使用顺序写替代了随机写,但是,每次事务提交,仍然需要有一次日志刷盘动作,受限于磁盘IO,这个操作仍然是事务并发的瓶颈。

组提交思想是,将多个事务redo log的刷盘动作合并,减少磁盘顺序写。Innodb的日志系统里面,每条redo log都有一个LSN(Log Sequence Number),LSN是单调递增的。每个事务执行更新操作都会包含一条或多条redo log,各个事务将日志拷贝到log_sys_buffer时(log_sys_buffer通过log_mutex保护),都会获取当前最大的LSN,因此可以保证不同事务的LSN不会重复。那么假设三个事务Trx1,Trx2和Trx3的日志的最大LSN分别为LSN1,LSN2,LSN3(LSN1<LSN2<LSN3),它们同时进行提交,那么如果Trx3日志先获取到log_mutex进行落盘,它就可以顺便把[LSN1—LSN3]这段日志也刷了,这样Trx1和Trx2就不用再次请求磁盘IO。组提交的基本流程如下:

1)获取log_mutex;

2)若flushed_to_disk_lsn>=lsn,表示日志已经被刷盘,跳转到5;

3)若current_flush_lsn>=lsn,表示日志正在刷盘中,跳转5后进入等待状态;

4)将小于LSN的日志刷盘(flush and sync);

5)退出log_mutex;

备注:lsn表示事务的lsn,flushed_to_disk_lsn和current_flush_lsn分别表示已刷盘的LSN和正在刷盘的LSN。

MySQL开启Binary log的情况下?

在开启binlog后,MySQL通过两阶段提交保证了MySQL数据库上层二进制日志的写入顺序和InnoDB层事务提交顺序一致性,从而保证了主库的CrashSafe(主从数据安全)。但在Innodb 1.2版本之前(MySQL 5.6),在开启了二进制日志后,为了保证这MySQL数据库上层二进制日志的写入顺序和InnoDB层事务提交顺序一致性,是通过一个prepare_commit_mutex锁来实现的,但会导致InnoDB存储引擎的group commit功能会失效,从而导致性能的下降。并且线上环境多使用复制环境,因此二进制日志的选项基本都是开着的,因此这个问题尤为显著。

然而,为什么需要保证这MySQL数据库上层二进制日志的写入顺序和InnoDB层事务提交顺序一致性呢?

以上提到单个事务的二阶段提交过程,能够保证存储引擎和binary log日志保持一致,但是在并发的情况下怎么保证InnoDB层事务日志和MySQL数据库二进制日志的提交的顺序一致?当多个事务并发提交的情况,如果Binary Log和存储引擎顺序不一致会造成什么影响?

这是因为备份及恢复需要,例如通过xtrabackup或ibbackup这种物理备份工具进行备份时,并使用备份来建立复制,如下图:

MySQL Group Commit

如上图,事务按照T1T2T3顺序开始执行,将二进制日志(按照T1、T2、T3顺序)写入日志文件系统缓冲,调用fsync()进行一次group commit将日志文件永久写入磁盘,但是存储引擎提交的顺序为T2、T3、T1当T2、T3提交事务之后,若通过在线物理备份进行数据库恢复来建立复制时,因为在InnoDB存储引擎层会检测事务T3在上下两层都完成了事务提交,不需要在进行恢复了,此时主备数据不一致(搭建Slave时,change master to的日志偏移量记录T3在事务位置之后)。

为了解决以上问题,在早期的MySQL 5.6版本之前,通过prepare_commit_mutex锁以串行的方式来保证MySQL数据库上层二进制日志和Innodb存储引擎层的事务提交顺序一致,然后会导致group commit无法生效。MySQL数据库内部在prepare redo阶段获取prepare_commit_mutex锁,一次只能有一个事务可获取该mutex。通过这个臭名昭著prepare_commit_mutex锁,将redo log和binlog刷盘串行化,串行化的目的也仅仅是为了保证redo log和Binlog一致,继而无法实现group commit,牺牲了性能。整个过程如下图:

MySQL Group Commit

上图可以看出在prepare_commit_mutex,只有当上一个事务commit后释放锁,下一个事务才可以进行prepare操作,并且在每个事务过程中Binary log没有fsync()的调用。由于内存数据写入磁盘的开销很大,如果频繁fsync()把日志数据永久写入磁盘数据库的性能将会急剧下降。此时MySQL数据库提供sync_binlog参数来设置多少个binlog日志产生的时候调用一次fsync()把二进制日志刷入磁盘来提高整体性能。

上图所示MySQL开启Binary log时使用prepare_commit_mutex和sync_log保证二进制日志和存储引擎顺序保持一致,prepare_commit_mutex的锁机制造成高并发提交事务的时候性能非常差而且二进制日志也无法group commit。

这个问题早在2010年的MySQL数据库大会中提出,Facebook MySQL技术组,Percona公司都提出过解决方案,最后由MariaDB数据库的开发人员Kristian Nielsen完成了最终的”完美”解决方案。在这种情况下,不但MySQL数据库上层二进制日志写入是group commit的,InnoDB存储引擎层也是group commit的。此外还移除了原先的锁prepare_commit_mutex,从而大大提高了数据库的整体性。MySQL 5.6采用了类似的实现方式,并将其称为BLGC(Binary Log Group Commit),并把事务提交过程分成三个阶段,Flush stage、Sync stage、Commit stage。

四、BLGC(Binary Log Group Commit)

MySQL 5.6 BLGC技术出现后,在这种情况下,不但MySQL数据库上层二进制日志写入是group commit的,InnoDB存储引擎层也是group commit的。此外还移除了原先的锁prepare_commit_mutex,从而大大提高了数据库的整体性。其事务的提交(commit)过程分成三个阶段,Flush stage、Sync stage、Commit stage。如下图:

MySQL Group Commit

Binlog组提交的基本思想是,引入队列机制保证Innodb commit顺序与binlog落盘顺序一致,并将事务分组,组内的binlog刷盘动作交给一个事务进行,实现组提交目的。在MySQL数据库上层进行提交时首先按顺序将其放入一个队列中,队列中的第一个事务称为leader,其他事务称为follow,leader控制着follow的行为。

从上图可以看出,每个阶段都有一个队列,每个队列有一个mutex保护,约定进入队列第一个线程为leader,其他线程为follower,所有事情交由leader去做,leader做完所有动作后,通知follower刷盘结束。BLGC就是将事务提交分为了3个阶段,FLUSH阶段,SYNC阶段和COMMIT阶段。

  • Flush Stage

将每个事务的二进制日志写入内存中。

1) 持有Lock_log mutex [leader持有,follower等待]。

2) 获取队列中的一组binlog(队列中的所有事务)。

3) 将binlog buffer到I/O cache。

4) 通知dump线程dump binlog。

  • Sync Stage

将内存中的二进制日志刷新到磁盘,若队列中有多个事务,那么仅一次fsync操作就完成了二进制日志的写入,这就是BLGC。

1) 释放Lock_log mutex,持有Lock_sync mutex[leader持有,follower等待]。

2) 将一组binlog 落盘(sync动作,最耗时,假设sync_binlog为1)。

  • Commit Stage

leader根据顺序调用存储引擎层事务的提交,Innodb本身就支持group commit,因此修复了原先由于锁prepare_commit_mutex导致group commit失效的问题。

1) 释放Lock_sync mutex,持有Lock_commit mutex[leader持有,follower等待]。

2) 遍历队列中的事务,逐一进行innodb commit。

3) 释放Lock_commit mutex。

4) 唤醒队列中等待的线程。

说明:由于有多个队列,每个队列各自有mutex保护,队列之间是顺序的,约定进入队列的一个线程为leader,因此FLUSH阶段的leader可能是SYNC阶段的follower,但是follower永远是follower。

当有一组事务在进行commit阶段时,其他新事物可以进行Flush阶段,从而使group commit不断生效。当然group commit的效果由队列中事务的数量决定,若每次队列中仅有一个事务,那么可能效果和之前差不多,甚至会更差。但当提交的事务越多时,group commit的效果越明显,数据库性能的提升也就越大。

MySQL提供了一个参数binlog_max_flush_queue_time(MySQL 5.7.9版本失效),默认值为0,用来控制MySQL 5.6新增的BLGC(binary log group commit),就是二进制日志组提交中Flush阶段中等待的时间,即使之前的一组事务完成提交,当前一组的事务也不马上进入Sync阶段,而是至少需要等待一段时间,这样做的好处是group commit的事务数量更多,然而这也可能会导致事务的响应时间变慢。该参数默认为0表示不等待,且推荐设置依然为0。除非用户的MySQL数据库系统中有大量的连接(如100个连接),并且不断地在进行事务的写入或更新操作。

MySQL 5.7 Parallel replication实现主备多线程复制基于主库BLGC(Binary Log Group Commit)机制,并在Binary log日志中标识同一组事务的last_commited=N和该组事务内所有的事务提交顺序。为了增加一组事务内的事务数量提高备库组提交时的并发量引入了binlog_group_commit_sync_delay=Nbinlog_group_commit_sync_no_delay_count=N

注:binlog_max_flush_queue_time在MySQL的5.7.9及之后版本不再生效)参数,MySQL等待binlog_group_commit_sync_delay毫秒直到达到binlog_group_commit_sync_no_delay_count事务个数时,将进行一次组提交。

下面是提供测试组提交的一张图,可以看到组提交的TPS高不少。

MySQL Group Commit

<参考>

https://yq.aliyun.com/articles/294051

https://cloud.tencent.com/developer/article/1008565


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (8)or分享 (0)
关于作者:

您必须 登录 才能发表评论!

(6)个小伙伴在吐槽
  1. 你好,看完这篇文章我有几个问题想请教一下。 1.Flush Stage阶段的4,你说是dump线程去dump binlog,但此时并没有调用fsync,即binlog没有落盘,难道dump线程可以去IO Cache里dump binlog吗? 如果能从IO Cache中dump binlog 那如果从库执行了语句后 主库binlog落盘前,主库宕机,那岂不是从库比主库数据还多? 2.如果binlog落盘后,redo log落盘前 (Sync Stage和commit stage之间)系统宕机了 那么如何恢复这些事务?
    9452773362018-06-16 19:22 Windows 10 | Chrome 67.0.3396.87
    • 这里是说组提交机制,没有涉及主从crash safe。关于crash safe我想你应该看异步、同步、半同步复制,他们是解决安全问题的。不同的机制dump binlog的时机是不同的。
      彭东稳2018-06-21 15:50 未知操作系统 | Chrome 66.0.3359.117
  2. 一旦步骤2中的操作完成,就确保了事务的提交,即使在执行步骤3时数据库发送了宕机。此外需要注意的是,每个步骤都需要进行一次fsync操作才能保证上下两层数据的一致性。步骤2的fsync参数由sync_binlog控制,步骤2的fsync由参数innodb_flush_log_at_trx_commit控制。 最后这句话有笔误吧。
    johny6662017-12-27 17:52 Windows 10 | Chrome 55.0.2883.87
    • :wink: 是的,已经改了。应该有很多笔误和错误的地方,看到希望指正。
      彭东稳2017-12-28 16:02 未知操作系统 | Chrome 63.0.3239.84