博客 / 詳情

返回

MySQL 在哪些場景下不會寫 binlog

背景

在 MySQL 中,慢日誌不僅可以記錄在文件中,還可以記錄在表中。具體是記錄在文件還是表中是由log_output參數決定的。

該參數默認為FILE,即慢日誌默認會記錄在文件中。如果參數中包含TABLE,則慢日誌還會記錄在mysql.slow_log中,而mysql.slow_log使用的是 CSV 存儲引擎。

最初研究這一問題,是為了確認在主從複製以及組複製(MGR)環境下,mysql.slow_log表中的慢日誌是否會同步到其他節點。

隨着分析的深入,發現 MySQL 實際上提供了多種機制和開關,用於確保操作不會寫入 binlog。

由於 ROW 格式 是目前最常用的 binlog 格式,本文將從 ROW 模式下 MySQL 判斷操作是否寫入 binlog 的實現邏輯 入手,逐步引出相關控制開關,並分析它們各自的使用場景。

ROW 格式下判斷操作是否寫入 binlog 的實現邏輯

在 ROW 格式下,將數據變化記錄到 binlog 的核心是在binlog_log_row函數中實現的:

int binlog_log_row(TABLE *table, const uchar *before_record,
                   const uchar *after_record, Log_func *log_func) {
bool error = false;
  THD *const thd = table->in_use;
// 判斷當前操作是否需要寫入 binlog
if (check_table_binlog_row_based(thd, table)) {
    ...
    if (likely(!(error = write_locked_table_maps(thd)))) {
      boolconst has_trans = thd->lex->sql_command == SQLCOM_CREATE_TABLE ||
                             table->file->has_transactions();
      // 根據操作類型,將行鏡像寫入 binlog
      error = (*log_func)(thd, table, has_trans, before_record, after_record);
    }
  }

return error ? HA_ERR_RBR_LOGGING_FAILED : 0;
}

首先調用 check_table_binlog_row_based 判斷當前操作是否需要寫入 binlog,若需要,則會針對不同的操作類型,調用不同的函數來處理。具體來説:

  • INSERT:Write_rows_log_event::binlog_row_logging_function
  • UPDATE:Update_rows_log_event::binlog_row_logging_function
  • DELETE:Delete_rows_log_event::binlog_row_logging_function

接下來,重點看看check_table_binlog_row_based函數的處理邏輯。

static bool check_table_binlog_row_based(THD *thd, TABLE *table) {
if (table->s->cached_row_logging_check == -1) {
    int const check(table->s->tmp_table == NO_TMP_TABLE &&
                    !table->no_replicate &&
                    binlog_filter->db_ok(table->s->db.str));
    table->s->cached_row_logging_check = check;
  }

  assert(table->s->cached_row_logging_check == 0 ||
         table->s->cached_row_logging_check == 1);

return (thd->is_current_stmt_binlog_format_row() &&
          table->s->cached_row_logging_check &&
          (thd->variables.option_bits & OPTION_BIN_LOG) &&
          mysql_bin_log.is_open());
}

要返回 false,只需滿足以下任意一個條件:

  • 當前 SQL 語句不能以 ROW 格式記錄到 binlog 中:thd->is_current_stmt_binlog_format_row()為 false,例如 DDL 語句。
  • 表不允許寫入 binlog:table->s->cached_row_logging_check為 false。
  • 當前線程未啓用 binlog:thd->variables.option_bits & OPTION_BIN_LOG為 false。
  • binlog 未打開:mysql_bin_log.is_open() 為 false。

因為第一個條件和第四個條件為 false 的情況並不常見,下面將重點分析table->s->cached_row_logging_checkthd->variables.option_bits & OPTION_BIN_LOG為 false 時的場景。

cached_row_logging_check 為 false 的場景

table->s->cached_row_logging_check的賦值邏輯如下:

  if (table->s->cached_row_logging_check == -1) {
    int const check(table->s->tmp_table == NO_TMP_TABLE &&
                    !table->no_replicate &&
                    binlog_filter->db_ok(table->s->db.str));
    table->s->cached_row_logging_check = check;
  }

要使其為 false,必須滿足以下任意一個條件:

  1. 當前表是臨時表: table->s->tmp_table == NO_TMP_TABLE為 false。
  2. 庫名不滿足 --replicate-do-db、--replicate-ignore-db 複製規則:binlog_filter->db_ok(table->s->db.str)為 false。
  3. 表設置了 no_replicate。該屬性是在open_table_from_share()函數中根據表的類型和存儲引擎能力標誌設置的。

no_replicate 的設置邏輯如下:

  if ((share->table_category == TABLE_CATEGORY_LOG) ||
      (share->table_category == TABLE_CATEGORY_RPL_INFO) ||
      (share->table_category == TABLE_CATEGORY_GTID)) {
    outparam->no_replicate = true;
  } else if (outparam->file) {
    const handler::Table_flags flags = outparam->file->ha_table_flags();
    outparam->no_replicate =
        !(flags & (HA_BINLOG_STMT_CAPABLE | HA_BINLOG_ROW_CAPABLE)) ||
        (flags & HA_HAS_OWN_BINLOGGING);
  } else {
    outparam->no_replicate = false;
  }

可以看到,no_replicate 會在以下幾種情況設置為 true。

一、特殊類別的表。包括:

  • TABLE_CATEGORY_LOG 類別的表,具體包括 mysql.general_log, mysql.slow_log。
  • TABLE_CATEGORY_RPL_INFO 類別的表,具體包括 mysql.slave_relay_log_info,mysql.slave_master_info,mysql.slave_worker_info。
  • TABLE_CATEGORY_GTID 類別的表,具體包括 mysql.gtid_executed。

二、根據存儲引擎的能力標誌判斷。

這些標誌是每個存儲引擎單獨設置的,一般是在m_int_table_flagstable_flags函數中定義的,主要是用來向 Server 層聲明:這個存儲引擎的表,支持哪些能力/約束。與複製相關的標誌有三個:

  • HA_BINLOG_STMT_CAPABLE:支持 STATEMENT 格式 binlog。
  • HA_BINLOG_ROW_CAPABLE:支持 ROW 格式 binlog
  • HA_HAS_OWN_BINLOGGING:該引擎自己管理 binlog(如 NDB Cluster)。

在 MySQL 支持的存儲引擎中,只有 perfschema(對應 performance_schema)和 temptable(MySQL 8.0 引入的內部臨時表存儲引擎,主要用來替代老的 MEMORY/MyISAM 內部臨時表)不會設置 HA_BINLOG_STMT_CAPABLE 或 HA_BINLOG_ROW_CAPABLE。

所以,針對 performance_schema 表的操作不會寫入 binlog。

# ls mysql-8.4.3/storage/
archive  blackhole  csv  example  federated  heap  innobase  myisam  myisammrg  ndb  perfschema  secondary_engine_mock  temptable

OPTION_BIN_LOG 為 false 的場景

thd->variables保存當前線程的會話級系統變量狀態。其中,option_bits 是一個位圖(bitmap),用於記錄多個線程級選項標誌,OPTION_BIN_LOG 則表示是否將當前線程的操作寫入 binlog。

以下是幾種典型場景。

一、顯式關閉會話級 binlog

SET SESSION sql_log_bin = 0;

該參數對應的回調函數是fix_sql_log_bin_after_update

sql_log_bin = 1時,打開 OPTION_BIN_LOG,反之,則清除 OPTION_BIN_LOG。

static bool fix_sql_log_bin_after_update(sys_var *, THD *thd,
                                         enum_var_type type [[maybe_unused]]) {
  assert(type == OPT_SESSION);

  if (thd->variables.sql_log_bin)
    thd->variables.option_bits |= OPTION_BIN_LOG;
  else
    thd->variables.option_bits &= ~OPTION_BIN_LOG;

  return false;
}

二、從庫未啓用 log_replica_updates

當實例作為從庫運行,且未開啓 log_replica_updates 時,從庫 SQL 線程重放的操作默認不寫 binlog。

void set_slave_thread_options(THD *thd) {
  ...
  ulonglong options = thd->variables.option_bits | OPTION_BIG_SELECTS;
  if (opt_log_replica_updates)
    options |= OPTION_BIN_LOG;
  else
    options &= ~OPTION_BIN_LOG;
  ...
}

三、使用Disable_binlog_guard臨時關閉 binlog

Disable_binlog_guard用於在特定代碼塊內臨時關閉 binlog,並在離開作用域時自動恢復原狀態。

class Disable_binlog_guard {
 public:
explicit Disable_binlog_guard(THD *thd)
      : m_thd(thd),
        m_binlog_disabled(thd->variables.option_bits & OPTION_BIN_LOG) {
    thd->variables.option_bits &= ~OPTION_BIN_LOG;
  }

  ~Disable_binlog_guard() {
    if (m_binlog_disabled) m_thd->variables.option_bits |= OPTION_BIN_LOG;
  }

private:
  THD *const m_thd;
constbool m_binlog_disabled;
};

Disable_binlog_guard 被調用的場景有:

3.1 實例初始化(--initialize

static bool handle_bootstrap_impl(handle_bootstrap_args *args) {
  ...
if (opt_initialize) {
    assert(thd->system_thread == SYSTEM_THREAD_SERVER_INITIALIZE);

    sysd::notify("STATUS=Initialization of MySQL system tables in progress\n");
    
    const Disable_binlog_guard disable_binlog(thd);
    const Disable_sql_log_bin_guard disable_sql_log_bin(thd);

    Compiled_in_command_iterator comp_iter;
    rc = process_iterator(thd, &comp_iter, true);

    thd->system_thread = SYSTEM_THREAD_INIT_FILE;

    sysd::notify("STATUS=Initialization of MySQL system tables ",
                 rc ? "unsuccessful" : "successful", "\n");

    if (rc != 0) {
      returntrue;
    }
  }
  ...
returnfalse;
}

3.2 實例升級

bool upgrade_system_schemas(THD *thd) {
Disable_autocommit_guard autocommit_guard(thd);
  Bootstrap_error_handler bootstrap_error_handler;

Server_option_guard<boolacl_guard(&opt_noacl, true);
Server_option_guard<boolgeneral_log_guard(&opt_general_log, false);
Server_option_guard<boolslow_log_guard(&opt_slow_log, false);
Disable_binlog_guard disable_binlog(thd);
Disable_sql_log_bin_guard disable_sql_log_bin(thd);
  ...
  bootstrap_error_handler.set_log_error(false);
bool err =
      fix_mysql_tables(thd) || fix_sys_schema(thd) || upgrade_help_tables(thd);
if (!err) {
    /*
      Initialize structures necessary for federated server from mysql.servers
      table.
    */
    servers_init(thd);
    err = (DBUG_EVALUATE_IF("force_fix_user_schemas", true,
                            dd::bootstrap::DD_bootstrap_ctx::instance()
                                .is_server_upgrade_from_before(
                                    bootstrap::SERVER_VERSION_80011))
               ? check.check_all_schemas(thd)
               : check.check_system_schemas(thd)) ||
          check.repair_tables(thd) ||
          dd::tables::DD_properties::instance().set(
              thd, "MYSQLD_VERSION_UPGRADED", MYSQL_VERSION_ID);
  }
  ...
return dd::end_transaction(thd, err);
}

3.3 CREATE SERVER, ALTER SERVER 和 DROP SERVER 操作。

3.4 INSTALL COMPONENT, UNINSTALL COMPONENT 操作。

3.5 INSTALL PLUGIN, UNINSTALL PLUGIN 操作。

3.6 一些內部操作,例如 ALTER TABLE 過程中創建/刪除臨時表、DROP DATABASE 時清理數據庫對象、更新數據字典表、後台線程自動更新列直方圖。

除了上面介紹的這些場景,通過將 thd->lex->no_write_to_binlog 設置為truethd->lex表示當前 SQL 語句的語法解析上下文),可以在語句級別控制該語句不寫入 binlog。

NO_WRITE_TO_BINLOG 為 true 的場景

以下場景會將 no_write_to_binlog 設置為 true。

  1. SHUTDOWN、RESTART 命令。
  2. RESET 系列命令,包括:RESET MASTER, RESET SLAVE, RESET PERSIST。
  3. 顯式指定NO_WRITE_TO_BINLOGLOCAL。部分維護類 SQL 命令(OPTIMIZE, ANALYZE, REPAIR, FLUSH)支持在語句中顯式指定不寫 binlog,如,
OPTIMIZE NO_WRITE_TO_BINLOG TABLE t1;
ANALYZE LOCAL TABLE t1;
REPAIR NO_WRITE_TO_BINLOG TABLE t1;
FLUSH LOCAL PRIVILEGES;

需要注意的是,對於FLUSH命令,即使未顯式指定NO_WRITE_TO_BINLOG,以下命令默認也不會寫入 binlog:NO_WRITE_TO_BINLOG,FLUSH LOGS、FLUSH BINARY LOGS、FLUSH TABLES WITH READ LOCK、FLUSH TABLES tbl_name ... FOR EXPORT。

總結

雖然上面列舉的場景較多,但實際上並不需要大家刻意去記。

簡單來説,

  • 凡是 MySQL 內部自動執行的操作(即非用户手動執行的操作),通常不會寫入 binlog。 典型場景包括:實例初始化與升級、mysql.slow_log表的寫入、數據字典的維護、performance_schema表數據的更新等。

  • 對 mysql 庫下的表進行 DML 操作,只要不屬於上面提到的特殊類別的表,基本都會寫入 binlog。

    但若執行的是 DDL 操作(如 truncate),基本都會寫入 binlog。

  • 對 performance_schema 中的表進行 DML、DDL 操作會提示權限不足,即便是用 root 用户執行。但部分表允許執行 truncate 操作,且 truncate 操作不會寫入 binlog。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.