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 。

  • 相关阅读:
    170601、单例模式的三种水平代码(第三种最佳)
    解决打开pycharm有带图片md文件卡死问题
    Dockerfile 操作
    Docker 命令大全
    MAC
    mac 搭建selenium与ChromeDriver环境
    Mac进行 usr/bin 目录下修改权限问题,operation not permitted
    pytest文档6-fixture之yield实现teardown
    pytest文档5-fixture之conftest.py
    pytest文档4-测试用例setup和teardown
  • 原文地址:https://www.cnblogs.com/liujiacai/p/7777528.html
Copyright © 2011-2022 走看看