innodb二阶段日志提交机制和组提交解析

前些天在查看关于innodb_flush_log_at_trx_commit的官网解释时产生了一些疑问,关于innodb_flush_log_at_trx_commit参数的详细解释参见官网:

https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_flush_log_at_trx_commit

其中有一段是这么写的:
With a value of 2, the contents of the InnoDB log buffer are written to the log file after each transaction commit and the log file is flushed to disk approximately once per second.
意思是:如果innodb_flush_log_at_trx_commit的值设为2,那么log buffer里的内容会在每次提交时被写入redo log file,然后redo log file每秒被flush到disk。
由于innodb的redo log file据我所知是在硬盘上的ib_logfile,所以对于这里的log file被flush到disk很疑惑,难道log buffer和disk之间还存在了一层可以缓存log file的结构?
在查阅了大量中英文资料后,总算有了初步的了解,暂总结于此。
相关参考资料:
 
一、名词解释
在innodb存储引擎中,有一种独有的log file,即redo log file,因此对于innodb存储引擎来说,就存在两种logfile:redo log和binlog.
redo log:即data目录下的ib_logfile0,ib_logfile1(个数由innodb_log_files_in_group控制),innodb存储引擎特有的redo,在内存中有相应的redo log buffer。
因此写redo时的3层结构为:redo log buffer--->文件系统缓存中的redo logfile--->disk上的redo log file
binlog:默认在data目录下,也可以通过log_bin参数直接指定路径,文件名为默认为<hostname>-bin前缀的文件,在内存中没有log buffer。
因此写binlog时的3层结构为:binlog_cache-->文件系统缓存中的binlog--->disk上的binlog
 
二、二阶段日志写的流程
为确保innodb的redo与MySQL的binlog一致,innodb的事务提交采用了two-phase commit的二阶段提交机制。
所谓二阶段就是指server层写binlog和innodb层写redo的阶段。
使用Innodb引擎并开启binlog后,如果会话发出了commit的请求,那么在committed之前,一系列的流程为:
1.prepare阶段:
此阶段负责:
在Innodb层获取独占模式的prepare_commit_mutex,将事务的trx_id写入redo log(redo日志的写机制为WAL所以在事务修改前就会写redo buffer而不是commit时一次性写入)。
2.commit阶段:
2.1:第一步,写binlog
此阶段调用两个方法write()和fsync(),前者负责将binlog从binlog cache写入文件系统缓存,后者负责将文件系统缓存中的binlog写入disk,后者的调用机制是由sync_binlog参数控制的。
关于sync_binlog参数:
  • sync_binlog=0:表示fsync()的调用完全交给操作系统,即文件系统缓存中的binlog是否刷新到disk完全由操作系统控制。
  • sync_binlog=1:表示在发出事务提交请求时,binlog一定会被固化到disk,write()跳过文件系统缓存直接写入disk。
  • sync_binlog=N(N>1):数据库崩溃时,可能会丢失N-1个事务。
注意binlog也是有cache的,在事务执行过程中生成的binlog会被存储在binlog cache中,此cache大小由binlog_cache_size,这个size是session级别的,即每个会话都有一个binlog cache。
2.2:第二步,innodb进行commit
在Innodb层写入commit flag,调用write和fsync将commit信息的redo写入磁盘,然后释放prepare_commit_mutex
引擎层将redo log buffer中的redo写入文件系统缓存(write),然后将文件系统缓存中的redo log写入disk(fsync),写入机制取决于innodb_flush_log_at_trx_commit参数。
innodb_flush_log_at_trx_commit:(默认值为1)
  • 此值为0表示:redo log buffer的内容每秒会被写入文件系统缓存的redo log里,同时被flush(固化)到disk上的redo log file中。
  • 此值为1表示:redo log buffer的内容会在事务commit时被写入文件系统缓存的redo log里,同时被flush(固化)到disk上的redo log file中。
  • 此值为2表示:redo log buffer的内容会在事务commit时被写入文件系统缓存的redo log里,而文件系统缓存的redo log每秒一次被flush(固化)到disk上的redo log file中。

注意redo和undo是在事务执行过程中就即时生成的,且早于数据库真正被修改,这被称作write ahead logging(WAL),undo的disk文件位置默认在系统表空间中,5.6以后也可以指定独立的undo表空间。

至此完成事务提交,清除会话undo信息,将事务设置为TRX_NOT_STARTED状态。
 
三、故障恢复解读
Innodb进行crash recovery时是根据binlog来进行前滚回滚的,只有记录了binlog才会根据redo log前滚或回滚事务。
crash recovery的流程其实是:先扫描binlog,提取出xid,然后比较redo中checkpoint之后的xid,如果在binlog存在,那么提交,如果不存在那么回滚。
二阶段日志提交其实是依靠一种内部的分布式(XA)机制避免的,因此MySQL的innodb_support_xa必须设置为1(默认为1且5.7.10后已经弃用)。
来具体分析下二阶段提交各个阶段crash的恢复情况:
  如果是在一阶段(prepare阶段)后crash,那么binlog未写,事务回滚。
  如果在二阶段第一步后crash,那么binlog已写,重做事务。
由于二阶段事务的提交是原子性的,这样总能保证binlog与innodb的一致,即便出现了XA事务内的crash,也能合理的进行事务前滚或回滚。
对于主从复制的影响:
上面讨论的二阶段日志提交解决了mysql server与innodb层的日志一致性的问题,单实例的情况下最多因为sync_binlog和innodb_flush_log_at_trx_commit的设置问题导致事务丢失,但是对于主从复制事务丢失却是很严重的问题-->主从不一致。
在主从复制的情况下如果innodb_flush_log_at_trx_commit不为1则有可能出现binlog已写但是redo log未写的情况,此时主库崩溃后在事务前滚时会出现找不到redo的情况导致前滚失败,而从库已经应用binlog,导致主从不一致。
而sync_binlog不为1则可能出现主库直接丢失事务的情况。
因此,为保证主从完全一致且事务不丢失,主库的innodb_flush_log_at_trx_commit和sync_binlog都必须设置为1。
 
四、Binlog Group Commit的出现
以上提到单个事务的二阶段提交过程(XA事务),可以保证 InnoDB redo和 binlog的一致性。

使用 prepare_commit_mutex 来保证事务提交的顺序,只有当上一个事务 commit 后释放锁,下个事务才可以进行 prepare 操作,这样并发事务之间的mutex争用可能比较高。

由于内存数据写入磁盘的开销很大,如果频繁 fsync() 把日志数据永久写入磁盘,数据库的性能将会急剧下降。高并发事务带来的频繁磁盘写会导致事务提交等待带来性能瓶颈,为此提供 sync_binlog 参数来设置多少个 binlog 日志产生的时候调用一次 fsync() 把二进制日志刷入磁盘来提高整体性能,但这可能导致主从不一致。

因此针对innodb事务出现了binlog的组提交方式,其基本原理就是将多个并发事务的binlog(3个以上)通过队列机制一次性写入磁盘,从而减小磁盘写次数,也避免了prepare_commit_mutex 的争用。

虽然组提交机制可以有效的提升高并发时的日志写效率,但是官网也明确说明只有高并发时效果才比较显著,如果数据库没什么并发反而效率还会降低。

改进方案:

Mysql5.6 引入了组提交,并将提交过程分成 Flush stage、Sync stage、Commit stage 三个阶段。其实简单的说就是加入队列机制使得binlog写入顺序与事务执行顺序一致,加入队列的最大好处就是可以不获取prepare_commit_mutex锁也能实现不降低性能的日志顺序写。

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

  • Flush Stage

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

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

3) 将binlog buffer到I/O cache。

4) 通知dump线程dump binlog。

  • Sync Stage

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

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

  • Commit Stage

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的效果越明显,数据库性能的提升也就越大。

与 binlog 组提交相关的参数主要包括了如下两个:

  • binlog_max_flush_queue_time

单位为微秒,用于从 flush 队列中取事务的超时时间,这主要是防止并发事务过高,导致某些事务的 RT 上升,详细内容可以查看函数MYSQL_BIN_LOG::process_flush_stage_queue() 。

注意:该参数在 5.7 之后已经取消了。

  • binlog_order_commits

当设置为 0 时,事务可能以和 binlog 不同的顺序提交,其性能会有稍微提升,但并不是特别明显.

原文地址:https://www.cnblogs.com/leohahah/p/8176553.html