zoukankan      html  css  js  c++  java
  • (转)MySQL 日志组提交

    原文:https://jin-yang.github.io/post/mysql-group-commit.html

    组提交 (group commit) 是为了优化写日志时的刷磁盘问题,从最初只支持 InnoDB redo log 组提交,到 5.6 官方版本同时支持 redo log 和 binlog 组提交,大大提高了 MySQL 的事务处理性能。

    下面将以 InnoDB 存储引擎为例,详细介绍组提交在各个阶段的实现原理。

    简介

    自 5.1 之后,binlog 和 innodb 采用类似两阶段提交的方式,不过不支持 group commit;在 5.6 中,将 binlog 的 commit 阶段分为三个阶段:flush stage、sync stage 以及 commit stage。

    这三个阶段中,每个阶段都会去维护一个队列,各个列表的定义如下。

    Mutex_queue m_queue[STAGE_COUNTER];

    如上,每个阶段都在维护一个队列,第一个进入该队列的作为 leader 线程,否则作为 follower 线程;leader 线程会收集 follower 的事务,并负责做 sync,follower 线程等待 leader 通知操作完成。

    尽管维护了三个队列,但队列中所有的 THD 实际上都是通过 next_to_commit 连接起来。binlog 在事务提交阶段,也就是在 MYSQL_BIN_LOG::ordered_commit() 函数中,开始 3 个阶段的流程。

    接下来,看看 MySQL 中事务是如何提交的。

    事务提交

    接下来,看看 InnoDB 和 binlog 提交的流程。

    二阶段提交

    详细介绍下二阶段提交的过程。

    未开启binlog时

    InnoDB 通过 redo 和 undo 日志来恢复数据库 (safe crash recovery),当数据恢复时,通过 redo 日志将所有已经在存储引擎内部提交的事务应用 redo log 恢复,所有已经 prepared 但是没有 commit 的事务则会通过 undo log 做回滚。

    然后客户端连接时就能看到已经提交的数据存在数据库内,未提交被回滚地数据需要重新执行。

    开启binlog时

    为了保证存储引擎和 MySQL 的 binlog 保持一致,引入二阶段提交 (two phase commit, 2pc) 。

    因为备库通过 binlog 重放主库提交的事务,假设主库存储引擎已经提交而 binlog 没有保持一致,则会使备库数据丢失造成主备数据不一致。

    二阶段提交

    如下是二阶段提交流程。

    group commit 2pc

    详细执行流程为:

    1. InnoDB 的事务 Prepare 阶段,即 SQL 已经成功执行并生成 redo 和 undo 的内存日志;

    2. binlog 提交,通过 write() 将 binlog 内存日志数据写入文件系统缓存;

    3. fsync() 将 binlog 文件系统缓存日志数据永久写入磁盘;

    4. InnoDB 内部提交,commit 阶段在存储引擎内提交,通过 innodb_flush_log_at_trx_commit 参数控制,使 undo 和 redo 永久写入磁盘。

    开启 binlog 的 MySQL 在崩溃恢复 (crash recovery) 时:

    • 在 prepare 阶段崩溃,恢复时该事务未写入 binlog 且 InnoDB 未提交,该事务直接回滚;

    • 在 binlog 已经 fsync() 永久写入 binlog,但 InnoDB 未来得及 commit 时崩溃;恢复时,将会从 binlog 中获取提交的信息,重做该事务并提交,使 InnoDB 和 binlog 始终保持一致。

    以上提到单个事务的二阶段提交过程,能够保证 InnoDB 和 binlog 保持一致,但是在并发的情况下怎么保证存储引擎和 binlog 提交的顺序一致?当并发提交的时,如果两者不一致会造成什么影响?

    组提交异常

    首先看看,对于上述的问题,当并发提交的时,如果两者不一致会造成什么影响?

    group commit 2pc concurrency bug

    如上所示,事务按照 T1、T2、T3 顺序开始执行,并依相同次序按照写入 binlog 日志文件系统缓存,调用 fsync() 进行一次组提交,将日志文件永久写入磁盘。

    但是存储引擎提交的顺序为 T2、T3、T1,当 T2、T3 提交事务之后做了一个 On-line 的备份程序新建一个 slave 来做复制;而搭建备库时,CHANGE MASTER TO 的日志偏移量在 T3 事务之后。

    那么事务 T1 在备机恢复 MySQL 数据库时,发现 T1 未在存储引擎内提交,那么在恢复时,T1 事务就会被回滚,此时就会导致主备数据不一致。

    结论:需要保证 binlog 的写入顺序和 InnoDB 事务提交顺序一致,用于 xtrabackup 备份恢复。

    早期解决方案

    早期,使用 prepare_commit_mutex 保证顺序,只有当上一个事务 commit 后释放锁,下个事务才可以进行 prepara 操作,并且在每个事务过程中 binlog 没有 fsync() 的调用。

    group commit 2pc concurrency mutex

    由于内存数据写入磁盘的开销很大,如果频繁 fsync() 把日志数据永久写入磁盘,数据库的性能将会急剧下降。为此提供 sync_binlog 参数来设置多少个 binlog 日志产生的时候调用一次 fsync() 把二进制日志刷入磁盘来提高整体性能,该参数的设置作用为:

    • sync_binlog=0,二进制日志 fsync() 的操作基于系统自动执行。

    • sync_binlog=1,每次事务提交都会调用 fsync(),最大限度保证数据安全,但影响性能。

    • sync_binlog=N,当数据库崩溃时,可能会丢失 N-1 个事务。

    prepare_commit_mutex 的锁机制会严重影响高并发时的性能,而且 binlog 也无法执行组提交。

    改进方案

    接下来,看看如何保证 binlog 写入顺序和存储引擎提交顺序是一致的,并且能够进行 binlog 的组提交?5.6 引入了组提交,并将提交过程分成 Flush stage、Sync stage、Commit stage 三个阶段。

    这样,事务提交时分为了如下的阶段:

    InnoDB, Prepare
        SQL已经成功执行并生成了相应的redo和undo内存日志;
    Binlog, Flush Stage
        所有已经注册线程都将写入binlog缓存;
    Binlog, Sync Stage
        binlog缓存将sync到磁盘,sync_binlog=1时该队列中所有事务的binlog将永久写入磁盘;
    InnoDB, Commit stage
        leader根据顺序调用存储引擎提交事务;

    每个 Stage 阶段都有各自的队列,从而使每个会话的事务进行排队,提高并发性能。

    如果当一个线程注册到一个空队列时,该线程就做为该队列的 leader,后注册到该队列的线程均为 follower,后续的操作,都由 leader 控制队列中 follower 行为。

    leader 同时会带领当前队列的所有 follower 到下一个 stage 去执行,当遇到下一个 stage 为非空队列时,leader 会变成 follower 注册到此队列中;注意:follower 线程绝不可能变成 leader 。

    配置参数

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

    binlog_max_flush_queue_time

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

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

    binlog_order_commits

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

    源码解析

    binlog 的组提交是通过 Stage_manager 管理,其中比较核心内容如下。

    class Stage_manager {
      public:
        enum StageID {         // binlog的组提交包括了三个阶段
          FLUSH_STAGE,
          SYNC_STAGE,
          COMMIT_STAGE,
          STAGE_COUNTER
        };
      private:
        Mutex_queue m_queue[STAGE_COUNTER];
    };

    组提交 (Group Commit) 三阶段流程,详细实现如下。

    MYSQL_BIN_LOG::ordered_commit()           ← 执行事务顺序提交,binlog group commit的主流程
     |
     |-#########>>>>>>>>>                     ← 进入Stage_manager::FLUSH_STAGE阶段
     |-change_stage(..., &LOCK_log)
     | |-stage_manager.enroll_for()           ← 将当前线程加入到m_queue[FLUSH_STAGE]中
     | |
     | |                                      ← (follower)返回true
     | |-mysql_mutex_lock()                   ← (leader)对LOCK_log加锁,并返回false
     |
     |-finish_commit()                        ← (follower)对于follower则直接返回
     | |-ha_commit_low()
     |
     |-process_flush_stage_queue()            ← (leader)对于follower则直接返回
     | |-fetch_queue_for()                    ← 通过stage_manager获取队列中的成员
     | | |-fetch_and_empty()                  ← 获取元素并清空队列
     | |-ha_flush_log()
     | |-flush_thread_caches()                ← 对于每个线程做该操作
     | |-my_b_tell()                          ← 判断是否超过了max_bin_log_size,如果是则切换binlog文件
     |
     |-flush_cache_to_file()                  ← (follower)将I/O Cache中的内容写到文件中
     |-RUN_HOOK()                             ← 调用HOOK函数,也就是binlog_storage->after_flush()
     |
     |-#########>>>>>>>>>                     ← 进入Stage_manager::SYNC_STAGE阶段
     |-change_stage()
     |-sync_binlog_file()
     | |-mysql_file_sync()
     |   |-my_sync()
     |     |-fdatasync()                      ← 调用系统API写入磁盘,也可以是fsync()
     |
     |-#########>>>>>>>>>                     ← 进入Stage_manager::COMMIT_STAGE阶段
     |-change_stage()                         ← 该阶段会受到binlog_order_commits参数限制
     |-process_commit_stage_queue()           ← 会遍厉所有线程,然后调用如下存储引擎接口
     | |-ha_commit_low()
     |   |-ht->commit()                       ← 调用存储引擎handlerton->commit()
     |   |                                    ← ### 注意,实际调用如下的两个函数
     |   |-binlog_commit()
     |   |-innobase_commit()
     |-process_after_commit_stage_queue()     ← 提交之后的后续处理,例如semisync
     | |-RUN_HOOK()                           ← 调用transaction->after_commit
     |
     |-stage_manager.signal_done()            ← 通知其它线程事务已经提交
     |
     |-finish_commit()

    在 enroll_for() 函数中,刚添加的线程如果是队列的第一个线程,就将其设置为 leader 线程;否则就是 follower 线程,此时线程会睡眠,直到被 leader 唤醒 (m_cond_done) 。

    注意,binlog_max_flush_queue_time 参数已经取消。

    commit stage

    如上所述,commit 阶段会受到参数 binlog_order_commits 的影响,当该参数关闭时,会直接释放 LOCK_sync ,各个 session 自行进入 InnoDB commit 阶段,这样不会保证 binlog 和事务 commit 的顺序一致。

    当然,如果你不关注两者的一致性,那么可以关闭这个选项来稍微提高点性能;当打开了上述的参数,才会进入 commit stage 。

  • 相关阅读:
    第一节:SpringMVC概述
    SpringMVC【目录】
    Windows 系统快速查看文件MD5
    (error) ERR wrong number of arguments for 'hmset' command
    hive使用遇到的问题 cannot recognize input
    Overleaf支持的部分中文字体预览
    Understanding and Improving Fast Adversarial Training
    Django2实战示例 第十三章 上线
    Django2实战示例 第十二章 创建API
    Django2实战示例 第十一章 渲染和缓存课程内容
  • 原文地址:https://www.cnblogs.com/liujiacai/p/7777528.html
Copyright © 2011-2022 走看看