Skip to content

Commit 985ede9

Browse files
committed
MDEV-20755 InnoDB: Database page corruption on disk or a failed file read of tablespace upon prepare of mariabackup incremental backup
The problem: When incremental backup is taken, delta files are created for innodb tables which are marked as new tables during innodb ddl tracking. When such tablespace is tried to be opened during prepare in xb_delta_open_matching_space(), it is "created", i.e. xb_space_create_file() is invoked, instead of opening, even if a tablespace with the same name exists in the base backup directory. xb_space_create_file() writes page 0 header the tablespace. This header does not contain crypt data, as mariabackup does not have any information about crypt data in delta file metadata for tablespaces. After delta file is applied, recovery process is started. As the sequence of recovery for different pages is not defined, there can be the situation when crypt data redo log event is executed after some other page is read for recovery. When some page is read for recovery, it's decrypted using crypt data stored in tablespace header in page 0, if there is no crypt data, the page is not decryped and does not pass corruption test. This causes error for incremental backup --prepare for encrypted tablespaces. The error is not stable because crypt data redo log event updates crypt data on page 0, and recovery for different pages can be executed in undefined order. The fix: When delta file is created, the corresponding write filter copies only the pages which LSN is greater then some incremental LSN. When new file is created during incremental backup, the LSN of all it's pages must be greater then incremental LSN, so there is no need to create delta for such table, we can just copy it completely. The fix is to copy the whole file which was tracked during incremental backup with innodb ddl tracker, and copy it to base directory during --prepare instead of delta applying. There is also DBUG_EXECUTE_IF() in innodb code to avoid writing redo log record for crypt data updating on page 0 to make the test case stable. Note: The issue is not reproducible in 10.5 as optimized DDL's are deprecated in 10.5. But the fix is still useful because it allows to decrease data copy size during backup, as delta file contains some extra info. The test case should be removed for 10.5 as it will always pass.
1 parent cc1646d commit 985ede9

File tree

5 files changed

+203
-51
lines changed

5 files changed

+203
-51
lines changed

extra/mariabackup/xtrabackup.cc

Lines changed: 102 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2537,17 +2537,24 @@ xb_get_copy_action(const char *dflt)
25372537
return(action);
25382538
}
25392539

2540-
/* TODO: We may tune the behavior (e.g. by fil_aio)*/
25412540

2542-
static
2543-
my_bool
2544-
xtrabackup_copy_datafile(fil_node_t* node, uint thread_n, const char *dest_name=0, ulonglong max_size=ULLONG_MAX)
2541+
/** Copy innodb data file to the specified destination.
2542+
2543+
@param[in] node file node of a tablespace
2544+
@param[in] thread_n thread id, used in the text of diagnostic messages
2545+
@param[in] dest_name destination file name
2546+
@param[in] write_filter write filter to copy data, can be pass-through filter
2547+
for full backup, pages filter for incremental backup, etc.
2548+
2549+
@return FALSE on success and TRUE on error */
2550+
static my_bool xtrabackup_copy_datafile(fil_node_t *node, uint thread_n,
2551+
const char *dest_name,
2552+
const xb_write_filt_t &write_filter)
25452553
{
25462554
char dst_name[FN_REFLEN];
25472555
ds_file_t*dstfile = NULL;
25482556
xb_fil_cur_t cursor;
25492557
xb_fil_cur_result_t res;
2550-
xb_write_filt_t*write_filter = NULL;
25512558
xb_write_filt_ctxt_t write_filt_ctxt;
25522559
const char*action;
25532560
xb_read_filt_t*read_filter;
@@ -2587,7 +2594,7 @@ xtrabackup_copy_datafile(fil_node_t* node, uint thread_n, const char *dest_name=
25872594
read_filter = &rf_bitmap;
25882595
}
25892596

2590-
res = xb_fil_cur_open(&cursor, read_filter, node, thread_n,max_size);
2597+
res = xb_fil_cur_open(&cursor, read_filter, node, thread_n, ULLONG_MAX);
25912598
if (res == XB_FIL_CUR_SKIP) {
25922599
goto skip;
25932600
} else if (res == XB_FIL_CUR_ERROR) {
@@ -2598,18 +2605,11 @@ xtrabackup_copy_datafile(fil_node_t* node, uint thread_n, const char *dest_name=
25982605
sizeof dst_name - 1);
25992606
dst_name[sizeof dst_name - 1] = '\0';
26002607

2601-
/* Setup the page write filter */
2602-
if (xtrabackup_incremental) {
2603-
write_filter = &wf_incremental;
2604-
} else {
2605-
write_filter = &wf_write_through;
2606-
}
2607-
26082608
memset(&write_filt_ctxt, 0, sizeof(xb_write_filt_ctxt_t));
2609-
ut_a(write_filter->process != NULL);
2609+
ut_a(write_filter.process != NULL);
26102610

2611-
if (write_filter->init != NULL &&
2612-
!write_filter->init(&write_filt_ctxt, dst_name, &cursor)) {
2611+
if (write_filter.init != NULL &&
2612+
!write_filter.init(&write_filt_ctxt, dst_name, &cursor)) {
26132613
msg (thread_n, "mariabackup: error: failed to initialize page write filter.");
26142614
goto error;
26152615
}
@@ -2630,7 +2630,7 @@ xtrabackup_copy_datafile(fil_node_t* node, uint thread_n, const char *dest_name=
26302630

26312631
/* The main copy loop */
26322632
while ((res = xb_fil_cur_read(&cursor)) == XB_FIL_CUR_SUCCESS) {
2633-
if (!write_filter->process(&write_filt_ctxt, dstfile)) {
2633+
if (!write_filter.process(&write_filt_ctxt, dstfile)) {
26342634
goto error;
26352635
}
26362636
}
@@ -2639,8 +2639,8 @@ xtrabackup_copy_datafile(fil_node_t* node, uint thread_n, const char *dest_name=
26392639
goto error;
26402640
}
26412641

2642-
if (write_filter->finalize
2643-
&& !write_filter->finalize(&write_filt_ctxt, dstfile)) {
2642+
if (write_filter.finalize
2643+
&& !write_filter.finalize(&write_filt_ctxt, dstfile)) {
26442644
goto error;
26452645
}
26462646

@@ -2654,8 +2654,8 @@ xtrabackup_copy_datafile(fil_node_t* node, uint thread_n, const char *dest_name=
26542654
if (ds_close(dstfile)) {
26552655
rc = TRUE;
26562656
}
2657-
if (write_filter && write_filter->deinit) {
2658-
write_filter->deinit(&write_filt_ctxt);
2657+
if (write_filter.deinit) {
2658+
write_filter.deinit(&write_filt_ctxt);
26592659
}
26602660
return(rc);
26612661

@@ -2664,8 +2664,8 @@ xtrabackup_copy_datafile(fil_node_t* node, uint thread_n, const char *dest_name=
26642664
if (dstfile != NULL) {
26652665
ds_close(dstfile);
26662666
}
2667-
if (write_filter && write_filter->deinit) {
2668-
write_filter->deinit(&write_filt_ctxt);;
2667+
if (write_filter.deinit) {
2668+
write_filter.deinit(&write_filt_ctxt);;
26692669
}
26702670
msg(thread_n, "mariabackup: xtrabackup_copy_datafile() failed.");
26712671
return(TRUE); /*ERROR*/
@@ -2675,8 +2675,8 @@ xtrabackup_copy_datafile(fil_node_t* node, uint thread_n, const char *dest_name=
26752675
if (dstfile != NULL) {
26762676
ds_close(dstfile);
26772677
}
2678-
if (write_filter && write_filter->deinit) {
2679-
write_filter->deinit(&write_filt_ctxt);
2678+
if (write_filter.deinit) {
2679+
write_filter.deinit(&write_filt_ctxt);
26802680
}
26812681
msg(thread_n,"Warning: We assume the table was dropped during xtrabackup execution and ignore the tablespace %s", node_name);
26822682
return(FALSE);
@@ -2974,9 +2974,9 @@ data_copy_thread_func(
29742974
while ((node = datafiles_iter_next(ctxt->it)) != NULL) {
29752975
DBUG_MARIABACKUP_EVENT("before_copy", node->space->name);
29762976
/* copy the datafile */
2977-
if(xtrabackup_copy_datafile(node, num)) {
2977+
if (xtrabackup_copy_datafile(node, num, NULL,
2978+
xtrabackup_incremental ? wf_incremental : wf_write_through))
29782979
die("failed to copy datafile.");
2979-
}
29802980

29812981
DBUG_MARIABACKUP_EVENT("after_copy", node->space->name);
29822982

@@ -4620,7 +4620,7 @@ void backup_fix_ddl(void)
46204620
continue;
46214621
std::string dest_name(node->space->name);
46224622
dest_name.append(".new");
4623-
xtrabackup_copy_datafile(node, 0, dest_name.c_str()/*, do_full_copy ? ULONGLONG_MAX:UNIV_PAGE_SIZE */);
4623+
xtrabackup_copy_datafile(node, 0, dest_name.c_str(), wf_write_through);
46244624
}
46254625

46264626
datafiles_iter_free(it);
@@ -5176,22 +5176,66 @@ static void rename_force(const char *from, const char *to) {
51765176
rename_file(from,to);
51775177
}
51785178

5179-
/* During prepare phase, rename ".new" files , that were created in backup_fix_ddl(),
5180-
to ".ibd".*/
5181-
static ibool prepare_handle_new_files(
5182-
const char* data_home_dir,/*!<in: path to datadir */
5183-
const char* db_name,/*!<in: database name */
5184-
const char* file_name,/*!<in: file name with suffix */
5185-
void *)
5186-
{
51875179

5180+
/** During prepare phase, rename ".new" files, that were created in
5181+
backup_fix_ddl() and backup_optimized_ddl_op(), to ".ibd". In the case of
5182+
incremental backup, i.e. of arg argument is set, move ".new" files to
5183+
destination directory and rename them to ".ibd", remove existing ".ibd.delta"
5184+
and ".idb.meta" files in incremental directory to avoid applying delta to
5185+
".ibd" file.
5186+
5187+
@param[in] data_home_dir path to datadir
5188+
@param[in] db_name database name
5189+
@param[in] file_name file name with suffix
5190+
@param[in] arg destination path, used in incremental backup to notify, that
5191+
*.new file must be moved to destibation directory
5192+
5193+
@return true */
5194+
static ibool prepare_handle_new_files(const char *data_home_dir,
5195+
const char *db_name,
5196+
const char *file_name, void *arg)
5197+
{
5198+
const char *dest_dir = static_cast<const char *>(arg);
51885199
std::string src_path = std::string(data_home_dir) + '/' + std::string(db_name) + '/' + file_name;
5189-
std::string dest_path = src_path;
5200+
/* Copy "*.new" files from incremental to base dir for incremental backup */
5201+
std::string dest_path=
5202+
dest_dir ? std::string(dest_dir) + '/' + std::string(db_name) +
5203+
'/' + file_name : src_path;
51905204

51915205
size_t index = dest_path.find(".new");
51925206
DBUG_ASSERT(index != std::string::npos);
5193-
dest_path.replace(index, 4, ".ibd");
5207+
dest_path.replace(index, strlen(".ibd"), ".ibd");
51945208
rename_force(src_path.c_str(),dest_path.c_str());
5209+
5210+
if (dest_dir) {
5211+
/* remove delta and meta files to avoid delta applying for new file */
5212+
index = src_path.find(".new");
5213+
DBUG_ASSERT(index != std::string::npos);
5214+
src_path.replace(index, std::string::npos, ".ibd.delta");
5215+
if (access(src_path.c_str(), R_OK) == 0) {
5216+
msg("Removing %s", src_path.c_str());
5217+
if (my_delete(src_path.c_str(), MYF(MY_WME)))
5218+
die("Can't remove %s, errno %d", src_path.c_str(), errno);
5219+
}
5220+
src_path.replace(index, std::string::npos, ".ibd.meta");
5221+
if (access(src_path.c_str(), R_OK) == 0) {
5222+
msg("Removing %s", src_path.c_str());
5223+
if (my_delete(src_path.c_str(), MYF(MY_WME)))
5224+
die("Can't remove %s, errno %d", src_path.c_str(), errno);
5225+
}
5226+
5227+
/* add table name to the container to avoid it's deletion at the end of
5228+
prepare */
5229+
std::string table_name = std::string(db_name) + '/'
5230+
+ std::string(file_name, file_name + strlen(file_name) - strlen(".new"));
5231+
xb_filter_entry_t *table = static_cast<xb_filter_entry_t *>
5232+
(malloc(sizeof(xb_filter_entry_t) + table_name.size() + 1));
5233+
table->name = ((char*)table) + sizeof(xb_filter_entry_t);
5234+
strcpy(table->name, table_name.c_str());
5235+
HASH_INSERT(xb_filter_entry_t, name_hash, inc_dir_tables_hash,
5236+
ut_fold_string(table->name), table);
5237+
}
5238+
51955239
return TRUE;
51965240
}
51975241

@@ -5228,17 +5272,18 @@ rm_if_not_found(
52285272
return(TRUE);
52295273
}
52305274

5231-
/************************************************************************
5232-
Function enumerates files in datadir (provided by path) which are matched
5275+
/** Function enumerates files in datadir (provided by path) which are matched
52335276
by provided suffix. For each entry callback is called.
5277+
5278+
@param[in] path datadir path
5279+
@param[in] suffix suffix to match against
5280+
@param[in] func callback
5281+
@param[in] func_arg arguments for the above callback
5282+
52345283
@return FALSE if callback for some entry returned FALSE */
5235-
static
5236-
ibool
5237-
xb_process_datadir(
5238-
const char* path,/*!<in: datadir path */
5239-
const char* suffix,/*!<in: suffix to match
5240-
against */
5241-
handle_datadir_entry_func_tfunc)/*!<in: callback */
5284+
static ibool xb_process_datadir(const char *path, const char *suffix,
5285+
handle_datadir_entry_func_t func,
5286+
void *func_arg = NULL)
52425287
{
52435288
ulint ret;
52445289
chardbpath[OS_FILE_MAX_PATH];
@@ -5273,7 +5318,7 @@ xb_process_datadir(
52735318
suffix)) {
52745319
if (!func(
52755320
path, NULL,
5276-
fileinfo.name, NULL))
5321+
fileinfo.name, func_arg))
52775322
{
52785323
os_file_closedir(dbdir);
52795324
return(FALSE);
@@ -5337,7 +5382,7 @@ xb_process_datadir(
53375382
if (!func(
53385383
path,
53395384
dbinfo.name,
5340-
fileinfo.name, NULL))
5385+
fileinfo.name, func_arg))
53415386
{
53425387
os_file_closedir(dbdir);
53435388
os_file_closedir(dir);
@@ -5497,6 +5542,12 @@ static bool xtrabackup_prepare_func(char** argv)
54975542

54985543
fil_path_to_mysql_datadir = ".";
54995544

5545+
ut_ad(xtrabackup_incremental == xtrabackup_incremental_dir);
5546+
if (xtrabackup_incremental) {
5547+
inc_dir_tables_hash = hash_create(1000);
5548+
ut_ad(inc_dir_tables_hash);
5549+
}
5550+
55005551
/* Fix DDL for prepare. Process .del,.ren, and .new files.
55015552
The order in which files are processed, is important
55025553
(see MDEV-18185, MDEV-18201)
@@ -5508,6 +5559,8 @@ static bool xtrabackup_prepare_func(char** argv)
55085559
if (xtrabackup_incremental_dir) {
55095560
xb_process_datadir(xtrabackup_incremental_dir, ".new.meta", prepare_handle_new_files);
55105561
xb_process_datadir(xtrabackup_incremental_dir, ".new.delta", prepare_handle_new_files);
5562+
xb_process_datadir(xtrabackup_incremental_dir, ".new",
5563+
prepare_handle_new_files, (void *)".");
55115564
}
55125565
else {
55135566
xb_process_datadir(".", ".new", prepare_handle_new_files);
@@ -5592,8 +5645,6 @@ static bool xtrabackup_prepare_func(char** argv)
55925645
goto error_cleanup;
55935646
}
55945647

5595-
inc_dir_tables_hash = hash_create(1000);
5596-
55975648
ok = xtrabackup_apply_deltas();
55985649

55995650
xb_data_files_close();
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
--plugin-load-add=$FILE_KEY_MANAGEMENT_SO
2+
--innodb-file-per-table
3+
--innodb-encryption-threads=4
4+
--innodb-encrypt-tables
5+
--innodb-encrypt-log
6+
--loose-file-key-management
7+
--loose-file-key-management-filename=$MYSQL_TEST_DIR/std_data/keys.txt
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
SET @old_innodb_log_optimize_ddl=@@global.innodb_log_optimize_ddl;
2+
SET GLOBAL innodb_log_optimize_ddl=ON;
3+
SET @old_debug_dbug=@@global.debug_dbug;
4+
SET GLOBAL debug_dbug="+d,ib_log_checkpoint_avoid";
5+
SET @old_innodb_page_cleaner_disabled_debug=@@global.innodb_page_cleaner_disabled_debug;
6+
SET GLOBAL innodb_page_cleaner_disabled_debug=ON;
7+
CREATE TABLE t1 (c INT) ENGINE=InnoDB;
8+
INSERT INTO t1 VALUES (1);
9+
# backup
10+
SET GLOBAL debug_dbug="+d,ib_do_not_log_crypt_data";
11+
INSERT INTO t1 VALUES (2);
12+
# incremental backup
13+
# prepare
14+
# incremental prepare
15+
SET GLOBAL innodb_log_optimize_ddl=@old_innodb_log_optimize_ddl;
16+
SET GLOBAL innodb_page_cleaner_disabled_debug=@old_innodb_page_cleaner_disabled_debug;
17+
SET GLOBAL debug_dbug=@old_debug_dbug;
18+
# Restore and check results
19+
# shutdown server
20+
# remove datadir
21+
# xtrabackup move back
22+
# restart server
23+
SELECT count(*) FROM t1;
24+
count(*)
25+
2
26+
DROP TABLE t1;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#
2+
# If MDEV-20755 bug is no fixed, incremental prepare will fail.
3+
#
4+
--source include/have_debug.inc
5+
--source include/have_file_key_management.inc
6+
7+
--let $base_dir=$MYSQLTEST_VARDIR/tmp/backup
8+
--let $inc_dir=$MYSQLTEST_VARDIR/tmp/backup_inc
9+
10+
SET @old_innodb_log_optimize_ddl=@@global.innodb_log_optimize_ddl;
11+
SET GLOBAL innodb_log_optimize_ddl=ON;
12+
13+
# Disable pages flushing to allow redo log records to be executed on --prepare.
14+
SET @old_debug_dbug=@@global.debug_dbug;
15+
SET GLOBAL debug_dbug="+d,ib_log_checkpoint_avoid";
16+
SET @old_innodb_page_cleaner_disabled_debug=@@global.innodb_page_cleaner_disabled_debug;
17+
SET GLOBAL innodb_page_cleaner_disabled_debug=ON;
18+
19+
CREATE TABLE t1 (c INT) ENGINE=InnoDB;
20+
INSERT INTO t1 VALUES (1);
21+
22+
--echo # backup
23+
--disable_result_log
24+
--exec $XTRABACKUP --defaults-file=$MYSQLTEST_VARDIR/my.cnf --backup --target-dir=$base_dir
25+
--enable_result_log
26+
27+
# Do not log crypt data to avoid it's execution on --prepare.
28+
SET GLOBAL debug_dbug="+d,ib_do_not_log_crypt_data";
29+
INSERT INTO t1 VALUES (2);
30+
31+
--disable_result_log
32+
33+
# Execute OPTIMIZE TABLE after the table is copied to execute optimized ddl
34+
# callback during backup, which, in turns, will create t1.new file in backup
35+
# directory.
36+
--let after_copy_test_t1=OPTIMIZE TABLE test.t1;
37+
38+
--echo # incremental backup
39+
--exec $XTRABACKUP --defaults-file=$MYSQLTEST_VARDIR/my.cnf --backup --target-dir=$inc_dir --incremental-basedir=$base_dir --dbug=+d,mariabackup_events
40+
41+
--echo # prepare
42+
--exec $XTRABACKUP --prepare --target-dir=$base_dir
43+
44+
# If the tablespace is created during delta tablespace open procedure, then
45+
# crypt data will be not written, and page corruption test will not pass
46+
# when some redo log event is executed, and --prepare will fail.
47+
--echo # incremental prepare
48+
--exec $XTRABACKUP --prepare --target-dir=$base_dir --incremental-dir=$inc_dir
49+
50+
--enable_result_log
51+
52+
SET GLOBAL innodb_log_optimize_ddl=@old_innodb_log_optimize_ddl;
53+
SET GLOBAL innodb_page_cleaner_disabled_debug=@old_innodb_page_cleaner_disabled_debug;
54+
SET GLOBAL debug_dbug=@old_debug_dbug;
55+
56+
--echo # Restore and check results
57+
--let $targetdir=$base_dir
58+
--source include/restart_and_restore.inc
59+
--enable_result_log
60+
61+
SELECT count(*) FROM t1;
62+
63+
# Cleanup
64+
DROP TABLE t1;
65+
--rmdir $base_dir
66+
--rmdir $inc_dir

storage/innobase/fil/fil0crypt.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,8 @@ fil_space_crypt_t::write_page0(
407407
mlog_write_ulint(page + offset + MAGIC_SZ + 2 + len + 8, encryption,
408408
MLOG_1BYTE, mtr);
409409

410+
DBUG_EXECUTE_IF("ib_do_not_log_crypt_data", return;);
411+
410412
byte* log_ptr = mlog_open(mtr, 11 + 17 + len);
411413

412414
if (log_ptr != NULL) {

0 commit comments

Comments
 (0)