背景
在 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_check和thd->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,必須滿足以下任意一個條件:
- 當前表是臨時表:
table->s->tmp_table == NO_TMP_TABLE為 false。 - 庫名不滿足 --replicate-do-db、--replicate-ignore-db 複製規則:
binlog_filter->db_ok(table->s->db.str)為 false。 - 表設置了 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_flags或table_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<bool> acl_guard(&opt_noacl, true);
Server_option_guard<bool> general_log_guard(&opt_general_log, false);
Server_option_guard<bool> slow_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 設置為true(thd->lex表示當前 SQL 語句的語法解析上下文),可以在語句級別控制該語句不寫入 binlog。
NO_WRITE_TO_BINLOG 為 true 的場景
以下場景會將 no_write_to_binlog 設置為 true。
- SHUTDOWN、RESTART 命令。
- RESET 系列命令,包括:RESET MASTER, RESET SLAVE, RESET PERSIST。
- 顯式指定
NO_WRITE_TO_BINLOG或LOCAL。部分維護類 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。