MDEV-14992 BACKUP SERVER - #4817
Conversation
|
|
2723322 to
1703796
Compare
9a529de to
857edeb
Compare
8149b3d to
c08d121
Compare
| log_sys.latch.wr_unlock(); | ||
| sql_print_information("stop archiving: " LSN_PF, | ||
| log_sys.last_checkpoint_lsn.load()); | ||
| /* FIXME: execute this at a later stage, | ||
| after MDL_BACKUP_WAIT_COMMIT has been released! | ||
| This may wait several seconds for some page flushing! */ | ||
| fail= log_sys.backup_stop_archiving(thd); | ||
| sql_print_information("stopped archiving: " LSN_PF, | ||
| log_sys.last_checkpoint_lsn.load()); | ||
| log_sys.latch.wr_lock(); | ||
| mutex.wr_lock(); | ||
| delete_logs(); | ||
| mutex.wr_unlock(); |
There was a problem hiding this comment.
This cleanup (restoring innodb_log_archive=OFF) is being executed while all transaction commits are blocked. In my tiny test, this step was instantenous, now that the fix of #5576 is included in this branch.
In a larger test, which produces a 46 GiB compressed backup, this step consumed 5 seconds. It looks like we must move most of all of this logic to be executed as part of the BACKUP_PHASE_FINISH step, or InnoDB_backup::fini().
There was a problem hiding this comment.
file_name_t::page0_lsn: Keep track of the last applied recv_sys_t::parse_page0() so that a multi-batch recovery will not reset the file to a smaller size. Reviewed by: Thirunarayanan Balathandayuthapani (cherry picked from commit 8f00e6c)
The following SQL statements will be introduced: BACKUP SERVER TO '/path/to/directory' [ 1 CONCURRENT ]; BACKUP SERVER WITH [ 1 CONCURRENT ] 'command'; In place of the 1, any positive number of threads may be specified. For the first variant, '/path/to' must exist and '/path/to/directory' must be compatible with secure_file_priv and not exist; that is where the backup will be written to. For the second variant, 'command' must be the name of a script or command that will be executed in a child process. The standard input of that command will be in a format that is compatible with GNU tar --format=oldgnu (and also BSD tar variants that are also part of Microsoft Windows and Apple macOS). The command is expected to optionally compress and encrypt the stream and redirect it to a file on a local or a remote server. The BACKUP SERVER WITH will append an additional argument, a positive base-ten number in ASCII, starting with 1, to identify the current thread. In this way, each concurrent stream can write a separate file. The backup or the first stream will contain a file backup.cnf, which includes parameters needed for restoring the backup. Currently, these are innodb_log_recovery_start and innodb_log_recovery_target. If innodb_log_recovery_target>0, InnoDB will be in read-only mode, not allowing any writes to persistent files other than via the log application. To restore a streaming backup made with BACKUP SERVER WITH, an empty directory needs to be created and all streams be extracted there using the standard tar utility of the operating system, optionally after undoing any encryption or compression that had been added by the backup command. Then, the backup is prepared or MariaDB server started up on the extracted directory, similar to as if the BACKUP SERVER TO statement had been used. Note: The parameter innodb_log_recovery_start in backup.cnf is STRICTLY NECESSARY TO AVOID CORRUPTION! By default, InnoDB crash recovery starts from the latest available log checkpoint. However, for restoring a backup, recovery must start from the checkpoint that was the latest when the backup was started. Starting recovery from a possible later checkpoint will result in a corrupted database! The following will be implemented separately: MDEV-39061 mariadb-backup compatible wrapper script for BACKUP SERVER MDEV-40163 Partial backup and restore MDEV-39091 Back up ENGINE=RocksDB MDEV-39092 Less blocking backup of ENGINE=Aria The implementation introduces a basic driver Sql_cmd_backup, storage engine interfaces, and basic copying of the storage engines InnoDB, Aria, MyISAM, MERGE (MyISAM), Archive, CSV. aria_backup_end(): A crude prototype that copies non-InnoDB files. Scans and copies the data directories in a single thread, while everything is locked. This will be refactored in MDEV-39092. backup_target: A structured data type to represent a target directory. On Microsoft Windows, we must use directory paths because there is no variant of CopyFileEx() that would work on file handles. backup_sink: Wraps a per-thread output stream as well as storage engine specific context. handlerton::backup_start(), handlerton::backup_end(): Invoked at the start or end of a backup phase, in the thread that executes a BACKUP SERVER statement. handlerton::backup_step(): A backup step that can be invoked from multiple threads concurrently, between the execution of the corresponding handlerton::backup_start() and handlerton::backup_end() of the same phase. copy_entire_file(): A file copying service for POSIX systems. copy_mmap(): A zero-copy alternative to backup::copy(), to copy from a memory-mapped buffer. copy_file_range_try(): A wrapper for Linux copy_file_range(2), which may fail with EOPNOTSUPP or EXDEV and thus require a fallback to copy_mmap() or backup::copy(). backup::copy(): A partial or sparse file-copying service. On other platforms than FreeBSD or Microsoft Windows, there are shortcut alternatives to this. Note: On Linux we never invoke sendfile(2) for copying between files, because can be much slower than the alternatives. backup_stream_append_plain(): Equivalent to backup::copy(), but appending to a stream. On Linux, this uses sendfile(2), which assumes that the source data will not be changed before the data has been consumed from the pipe. backup_stream_append_async(): A variant of backup_stream_append_plain() where the source file region is guaranteed to be immutable after the call returns. We must not use zero-copy mmap(2) or Linux sendfile(2) for copying data files that may be modified in place, because it could introduce a race condition between a page write that runs concurrently with a child process that is reading the data from the pipe. backup_stream_zeropad(): Zero-pad the last tar block if needed. InnoDB_backup::context: Backup context, attached to backup_sink so that context can continue to exist between the time a BACKUP SERVER releases all locks and another BACKUP SERVER starts executing, with innodb_backup pointing to the new backup, while the old backup is still being finished. InnoDB_backup::queue: Collection of tablespace IDs and payload sizes at the start of the backup, and the log_sys.first_lsn of log files that have to be included in the backup. If any data file is created or extended while the backup is executing, we must have the corresponding write-ahead-log entries that we are copying since the latest checkpoint that was completed when the backup started. If any tablespaces are deleted during the backup, we may or may not copy them, and the application of a FILE_DELETE record will remove them. Similarly, applying FILE_RENAME or FILE_CREATE records will rename or create files during recovery as needed. log_sys.backup: Whether BACKUP SERVER is in progress. The purpose of this is to make BACKUP SERVER prevent the concurrent execution of SET GLOBAL innodb_log_archive=OFF or SET GLOBAL innodb_log_file_size when innodb_log_archive=OFF. log_sys.archived_checkpoint: Keep track of the earliest available checkpoint, corresponding to log_sys.archived_lsn. This reflects SET GLOBAL innodb_log_recovery_start (which is settable now), for incremental backup. fil_system.have_all_spaces: Whether all tablespace metadata is guaranteed to be known. To speed up startup, InnoDB does not normally open all tablespace files. fil_space_t::create_lsn: Change to Atomic_relaxed and use this to indicate tablespace creation LSN, in addition to indicate undo tablespace rebuild LSN. fil_space_t::backup_end: The first page number that is not being backed up (by default 0, to indicate that no backup is in progress). fil_space_t::BACKUP_BATCH_SIZE: The number of preceding pages that will be covered by fil_space_t::backup_end. This is the unit of "page range locking" during InnoDB backup. buf_page_t::write_fix_try(), buf_page_t::write_unfix_try(): Try to set or unset a fake "write fix" on a page, to prevent concurrent flush() during a backup batch. The atomic operations may run concurrently with set_reinit() and set_freed(). The fake "write fix" does not prevent any concurrent read or write of the page data in the buffer pool; it only blocks writes to the underlying data file. buf_page_t::flush(): Atomically test and set write fix, and skip the operation if the fake "write fix" was set. buf_page_t::set_freed(), buf_page_t::set_reinit(): Employ a compare-and-exchange loop to accommodate for the "write fix". innodb_backup_batch_wait(): Look up any pages that we are about to back up. For any dirty pages, invoke buf_page_t::write_fix_try() to try to set a fake "write fix" lock-free. If the page is currently write-fixed between buf_page_t::flush() and buf_page_t::write_complete(), acquire and release a page U-latch to wait for the conflicting write to complete. InnoDB_backup::backup_batch_start(), InnoDB_backup::backup_batch_stop(): Adjust fil_space_t::backup_end and fake "write fix" of dirty pages to protect the copying of a range of pages from the underlying file. log_t::backup_start(): If we were running with innodb_log_archive=ON, ensure that the latest file is a valid recovery starting point. That is, wait for the latest log checkpoint to be within the file. buf_flush_list_space(): Check for concurrent backup before writing each page. This is inefficient, but this function may be invoked from multiple threads concurrently, and it cannot be changed easily, especially for fil_crypt_thread(). fil_ibd_create(): Set fil_space_t::create_lsn after the file has been created. dict_load_tablespaces(): Determine the size of each file if upgrade==true. Backup depends on that. buf_dblwr_t::begin(), buf_dblwr_t::end(), buf_dblwr_t::size(): Accessors to allow BACKUP SERVER to skip the contents of the doublewrite buffer in the system tablespace. It is only useful for crash recovery in case a data page had been incompletely written by the time the server was killed. If the server is killed during a backup, the backup will be incomplete and unusable anyway. Furthermore, the page range locking makes page writes and backup mutually exclusive.
InnoDB_backup::context: Remove the pointer indirection and do not allow two overlapping backup operations. InnoDB_backup::init(): Wait for a possible previous BACKUP SERVER operation to reach the very end of InnoDB_backup::context::cleanup() so that the context can be safely reused.
aria_backup_end(): Removed. aria_backup_start(): A crude single-threaded implementation of copying files that do not belong to ACID storage engines. Most files are copied in BACKUP_PHASE_NO_DDL after flush_tables(thd, FLUSH_NON_TRANS_TABLES) has been invoked. All ENGINE=Aria files (including TRANSACTIONAL=0) are copied in BACKUP_PHASE_NO_COMMIT. Thanks to Andrzej Jarząbek for implementing test cases and suggesting this logic.
Thirunarayanan
left a comment
There was a problem hiding this comment.
First round of review.
| { | ||
| /* Copy the rest after the doublewrite buffer. */ | ||
| limit= final_limit; | ||
| page+= buf_dblwr.size(); |
There was a problem hiding this comment.
I think we're failing to move the start variable offset by buf_dblwr.size(). start still pointing to 64. Even though page is pointing towards 192. All the remaining pages in system tablespace will be copied without any protection.
| { | ||
| /* Copy the rest after the doublewrite buffer. */ | ||
| limit= final_limit; | ||
| page+= buf_dblwr.size(); |
There was a problem hiding this comment.
Same problem here (fail to move start variable)
| if (limit == buf_dblwr.begin() && n_chunk == 3) | ||
| { | ||
| /* Copy the rest after the doublewrite buffer. */ | ||
| page+= buf_dblwr.size(); |
There was a problem hiding this comment.
same here (fail to move start variable?)
| const page_id_t start | ||
| {space_id, end_page & ~(fil_space_t::BACKUP_BATCH_SIZE - 1)}; | ||
| ut_ad(end_page - 1 > start.page_no()); | ||
| for (page_id_t id{space_id, end_page}; id != start; --id) |
There was a problem hiding this comment.
In the page (0..63). we lock only from 63, 62, 61...,till 1. not 0. first page is not being fake locked, I guess.
I think we should do
for (page_id_t id{space_id, end_page}; ; --id) {
.....
if (id == start)
break;
}
| auto p= buf_flush_space(space_id); | ||
| space= p.first; | ||
| last_space_id= space_id; | ||
| auto p= buf_flush_space(space_id, &backup_page_end); |
There was a problem hiding this comment.
Something like this:
- page cleaner acquires space S and caches backup_page_end = 0 (no
backup active yet) - backup: backup_start(1064) → window [1000,1064) published
- backup: batch_wait scans 1063..1001; page P=1020 is CLEAN and skipped
- backup: backup::copy(node->handle, dst, 100016k, 106416k) : 1 MB read in flight
- user thread: mtr_t::commit() modifies P, inserted into
flush_list, oldest_modification = LSN - page cleaner reaches P: cached backup_page_end is 0
-page < backup_page_endis1020 < 0= false- bpage->flush(space), real write fix, 16 KB pwrite at offset 1020*16k.
Likebuf_flush_list_space(), we shouldn't rely on cached backup_page_end.
- bpage->flush(space), real write fix, 16 KB pwrite at offset 1020*16k.
we should change in buf_do_flush_list_batch() and same for the callers of buf_flush_try_neighbors()
| uint64_t id_limit{0}; | ||
| mutex.wr_lock(); | ||
| ut_ad(sink.ha_data); | ||
| ut_ad(ctx ? ctx == sink.ha_data |
There was a problem hiding this comment.
There is a possiblity that 2 backup server can exist as the same time.
backup A: end() @ NO_COMMIT: queue.clear(), non_log = 0, this->ctx =nullptr, log_sys.backup_stop()
backup B:
step 2:
backup A: release_lock()
backup B:
step 3 :
backup A:
backup B: acquires MDL_BACKUP_START, opens target, init(): this->ctx= ctx_B, queue adds N tablespaces, non_log = N
step 4:
backup A: worker calls step(FINISH)
backup B:
ctx could be belong to backup B and sink.ha_data could be belong to backup A. Assert will fail here. May be we need to move queue, non_log and old_size into context instead of handlerton?
| delete ctx; | ||
| ctx= nullptr; | ||
| log_sys.backup_stop(old_size, thd); | ||
| goto fail; |
There was a problem hiding this comment.
we fail to unlock the mutex in case of bad_alloc catch ?
| { | ||
| const lsn_t lsn{log_sys.get_first_lsn() - log_sys.capacity()}; | ||
| mutex.wr_lock(); | ||
| queue.emplace_back(lsn); |
There was a problem hiding this comment.
fail to catch bad allocation error?
Thirunarayanan
left a comment
There was a problem hiding this comment.
I do have only these comments as of now
| mysql_mutex_lock(&fil_system.mutex); | ||
| for (fil_space_t &space : fil_system.space_list) | ||
| if (space.id < SRV_SPACE_ID_UPPER_BOUND && | ||
| !space.is_being_imported() && !space.is_stopping() && |
There was a problem hiding this comment.
what if the imported tablespace started before init() and ends up before NO_DDL? will it be copied or reconstructable ?
There was a problem hiding this comment.
Because ALTER TABLE…IMPORT TABLESPACE is currently bypassing the write-ahead-logging protocol that both BACKUP SERVER and mariadb-backup will depend on, the effects of any such statements executed during the backup could be lost in the backup. For mariadb-backup --backup there already is an existing report MDEV-29208 about this. A possible fix might be to make ALTER TABLE…IMPORT TABLESPACE mutually exclusive with backup locks.
| skip: | ||
| mysql_mutex_lock(&buf_pool.flush_list_mutex); | ||
| may_have_skipped= true; | ||
| goto done; |
There was a problem hiding this comment.
why terminating on the first in-window page? should it skip or continue? All callers of the function is spin.
|
|
||
| const uint32_t final_limit= | ||
| node == fil_system.sys_space->chain.start && | ||
| buf_dblwr.begin() + buf_dblwr.size() == buf_dblwr.end() && |
There was a problem hiding this comment.
Are we fail to check buf_dblwr.is_initialized() check?
| if (neighbors && space->is_rotational()) | ||
| if (neighbors && UNIV_LIKELY(!backup_page_end) && | ||
| space->is_rotational()) | ||
| count+= buf_flush_try_neighbors(space, page_id, bpage, |
There was a problem hiding this comment.
Are we blocking the flushing of neighbors for the whole tablespace even though backup range exist in other part of tablespace?
Thirunarayanan
left a comment
There was a problem hiding this comment.
- FLUSH TABLES FOR EXPORT and encryption_rotate_key_age key rotation concurrent with backup server
- Run 2 concurrent backup server statement
- log rotation during backup
- system tablespace larger than dblwr
These test case exist already?
| | SERVER_SYM TO_SYM TEXT_STRING_sys opt_concurrent | ||
| { | ||
| Lex->sql_command= SQLCOM_BACKUP_SERVER; | ||
| Lex->m_sql_cmd= new (thd->mem_root) Sql_cmd_backup($3, $4); | ||
| } | ||
| | SERVER_SYM WITH opt_concurrent TEXT_STRING_sys | ||
| { | ||
| Lex->sql_command= SQLCOM_BACKUP_SERVER; | ||
| Lex->m_sql_cmd= new (thd->mem_root) Sql_cmd_backup($3, $4); | ||
| } |
There was a problem hiding this comment.
We are missing an option to copy binlog_storage_engine logs, similar to mariadb-backup --backup --binlog that was introduced in 7081f2a.
| --connect backup,localhost,root | ||
| if ($have_debug) { | ||
| SET DEBUG_SYNC='innodb_backup_start SIGNAL start WAIT_FOR resume'; | ||
| --replace_result $target_directory target_directory | ||
| send_eval BACKUP SERVER TO '$target_directory' 4 CONCURRENT; | ||
| --connection default | ||
| SET DEBUG_SYNC='now WAIT_FOR start'; | ||
| INSERT INTO t(a) SELECT * FROM seq_1_to_30000; | ||
| SET DEBUG_SYNC='now SIGNAL resume'; | ||
| --connection backup | ||
| --reap | ||
| } | ||
| if (!$have_debug) { | ||
| --replace_result $target_directory target_directory | ||
| eval BACKUP SERVER TO '$target_directory' 4 CONCURRENT; | ||
| } |
There was a problem hiding this comment.
In the !$have_debug variant we are currently missing any concurrent operation with BACKUP SERVER.
| --error ER_BACKUP_LOCK_IS_ACTIVE | ||
| evalp BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; |
There was a problem hiding this comment.
A test needs to cover FLUSH TABLES … FOR EXPORT. There is only minimal FLUSH TABLES coverage in mysql-test/main/backup_lock_binlog.test.
| --source include/have_sequence.inc | ||
| --source include/have_innodb.inc | ||
| --source include/maybe_debug.inc |
There was a problem hiding this comment.
We are currently missing a test that covers innodb_encrypt_log, innodb_encrypt_tables, and innodb_encryption_rotate_key_age.
|
@Thirunarayanan, thank you for your review comments. I am linking to additional comments to individual source files so that each open topic can be followed up in separate threads and marked as resolved individually.
This will be followed up here and here.
Please refer to That scenario confirms the mutual exclusion of
"Log rotation" is a term that I associate with If you are referring to diff --git a/mysql-test/suite/backup/backup_innodb.test b/mysql-test/suite/backup/backup_innodb.test
index 87b56fb7614..bdc75c8bacd 100644
--- a/mysql-test/suite/backup/backup_innodb.test
+++ b/mysql-test/suite/backup/backup_innodb.test
@@ -84,7 +84,7 @@ if (!$MARIADB_UPGRADE_EXE) {
--exec cat $MYSQLTEST_VARDIR/my.cnf >> $target_directory/backup.cnf
}
--let $restart_parameters=--defaults-file=$target_directory/backup.cnf --datadir=$target_directory
---source include/restart_mysqld.inc
+--source include/shutdown_mysqld.inc
SELECT * FROM t;
# A nonzero innodb_log_recovery_target makes InnoDB read-only.you will find two log files
|
Implement multi-threaded non-InnoDB backup. struct Aria_backup: Context for multi-threaded backup. aria_backup_start(): Prepare the context for aria_backup_step(). aria_backup_step(): Copy one file. aria_backup_data(): Copy one data file. We assume that the current working directory is the datadir, which holds for MariaDB Server but not the Embedded Server library. aria_backup_log(): Copy one log file. aria_backup_end(): Finish a copying phase and clean up the context.
The following SQL statements will be introduced:
In place of the
1, any positive number of threads may be specified. For the first variant,'/path/to'must exist and'/path/to/directory'must not exist; that is where the backup will be written to.For the second variant,
'command'must be the name of a script or command that will be executed in a child process. The standard input of that command will be in a format that is compatible with GNUtar --format=oldgnu(and also BSDtarvariants that are also part of Microsoft Windows and Apple macOS). The command is expected to optionally compress and encrypt the stream and redirect it to a file on a local or a remote server. TheBACKUP SERVER WITH willappend an additional argument, a positive base-ten number in ASCII, starting with1, to identify the current thread. In this way, each concurrent stream can write a separate file.The backup or the first stream will contain a file
backup.cnf, which includes parameters needed for restoring the backup. Currently, these areinnodb_log_recovery_startandinnodb_log_recovery_target. Ifinnodb_log_recovery_target>0, InnoDB will be in read-only mode, not allowing any writes to persistent files other than via the log application.To restore a streaming backup made with
BACKUP SERVER WITH, an empty directory needs to be created and all streams be extracted there using the standardtarutility of the operating system, optionally after undoing any encryption or compression that had been added by the backup command. Then, the backup is prepared or MariaDB server started up on the extracted directory, similar to as if theBACKUP SERVER TOstatement had been used.Note: The parameter
innodb_log_recovery_startinbackup.cnfis STRICTLY NECESSARY TO AVOID CORRUPTION! By default, InnoDB crash recovery starts from the latest available log checkpoint. However, for restoring a backup, recovery must start from the checkpoint that was the latest when the backup was started. Starting recovery from a possible later checkpoint will result in a corrupted database!The following will be implemented separately:
MDEV-39061
mariadb-backupcompatible wrapper script forBACKUP SERVERMDEV-40163 Partial backup and restore
MDEV-39091 Back up
ENGINE=RocksDBMDEV-39092 Less blocking backup of
ENGINE=AriaThe implementation introduces a basic driver
Sql_cmd_backup, storage engine interfaces, and basic copying of the storage engines InnoDB, Aria, MyISAM, MERGE (MyISAM), Archive, CSV.backup_target: A structured data type to represent a target directory. On Microsoft Windows, we must use directory paths because there is no variant ofCopyFileEx()that would work on file handles.backup_sink: Wraps a per-thread output stream as well as storage engine specific context.handlerton::backup_start(),handlerton::backup_end(): Invoked at the start or end of a backup phase, in the thread that executes aBACKUP SERVERstatement.handlerton::backup_step(): A backup step that can be invoked from multiple threads concurrently, between the execution of the correspondinghandlerton::backup_start()andhandlerton::backup_end()of the same phase.copy_entire_file(): A file copying service for POSIX systems.copy_file(): A partial or sparse file-copying service for all systems.backup_stream_append(): Equivalent tocopy_file(), but appending to a stream. On Linux, this usessendfile(2), which assumes that the source data will not be changed before the data has been consumed from the pipe.backup_stream_append_async(): A variant ofbackup_stream_append()where the source file region is guaranteed to be immutable after the call returns. We must not use Linuxsendfile(2)for copying data files that may be modified in place, because it could introduce a race condition between a page write that runs concurrently with a child process that is reading the data from the pipe.InnoDB_backup::context: Backup context, attached tobackup_sinkso that context can continue to exist between the time aBACKUP SERVERreleases all locks and anotherBACKUP SERVERstarts executing, withinnodb_backuppointing to the new backup, while the old backup is still being finished.fil_space_t::write_or_backup: Keep track of in-flight page writes and pending backup operation. We must not allow them concurrently, because that could lead into torn pages in the backup.fil_space_t::backup_end: The first page number that is not being backed up (by default 0, to indicate that no backup is in progress).fil_space_t::BACKUP_BATCH_SIZE: The number of preceding pages that will be covered byfil_space_t::backup_end. This is the unit of "page range locking" during InnoDB backup.log_sys.backup: WhetherBACKUP SERVERis in progress. The purpose of this is to makeBACKUP SERVERprevent the concurrent execution ofSET GLOBAL innodb_log_archive=OFForSET GLOBAL innodb_log_file_sizewheninnodb_log_archive=OFF.log_sys.archived_checkpoint: Keep track of the earliest available checkpoint, corresponding tolog_sys.archived_lsn. This reflectsSET GLOBAL innodb_log_recovery_start(which is settable now), for incremental backup.buf_flush_list_space(): Check for concurrent backup before writing each page. This is inefficient, but this function may be invoked from multiple threads concurrently, and it cannot be changed easily, especially forfil_crypt_thread().fil_system.have_all_spaces: Whether all tablespace metadata is guaranteed to be known. To speed up startup, InnoDB does not normally open all tablespace files.