前言

我们都知道数据库利用write-ahead logging(WAL)的机制,来保证异常宕机后数据的持久性。即提交事务之前,不仅要更新所有事务相关的Page,也要确保所有的WAL日志都写入磁盘。在InnoDB引擎中,这个WAL就是InnoDB的redo log,一般存储在ib_logfilexxx文件中,文件数量可通过my.cnf配置。

在MySQL 8.0官方发布了新版本8.0.21中,支持了一个新特性“Redo Logging动态开关”。借助这个功能,在新实例导数据的场景下,相关事务可以跳过记录redo日志和doublewrite buffer,从而加快数据的导入速度。同时,付出的代价是短时间牺牲了数据库的ACID保障。

用法介绍

新增内容

  • SQL语法ALTER INSTANCE {ENABLE | DISABLE} INNODB REDO_LOG
  • INNODB_REDO_LOG_ENABLE权限,允许执行Redo Logging动态开关的操作。
  • Innodb_redo_log_enabled的status,用于显示当前Redo Logging开关状态。

操作步骤

  • 创建新的MySQL实例,账号赋权

    1. mysql> GRANT INNODB_REDO_LOG_ENABLE ON *.* to 'data_load_admin';
  • 关闭redo logging

    1. mysql> ALTER INSTANCE DISABLE INNODB REDO_LOG;
  • 检查redo logging是否成功关闭

    1. mysql> SHOW GLOBAL STATUS LIKE 'Innodb_redo_log_enabled';
    2. +-------------------------+-------+
    3. | Variable_name | Value |
    4. +-------------------------+-------+
    5. | Innodb_redo_log_enabled | OFF |
    6. +-------------------------+-------+
  • 导数据

  • 重新开启redo logging

    1. mysql> ALTER INSTANCE ENABLE INNODB REDO_LOG;
  • 确认redo logging状态

    1. mysql> SHOW GLOBAL STATUS LIKE 'Innodb_redo_log_enabled';
    2. +-------------------------+-------+
    3. | Variable_name | Value |
    4. +-------------------------+-------+
    5. | Innodb_redo_log_enabled | ON |
    6. +-------------------------+-------+

注意事项

  • 该特性仅用于新实例导数据场景,不可用于线上的生产环境;
  • Redo logging关闭状态下,支持正常流程的关闭和重启实例;但在异常宕机情况下,可能会导致丢数据和页面损坏;Redo logging关闭后异常宕机的实例需要废弃重建,直接重启会有如下报错:[ERROR] [MY-013578] [InnoDB] Server was killed when Innodb Redo logging was disabled. Data files could be corrupt. You can try to restart the database with innodb_force_recovery=6.

  • Redo logging关闭状态下,不支持cloning operations和redo log archiving这两个功能;

  • 执行过程中不支持其他并发的ALTER INSTANCE操作;

代码分析

新增handler接口如下

  1. /**
  2. @brief
  3. Enable or Disable SE write ahead logging.
  4. @param[in] thd server thread handle
  5. @param[in] enable enable/disable redo logging
  6. @return true iff failed.
  7. */
  8. typedef bool (*redo_log_set_state_t)(THD *thd, bool enable);
  9. struct handlerton {
  10. ...
  11. redo_log_set_state_t redo_log_set_state;
  12. ...
  13. }

MySQL上层链路是常见的SQL执行链路。

  1. mysql_parse
  2. mysql_execute_command
  3. Sql_cmd_alter_instance::execute
  4. // case ALTER_INSTANCE_ENABLE_INNODB_REDO
  5. // 或者 case ALTER_INSTANCE_DISABLE_INNODB_REDO
  6. Innodb_redo_log::execute
  7. /*
  8. Acquire shared backup lock to block concurrent backup. Acquire exclusive
  9. backup lock to block any concurrent DDL. This would also serialize any
  10. concurrent key rotation and other redo log enable/disable calls.
  11. */
  12. // 通过mdl锁阻止并发
  13. if (acquire_exclusive_backup_lock(m_thd, m_thd->variables.lock_wait_timeout,
  14. true) ||
  15. acquire_shared_backup_lock(m_thd, m_thd->variables.lock_wait_timeout)) {
  16. DBUG_ASSERT(m_thd->get_stmt_da()->is_error());
  17. return true;
  18. }
  19. hton->redo_log_set_state(m_thd, m_enable)

hton->redo_log_set_state在InnoDB引擎对应函数innobase_redo_set_state,最终分别调用mtr_t::s_logging.disable和mtr_t::s_logging.enable。

  1. static bool innobase_redo_set_state(THD *thd, bool enable) {
  2. if (srv_read_only_mode) {
  3. my_error(ER_INNODB_READ_ONLY, MYF(0));
  4. return (true);
  5. }
  6. int err = 0;
  7. if (enable) {
  8. err = mtr_t::s_logging.enable(thd); // 开启redo
  9. } else {
  10. err = mtr_t::s_logging.disable(thd); // 关闭redo
  11. }
  12. if (err != 0) {
  13. return (true);
  14. }
  15. // 设置global status
  16. set_srv_redo_log(enable);
  17. return (false);
  18. }

在InnoDB引擎层的mtr模块中,新增了一个Logging子模块。该子模块有四种状态,分别的含义如下:

ENABLEDRedo log打开。
ENABLED_DBLWRRedo log打开,所有关闭redo状态的mtr对应的page都已经刷盘,doublewrite buffer打开,但是仍有部分page走非doublewrite模式刷盘。
ENABLED_RESTRICTRedo log打开,但是仍有部分关闭redo状态的mtr,且doublewrite buffer未打开。
DISABLEDRedo log关闭。

除了ENABLED,其他都是不crash safe的状态。其中,开启redo的状态变化为[DISABLED] -> [ENABLED_RESTRICT] -> [ENABLED_DBLWR] -> [ENABLED],对应函数mtr::Logging::enable;关闭redo的状态变化为[ENABLED] -> [ENABLED_RESTRICT] -> [DISABLED],对应函数mtr::Logging::disable。
同时该模块也包含一个Shards类型的m_count_nologging_mtr统计值,记录当前正在运行的关闭redo状态的mtr数量。该统计值使用shared counter类型(Shards),可以减少CPU缓存失效,起到性能优化的作用。

Redo log关闭流程(mtr::Logging::disable)

  1. int mtr_t::Logging::disable(THD *) {
  2. // 检查是否已经是DISABLED状态
  3. if (is_disabled()) {
  4. return (0);
  5. }
  6. /* Disallow archiving to start. */
  7. ut_ad(m_state.load() == ENABLED);
  8. m_state.store(ENABLED_RESTRICT);
  9. /* Check if redo log archiving is active. */
  10. // 检查是否有redo archive正在进行
  11. if (meb::redo_log_archive_is_active()) {
  12. m_state.store(ENABLED);
  13. my_error(ER_INNODB_REDO_ARCHIVING_ENABLED, MYF(0));
  14. return (ER_INNODB_REDO_ARCHIVING_ENABLED);
  15. }
  16. /* Concurrent clone is blocked by BACKUP MDL lock except when
  17. clone_ddl_timeout = 0. Force any existing clone to abort. */
  18. // 停止clone功能
  19. clone_mark_abort(true);
  20. ut_ad(!clone_check_active());
  21. /* Mark that it is unsafe to crash going forward. */
  22. // 设置redolog的m_disable和m_crash_unsafe标志位
  23. // 内部调用log_files_header_fill将标志位持久化
  24. log_persist_disable(*log_sys);
  25. ib::warn(ER_IB_WRN_REDO_DISABLED);
  26. m_state.store(DISABLED);
  27. clone_mark_active();
  28. /* Reset sync LSN if beyond current system LSN. */
  29. reset_buf_flush_sync_lsn();
  30. return (0);
  31. }

Redo log打开流程(mtr::Logging::enable)

  1. int mtr_t::Logging::enable(THD *thd) {
  2. if (is_enabled()) {
  3. return (0);
  4. }
  5. /* Allow mtrs to generate redo log. Concurrent clone and redo
  6. log archiving is still restricted till we reach a recoverable state. */
  7. ut_ad(m_state.load() == DISABLED);
  8. m_state.store(ENABLED_RESTRICT);
  9. /* 1. Wait for all no-log mtrs to finish and add dirty pages to disk.*/
  10. // 等待m_count_nologging_mtr计数器为0或者thd被kill
  11. auto err = wait_no_log_mtr(thd);
  12. if (err != 0) {
  13. m_state.store(DISABLED);
  14. return (err);
  15. }
  16. /* 2. Wait for dirty pages to flush by forcing checkpoint at current LSN.
  17. All no-logging page modification are done with the LSN when we stopped
  18. redo logging. We need to have one write mini-transaction after enabling redo
  19. to progress the system LSN and take a checkpoint. An easy way is to flush
  20. the max transaction ID which is generally done at TRX_SYS_TRX_ID_WRITE_MARGIN
  21. interval but safe to do any time. */
  22. trx_sys_mutex_enter();
  23. // 通过更新trx_id的接口生成一个mtr,目的是提供一个lsn推进的位点
  24. trx_sys_flush_max_trx_id();
  25. trx_sys_mutex_exit();
  26. /* It would ensure that the modified page in previous mtr and all other
  27. pages modified before are flushed to disk. Since there could be large
  28. number of left over pages from LAD operation, we still don't enable
  29. double-write at this stage. */
  30. // 不开double-write的状态checkpoint到最新的lsn
  31. log_make_latest_checkpoint(*log_sys);
  32. m_state.store(ENABLED_DBLWR);
  33. /* 3. Take another checkpoint after enabling double write to ensure any page
  34. being written without double write are already synced to disk. */
  35. // 再次checkpoint到最新的lsn
  36. log_make_latest_checkpoint(*log_sys);
  37. /* 4. Mark that it is safe to recover from crash. */
  38. // 设回m_disable和m_crash_unsafe标志位,并持久化
  39. log_persist_enable(*log_sys);
  40. ib::warn(ER_IB_WRN_REDO_ENABLED);
  41. m_state.store(ENABLED);
  42. return (0);
  43. }

从以上代码我们可以看到,redo开启的过程中为了优化状态切换的性能,专门增加了ENABLED_DBLWR阶段,并在前后分别执行了一次checkpoint。
然后我们来看下关闭redo logging的行为对其他子模块的影响。Logging系统里面定义了如下几个返回bool类型的函数:

  1. bool dblwr_disabled() const {
  2. auto state = m_state.load();
  3. return (state == DISABLED || state == ENABLED_RESTRICT);
  4. }
  5. bool is_enabled() const { return (m_state.load() == ENABLED); }
  6. bool is_disabled() const { return (m_state.load() == DISABLED); }

追溯这些函数的调用方发现:dblwr_disabled用于限制doublewrite buffer的写入。is_enabled用于调整adaptive flush,和阻止cloning operations和redo log archiving这两个功能。is_disabled调用的地方多一些,包含以下几个判断点:

  • 调整adaptive flush的速度,加快刷脏;
  • page cleaner线程正常退出时在redo header标记当前是crash-safe状态;
  • 当innodb_fast_shutdown=2时,自动调整为1确保正常shutdown的时候是crash-safe的;
  • 开启新的mtr的时候,调整m_count_nologging_mtr统计值,标记当前mtr为MTR_LOG_NO_REDO状态;

由于adaptive flush依据redo的lsn推进速度才决策刷盘脏页数量,因此adaptive flush的算法需要微调,这一块的逻辑可以参考Adaptive_flush::page_recommendation中的set_flush_target_by_page

  1. ulint page_recommendation(ulint last_pages_in, bool is_sync_flush) {
  2. ...
  3. /* Set page flush target based on LSN. */
  4. auto n_pages = skip_lsn ? 0 : set_flush_target_by_lsn(is_sync_flush);
  5. /* Estimate based on only dirty pages. We don't want to flush at lesser rate
  6. as LSN based estimate may not represent the right picture for modifications
  7. without redo logging - temp tables, bulk load and global redo off. */
  8. n_pages = set_flush_target_by_page(n_pages);
  9. ...
  10. }

参考资料