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

新增内容

  • SQL语法。
  • INNODB_REDO_LOG_ENABLE权限,允许执行Redo Logging动态开关的操作。
  • Innodb_redo_log_enabled的status,用于显示当前Redo Logging开关状态。

操作步骤

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

  • 关闭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状态

  • 该特性仅用于新实例导数据场景,不可用于线上的生产环境;
  • 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. // 通过mdl锁阻止并发
  12. if (acquire_exclusive_backup_lock(m_thd, m_thd->variables.lock_wait_timeout,
  13. true) ||
  14. acquire_shared_backup_lock(m_thd, m_thd->variables.lock_wait_timeout)) {
  15. DBUG_ASSERT(m_thd->get_stmt_da()->is_error());
  16. return true;
  17. }
  18. hton->redo_log_set_state(m_thd, m_enable)
  1. static bool innobase_redo_set_state(THD *thd, bool enable) {
  2. my_error(ER_INNODB_READ_ONLY, MYF(0));
  3. return (true);
  4. }
  5. int err = 0;
  6. if (enable) {
  7. err = mtr_t::s_logging.enable(thd); // 开启redo
  8. } else {
  9. err = mtr_t::s_logging.disable(thd); // 关闭redo
  10. }
  11. if (err != 0) {
  12. return (true);
  13. }
  14. // 设置global status
  15. set_srv_redo_log(enable);
  16. return (false);
  17. }

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

除了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)

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. interval but safe to do any time. */
  21. trx_sys_mutex_enter();
  22. trx_sys_flush_max_trx_id();
  23. trx_sys_mutex_exit();
  24. /* It would ensure that the modified page in previous mtr and all other
  25. pages modified before are flushed to disk. Since there could be large
  26. number of left over pages from LAD operation, we still don't enable
  27. double-write at this stage. */
  28. // 不开double-write的状态checkpoint到最新的lsn
  29. log_make_latest_checkpoint(*log_sys);
  30. m_state.store(ENABLED_DBLWR);
  31. /* 3. Take another checkpoint after enabling double write to ensure any page
  32. being written without double write are already synced to disk. */
  33. // 再次checkpoint到最新的lsn
  34. log_make_latest_checkpoint(*log_sys);
  35. /* 4. Mark that it is safe to recover from crash. */
  36. // 设回m_disable和m_crash_unsafe标志位,并持久化
  37. log_persist_enable(*log_sys);
  38. ib::warn(ER_IB_WRN_REDO_ENABLED);
  39. m_state.store(ENABLED);
  40. return (0);
  41. }

从以上代码我们可以看到,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状态;
  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. }