Optimize binlog handling of large transactions (BOLT) - #722
Conversation
Problem:
Binlog does not handle large transactions well; both commit and recovery
time are proportional to transaction size. Committing a large transaction
copies its entire binlog cache into the active binary log, so commit
latency grows with the transaction. Because the copy happens while
holding LOCK_log, concurrent commits stall behind it. Recovery is
likewise expensive; it must scan the last active binlog file, and when
that file contains a large transaction it reads and deserializes every
byte, prolonging unavailability.
How BOLT solves it:
Instead of writing the spilled cache to a temporary file and then copying
that file into the active set of binlog files, we promote the temporary
file to become the last active binlog file. At commit we only rename the
file and sync it, so the work is no longer proportional to transaction
size. Commit latency stays minimal, and smaller transactions can commit
in parallel alongside a large one.
The challenge is that a binlog file begins with header events, including
Format_description_event and Previous_gtids_log_event.
Format_description_event is static, but Previous_gtids_log_event is
dynamic, so at spill time we cannot know the offset of the transaction's
first event. To solve this we reserve additional space at the front of
the header, captured by a new event we introduce called
Large_transaction_header. Besides the reserved bytes, this event records
the coordinates of the transaction's terminating event (XID, Query, or
XA_PREPARE). Recovery uses this event to seek directly to the terminating
event instead of scanning the file, skipping an expensive read of the
transaction body.
Binlog crash recovery first validates the promoted file's
terminating-event metadata before trusting it, and falls back to the
standard sequential scan when source_verify_checksum is ON (OFF by
default).
System variables:
- binlog_large_transaction_optimization_enabled (default ON) enables or
disables the optimization.
- binlog_large_transaction_optimization_threshold sets the spilled
cache size above which a transaction becomes eligible.
Status variables:
- binlog_large_transaction_optimization_count reports the number of
transactions that successfully committed through the optimization's
code path since server startup.
- binlog_large_transaction_optimization_missed_count reports the number
of transactions that exceeded
binlog_large_transaction_optimization_threshold but could not use the
optimized code path since server startup.
Both are exposed through SHOW GLOBAL STATUS and
performance_schema.global_status.
When disabled and fallback:
A transaction falls back to the standard commit path, emits a diagnostic,
and increments binlog_large_transaction_optimization_missed_count when any
of the following hold:
- binlog_format is not ROW,
- the reserved header space is insufficient for the required header
events,
- binlog encryption is enabled,
- binlog transaction compression is enabled,
- binlog_checksum changed while the transaction was in progress, or
- a single statement updated both transactional and non-transactional
tables.
MTR tests:
- Validate sys_var behavior at runtime and startup.
- Validate commit and rollback code paths for XA and non-XA
transactions.
- Validate the binlog 2PC commit protocol with detailed debug points.
- Validate the optimized recovery behavior.
- Validate savepoint correctness behavior.
- Validate rotate, purge, and other operations that alter the index
file.
- Validate conditions where the optimized commit path falls back to the
standard commit path.
- Validate creation of GTID events at the beginning of the promoted
file, including tagged GTIDs.
- Validate that MySQL maintains the #binlog_temp_files directory at
startup time.
- Validate that a replica can receive a promoted file and handles the
added event correctly.
- Validate that the dependency-tracking metadata generated in the
promoted binlog file is correct.
This contribution is under the OCA signed by Amazon and covering
submissions to the MySQL project.
|
Please note that this code change has a significant number of tests. These tests are based on years of operating this feature in Aurora MySQL for the last 6 years and catching many corner cases. Furthermore, we have run benchmarks against this change, you can find the results here: #683 (comment). |
|
Thank you @wxueting-aws for the PR, we will review it. |
| right before flushing them to binary log during binlog group | ||
| commit flush stage. Reset to HA_REGULAR_DURABILITY at the | ||
| beginning of parsing next command. | ||
| Non-BOLT transactions use HA_IGNORE_DURABILITY so the prepared record is |
There was a problem hiding this comment.
I guess I need the Low-Level design to understand these nuaces better
| @@ -0,0 +1,722 @@ | |||
| #ifndef BINLOG_CACHE_DATA_H_INCLUDED | |||
There was a problem hiding this comment.
Same on this file. I need to read the Low-Level design to understand the purpose of this file.
As you know, we currently cache in IO_CACHE, which inevitably spills to disk, and it is blind to what is inside. But this seems to be yet another cache
| encrypted file's physical size includes the encryption header. The only | ||
| caller (opening a promoted binary log file) never encrypts, so require it. | ||
| */ | ||
| assert(!is_encrypted()); |
There was a problem hiding this comment.
Is this a runtime production requirement?
| */ | ||
| class binlog_cache_data { | ||
| public: | ||
| binlog_cache_data(class binlog_cache_mngr &cache_mngr, bool trx_cache_arg, |
There was a problem hiding this comment.
Please add doxygen documentation to all the files
| Caches for non-transactional and transactional data before writing | ||
| it to the binary log. | ||
|
|
||
| @todo All the access functions for the flags suggest that the |
There was a problem hiding this comment.
We try to avoid TODOs in the code. Is something stopping you from doing this?
| constexpr char kBinlogTempFilePrefix[] = "bolt_"; | ||
|
|
||
| /* | ||
| Returns true if 'name' is a temp file created by this feature, i.e. matches |
There was a problem hiding this comment.
In the global context, "this feature" does not have context. Please make the comment generic.
| purely an ownership check so startup cleanup only deletes files this feature | ||
| created. | ||
| */ | ||
| bool is_bolt_temp_file(const char *name) { |
There was a problem hiding this comment.
Maybe we can modernize this a bit, since we have C++20. Codex hints this:
| bool is_bolt_temp_file(const char *name) { | |
| #include <algorithm> | |
| #include <string_view> | |
| bool is_bolt_temp_file(const char *name) { | |
| const std::string_view file_name{name}; | |
| constexpr std::string_view prefix{kBinlogTempFilePrefix}; | |
| return file_name.starts_with(prefix) && | |
| file_name.size() > prefix.size() && | |
| std::ranges::all_of(file_name.substr(prefix.size()), [](char c) { | |
| return (c >= 'a' && c <= 'z') || | |
| (c >= '0' && c <= '9') || c == '_'; | |
| return true; | ||
| } | ||
|
|
||
| ulong binlog_temp_file_permissions() { |
There was a problem hiding this comment.
Is this different from a regular binlog?
| char new_name[FN_REFLEN]; | ||
| int length; | ||
| if (m_dir != nullptr) | ||
| length = snprintf(new_name, sizeof(new_name), "%s%c%s%llx_%llx", m_dir, |
There was a problem hiding this comment.
Consider std::format_to_n instead of snprintf
|
|
||
| m_initialized = true; | ||
| return false; | ||
| } |
There was a problem hiding this comment.
I have a fundamental question and maybe it will be answered in the LLD. Will this happen for all IO_CACHE? All instances of IO_CACHE, when they spill, will become a BOLT file?
|
@wxueting-aws Can you please rebase your branch on latest trunk to pick the fixes of the CI/CD pipelines ? |
Problem:
Binlog does not handle large transactions well; both commit and recovery time are proportional to transaction size. Committing a large transaction copies its entire binlog cache into the active binary log, so commit latency grows with the transaction. Because the copy happens while holding LOCK_log, concurrent commits stall behind it. Recovery is likewise expensive; it must scan the last active binlog file, and when that file contains a large transaction it reads and deserializes every byte, prolonging unavailability.
How BOLT solves it:
Instead of writing the spilled cache to a temporary file and then copying that file into the active set of binlog files, we promote the temporary file to become the last active binlog file. At commit we only rename the file and sync it, so the work is no longer proportional to transaction size. Commit latency stays minimal, and smaller transactions can commit in parallel alongside a large one.
The challenge is that a binlog file begins with header events, including Format_description_event and Previous_gtids_log_event. Format_description_event is static, but Previous_gtids_log_event is dynamic, so at spill time we cannot know the offset of the transaction's first event. To solve this we reserve additional space at the front of the header, captured by a new event we introduce called Large_transaction_header. Besides the reserved bytes, this event records the coordinates of the transaction's terminating event (XID, Query, or XA_PREPARE). Recovery uses this event to seek directly to the terminating event instead of scanning the file, skipping an expensive read of the transaction body.
Binlog crash recovery first validates the promoted file's terminating-event metadata before trusting it, and falls back to the standard sequential scan when source_verify_checksum is ON (OFF by default).
System variables:
Status variables:
Both are exposed through SHOW GLOBAL STATUS and
performance_schema.global_status.
When disabled and fallback:
A transaction falls back to the standard commit path, emits a diagnostic, and increments binlog_large_transaction_optimization_missed_count when any of the following hold:
MTR tests:
This contribution is under the OCA signed by Amazon and covering submissions to the MySQL project.
Copyright (c) 2026, Oracle and/or its affiliates.
What does this change do?
#683
Why is it needed?
#683
How was it tested?
mysql-test/scripts/ci/mtr.shpasses locallyContributor checklist
scripts/ci/format.sh)AI assistance
If AI assistance was used, describe the tool(s) and extent of use:
Areas touched
Binlog, binlog replica