From 7c2e9d5bdba49498a89029a4470c352a7e7e5c95 Mon Sep 17 00:00:00 2001 From: Libing Song Date: Mon, 10 Aug 2026 22:16:46 +0800 Subject: [PATCH] Binlog Commit Optimization for Large Transactions Description =========== When a transaction commits, it copies the binlog events from the binlog cache to the binlog file. Very large transactions (e.g., gigabytes) prevent other transactions from writing to the binary log for a long time. The solution is to rename the binlog cache file to the new binlog file instead of copying its content, if the committing transaction has a large binlog cache. Since copying is avoided and most of the I/O is outside the protection of LOCK_log, the commits of large transactions are as fast as small transactions. Design ====== * binlog_large_commit_threshold type: ulonglong scope: global dynamic: yes default: 128 MB Binlog cache of transactions larger than the threshold trigger this feature. This feature will be disabled if binlog_large_commit_threshold is set to 0. * #binlog_cache_files directory To support renaming, all binlog cache temporary files are managed as regular files now. The #binlog_cache_files directory is in the same directory as binlog files. It is created at server startup if it doesn't exist. Otherwise, all files with ML_ prefix in the directory are deleted at startup. The temporary files are named with the ML_ prefix and the memory address of the binlog_cache_data object. * Reserve space The cache files must reserve enough space at the beginning for header events and GTID event. - IO_CACHE_binlog_cache_storage::m_file_reserved_bytes Stores the bytes reserved at the beginning of the cache file. The reserved space is hidden from callers. Thus, there is no change for callers. For example, - get_byte_position() still returns the length of binlog data written to the cache, but not the file length. - truncate(0) truncates the file to m_file_reserved_bytes rather than 0. - Empty_log_event It is used to pad the unused space after the rename. Empty_log_event is immediately after Previous_gtids_log_event. Dump thread doesn't send the event to replicas for compatibility reasons. +-----------------+ | Magic | +-----------------+ | FDE | +-----------------+ | Prev_gtid | +-----------------+ | Empty_log_event | +-----------------+ | GTID | +-----------------+ * Binlog_commit_by_rotate It is used to encapsulate the code for renaming a binlog cache temporary file to a binlog file. - should_commit_by_rotate() Checks whether a binlog cache should be renamed to a binlog file. - commit() This is the entry point to rename a binlog cache and commit the transaction. Two rotations are performed to guarantee the renamed binlog file includes only one transaction in the renamed binlog file. - acquire LOCK_log - rotate 1: renaming happens here - commit to the storage engine. - rotate 2: prevent other transactions from being written. - release LOCK_log - replace_binlog_file() Renaming happens in the middle of a rotation. After the new binlog file is generated, replace_binlog_file() is called to: - copy the header bytes from the new binlog file to the binlog cache file. - delete the binlog file. - rename the binlog cache file to the binlog file. - write Empty event, Gtid event and Xid event. Writing the transaction's events during the rotation is more convenient for the binlog backup system which reads the binlog file directly. * Limits - Neither binlog encryption nor binlog cache encryption is supported. Renaming will not be triggered. - In the renamed binlog file, checksum and compression are disabled. --- mysql-test/r/all_persisted_variables.result | 8 +- mysql-test/r/mysqld--help-notwin.result | 10 + .../r/binlog_commit_by_rotate_atomic.result | 66 ++ .../binlog_commit_by_rotate_atomic-master.opt | 1 + .../t/binlog_commit_by_rotate_atomic.test | 135 ++++ .../r/binlog_persist_only_variables.result | 12 +- .../t/binlog_persist_only_variables.test | 4 +- .../rpl/r/rpl_binlog_commit_by_rotate.result | 100 +++ .../rpl/t/rpl_binlog_commit_by_rotate.test | 165 +++++ ...binlog_large_commit_threshold_basic.result | 30 + .../binlog_large_commit_threshold_basic.test | 31 + mysql-test/t/all_persisted_variables.test | 2 +- sql/basic_istream.cc | 15 + sql/basic_istream.h | 25 + sql/binlog.cc | 645 +++++++++++++++++- sql/binlog.h | 16 + sql/binlog/binlog_ofile.h | 18 + sql/binlog_ostream.cc | 150 +++- sql/binlog_ostream.h | 118 +++- sql/log_event.cc | 27 + sql/log_event.h | 50 ++ sql/mysqld.cc | 7 +- sql/rpl_binlog_sender.cc | 5 +- sql/sys_vars.cc | 16 +- 24 files changed, 1629 insertions(+), 27 deletions(-) create mode 100644 mysql-test/suite/binlog/r/binlog_commit_by_rotate_atomic.result create mode 100644 mysql-test/suite/binlog/t/binlog_commit_by_rotate_atomic-master.opt create mode 100644 mysql-test/suite/binlog/t/binlog_commit_by_rotate_atomic.test create mode 100644 mysql-test/suite/rpl/r/rpl_binlog_commit_by_rotate.result create mode 100644 mysql-test/suite/rpl/t/rpl_binlog_commit_by_rotate.test create mode 100644 mysql-test/suite/sys_vars/r/binlog_large_commit_threshold_basic.result create mode 100644 mysql-test/suite/sys_vars/t/binlog_large_commit_threshold_basic.test diff --git a/mysql-test/r/all_persisted_variables.result b/mysql-test/r/all_persisted_variables.result index 57c9777c0fa5..367de83ec142 100644 --- a/mysql-test/r/all_persisted_variables.result +++ b/mysql-test/r/all_persisted_variables.result @@ -48,7 +48,7 @@ include/assert.inc [Expect 500+ variables in the table. Due to open Bugs, we are # Test SET PERSIST -include/assert.inc [Expect 451 persisted variables in the table.] +include/assert.inc [Expect 452 persisted variables in the table.] ************************************************************ * 3. Restart server, it must preserve the persisted variable @@ -56,9 +56,9 @@ include/assert.inc [Expect 451 persisted variables in the table.] ************************************************************ # restart -include/assert.inc [Expect 451 persisted variables in persisted_variables table.] -include/assert.inc [Expect 451 persisted variables shown as PERSISTED in variables_info table.] -include/assert.inc [Expect 451 persisted variables with matching peristed and global values.] +include/assert.inc [Expect 452 persisted variables in persisted_variables table.] +include/assert.inc [Expect 452 persisted variables shown as PERSISTED in variables_info table.] +include/assert.inc [Expect 452 persisted variables with matching peristed and global values.] ************************************************************ * 4. Test RESET PERSIST IF EXISTS. Verify persisted variable diff --git a/mysql-test/r/mysqld--help-notwin.result b/mysql-test/r/mysqld--help-notwin.result index 036e914de1d9..78d47d7df9c6 100644 --- a/mysql-test/r/mysqld--help-notwin.result +++ b/mysql-test/r/mysqld--help-notwin.result @@ -187,6 +187,15 @@ The following options may be given as the first argument: --binlog-ignore-db=name Exclude updates to the specified database when writing the binary log. + --binlog-large-commit-threshold=# + Increases transaction concurrency for large transactions + (i.e. those with sizes larger than this value) by + renaming the large transaction's binlog cache temporary + file to a new binary log file at commit time, instead of + copying the transaction cache data to the end of the + active binary log file while holding a lock that prevents + other transactions from binlogging. 0 disables the + feature. --binlog-max-flush-queue-time=# The maximum time that the binary log group commit will keep reading transactions before it flush the @@ -1696,6 +1705,7 @@ binlog-format ROW binlog-group-commit-sync-delay 0 binlog-group-commit-sync-no-delay-count 0 binlog-gtid-simple-recovery TRUE +binlog-large-commit-threshold 134217728 binlog-max-flush-queue-time 0 binlog-order-commits TRUE binlog-rotate-encryption-master-key-at-startup FALSE diff --git a/mysql-test/suite/binlog/r/binlog_commit_by_rotate_atomic.result b/mysql-test/suite/binlog/r/binlog_commit_by_rotate_atomic.result new file mode 100644 index 000000000000..c035fc65d03c --- /dev/null +++ b/mysql-test/suite/binlog/r/binlog_commit_by_rotate_atomic.result @@ -0,0 +1,66 @@ +RESET BINARY LOGS AND GTIDS; +# +# A binlog cache file is created in the #binlog_cache_files directory +# and it is deleted at disconnect. +# +CREATE TABLE t1 (c1 LONGTEXT) ENGINE = InnoDB; +# list #binlog_cache_files/ (empty) +INSERT INTO t1 values(repeat("1", 5242880)); +INSERT INTO t1 values(repeat("1", 5242880)); +FLUSH BINARY LOGS; +# list #binlog_cache_files/ (one cache file) +ML_BINLOG_CACHE_FILE +# The binlog cache file is deleted at disconnection. +# list #binlog_cache_files/ (empty) +# +# Reserved space is not big enough for the header events. The rename is +# not done, but the rotation already created a fresh binary log file, so +# the transaction falls back to a normal commit into that new file. +# +SET GLOBAL binlog_large_commit_threshold = 10 * 1024 * 1024; +SET SESSION debug = "+d,simulate_reserve_size_not_enough"; +UPDATE t1 SET c1 = repeat('2', 5242880); +SET SESSION debug = "-d,simulate_reserve_size_not_enough"; +include/rpl/assert_binlog_events.inc [!Gtid_or_anon] +# +# Crash happens before renaming the file. +# +SET GLOBAL binlog_large_commit_threshold = 10 * 1024 * 1024; +SET SESSION debug = "+d,binlog_commit_by_rotate_crash_before_rename"; +UPDATE t1 SET c1 = repeat('4', 5242880); +ERROR HY000: Lost connection to MySQL server during query +# One cache file left after crash. +# list #binlog_cache_files/ +ML_BINLOG_CACHE_FILE +non_binlog_cache +# restart +# The cache files are deleted at startup (only the foreign file remains). +# list #binlog_cache_files/ +non_binlog_cache +include/assert_grep.inc [warning: non_binlog_cache is not a binlog cache file] +SELECT * FROM t1 WHERE c1 = 4; +c1 +include/rpl/assert_binlog_events.inc [()] +# +# Crash happens just after the rotation renamed the cache to the new +# binary log file (the whole transaction, including its Xid event, is +# already in that file) but before the engine commit finished. On restart +# crash recovery commits the transaction, and no cache file is left. +# +SET GLOBAL binlog_large_commit_threshold = 10 * 1024 * 1024; +BEGIN; +UPDATE t1 SET c1 = repeat('5', 5242880); +SAVEPOINT s1; +UPDATE t1 SET c1 = repeat('6', 5242880); +UPDATE t1 SET c1 = repeat('7', 5242880); +ROLLBACK TO SAVEPOINT s1; +INSERT INTO t1 VALUES('a'); +SET SESSION debug = "+d,binlog_commit_by_rotate_crash_after_rotate"; +COMMIT; +ERROR HY000: Lost connection to MySQL server during query +# No cache file left after crash. +# list #binlog_cache_files/ +# restart +include/rpl/assert_binlog_events.inc [Ignorable] +call mtr.add_suppression(".*not a binlog cache file.*"); +DROP TABLE t1; diff --git a/mysql-test/suite/binlog/t/binlog_commit_by_rotate_atomic-master.opt b/mysql-test/suite/binlog/t/binlog_commit_by_rotate_atomic-master.opt new file mode 100644 index 000000000000..eeff9faf80d2 --- /dev/null +++ b/mysql-test/suite/binlog/t/binlog_commit_by_rotate_atomic-master.opt @@ -0,0 +1 @@ +--log-bin=master-bin --binlog-large-commit-threshold=10485760 diff --git a/mysql-test/suite/binlog/t/binlog_commit_by_rotate_atomic.test b/mysql-test/suite/binlog/t/binlog_commit_by_rotate_atomic.test new file mode 100644 index 000000000000..87a584773c7c --- /dev/null +++ b/mysql-test/suite/binlog/t/binlog_commit_by_rotate_atomic.test @@ -0,0 +1,135 @@ +################################################################################ +# Rename binlog cache to binlog file (ported to MySQL trunk) +# +# It verifies that the rename logic is handled correctly if an error or a crash +# happens. +################################################################################ +--source include/have_binlog_format_row.inc +--source include/have_debug.inc +--source include/have_debug_sync.inc +RESET BINARY LOGS AND GTIDS; + +--echo # +--echo # A binlog cache file is created in the #binlog_cache_files directory +--echo # and it is deleted at disconnect. +--echo # +--connect(con1,localhost,root,,) +CREATE TABLE t1 (c1 LONGTEXT) ENGINE = InnoDB; + +--echo # list #binlog_cache_files/ (empty) +--let $datadir= `SELECT @@datadir` +--list_files $datadir/#binlog_cache_files + +INSERT INTO t1 values(repeat("1", 5242880)); +INSERT INTO t1 values(repeat("1", 5242880)); +FLUSH BINARY LOGS; + +--echo # list #binlog_cache_files/ (one cache file) +--replace_regex /ML_[0-9]+/ML_BINLOG_CACHE_FILE/ +--list_files $datadir/#binlog_cache_files + +--disconnect con1 +--connection default +# Wait until the cache file of the disconnected session has been deleted. +--let $wait_condition= SELECT COUNT(*) = 0 FROM performance_schema.threads WHERE PROCESSLIST_USER = 'root' AND PROCESSLIST_ID != CONNECTION_ID() +--source include/wait_condition.inc + +--echo # The binlog cache file is deleted at disconnection. +--echo # list #binlog_cache_files/ (empty) +--list_files $datadir/#binlog_cache_files + +--echo # +--echo # Reserved space is not big enough for the header events. The rename is +--echo # not done, but the rotation already created a fresh binary log file, so +--echo # the transaction falls back to a normal commit into that new file. +--echo # +SET GLOBAL binlog_large_commit_threshold = 10 * 1024 * 1024; +--let $before_file= query_get_value(SHOW BINARY LOG STATUS, File, 1) +SET SESSION debug = "+d,simulate_reserve_size_not_enough"; +UPDATE t1 SET c1 = repeat('2', 5242880); +SET SESSION debug = "-d,simulate_reserve_size_not_enough"; + +--let $event_sequence= !Gtid_or_anon +--let $limit= 3 +--let $binlog_file= master-bin.000003 +--let $binlog_position= 4 +--source include/rpl/assert_binlog_events.inc + +--echo # +--echo # Crash happens before renaming the file. +--echo # +SET GLOBAL binlog_large_commit_threshold = 10 * 1024 * 1024; +--let $datadir= `SELECT @@datadir` + +SET SESSION debug = "+d,binlog_commit_by_rotate_crash_before_rename"; +--source include/expect_crash.inc +--error CR_SERVER_LOST +UPDATE t1 SET c1 = repeat('4', 5242880); + +--write_file $datadir/#binlog_cache_files/non_binlog_cache +It is not a binlog cache file +EOF + +--echo # One cache file left after crash. +--echo # list #binlog_cache_files/ +--replace_regex /ML_[0-9]+/ML_BINLOG_CACHE_FILE/ +--list_files $datadir/#binlog_cache_files + +--source include/start_mysqld.inc + +--echo # The cache files are deleted at startup (only the foreign file remains). +--echo # list #binlog_cache_files/ +--list_files $datadir/#binlog_cache_files + +--let $assert_text= warning: non_binlog_cache is not a binlog cache file +--let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.1.err +--let $assert_select= not a binlog cache file +--let $assert_count= 1 +--let $assert_only_after= CURRENT_TEST: binlog.binlog_commit_by_rotate_atomic +--source include/assert_grep.inc + +--remove_file $datadir/#binlog_cache_files/non_binlog_cache + +SELECT * FROM t1 WHERE c1 = 4; + +--let $event_sequence= () +--let $binlog_file= master-bin.000004 +--let $binlog_position= 4 +--source include/rpl/assert_binlog_events.inc + +--echo # +--echo # Crash happens just after the rotation renamed the cache to the new +--echo # binary log file (the whole transaction, including its Xid event, is +--echo # already in that file) but before the engine commit finished. On restart +--echo # crash recovery commits the transaction, and no cache file is left. +--echo # +SET GLOBAL binlog_large_commit_threshold = 10 * 1024 * 1024; + +BEGIN; +UPDATE t1 SET c1 = repeat('5', 5242880); +SAVEPOINT s1; +UPDATE t1 SET c1 = repeat('6', 5242880); +UPDATE t1 SET c1 = repeat('7', 5242880); +ROLLBACK TO SAVEPOINT s1; +INSERT INTO t1 VALUES('a'); + +SET SESSION debug = "+d,binlog_commit_by_rotate_crash_after_rotate"; +--source include/expect_crash.inc +--error CR_SERVER_LOST +COMMIT; + +--echo # No cache file left after crash. +--echo # list #binlog_cache_files/ +--replace_regex /ML_[0-9]+/ML_BINLOG_CACHE_FILE/ +--list_files $datadir/#binlog_cache_files + +--source include/start_mysqld.inc + +--let $event_sequence= Ignorable +--let $limit= 3 +--let $binlog_file= master-bin.000005 +--let $binlog_position= 4 +--source include/rpl/assert_binlog_events.inc + +call mtr.add_suppression(".*not a binlog cache file.*"); +DROP TABLE t1; diff --git a/mysql-test/suite/binlog_nogtid/r/binlog_persist_only_variables.result b/mysql-test/suite/binlog_nogtid/r/binlog_persist_only_variables.result index cbd1e142f344..22acbcd45c8b 100644 --- a/mysql-test/suite/binlog_nogtid/r/binlog_persist_only_variables.result +++ b/mysql-test/suite/binlog_nogtid/r/binlog_persist_only_variables.result @@ -47,7 +47,7 @@ INSERT INTO aliases(name) VALUES ('slave_parallel_workers'), ('slave_pending_jobs_size_max'), ('pseudo_slave_mode'), ('skip_slave_start'); -include/assert.inc [Expect 110 variables in the table.] +include/assert.inc [Expect 111 variables in the table.] # Test SET PERSIST_ONLY SET PERSIST_ONLY binlog_cache_size = @@GLOBAL.binlog_cache_size; @@ -64,6 +64,7 @@ Warning 1287 '@@binlog_format' is deprecated and will be removed in a future rel SET PERSIST_ONLY binlog_group_commit_sync_delay = @@GLOBAL.binlog_group_commit_sync_delay; SET PERSIST_ONLY binlog_group_commit_sync_no_delay_count = @@GLOBAL.binlog_group_commit_sync_no_delay_count; SET PERSIST_ONLY binlog_gtid_simple_recovery = @@GLOBAL.binlog_gtid_simple_recovery; +SET PERSIST_ONLY binlog_large_commit_threshold = @@GLOBAL.binlog_large_commit_threshold; SET PERSIST_ONLY binlog_max_flush_queue_time = @@GLOBAL.binlog_max_flush_queue_time; Warnings: Warning 1287 '@@binlog_max_flush_queue_time' is deprecated and will be removed in a future release. @@ -257,16 +258,16 @@ Warning 1287 '@@sync_relay_log_info' is deprecated and will be removed in a futu Warning 1287 '@@sync_relay_log_info' is deprecated and will be removed in a future release. SET PERSIST_ONLY sync_source_info = @@GLOBAL.sync_source_info; -include/assert.inc [Expect 99 persisted variables in persisted_variables table.] +include/assert.inc [Expect 100 persisted variables in persisted_variables table.] ############################################################ # 2. Restart server, it must preserve the persisted variable # settings. Verify persisted configuration. # restart -include/assert.inc [Expect 99 persisted variables in persisted_variables table.] -include/assert.inc [Expect 99 persisted variables shown as PERSISTED in variables_info table.] -include/assert.inc [Expect 99 persisted variables with matching persisted and global values.] +include/assert.inc [Expect 100 persisted variables in persisted_variables table.] +include/assert.inc [Expect 100 persisted variables shown as PERSISTED in variables_info table.] +include/assert.inc [Expect 100 persisted variables with matching persisted and global values.] ############################################################ # 3. Test RESET PERSIST. Verify persisted variable settings @@ -282,6 +283,7 @@ RESET PERSIST binlog_format; RESET PERSIST binlog_group_commit_sync_delay; RESET PERSIST binlog_group_commit_sync_no_delay_count; RESET PERSIST binlog_gtid_simple_recovery; +RESET PERSIST binlog_large_commit_threshold; RESET PERSIST binlog_max_flush_queue_time; RESET PERSIST binlog_order_commits; RESET PERSIST binlog_rotate_encryption_master_key_at_startup; diff --git a/mysql-test/suite/binlog_nogtid/t/binlog_persist_only_variables.test b/mysql-test/suite/binlog_nogtid/t/binlog_persist_only_variables.test index b7a5237e2ba1..3207e6bd812e 100644 --- a/mysql-test/suite/binlog_nogtid/t/binlog_persist_only_variables.test +++ b/mysql-test/suite/binlog_nogtid/t/binlog_persist_only_variables.test @@ -84,7 +84,7 @@ INSERT INTO aliases(name) VALUES # If this count differs, it means a variable has been added or removed. # In that case, this testcase needs to be updated accordingly. --echo ---let $expected = 110 +--let $expected = 111 --let $assert_text = Expect $expected variables in the table. --let $assert_cond = [SELECT COUNT(*) as count FROM rplvars, count, 1] = $expected --source include/assert.inc @@ -116,7 +116,7 @@ while ( $varid <= $countvars ) } --echo ---let $expected = 99 +--let $expected = 100 --let $assert_text = Expect $expected persisted variables in persisted_variables table. --let $assert_cond = [SELECT COUNT(*) as count FROM performance_schema.persisted_variables, count, 1] = $expected --source include/assert.inc diff --git a/mysql-test/suite/rpl/r/rpl_binlog_commit_by_rotate.result b/mysql-test/suite/rpl/r/rpl_binlog_commit_by_rotate.result new file mode 100644 index 000000000000..967285eaa9d3 --- /dev/null +++ b/mysql-test/suite/rpl/r/rpl_binlog_commit_by_rotate.result @@ -0,0 +1,100 @@ +include/rpl/init_source_replica.inc +Warnings: +Note #### Sending passwords in plain text without SSL/TLS is extremely insecure. +Note #### Storing MySQL user name or password information in the connection metadata repository is not secure and is therefore not recommended. Please consider using the USER and PASSWORD connection options for START REPLICA; see the 'START REPLICA Syntax' in the MySQL Manual for more information. +[connection master] +# +# Prepare +# +SET @saved_binlog_large_commit_threshold= @@GLOBAL.binlog_large_commit_threshold; +SET @saved_binlog_checksum= @@GLOBAL.binlog_checksum; +SET GLOBAL binlog_checksum = "NONE"; +CREATE TABLE t1 (c1 LONGTEXT) ENGINE = InnoDB; +CREATE TABLE t2 (c1 LONGTEXT) ENGINE = MyISAM; +INSERT INTO t1 values(repeat("1", 5242880)); +INSERT INTO t1 values(repeat("1", 5242880)); +INSERT INTO t2 values(repeat("1", 5242880)); +INSERT INTO t2 values(repeat("1", 5242880)); +# +# Not renamed to binlog, since the binlog cache is not larger than the +# threshold. And it should works well after ROLLBACK TO SAVEPOINT +# +BEGIN; +SAVEPOINT s1; +UPDATE t1 SET c1 = repeat('1', 5242880); +ROLLBACK TO SAVEPOINT s1; +UPDATE t1 SET c1 = repeat('2', 5242880); +SAVEPOINT s2; +UPDATE t1 SET c1 = repeat('3', 5242880); +UPDATE t1 SET c1 = repeat('4', 5242880); +ROLLBACK TO SAVEPOINT s2; +COMMIT; +include/assert.inc [Binlog is not rotated] +# +# Test binlog cache rename to binlog file with checksum off +# +include/rpl/sync_to_replica.inc +include/rpl/stop_replica.inc +SET @saved_binlog_large_commit_threshold = @@GLOBAL.binlog_large_commit_threshold; +SET GLOBAL binlog_large_commit_threshold = 10 * 1024 * 1024; +include/rpl/start_replica.inc +[connection master] +SET GLOBAL binlog_large_commit_threshold = 10 * 1024 * 1024; +# Transaction cache can be renamed and works well with ROLLBACK TO SAVEPOINT +BEGIN; +SAVEPOINT s1; +UPDATE t1 SET c1 = repeat('2', 5242880); +ROLLBACK TO s1; +UPDATE t1 SET c1 = repeat('3', 5242880); +SAVEPOINT s2; +UPDATE t1 SET c1 = repeat('4', 5242880); +UPDATE t1 SET c1 = repeat('5', 5242880); +UPDATE t1 SET c1 = repeat('6', 5242880); +ROLLBACK TO SAVEPOINT s2; +COMMIT; +INSERT INTO t1 VALUES("after_update_t1"); +include/assert.inc [Rename is executed.] +# statement cache can be renamed +BEGIN; +UPDATE t2 SET c1 = repeat('4', 5242880); +INSERT INTO t1 VALUES("after_update_t2"); +COMMIT; +include/assert.inc [Rename is executed.] +[connection master] +include/rpl/sync_to_replica.inc +[connection slave] +include/assert.inc [Rename is executed.] +include/assert.inc [Rename is executed.] +include/rpl/stop_replica.inc +SET GLOBAL binlog_large_commit_threshold = @saved_binlog_large_commit_threshold; +include/rpl/start_replica.inc +# +# CREATE SELECT works well +# +[connection master] +CREATE TABLE t3 ENGINE = InnoDB SELECT * FROM t1; +include/assert.inc [Rename is executed.] +CREATE TABLE t4 ENGINE = MyISAM SELECT * FROM t2; +ERROR HY000: Statement violates GTID consistency: CREATE TABLE ... SELECT. +# XA statement works well +XA START "test-a-long-xid========================================"; +UPDATE t1 SET c1 = repeat('1', 5242880); +XA END "test-a-long-xid========================================"; +XA PREPARE "test-a-long-xid========================================"; +XA COMMIT "test-a-long-xid========================================"; +include/assert.inc [Rename is executed.] +XA START "test-xid"; +UPDATE t1 SET c1 = repeat('2', 5242880); +XA END "test-xid"; +XA COMMIT "test-xid" ONE PHASE; +include/assert.inc [Rename is executed.] +# +# Not rename to binlog file If both stmt and trx cache are not empty +# +UPDATE t1, t2 SET t1.c1 = repeat('8', 5242880), t2.c1 = repeat('7', 5242880); +ERROR HY000: Statement violates GTID consistency: Updates to non-transactional tables can only be done in either autocommitted statements or single-statement transactions, and never in the same statement as updates to transactional tables. +DROP TABLE t1, t2, t3; +SET GLOBAL binlog_large_commit_threshold = @saved_binlog_large_commit_threshold; +SET GLOBAL binlog_checksum = @saved_binlog_checksum; +include/rpl/sync_to_replica.inc +include/rpl/deinit.inc diff --git a/mysql-test/suite/rpl/t/rpl_binlog_commit_by_rotate.test b/mysql-test/suite/rpl/t/rpl_binlog_commit_by_rotate.test new file mode 100644 index 000000000000..5344bb42345f --- /dev/null +++ b/mysql-test/suite/rpl/t/rpl_binlog_commit_by_rotate.test @@ -0,0 +1,165 @@ +################################################################################ +# MDEV-32014 Rename binlog cache to binlog file (ported to MySQL trunk) +# +# It verifies that the binlog caches which are larger +# than binlog_large_commit_threshold can be move to a binlog file +# successfully. With a successful rename, +# - it rotates the binlog and the cache is renamed to the new binlog file +# - an ignorable event is generated just before the Gtid_log_event of the +# transaction to take the reserved spaces which is unused. +# +# It also verifies that rename is not supported in below cases +# though the cache is larger than the threshold +# - both statement and transaction cache should be flushed. +# +# Unlike MariaDB, commit by rotate rotates twice (the second rotation restores +# the checksum for the following binary logs), so the renamed transaction is in +# the binary log file just before the current one. +################################################################################ +--source include/have_binlog_format_row.inc +--source include/rpl/init_source_replica.inc + +--echo # +--echo # Prepare +--echo # +SET @saved_binlog_large_commit_threshold= @@GLOBAL.binlog_large_commit_threshold; +SET @saved_binlog_checksum= @@GLOBAL.binlog_checksum; + +SET GLOBAL binlog_checksum = "NONE"; + +CREATE TABLE t1 (c1 LONGTEXT) ENGINE = InnoDB; +CREATE TABLE t2 (c1 LONGTEXT) ENGINE = MyISAM; + +INSERT INTO t1 values(repeat("1", 5242880)); +INSERT INTO t1 values(repeat("1", 5242880)); +INSERT INTO t2 values(repeat("1", 5242880)); +INSERT INTO t2 values(repeat("1", 5242880)); + +--echo # +--echo # Not renamed to binlog, since the binlog cache is not larger than the +--echo # threshold. And it should works well after ROLLBACK TO SAVEPOINT +--echo # +BEGIN; +SAVEPOINT s1; +UPDATE t1 SET c1 = repeat('1', 5242880); +ROLLBACK TO SAVEPOINT s1; +UPDATE t1 SET c1 = repeat('2', 5242880); +SAVEPOINT s2; +UPDATE t1 SET c1 = repeat('3', 5242880); +UPDATE t1 SET c1 = repeat('4', 5242880); +ROLLBACK TO SAVEPOINT s2; +COMMIT; + +--let $binlog_file= query_get_value(SHOW BINARY LOG STATUS, File, 1) +--let $assert_cond= "$binlog_file" = "master-bin.000002" +--let $assert_text= Binlog is not rotated +--source include/assert.inc + +--echo # +--echo # Test binlog cache rename to binlog file with checksum off +--echo # +--source include/rpl/sync_to_replica.inc +--source include/rpl/stop_replica.inc +SET @saved_binlog_large_commit_threshold = @@GLOBAL.binlog_large_commit_threshold; +SET GLOBAL binlog_large_commit_threshold = 10 * 1024 * 1024; +--source include/rpl/start_replica.inc + +--let $rpl_connection_name= master +--source include/connection.inc +SET GLOBAL binlog_large_commit_threshold = 10 * 1024 * 1024; + +--echo # Transaction cache can be renamed and works well with ROLLBACK TO SAVEPOINT +BEGIN; +SAVEPOINT s1; +UPDATE t1 SET c1 = repeat('2', 5242880); +ROLLBACK TO s1; +UPDATE t1 SET c1 = repeat('3', 5242880); +SAVEPOINT s2; +UPDATE t1 SET c1 = repeat('4', 5242880); +UPDATE t1 SET c1 = repeat('5', 5242880); +UPDATE t1 SET c1 = repeat('6', 5242880); +ROLLBACK TO SAVEPOINT s2; +COMMIT; +INSERT INTO t1 VALUES("after_update_t1"); + +--let $gtid_end_pos= query_get_value(SHOW BINLOG EVENTS IN 'master-bin.000003' LIMIT 4, End_log_pos, 4) +--let $assert_cond= $gtid_end_pos = 4096 +--let $assert_text= Rename is executed. +--source include/assert.inc + +--echo # statement cache can be renamed +BEGIN; +UPDATE t2 SET c1 = repeat('4', 5242880); +INSERT INTO t1 VALUES("after_update_t2"); +COMMIT; +--let $gtid_end_pos= query_get_value(SHOW BINLOG EVENTS IN 'master-bin.000005' LIMIT 4, End_log_pos, 4) +--let $assert_cond= $gtid_end_pos = 4096 +--let $assert_text= Rename is executed. +--source include/assert.inc + +--let $rpl_connection_name= master +--source include/connection.inc +--source include/rpl/sync_to_replica.inc + +--let $rpl_connection_name= slave +--source include/connection.inc +--let $gtid_end_pos= query_get_value(SHOW BINLOG EVENTS IN 'slave-bin.000002' LIMIT 4, End_log_pos, 4) +--let $assert_cond= $gtid_end_pos = 4096 +--let $assert_text= Rename is executed. +--source include/assert.inc + +--let $gtid_end_pos= query_get_value(SHOW BINLOG EVENTS IN 'slave-bin.000004' LIMIT 4, End_log_pos, 4) +--let $assert_cond= $gtid_end_pos = 4096 +--let $assert_text= Rename is executed. +--source include/assert.inc + +--source include/rpl/stop_replica.inc +SET GLOBAL binlog_large_commit_threshold = @saved_binlog_large_commit_threshold; +--source include/rpl/start_replica.inc + +--echo # +--echo # CREATE SELECT works well +--echo # +--let $rpl_connection_name= master +--source include/connection.inc +CREATE TABLE t3 ENGINE = InnoDB SELECT * FROM t1; +--let $gtid_end_pos= query_get_value(SHOW BINLOG EVENTS IN 'master-bin.000007' LIMIT 4, End_log_pos, 4) +--let $assert_cond= $gtid_end_pos = 4096 +--let $assert_text= Rename is executed. +--source include/assert.inc + +--error ER_GTID_UNSAFE_CREATE_SELECT +CREATE TABLE t4 ENGINE = MyISAM SELECT * FROM t2; + +--echo # XA statement works well +XA START "test-a-long-xid========================================"; +UPDATE t1 SET c1 = repeat('1', 5242880); +XA END "test-a-long-xid========================================"; +XA PREPARE "test-a-long-xid========================================"; +XA COMMIT "test-a-long-xid========================================"; +--let $gtid_end_pos= query_get_value(SHOW BINLOG EVENTS IN 'master-bin.000009' LIMIT 4, End_log_pos, 4) +--let $assert_cond= $gtid_end_pos = 4096 +--let $assert_text= Rename is executed. +--source include/assert.inc + +XA START "test-xid"; +UPDATE t1 SET c1 = repeat('2', 5242880); +XA END "test-xid"; +XA COMMIT "test-xid" ONE PHASE; +--let $gtid_end_pos= query_get_value(SHOW BINLOG EVENTS IN 'master-bin.000011' LIMIT 4, End_log_pos, 4) +--let $assert_cond= $gtid_end_pos = 4096 +--let $assert_text= Rename is executed. +--source include/assert.inc + +--echo # +--echo # Not rename to binlog file If both stmt and trx cache are not empty +--echo # +--error ER_GTID_UNSAFE_NON_TRANSACTIONAL_TABLE +UPDATE t1, t2 SET t1.c1 = repeat('8', 5242880), t2.c1 = repeat('7', 5242880); + +# cleanup +DROP TABLE t1, t2, t3; +SET GLOBAL binlog_large_commit_threshold = @saved_binlog_large_commit_threshold; +SET GLOBAL binlog_checksum = @saved_binlog_checksum; +--source include/rpl/sync_to_replica.inc +--source include/rpl/deinit.inc diff --git a/mysql-test/suite/sys_vars/r/binlog_large_commit_threshold_basic.result b/mysql-test/suite/sys_vars/r/binlog_large_commit_threshold_basic.result new file mode 100644 index 000000000000..0ca1758caf44 --- /dev/null +++ b/mysql-test/suite/sys_vars/r/binlog_large_commit_threshold_basic.result @@ -0,0 +1,30 @@ +SET @start_value = @@GLOBAL.binlog_large_commit_threshold; +# Default value is 128MB. +SELECT @@GLOBAL.binlog_large_commit_threshold = 128 * 1024 * 1024; +@@GLOBAL.binlog_large_commit_threshold = 128 * 1024 * 1024 +1 +# It is a GLOBAL only variable, there is no SESSION scope. +SET @@SESSION.binlog_large_commit_threshold = 1; +ERROR HY000: Variable 'binlog_large_commit_threshold' is a GLOBAL variable and should be set with SET GLOBAL +SELECT @@SESSION.binlog_large_commit_threshold; +ERROR HY000: Variable 'binlog_large_commit_threshold' is a GLOBAL variable +# It is dynamic and accepts new values, 0 disables the feature. +SET @@GLOBAL.binlog_large_commit_threshold = 0; +SELECT @@GLOBAL.binlog_large_commit_threshold; +@@GLOBAL.binlog_large_commit_threshold +0 +SET @@GLOBAL.binlog_large_commit_threshold = 1073741824; +SELECT @@GLOBAL.binlog_large_commit_threshold; +@@GLOBAL.binlog_large_commit_threshold +1073741824 +# Invalid values are rejected or adjusted. +SET @@GLOBAL.binlog_large_commit_threshold = "abc"; +ERROR 42000: Incorrect argument type to variable 'binlog_large_commit_threshold' +SET @@GLOBAL.binlog_large_commit_threshold = -1; +Warnings: +Warning 1292 Truncated incorrect binlog_large_commit_threshold value: '-1' +SELECT @@GLOBAL.binlog_large_commit_threshold; +@@GLOBAL.binlog_large_commit_threshold +0 +# Cleanup +SET @@GLOBAL.binlog_large_commit_threshold = @start_value; diff --git a/mysql-test/suite/sys_vars/t/binlog_large_commit_threshold_basic.test b/mysql-test/suite/sys_vars/t/binlog_large_commit_threshold_basic.test new file mode 100644 index 000000000000..82a324f5218c --- /dev/null +++ b/mysql-test/suite/sys_vars/t/binlog_large_commit_threshold_basic.test @@ -0,0 +1,31 @@ +# ============================================================================ +# Basic test for the binlog_large_commit_threshold system variable. +# Scope: GLOBAL, dynamic, type ulonglong, default 128MB. +# ============================================================================ +--source include/have_log_bin.inc + +SET @start_value = @@GLOBAL.binlog_large_commit_threshold; + +--echo # Default value is 128MB. +SELECT @@GLOBAL.binlog_large_commit_threshold = 128 * 1024 * 1024; + +--echo # It is a GLOBAL only variable, there is no SESSION scope. +--error ER_GLOBAL_VARIABLE +SET @@SESSION.binlog_large_commit_threshold = 1; +--error ER_INCORRECT_GLOBAL_LOCAL_VAR +SELECT @@SESSION.binlog_large_commit_threshold; + +--echo # It is dynamic and accepts new values, 0 disables the feature. +SET @@GLOBAL.binlog_large_commit_threshold = 0; +SELECT @@GLOBAL.binlog_large_commit_threshold; +SET @@GLOBAL.binlog_large_commit_threshold = 1073741824; +SELECT @@GLOBAL.binlog_large_commit_threshold; + +--echo # Invalid values are rejected or adjusted. +--error ER_WRONG_TYPE_FOR_VAR +SET @@GLOBAL.binlog_large_commit_threshold = "abc"; +SET @@GLOBAL.binlog_large_commit_threshold = -1; +SELECT @@GLOBAL.binlog_large_commit_threshold; + +--echo # Cleanup +SET @@GLOBAL.binlog_large_commit_threshold = @start_value; diff --git a/mysql-test/t/all_persisted_variables.test b/mysql-test/t/all_persisted_variables.test index 9cd22d1a0952..b33b0af72930 100644 --- a/mysql-test/t/all_persisted_variables.test +++ b/mysql-test/t/all_persisted_variables.test @@ -58,7 +58,7 @@ let $total_global_vars=`SELECT COUNT(*) AND variable_name NOT LIKE '%pqc%' AND variable_name NOT LIKE '%tls_kex%'`; -let $total_persistent_vars=451; +let $total_persistent_vars=452; --echo *************************************************************** --echo * 0. Verify that variables present in performance_schema.global diff --git a/sql/basic_istream.cc b/sql/basic_istream.cc index 091d52c8fa35..c1aa681aa768 100644 --- a/sql/basic_istream.cc +++ b/sql/basic_istream.cc @@ -81,6 +81,21 @@ bool IO_CACHE_istream::seek(my_off_t offset) { return res; } +bool IO_CACHE_istream::begin(unsigned char **buffer, my_off_t *length) { + return seek(0) || next(buffer, length); +} + +bool IO_CACHE_istream::next(unsigned char **buffer, my_off_t *length) { + my_b_fill(&m_io_cache); + + *buffer = m_io_cache.read_pos; + *length = my_b_bytes_in_cache(&m_io_cache); + + m_io_cache.read_pos = m_io_cache.read_end; + + return m_io_cache.error; +} + Stdin_istream::Stdin_istream() = default; Stdin_istream::~Stdin_istream() { close(); } diff --git a/sql/basic_istream.h b/sql/basic_istream.h index 8860dcb3adaf..c0886a467d35 100644 --- a/sql/basic_istream.h +++ b/sql/basic_istream.h @@ -123,6 +123,31 @@ class IO_CACHE_istream : public Basic_seekable_istream { */ my_off_t length() override; + /** + Initializes cache for reading and returns the data at the begin. + buffer is controlled by cache implementation, so caller should + not release it. If the function sets *length to 0 and no error happens, + it has reached the end of the cache. + + @param[out] buffer It points to buffer where data is read. + @param[out] length Length of the data in the buffer. + @retval false Success + @retval true Error +*/ + bool begin(unsigned char **buffer, my_off_t *length); + + /** + Returns next piece of data. buffer is controlled by cache + implementation, so caller should not release it. If the function sets + *length to 0 and no error happens, it has reached the end of the cache. + + @param[out] buffer It points to buffer where data is read. + @param[out] length Length of the data in the buffer. + @retval false Success + @retval true Error + */ + bool next(unsigned char **buffer, my_off_t *length); + private: IO_CACHE m_io_cache; }; diff --git a/sql/binlog.cc b/sql/binlog.cc index 3fa5865903a7..88528ee42015 100644 --- a/sql/binlog.cc +++ b/sql/binlog.cc @@ -121,8 +121,9 @@ #include "sql/raii/sentry.h" // raii::Sentry<> #include "sql/rpl_filter.h" #include "sql/rpl_gtid.h" -#include "sql/rpl_handler.h" // RUN_HOOK -#include "sql/rpl_mi.h" // Master_info +#include "sql/rpl_handler.h" // RUN_HOOK +#include "sql/rpl_log_encryption.h" // rpl_encryption +#include "sql/rpl_mi.h" // Master_info #include "sql/rpl_record.h" #include "sql/rpl_replica.h" #include "sql/rpl_replica_commit_order_manager.h" // Commit_order_manager @@ -153,6 +154,159 @@ class Item; +/** + This class implementes the feature to rename a binlog cache temporary file to + a binlog file. It is used to avoid holding LOCK_log long time when writting a + huge binlog cache to binlog file. + + With this feature, temporary files of binlog caches will be created in + BINLOG_CACHE_DIR which is created in the same directory to binlog files + at server startup. +*/ +class Binlog_commit_by_rotate { + public: + Binlog_commit_by_rotate() = default; + + /** + Check whether rename to binlog should be executed on the cache_data. + + @param[in] thd THD of the committing transaction.a + + @retval true It should do rename. + @retval false It should do normal commit. + */ + bool should_commit_by_rotate(THD *thd); + + /** + This function is the entry function to rename a binlog cache to a binary log + file. It first rotates the binary log, then renames the temporary file of + the binlog cache to the new binary log file, after that it commits the + transaction. + + @param[in] thd THD of the committing transaction. + + @retval true The transaction has been handled here (either committed by + rename, or an error was raised into thd->commit_error). + @retval false Rename could not be done, the caller should fall back to the + normal commit path. + */ + bool commit(THD *thd); + + /** + During the binlog rotation triggered by commit(), after creating the + binary log file and writing the events that describe its state (e.g. Format + description event), copy them into the binlog cache file (into its reserved + space), fill the remaining reserved space with an Empty_log_event, write the + GTID event and then rename the binlog cache file to the new binary log file. + + @retval false The binary log file was replaced successfully. + @retval true An error occurred while replacing the binary log file. + */ + bool replace_binlog_file(THD *thd); + + /** + The space required for the session binlog caches to reserve. It is + calculated from the length of the current binary log file when it is + generated and aligned to IO_SIZE. + + @param[in] header_len Length of the header of the current binary log file + (magic + Format description + Previous_gtids events). + */ + void set_reserved_bytes(uint32 header_len); + + /** + Return reserved space required for the binlog cache. It is NOT defined as an + atomic variable, while it is get and set in parallel. Synchronizing between + set and get is not really necessary, m_reserved_bytes does not get updated + often. A reader may read an old value, but it just affects the current + transaction. The next transaction will get the fresh value. And reserving + space is a transaction level action, so there always are some transactions + reserving space with the old value. + */ + uint32 get_reserved_size() const { return m_reserved_bytes; } + + /** + Whether the current thread is committing by renaming its binlog cache to a + binary log file. It is used with LOCK_log acquired by the open/rotate path + to decide whether to reuse the binlog cache temporary file as the new binary + log file. + */ + bool is_committing_by_rotate() const { return m_committing_by_rotate; } + + /** The name of the binlog cache temporary file that is being renamed. */ + const char *get_tmp_file_name() const { return m_tmp_file_name; } + + /** + Whether the last rename (replace_binlog_file) succeeded. It is used with + LOCK_log acquired: commit() reads it after the rotation to decide whether + the cache was renamed or whether to fall back to the normal commit. + */ + bool replaced() const { return m_replaced; } + + private: + /* Singleton object, disable the copy constructor and assignment. */ + Binlog_commit_by_rotate &operator=(const Binlog_commit_by_rotate &) = delete; + Binlog_commit_by_rotate(const Binlog_commit_by_rotate &) = delete; + + /** + Calculate the size of the GTID event that will be written for the + transaction. It uses the same logic as write_transaction() so that the + reserved space is filled exactly. + + @param[in] thd THD of the committing transaction. + + @return The GTID event size. + */ + my_off_t get_gtid_event_length(THD *thd); + + /** + The session cache that is being renamed to a binary log file: true for the + statement cache, false for the transaction cache. It is decided by + should_commit_by_rotate() and used with LOCK_log acquired. + */ + bool m_use_stmt_cache{false}; + + /** The name of the binlog cache temporary file being renamed. */ + char m_tmp_file_name[FN_REFLEN]{0}; + + /** + Whether the current thread is committing by renaming its binlog cache. It is + used with LOCK_log acquired. + */ + bool m_committing_by_rotate{false}; + + /** + Whether replace_binlog_file() succeeded to rename the cache to the new + binary log file. It is set inside the rotation (from open_binlog) and read + back by commit(). Used with LOCK_log acquired. + */ + bool m_replaced{false}; + + /** + The header size of the new binary log file (magic + Format description + + Previous_gtids events). It is captured by replace_binlog_file() and used to + size the Empty event that fills the reserved space. + */ + my_off_t m_header_size{0}; + + /** + The reserved space at the beginning of the renamed cache file and the end + position of the transaction data in it. Captured by commit() before the + rotation and used by replace_binlog_file(). Used with LOCK_log acquired. + */ + my_off_t m_reserved_size{0}; + my_off_t m_file_end_pos{0}; + + /** + Reserved space required for a binlog cache. See get_reserved_size(). It is + initialized to IO_SIZE and updated after every rotation by + set_reserved_bytes(). + */ + uint32 m_reserved_bytes{IO_SIZE}; +}; + +static Binlog_commit_by_rotate binlog_commit_by_rotate; + using mysql::binlog::event::enum_binlog_checksum_alg; using std::list; using std::max; @@ -977,6 +1131,411 @@ static binlog_cache_mngr *thd_get_cache_mngr(const THD *thd) { return (binlog_cache_mngr *)thd_get_ha_data(thd, binlog_hton); } +/* + The prefix of the binlog cache temporary files. It matches the prefix used by + Binlog_cache_storage::open() ("ML"). Only files with this prefix are deleted + when cleaning the #binlog_cache_files directory at startup. +*/ +static const char *BINLOG_CACHE_FILE_PREFIX = "ML"; + +/* Name of the directory that holds the binlog cache temporary files. */ +static const char *BINLOG_CACHE_DIR = "#binlog_cache_files"; + +char binlog_cache_dir[FN_REFLEN]; +ulonglong opt_binlog_large_commit_threshold = 128 * 1024 * 1024; + +bool binlog_commit_by_rotate_enabled() { + return opt_binlog_large_commit_threshold > 0; +} + +uint32 binlog_cache_reserved_size() { + return binlog_commit_by_rotate.get_reserved_size(); +} + +bool init_binlog_cache_dir() { + size_t length; + const uint max_tmp_file_name_len = + strlen(BINLOG_CACHE_FILE_PREFIX) + 1 /* underline */ + + 20 /* max len of the cache object address */; + + dirname_part(binlog_cache_dir, log_bin_basename, &length); + /* + Must ensure the full name of the temporary file is shorter than FN_REFLEN, + to avoid overflowing the name buffer in write and commit. + */ + if (length + strlen(BINLOG_CACHE_DIR) + max_tmp_file_name_len >= FN_REFLEN) { + LogErr(ERROR_LEVEL, ER_LOG_PRINTF_MSG, + "The binary log cache directory path is too long."); + return true; + } + + memcpy(binlog_cache_dir + length, BINLOG_CACHE_DIR, strlen(BINLOG_CACHE_DIR)); + binlog_cache_dir[length + strlen(BINLOG_CACHE_DIR)] = 0; + + MY_DIR *dir_info = my_dir(binlog_cache_dir, MYF(0)); + + if (!dir_info) { + /* Make a dir for binlog cache temp files if it does not exist. */ + if (my_mkdir(binlog_cache_dir, 0777, MYF(0)) < 0) { + LogErr(ERROR_LEVEL, ER_LOG_PRINTF_MSG, + "Failed to create the binary log cache directory."); + return true; + } + return false; + } + + /* Try to delete all cache files in the directory. */ + for (uint i = 0; i < dir_info->number_off_files; i++) { + FILEINFO *file = dir_info->dir_entry + i; + + /* Skip the names "." and "..". */ + if (!strcmp(file->name, ".") || !strcmp(file->name, "..")) continue; + + if (strncmp(file->name, BINLOG_CACHE_FILE_PREFIX, + strlen(BINLOG_CACHE_FILE_PREFIX))) { + char msg[FN_REFLEN + 128]; + snprintf(msg, sizeof(msg), + "%s is in %s/, but it is not a binlog cache file", file->name, + BINLOG_CACHE_DIR); + LogErr(WARNING_LEVEL, ER_LOG_PRINTF_MSG, msg); + continue; + } + + char file_path[FN_REFLEN]; + fn_format(file_path, file->name, binlog_cache_dir, "", MYF(MY_REPLACE_DIR)); + my_delete(file_path, MYF(0)); + } + + my_dirend(dir_info); + return false; +} + +void Binlog_commit_by_rotate::set_reserved_bytes(uint32 header_len) { + /* Add reserved space for the GTID event and the minimum Empty event. */ + header_len += LOG_EVENT_HEADER_LEN /* Empty event header */ + + mysql::binlog::event::Gtid_event::get_max_event_length() + + BINLOG_CHECKSUM_LEN * 2; + + /* The reserved size is aligned to IO_SIZE. */ + header_len = (header_len + (IO_SIZE - 1)) & ~(IO_SIZE - 1); + if (header_len != m_reserved_bytes) m_reserved_bytes = header_len; +} + +my_off_t Binlog_commit_by_rotate::get_gtid_event_length(THD *thd) { + binlog_cache_data *cache_data = + thd_get_cache_mngr(thd)->get_binlog_cache_data(!m_use_stmt_cache); + + Gtid_specification gtid_spec; + gtid_spec.type = ANONYMOUS_GTID; + gtid_spec.gtid = {0, 0}; + ulonglong immediate_commit_timestamp = my_micro_time(); + + Gtid_log_event ev( + 0 /* server_id */, cache_data->is_trx_cache(), 0 /* last_committed */, + 1 /* sequence_number */, cache_data->may_have_sbr_stmts(), + thd->variables.original_commit_timestamp, immediate_commit_timestamp, + gtid_spec, thd->variables.original_server_version, + do_server_version_int(::server_version)); + + if (ev.original_commit_timestamp == UNDEFINED_COMMIT_TIMESTAMP) { + if (thd->slave_thread || thd->is_binlog_applier()) + ev.original_commit_timestamp = 0; + else + ev.original_commit_timestamp = immediate_commit_timestamp; + } + if (ev.original_server_version == UNDEFINED_SERVER_VERSION) { + if (thd->slave_thread || thd->is_binlog_applier()) + ev.original_server_version = UNKNOWN_SERVER_VERSION; + else + ev.original_server_version = do_server_version_int(::server_version); + } + + ev.set_trx_length_by_cache_size( + cache_data->get_byte_position(), + binlog_checksum_options != mysql::binlog::event::BINLOG_CHECKSUM_ALG_OFF, + cache_data->get_event_counter()); + + return ev.get_event_length(); +} + +bool Binlog_commit_by_rotate::should_commit_by_rotate(THD *thd) { + if (!binlog_commit_by_rotate_enabled()) return false; + + Binlog_cache_storage *trx_cache = + thd_get_cache_mngr(thd)->get_binlog_cache_data(true)->get_cache(); + Binlog_cache_storage *stmt_cache = + thd_get_cache_mngr(thd)->get_binlog_cache_data(false)->get_cache(); + + /* + Only rename a cache to a binlog file if its temporary file is larger than + binlog_large_commit_threshold. + */ + if (DBUG_EVALUATE_IF("commit_by_rotate_skip_threshold_check", 0, 1) && + likely(trx_cache->length() <= opt_binlog_large_commit_threshold && + stmt_cache->length() <= opt_binlog_large_commit_threshold)) + return false; + + /* + Pick the cache to rename. Prefer the statement cache if it holds data, + otherwise the transaction cache. + */ + m_use_stmt_cache = !stmt_cache->is_empty(); + Binlog_cache_storage *cache = m_use_stmt_cache ? stmt_cache : trx_cache; + + /* + Do not rename if the temporary file was not written (the data fit in the + memory buffer) or if no space was reserved at the beginning of the file. + */ + if (cache->file_reserved_bytes() == 0 || cache->disk_writes() == 0) + return false; + + /* + - Do not rename if binlog encryption is enabled: the binlog cache temporary + file uses a different file password than the binary log file. + - It is not supported to rename both the statement cache and the + transaction cache to binary log files at the same time. + */ + if (rpl_encryption.is_enabled() || cache->is_encryption_enabled() || + (!stmt_cache->is_empty() && !trx_cache->is_empty())) + return false; + + if (unlikely(thd_get_cache_mngr(thd)->has_incident())) return false; + if (unlikely(!mysql_bin_log.is_open())) return false; + + return true; +} + +bool Binlog_commit_by_rotate::commit(THD *thd) { + DBUG_TRACE; + /* The cache being renamed was decided by should_commit_by_rotate(). */ + Binlog_cache_storage *cache = thd_get_cache_mngr(thd) + ->get_binlog_cache_data(!m_use_stmt_cache) + ->get_cache(); + bool check_purge = false; + bool flush_error = false; + + m_file_end_pos = cache->get_file_end_pos(); + m_reserved_size = cache->file_reserved_bytes(); + + /* + Sync the binlog cache temporary file before entering LOCK_log, to reduce the + time of holding LOCK_log. If it fails, fall back to the normal commit. + */ + if (cache->sync_temp_file()) return false; + + /* Save the temporary file name, replace_binlog_file() renames it. */ + strncpy(m_tmp_file_name, cache->tmp_file_name(), FN_REFLEN - 1); + m_tmp_file_name[FN_REFLEN - 1] = '\0'; + + mysql_mutex_lock(&mysql_bin_log.LOCK_log); + + m_committing_by_rotate = true; + m_replaced = false; + + /* + Rotate. Because m_committing_by_rotate is true, the rotation creates a new + binary log file normally and, right after writing its header events, calls + replace_binlog_file(), which copies the header into the temporary file, + renames the temporary file to the new binary log file, and writes the Empty + and GTID events into the reserved space. + */ + if ((flush_error = mysql_bin_log.rotate(true, &check_purge))) { + thd->commit_error = THD::CE_FLUSH_ERROR; + goto err; + } + + if (!m_replaced) { + /* + The reserved space was not enough to rename the cache. The rotation + created a fresh binary log file; fall back to the normal commit path, + which flushes the transaction into it. + */ + m_committing_by_rotate = false; + mysql_mutex_unlock(&mysql_bin_log.LOCK_log); + if (check_purge) mysql_bin_log.auto_purge(); + return false; + } + + DBUG_EXECUTE_IF("binlog_commit_by_rotate_crash_after_rotate", + DBUG_SUICIDE();); + + /* + The transaction is fully written into the renamed binary log file. Update + the end position and run the after_flush/after_sync replication hooks. + */ + mysql_bin_log.update_binlog_end_pos(); + mysql_bin_log.update_thd_next_event_pos(thd); + thd->set_trans_pos(mysql_bin_log.log_file_name, m_file_end_pos); + + m_committing_by_rotate = false; + + { + const char *file_name_ptr = mysql_bin_log.log_file_name + + dirname_length(mysql_bin_log.log_file_name); + if (RUN_HOOK(binlog_storage, after_flush, + (thd, file_name_ptr, m_file_end_pos))) { + LogErr(ERROR_LEVEL, ER_BINLOG_FAILED_TO_RUN_AFTER_FLUSH_HOOK); + flush_error = true; + goto err; + } + if (RUN_HOOK(binlog_storage, after_sync, + (thd, file_name_ptr, m_file_end_pos))) { + flush_error = true; + goto err; + } + } + + /* Finish the commit for the transaction and its followers. */ + (void)mysql_bin_log.finish_commit(thd); + + /* + Rotate to the next binary log file, so the renamed file holds only this + transaction. This rotation restores binlog_checksum_options from + checksum_alg_reset (see new_file_impl()); clear it afterwards so the next + rotation does not restore again. + */ + if (mysql_bin_log.rotate(true, &check_purge)) { + thd->commit_error = THD::CE_FLUSH_ERROR; + goto err; + } + mysql_bin_log.checksum_alg_reset = + mysql::binlog::event::BINLOG_CHECKSUM_ALG_UNDEF; + +err: + if (flush_error) + mysql_bin_log.handle_binlog_flush_or_sync_error( + thd, false /* need_lock_log */, nullptr); + + m_committing_by_rotate = false; + mysql_mutex_unlock(&mysql_bin_log.LOCK_log); + + if (check_purge) mysql_bin_log.auto_purge(); + + return true; +} + +bool Binlog_commit_by_rotate::replace_binlog_file(THD *thd) { + mysql_mutex_assert_owner(mysql_bin_log.get_log_lock()); + + MYSQL_BIN_LOG::Binlog_ofile *binlog_file = mysql_bin_log.get_binlog_file(); + + /* + The rotation just created a new binary log file (log_file_name) and wrote + its header events (magic, Format description, Previous_gtids) into it. Its + current size is the header size. + */ + m_header_size = binlog_file->position(); + + const my_off_t gtid_len = get_gtid_event_length(thd); + + /* + Required space for the header events plus the GTID event and the minimum + Empty event. If the reserved space is not enough (e.g. the Previous_gtids + event grew since the last rotation), do not rename: leave the freshly + created binary log file as the active one and let commit() fall back to the + normal commit path. + */ + const my_off_t required_size = + m_header_size + gtid_len + LOG_EVENT_HEADER_LEN /* minimum Empty event */; + + if (DBUG_EVALUATE_IF("simulate_reserve_size_not_enough", 1, 0) || + required_size > m_reserved_size) { + LogErr(INFORMATION_LEVEL, ER_LOG_PRINTF_MSG, + "The binary log cache file cannot be renamed to a binary log " + "because its reserved space is too small for the header events. " + "Falling back to the normal commit path."); + return false; // m_replaced stays false + } + + /* + Copy the header events from the new binary log file to the beginning of the + temporary file (into its reserved space). + */ + { + IO_CACHE_ostream dst; + IO_CACHE_istream src; + + if (dst.open(mysql_bin_log.m_log_file_key, m_tmp_file_name, MYF(MY_WME)) || + src.open(mysql_bin_log.m_log_file_key, key_file_binlog_cache, + mysql_bin_log.log_file_name, MYF(MY_WME)) || + stream_copy(&src, &dst) || dst.flush()) { + LogErr(ERROR_LEVEL, ER_LOG_PRINTF_MSG, + "Failed to copy the binary log header to the binary log cache " + "temporary file during commit-by-rotate."); + return false; + } + } + + binlog_file->close(); + my_delete(mysql_bin_log.log_file_name, MYF(MY_WME)); + /* Any error happens after the file is deleted should return true. */ + + DBUG_EXECUTE_IF("binlog_commit_by_rotate_crash_before_rename", + DBUG_SUICIDE();); + + if (DBUG_EVALUATE_IF("simulate_rename_binlog_cache_to_binlog_error", 1, 0) || + my_rename(m_tmp_file_name, mysql_bin_log.log_file_name, MYF(MY_WME))) { + LogErr(ERROR_LEVEL, ER_LOG_PRINTF_MSG, + "Failed to rename the binary log cache temporary file to a binary " + "log during commit-by-rotate."); + mysql_bin_log.atomic_log_state = MYSQL_BIN_LOG::LOG_CLOSED; + return true; + } + + DBUG_EXECUTE_IF("binlog_commit_by_rotate_crash_after_rename", + DBUG_SUICIDE();); + + /* + Reopen the renamed file as the binary log file and position it right after + the header, so that the Empty and GTID events can be written into the + reserved space. + */ + const myf flags = MY_WME | MY_NABP | MY_WAIT_IF_FULL; + if (binlog_file->open(mysql_bin_log.m_log_file_key, + mysql_bin_log.log_file_name, flags, + true /* existing */) || + binlog_file->seek_to(m_header_size)) { + LogErr(ERROR_LEVEL, ER_LOG_PRINTF_MSG, + "Failed to reopen the renamed binary log file during " + "commit-by-rotate."); + return true; + } + + binlog_cache_data *cache_data = + thd_get_cache_mngr(thd)->get_binlog_cache_data(!m_use_stmt_cache); + cache_data->get_cache()->detach_temp_file(); + + (void)mysql_bin_log.assign_automatic_gtids_to_flush_group(thd); + + Empty_log_event empty_event(thd, m_reserved_size - m_header_size - gtid_len); + my_off_t bytes = 0; + bool wrote_xid = false; + if (mysql_bin_log.write_event_to_binlog(&empty_event) || + cache_data->flush(thd, &bytes, &wrote_xid, false)) { + LogErr( + ERROR_LEVEL, ER_LOG_PRINTF_MSG, + "Failed to write the empty and GTID events during commit-by-rotate."); + return true; + } + + if (binlog_file->position() != m_reserved_size) { + LogErr( + ERROR_LEVEL, ER_LOG_PRINTF_MSG, + "Failed to fill the reserved space exactly during commit-by-rotate."); + return true; + } + + if (mysql_bin_log.flush_and_sync(true /* force */) || + binlog_file->seek_to(m_file_end_pos)) { + LogErr(ERROR_LEVEL, ER_LOG_PRINTF_MSG, + "Failed to flush and sync the binary log during commit-by-rotate."); + return true; + } + + m_replaced = true; + return false; +} + /** Checks if the BINLOG_CACHE_SIZE's value is greater than MAX_BINLOG_CACHE_SIZE. If this happens, the BINLOG_CACHE_SIZE is set to MAX_BINLOG_CACHE_SIZE. @@ -1231,6 +1790,17 @@ int binlog_cache_data::write_event(Log_event *ev) { DBUG_EXECUTE_IF("simulate_disk_full_at_flush_pending", { DBUG_SET("+d,simulate_file_write_error"); }); + /* + When space is reserved at the beginning of the cache temporary file (so + that it can be renamed to a binary log file, see + Binlog_commit_by_rotate), the binlog data does not start at offset 0 of + the file. The end_log_pos of the events must account for the reserved + space, so fill it with the actual end position of the temporary file, + including the reserved space. + */ + if (m_cache.get_file_reserved_size() > 0) + ev->common_header->log_pos = m_cache.get_file_end_pos(); + if (binary_event_serialize(ev, &m_cache)) { DBUG_EXECUTE_IF("simulate_disk_full_at_flush_pending", { DBUG_SET("-d,simulate_file_write_error"); @@ -1476,6 +2046,13 @@ bool MYSQL_BIN_LOG::write_transaction(THD *thd, binlog_cache_data *cache_data, gtid_event.write(writer)); if (ret) goto end; + /* + During commit-by-rotate the transaction data is already in the binary log + file (the renamed binlog cache temporary file), so only the GTID event needs + to be written above. Skip copying the cache data here. + */ + if (unlikely(binlog_commit_by_rotate.is_committing_by_rotate())) goto end; + /* finally write the transaction data, if it was not compressed and written as part of the gtid event already @@ -4543,6 +5120,24 @@ bool MYSQL_BIN_LOG::open_binlog( bool write_file_name_to_index_file = false; + /* + Commit by rotate: a MySQL binlog cache stores its events without a checksum + (the checksum is added by Binlog_event_writer only while copying the cache + to the binary log). So the renamed cache file, and therefore the whole new + binary log file, must use a checksum-off format regardless of the global + binlog_checksum. Disable the checksum here so the Format description and the + other header events of this file are written without a checksum. The next + rotation restores binlog_checksum_options from checksum_alg_reset (see + new_file_impl()). + */ + if (!is_relay_log && binlog_commit_by_rotate.is_committing_by_rotate() && + binlog_checksum_options != + mysql::binlog::event::BINLOG_CHECKSUM_ALG_OFF) { + checksum_alg_reset = + static_cast(binlog_checksum_options); + binlog_checksum_options = mysql::binlog::event::BINLOG_CHECKSUM_ALG_OFF; + } + Format_description_log_event s; if (m_binlog_file->is_empty()) { @@ -4697,6 +5292,25 @@ bool MYSQL_BIN_LOG::open_binlog( } if (m_binlog_file->flush_and_sync()) goto err; + if (!is_relay_log) { + /* + Update the space to reserve at the beginning of the session binlog caches + now that the header of a fresh binary log file is known. The next + transactions that use commit-by-rotate will reserve this much space. + */ + binlog_commit_by_rotate.set_reserved_bytes(m_binlog_file->position()); + + /* + Commit by rotate: the header events of the new binary log file have just + been written and synced. Copy them into the committing transaction's + binlog cache temporary file and rename that file to this new binary log + file. See Binlog_commit_by_rotate::replace_binlog_file(). + */ + if (binlog_commit_by_rotate.is_committing_by_rotate() && + binlog_commit_by_rotate.replace_binlog_file(current_thd)) + goto err; + } + if (write_file_name_to_index_file) { DBUG_EXECUTE_IF("crash_create_critical_before_update_index", DBUG_SUICIDE();); @@ -4732,7 +5346,15 @@ bool MYSQL_BIN_LOG::open_binlog( m_binlog_index_monitor.close_purge_index_file(); - update_binlog_end_pos(); + /* + During commit-by-rotate, the binary log file has been reopened on the + renamed temporary file and is positioned after the header. Skip updating the + end position here; Binlog_commit_by_rotate::commit() sets it after writing + the GTID event and seeking to the end of the transaction data. + */ + if (!(!is_relay_log && binlog_commit_by_rotate.is_committing_by_rotate() && + binlog_commit_by_rotate.replaced())) + update_binlog_end_pos(); return false; err: @@ -5429,7 +6051,7 @@ int MYSQL_BIN_LOG::new_file_impl( goto end; } - if (!is_relay_log) { + if (!is_relay_log && !binlog_commit_by_rotate.is_committing_by_rotate()) { /* Save set of GTIDs of the last binlog into table on binlog rotation */ if ((error = gtid_state->save_gtids_of_last_binlog_into_table())) { if (error == ER_RPL_GTID_TABLE_CANNOT_OPEN) { @@ -7554,6 +8176,21 @@ int MYSQL_BIN_LOG::ordered_commit(THD *thd, bool all, bool skip_commit) { } } + /* + For a very large transaction, rename its binlog cache temporary file to a + new binary log file instead of copying its data into the active binary log + while holding LOCK_log. This avoids stalling other transactions for a long + time. See Binlog_commit_by_rotate. + */ + if (unlikely(binlog_commit_by_rotate.should_commit_by_rotate(thd))) { + Commit_stage_manager::get_instance().wait_for_ticket_turn( + thd, true /* update_ticket_manager */); + + if (binlog_commit_by_rotate.commit(thd)) + return thd->commit_error == THD::CE_COMMIT_ERROR; + /* Rename could not be done, fall through to the normal commit path. */ + } + /* Stage #1: flushing transactions to binary log diff --git a/sql/binlog.h b/sql/binlog.h index 37885fa582fd..3e70b561ae28 100644 --- a/sql/binlog.h +++ b/sql/binlog.h @@ -68,6 +68,15 @@ class THD; class Transaction_boundary_parser; class binlog_cache_data; class user_var_entry; + +/** Initialize the directory used by large binlog cache files. */ +bool init_binlog_cache_dir(); +/** Whether commit-by-rotate is enabled. */ +bool binlog_commit_by_rotate_enabled(); +/** Reserved prefix size for a binlog cache temporary file. */ +uint32 binlog_cache_reserved_size(); +extern char binlog_cache_dir[FN_REFLEN]; +extern ulonglong opt_binlog_large_commit_threshold; class Binlog_cache_storage; struct Gtid; @@ -106,6 +115,13 @@ struct Binlog_user_var_event { (mmap+fsync is two times faster than write+fsync) */ class MYSQL_BIN_LOG : public TC_LOG { + /* + Binlog_commit_by_rotate renames a transaction's binlog cache temporary file + to a binary log file. It needs access to the internals of the binary log + (LOCK_log, the index file, the current file name, etc.) to do so. + */ + friend class Binlog_commit_by_rotate; + public: class Binlog_ofile; diff --git a/sql/binlog/binlog_ofile.h b/sql/binlog/binlog_ofile.h index f0680bb19970..b4c0a0ecc468 100644 --- a/sql/binlog/binlog_ofile.h +++ b/sql/binlog/binlog_ofile.h @@ -116,6 +116,24 @@ class MYSQL_BIN_LOG::Binlog_ofile : public Basic_ostream { [[nodiscard]] virtual bool is_empty(); [[nodiscard]] virtual bool is_open(); + /** + Position the stream at @p offset so that the next write happens there, + without truncating the file (unlike truncate()). It is used by + Binlog_commit_by_rotate to write the GTID event into the space reserved at + the beginning of a renamed binlog cache temporary file, and then to move to + the end of the transaction data. + + @param[in] offset Absolute position to seek to. + @retval false Success + @retval true Error + */ + [[nodiscard]] bool seek_to(my_off_t offset) { + assert(m_pipeline_head != nullptr); + if (m_pipeline_head->seek(offset)) return true; + m_position = offset; + return false; + } + /** Returns the encrypted header size of the binary log file. diff --git a/sql/binlog_ostream.cc b/sql/binlog_ostream.cc index 9c6b4680a93b..7d637feb6a73 100644 --- a/sql/binlog_ostream.cc +++ b/sql/binlog_ostream.cc @@ -34,6 +34,21 @@ #include "sql/rpl_log_encryption.h" #include "sql/sql_class.h" +/* + Globals owned by the Binlog_commit_by_rotate feature (defined in + sql/binlog.cc). They are declared here rather than pulling in the whole header + to keep this low level file free of binlog layer dependencies. + + - binlog_cache_dir The #binlog_cache_files directory where the reserved + (KEEP) temporary files are created. + - binlog_cache_reserved_size() The space (in bytes) to reserve at the begin + of the transactional cache temporary file. + - binlog_commit_by_rotate_enabled() Whether the feature is switched on. +*/ +extern char binlog_cache_dir[FN_REFLEN]; +extern uint32 binlog_cache_reserved_size(); +extern bool binlog_commit_by_rotate_enabled(); + #ifndef NDEBUG bool binlog_cache_is_reset = false; #endif @@ -56,10 +71,38 @@ bool IO_CACHE_binlog_cache_storage::open(const char *dir, const char *prefix, return false; } -void IO_CACHE_binlog_cache_storage::close() { close_cached_file(&m_io_cache); } +void IO_CACHE_binlog_cache_storage::close() { + /* + The binlog cache temporary file is a normal (KEEP) file, so it must be + unlinked explicitly here. It is not unlinked when it has been detached + (renamed to a binary log file), in which case m_io_cache.file is -1. + */ + if (m_io_cache.file != -1) unlink(tmp_file_name()); + + close_cached_file(&m_io_cache); +} bool IO_CACHE_binlog_cache_storage::write(const unsigned char *buffer, my_off_t length) { + /* + The binlog cache always uses a normal (KEEP) file in the #binlog_cache_files + directory as its temporary file, so that it can be renamed to a binary log + file when space is reserved (see Binlog_commit_by_rotate). Create it here, + before the IO_CACHE would create its own unlinked file on buffer overflow. + */ + if (m_io_cache.file == -1 && + m_io_cache.write_pos + length > m_io_cache.write_end) { + char name_buff[FN_REFLEN]; + generate_tmp_file_name(name_buff); + if ((m_io_cache.file = mysql_file_open(m_io_cache.file_key, name_buff, + O_CREAT | O_RDWR, MYF(MY_WME))) < + 0) { + LogErr(ERROR_LEVEL, ER_LOG_PRINTF_MSG, + "Failed to open a binlog cache temporary file."); + return true; + } + } + /* Enable/disable binlog cache temporary file encryption according to the setting of global binlog_encryption if both binlog cache temporary @@ -91,6 +134,12 @@ bool IO_CACHE_binlog_cache_storage::write(const unsigned char *buffer, } bool IO_CACHE_binlog_cache_storage::truncate(my_off_t offset) { + /* + Skip the reserved space at the beginning of the temporary file. It is hidden + from callers, so truncate(0) truncates the file to m_file_reserved_bytes, + not to 0. + */ + offset += m_file_reserved_bytes; /* It is not really necessary to flush the data will be truncated into temporary file before truncating . And it may cause write failure. So set @@ -106,6 +155,8 @@ bool IO_CACHE_binlog_cache_storage::truncate(my_off_t offset) { } bool IO_CACHE_binlog_cache_storage::reset() { + /* m_file_reserved_bytes must be reset to 0 before truncate. */ + m_file_reserved_bytes = 0; if (truncate(0)) return true; /* Truncate the temporary file if there is one. */ @@ -166,7 +217,13 @@ bool IO_CACHE_binlog_cache_storage::begin(unsigned char **buffer, m_io_cache.m_decryptor == nullptr); };); - if (reinit_io_cache(&m_io_cache, READ_CACHE, 0, false, false)) { + /* + Start reading after the reserved space at the beginning of the temporary + file. m_file_reserved_bytes is 0 unless the file reserves space for being + renamed to a binary log file (see Binlog_commit_by_rotate). + */ + if (reinit_io_cache(&m_io_cache, READ_CACHE, m_file_reserved_bytes, false, + false)) { DBUG_EXECUTE_IF("simulate_tmpdir_partition_full", { DBUG_SET("-d,simulate_file_write_error"); }); @@ -193,11 +250,98 @@ bool IO_CACHE_binlog_cache_storage::next(unsigned char **buffer, return m_io_cache.error; } -my_off_t IO_CACHE_binlog_cache_storage::length() const { +my_off_t IO_CACHE_binlog_cache_storage::raw_length() const { if (m_io_cache.type == WRITE_CACHE) return my_b_tell(&m_io_cache); return m_io_cache.end_of_file; } +my_off_t IO_CACHE_binlog_cache_storage::length() const { + /* + Hide the reserved space at the beginning of the temporary file. So length() + still returns the length of the binlog data written into the cache, not the + file length. m_file_reserved_bytes is 0 unless space is reserved. + */ + return raw_length() - m_file_reserved_bytes; +} + +my_off_t IO_CACHE_binlog_cache_storage::get_file_end_pos() const { + return raw_length(); +} + +void IO_CACHE_binlog_cache_storage::generate_tmp_file_name(char *name) { + /* + The temporary file is named with the cache prefix and the memory address of + the IO_CACHE which guarantees it is unique. The file is created in the + #binlog_cache_files directory, next to the binary log files, so that it can + be renamed to a binary log file at commit time. init_binlog_cache_dir() + guarantees the full name fits in FN_REFLEN; the return value is checked so + the compiler does not warn about a (here impossible) truncation. + */ + if (snprintf(name, FN_REFLEN, "%s/%s_%llu", binlog_cache_dir, + m_io_cache.prefix, (ulonglong)&m_io_cache) >= FN_REFLEN) + name[FN_REFLEN - 1] = '\0'; +} + +void IO_CACHE_binlog_cache_storage::init_file_reserved_bytes() { + /* + Space is reserved only while the binlog_large_commit_threshold feature is on + and the cache is not encrypted (the reserved file becomes a binary log file, + which uses a different encryption key than the cache temporary file). + */ + const bool should_enable = + binlog_commit_by_rotate_enabled() && !is_encryption_enabled(); + + /* + binlog_cache_reserved_size() is already aligned to IO_SIZE (see + Binlog_commit_by_rotate::set_reserved_bytes()), which keeps the reserved + region from reducing the cache buffer in reinit_io_cache(). + */ + my_off_t reserved = should_enable ? binlog_cache_reserved_size() : 0; + + DBUG_EXECUTE_IF("simulate_small_binlog_cache_reserved_space", + reserved = 100;); + + m_file_reserved_bytes = reserved; + + /* + Seek past the reserved space at the beginning of the temporary file. This + sets pos_in_file to m_file_reserved_bytes and seek_not_done to true. The + file is created when the buffer is full, and is sought to pos_in_file before + writing into it. + */ + if (reserved != 0) { + reinit_io_cache(&m_io_cache, WRITE_CACHE, reserved, false, true); + m_io_cache.end_of_file = m_max_cache_size; + } +} + +my_off_t IO_CACHE_binlog_cache_storage::get_file_reserved_size() { + /* Reserve space on the first write, while nothing is written yet. */ + if (raw_length() == 0) init_file_reserved_bytes(); + return m_file_reserved_bytes; +} + +void IO_CACHE_binlog_cache_storage::detach_temp_file() { + /* + If a rollback to savepoint happened before, the real length of the + temporary file can be greater than the binlog data end position. Truncate + the file to its end position so it becomes a valid binary log file. + */ + my_chsize(m_io_cache.file, get_file_end_pos(), 0, MYF(MY_WME)); + + mysql_file_close(m_io_cache.file, MYF(0)); + /* Reset the fd so that the cache no longer owns the (now binary log) file. */ + m_io_cache.file = -1; +} + +bool IO_CACHE_binlog_cache_storage::sync_temp_file() { + assert(m_io_cache.file != -1); + + if (my_b_flush_io_cache(&m_io_cache, 1)) return true; + if (mysql_file_sync(m_io_cache.file, MYF(MY_WME))) return true; + return false; +} + bool IO_CACHE_binlog_cache_storage::enable_encryption() { /* Return earlier if already enabled */ if (m_io_cache.m_encryptor != nullptr && m_io_cache.m_decryptor != nullptr) diff --git a/sql/binlog_ostream.h b/sql/binlog_ostream.h index 41dba074fd3a..e3ffb0648043 100644 --- a/sql/binlog_ostream.h +++ b/sql/binlog_ostream.h @@ -112,7 +112,8 @@ class IO_CACHE_binlog_cache_storage : public Truncatable_ostream { size_t disk_writes() const; /** - Initializes binlog cache for reading and returns the data at the begin. + Initializes the binlog cache for reading and returns the data at the + beginning. buffer is controlled by binlog cache implementation, so caller should not release it. If the function sets *length to 0 and no error happens, it has reached the end of the cache. @@ -138,9 +139,94 @@ class IO_CACHE_binlog_cache_storage : public Truncatable_ostream { bool flush() override { return false; } bool sync() override { return false; } + /* + ---------------------------------------------------------------------------- + Support for renaming a binlog cache temporary file to a binary log file + (see Binlog_commit_by_rotate). To rename the temporary file, enough space + must be reserved at the beginning of the file. The space is required for the + Format description, Previous_gtids and GTID events that describe the state + of the binary log the file becomes. The reserved header is hidden from + callers: get_byte_position()/length() still return the length of the binlog + data written to the cache, not the file length. + ---------------------------------------------------------------------------- + */ + + /** + It returns the actual length of the temporary file which includes the + reserved space at the beginning of the file. + */ + my_off_t get_file_end_pos() const; + + /** + Reserved bytes at the beginning of the temporary file. It could be 0 for the + cases in which reserving space is not supported. See + init_file_reserved_bytes(). + */ + my_off_t file_reserved_bytes() const { return m_file_reserved_bytes; } + + /** + It lazily initializes the reserved space of the temporary file on the first + write and returns the reserved bytes. Returns 0 if reserving space is + disabled. + */ + my_off_t get_file_reserved_size(); + + /** + It is called after renaming the temporary file to a binary log file. The + file now is a binary log file, so detach it from the binlog cache. + */ + void detach_temp_file(); + + /** + Flush and sync the data of the temporary file into storage. + + @retval true An error occurred while syncing the file. + @retval false The file was synced successfully. + */ + bool sync_temp_file(); + + /** + Returns true if the temporary file encryption is enabled. + */ + bool is_encryption_enabled() const { + return m_io_cache.m_encryptor != nullptr || + m_io_cache.m_decryptor != nullptr; + } + + IO_CACHE *get_io_cache() { return &m_io_cache; } + my_off_t get_max_cache_size() const { return m_max_cache_size; } + private: IO_CACHE m_io_cache; my_off_t m_max_cache_size = 0; + + /** + Stores the bytes reserved at the beginning of the temporary file. It is 0 + for the cases in which reserving space is not supported (encryption enabled, + feature disabled). It is cleared by reset(). + */ + my_off_t m_file_reserved_bytes = 0; + + /** + The raw length of the temporary file, which includes the reserved space. + */ + my_off_t raw_length() const; + + /** + Reserve the required space at the beginning of the temporary file. It + creates the temporary file if it does not exist yet. It is called by + get_file_reserved_size() the first time anything is written into the cache. + */ + void init_file_reserved_bytes(); + + /** + Generate a unique name for the (KEEP) temporary file. The file is created in + the #binlog_cache_files directory next to the binary log files, so that it + can be renamed to a binary log file at commit time. + + @param[out] name Buffer of at least FN_REFLEN bytes to hold the name. + */ + void generate_tmp_file_name(char *name); /** Enable IO Cache temporary file encryption. @@ -233,6 +319,36 @@ class Binlog_cache_storage : public Basic_ostream { */ bool is_empty() const { return length() == 0; } + /* + Accessors used to rename the transactional cache temporary file to a binary + log file (see Binlog_commit_by_rotate). + */ + + /** It returns the reserved bytes at the beginning of the temporary file. */ + my_off_t file_reserved_bytes() const { return m_file.file_reserved_bytes(); } + + /** + It lazily initializes and returns the reserved space of the temporary file. + */ + my_off_t get_file_reserved_size() { return m_file.get_file_reserved_size(); } + + /** + It returns the actual length of the temporary file which includes the + reserved space. + */ + my_off_t get_file_end_pos() const { return m_file.get_file_end_pos(); } + + /** Detach the temporary file after it was renamed to a binary log file. */ + void detach_temp_file() { m_file.detach_temp_file(); } + + /** Flush and sync the temporary file to storage. */ + bool sync_temp_file() { return m_file.sync_temp_file(); } + + /** Returns true if the temporary file encryption is enabled. */ + bool is_encryption_enabled() { return m_file.is_encryption_enabled(); } + + IO_CACHE_binlog_cache_storage *get_io_cache_storage() { return &m_file; } + private: Truncatable_ostream *m_pipeline_head = nullptr; IO_CACHE_binlog_cache_storage m_file; diff --git a/sql/log_event.cc b/sql/log_event.cc index a181de89dbe0..9c16a9b7edf0 100644 --- a/sql/log_event.cc +++ b/sql/log_event.cc @@ -12978,6 +12978,33 @@ int Ignorable_log_event::pack_info(Protocol *protocol) { protocol->store_string(buf, bytes, &my_charset_bin); return 0; } + +/* + The size of the buffer used to write the padding of an Empty_log_event. The + body of the event is just a run of zero bytes, so it is written out in chunks + of this size. +*/ +static const size_t EMPTY_BUFFER_SIZE = 1024; + +bool Empty_log_event::write_data_body(Basic_ostream *ostream) { + size_t data_len = + m_size - mysql::binlog::event::Binary_log_event::IGNORABLE_HEADER_LEN - + LOG_EVENT_HEADER_LEN; + + uchar empty_buffer[EMPTY_BUFFER_SIZE]; + memset(empty_buffer, 0, EMPTY_BUFFER_SIZE); + + while (data_len > EMPTY_BUFFER_SIZE) { + if (ostream->write(empty_buffer, EMPTY_BUFFER_SIZE)) return true; + + data_len -= EMPTY_BUFFER_SIZE; + } + + assert(data_len <= EMPTY_BUFFER_SIZE); + if (data_len > 0 && ostream->write(empty_buffer, data_len)) return true; + + return false; +} #endif #ifndef MYSQL_SERVER diff --git a/sql/log_event.h b/sql/log_event.h index f8715432ac02..da0ceac02127 100644 --- a/sql/log_event.h +++ b/sql/log_event.h @@ -3763,6 +3763,56 @@ class Ignorable_log_event } }; +/** + @class Empty_log_event + It is the subclass of Ignorable_log_event, used to fill the reserved space in + binary log. + + When a large transaction commits by renaming its binlog cache temporary file + to a new binary log file (see @ref Binlog_commit_by_rotate), some space is + reserved at the beginning of the cache file for the events that describe the + binary log's state (Format description, Previous-GTIDs and the transaction's + own GTID event). After those events are written there is usually some space + left before the transaction data. An Empty_log_event is written to consume the + remaining reserved bytes so that the transaction data still starts exactly at + the reserved offset. As it is an Ignorable_log_event, slaves and mysqlbinlog + that do not recognize it can safely skip it. + + @internal + The inheritance structure is as follows + + Binary_log_event + ^ + | + | + B_l:Ignorable_event Log_event + \ / + <>\ / + \ / + Ignorable_log_event + \ + \ + Empty_log_event + + This event is composed of Event header and empty buffer. +*/ +class Empty_log_event : public Ignorable_log_event { + public: +#ifdef MYSQL_SERVER + Empty_log_event(THD *thd_arg, size_t size) + : Ignorable_log_event(thd_arg), m_size(size) {} + + bool write_data_body(Basic_ostream *ostream) override; +#endif + + size_t get_data_size() override { return m_size - LOG_EVENT_HEADER_LEN; } + + void set_size(size_t size) { m_size = size; } + + private: + size_t m_size; +}; + /** @class Rows_query_log_event It is used to record the original query for the rows diff --git a/sql/mysqld.cc b/sql/mysqld.cc index 8e264455c53a..b224e561693b 100644 --- a/sql/mysqld.cc +++ b/sql/mysqld.cc @@ -794,9 +794,9 @@ MySQL clients support the protocol: #include "sql/auth/authentication_policy.h" #include "sql/auth/sql_authentication.h" // init_rsa_keys #include "sql/auth/sql_security_ctx.h" -#include "sql/auto_thd.h" // Auto_THD -#include "sql/binlog.h" // mysql_bin_log -#include "sql/bootstrap.h" // bootstrap +#include "sql/auto_thd.h" // Auto_THD +#include "sql/binlog.h" // mysql_bin_log +#include "sql/bootstrap.h" // bootstrap #include "sql/check_stack.h" #include "sql/conn_handler/connection_acceptor.h" // Connection_acceptor #include "sql/conn_handler/connection_handler_impl.h" // Per_thread_connection_handler @@ -8898,6 +8898,7 @@ static int init_server_components() { unireg_abort(MYSQLD_ABORT_EXIT); } mysql_mutex_unlock(log_lock); + if (unlikely(init_binlog_cache_dir())) unireg_abort(MYSQLD_ABORT_EXIT); } if (!opt_bin_log) { diff --git a/sql/rpl_binlog_sender.cc b/sql/rpl_binlog_sender.cc index e81c1c87415b..b9fc1a66cc7d 100644 --- a/sql/rpl_binlog_sender.cc +++ b/sql/rpl_binlog_sender.cc @@ -623,8 +623,9 @@ int Binlog_sender::send_events(File_reader &reader, my_off_t end_pos) { be skipped. and maybe removing the gtid from m_exclude_gtid will make skip_event has better performance. */ - if (m_exclude_gtid && - (in_exclude_group = skip_event(event_ptr, in_exclude_group))) { + if ((m_exclude_gtid && + (in_exclude_group = skip_event(event_ptr, in_exclude_group))) || + unlikely(event_type == mysql::binlog::event::IGNORABLE_LOG_EVENT)) { /* If we have not send any event from past 'heartbeat_period' time period, then it is time to send a packet before skipping this group. diff --git a/sql/sys_vars.cc b/sql/sys_vars.cc index 09edc90041a3..72b70e71e933 100644 --- a/sql/sys_vars.cc +++ b/sql/sys_vars.cc @@ -101,8 +101,8 @@ #include "nulls.h" #include "sql-common/my_decimal.h" #include "sql/auth/auth_acls.h" -#include "sql/auth/auth_common.h" // validate_user_plugins -#include "sql/binlog.h" // mysql_bin_log +#include "sql/auth/auth_common.h" // validate_user_plugins +#include "sql/binlog.h" // mysql_bin_log #include "sql/changestreams/apply/replication_thread_status.h" #include "sql/clone_handler.h" #include "sql/conn_handler/connection_handler_impl.h" // Per_thread_connection_handler @@ -2819,6 +2819,18 @@ static Sys_var_ulonglong Sys_max_binlog_cache_size( BLOCK_SIZE(IO_SIZE), NO_MUTEX_GUARD, NOT_IN_BINLOG, ON_CHECK(nullptr), ON_UPDATE(fix_binlog_cache_size)); +static Sys_var_ulonglong Sys_binlog_large_commit_threshold( + "binlog_large_commit_threshold", + "Increases transaction concurrency for large transactions (i.e. those " + "with sizes larger than this value) by renaming the large transaction's " + "binlog cache temporary file to a new binary log file at commit time, " + "instead of copying the transaction cache data to the end of the active " + "binary log file while holding a lock that prevents other transactions " + "from binlogging. 0 disables the feature.", + GLOBAL_VAR(opt_binlog_large_commit_threshold), CMD_LINE(REQUIRED_ARG), + VALID_RANGE(0, ULLONG_MAX), DEFAULT(128 * 1024 * 1024), BLOCK_SIZE(1), + NO_MUTEX_GUARD, NOT_IN_BINLOG); + static Sys_var_ulonglong Sys_max_binlog_stmt_cache_size( "max_binlog_stmt_cache_size", "Sets the total size of the statement cache", GLOBAL_VAR(max_binlog_stmt_cache_size), CMD_LINE(REQUIRED_ARG),