MDEV-39092 Copy Aria data and logs as part of backup#4971
MDEV-39092 Copy Aria data and logs as part of backup#4971mariadb-andrzejjarzabek wants to merge 2 commits into
Conversation
|
|
4ef94be to
c143ff2
Compare
efbc62b to
c77278b
Compare
e89625e to
06fd556
Compare
4d6a19c to
d86a6a1
Compare
c6f5119 to
9ebb23e
Compare
8565956 to
04e3bc2
Compare
14ca552 to
c9429eb
Compare
d35cd47 to
824afeb
Compare
23ac784 to
2dfc033
Compare
2dfc033 to
01a069c
Compare
| backup_target target; | ||
| #ifndef _WIN32 | ||
| const int datadir_fd; | ||
| #endif |
There was a problem hiding this comment.
Why is there a declaration of datadir_fd in the first place? It turns out that it is never being assigned, only passed to openat(2). This seems to rely on zero-initialization and some undefined behaviour. For example, in Linux, AT_FDCWD is defined as -100.
There was a problem hiding this comment.
I noticed later that this data member is being initialized from a call to open(…, O_DIRECTORY) in the constructor. I think that this is bad practice; we should allow the singleton object to be initialized statically.
It is unclear when if ever the directory handle would be closed. I suspect that we would hold the handle open until the server is shut down.
There was a problem hiding this comment.
datadir_fd is initialized in the Aria_backup constructor.
There was a problem hiding this comment.
Is it a reasonable trade-off to have a descriptor permanently opened for backups for the purpose of just one plugin? Should each plugin that performs the backup keep an open descriptor to the data directory? When adding another bit of functionality (not backup) to any part of the server or a plugin, with its own class/module/translation unit, which needs to open files in the data directory, should it also maintain a file descriptor to the data directory? There may be a case for defining an API for the server to maintain a single descriptor and make it available for other code, but given that there isn't one, I would argue that opening a descriptor for the duration of the backup and closing it on backup end. A backup isn't a fine-grained operation which we expect to be performed multiple times per second (or even minute), but we expect that many hours may pass between successive backups. And every backup will typically open hundreds or thousands of files, so saving the time and I/O of one open/close is not significant and it's not clear that maintaining an additional open descriptor the whole time the server is running is is balanced out by that.
In any case I propose putting that discussion off for a possible future improvement, where we could discuss defining an API to make a data directory descriptor available to the whole server.
There was a problem hiding this comment.
In the latest commit there are 2 directory descriptors involved, one for the data directory (which is shared by SQL layer and plugins) and one for Aria log directory. The data directory descriptor is opened once and never closed, and the Aria log directory descriptor is opened and closed once per backup. By default they point to the same directory, so for the default case the second descriptor could be optimized away altogether. This would be a small increase in code complexity, but the real cost is that we would have an additional path through code that would only be exercised by the non-default case. I have opted not to do this optimization and keep the code simple on the assumption that the cost of opening and closing one descriptor per backup is negligible.
If we decide otherwise, I can include this optimization and then perhaps also keep the log directory descriptor opened in the non-default case.
0de6368 to
8b1c81b
Compare
| case BACKUP_PHASE_NO_DML_NON_TRANS: | ||
| /* FIXME: Would be better to selectively purge only the tables we need. */ | ||
| tc_purge(); | ||
| tdc_purge(true); | ||
| return 0; |
There was a problem hiding this comment.
The comment is missing a reference to a ticket where this performance problem will be fixed. As far as I understand, we only need such cleanup for ENGINE=Aria and ENGINE=MyISAM. We don’t want unnecessary disruption of ENGINE=InnoDB. The BACKUP_PHASE_NO_COMMIT that will be executed a couple of steps after this one will be disruptive enough.
87036f8 to
4769a43
Compare
8b1c81b to
d23446e
Compare
| struct backup_sink; | ||
|
|
||
| /** A payload chunk in a sparse file that is being streamed */ | ||
| struct backup_chunk |
There was a problem hiding this comment.
What is the point of adding a forward declaration right before an actual declaration?
There was a problem hiding this comment.
backup_sink is not defined in this header (the thing defined is backup_chunk).
| /* RAII wrapper for my_dir() */ | ||
| class Dir_scan | ||
| { | ||
| public: | ||
| explicit Dir_scan(const char* path, myf flags) noexcept | ||
| { | ||
| dir_info= my_dir(path, flags); | ||
| if (!dir_info) | ||
| { | ||
| my_error(ER_CANT_READ_DIR, MYF(0), path, my_errno); | ||
| } | ||
| } | ||
| ~Dir_scan() noexcept | ||
| { | ||
| my_dirend(dir_info); | ||
| } | ||
| bool is_error() const noexcept | ||
| { | ||
| return !dir_info; | ||
| } |
There was a problem hiding this comment.
Please, do not execute potentially failing operations in a constructor.
Why is the destructor potentially invoking my_dirend(nullptr)?
What purpose does explicit have on a constructor that takes more than 1 parameter?
There was a problem hiding this comment.
nullptr seems to be valid input to my_dirend: there's no documentation for that function, but the definition checks for null and does nothing if it's passed. It's also a common convention: both the standard library free and the built-in delete operator are defined as no-op for nullptr.
Explicit constructors prevent the object from being implicitly constructed from a braced init list. In this case either case could be argued, I prefer to use explicit constructors everywhere unless conversion semantics are specifically wanted.
| MY_DIR *dir_info {nullptr}; | ||
| }; | ||
|
|
||
| std::string build_path(const char *base_path, const char *filename) noexcept; |
There was a problem hiding this comment.
It would be better to refactor my_dir() so that it can return a directory handle, on which openat may be invoked. We generally try to avoid operations on std::string because there already are enough issues with heap fragmentation.
There was a problem hiding this comment.
Given that this path needs to be constructed anyway for the target, and it would only be avoidable for non-streaming backup, I'm not sure how to write this code "well" to cover all cases (Windows, non-Windows streaming, non-Windows to directory) so as to strike the right between duplication and complexity. I propose that this PR be merged into MDEV-14992 without this particular optimization and then to figure out how to best optimize away string allocation.
There was a problem hiding this comment.
To my previous comment: because I fixed the generally incorrect assumption that the data directory is the current directory, there do need to be additional paths constructed to list subdirectories of the data directory. There is room for optimization to reduce the number of allocations, but since there are a number of ways to do this with trade-offs that are not always obvious, I propose to consider doing it as part of in MDEV-39987.
| if (phase == BACKUP_PHASE_NO_DDL) | ||
| fail= copy_misc_files(&target_phase->target, &target_phase->sink); | ||
| if (fail) | ||
| break; |
There was a problem hiding this comment.
This is unnecessarily testing fail when the first if does not hold.
| const backup_sink *sink) | ||
| { | ||
| Dir_scan datadir(".", MYF(MY_WANT_STAT)); | ||
| if (datadir.is_error()) |
There was a problem hiding this comment.
In the embedded server library libmariadbd, the data directory is not necessarily the current directory.
There was a problem hiding this comment.
Changed this to use mysql_real_data_home everywhere. Leaving open the questions for future improvements:
- Is it better to use
mysql_data_homeinstead? This is what's used in the logic to locate table files, - Is it worth to optimize for the special case where data directory is current directory, if we could e.g. reduce the number of heap allocations? (because we have fewer paths to format)
| static bool is_misc_file(const char* filename) | ||
| { | ||
| size_t filename_len= strlen(filename); | ||
| if (filename_len < ext_len) | ||
| return false; | ||
| const char *file_ext = filename + filename_len - ext_len; | ||
| return match_misc_ext(file_ext) || is_db_opt(filename, filename_len); | ||
| } |
There was a problem hiding this comment.
In 4769a43, Aria_backup::is_db_file() is using strrchr and strcmp for these, and it is omitting a . prefix from the suffix string that are being compared to. This feels unnecessarily complicated. Can you demonstrate that this approach results in smaller or more efficient code on at least one ISA?
There was a problem hiding this comment.
Locally benchmarking
a. the strrchr/strcmp method in 4769a43,
b. the method in 5235e7a Aria_backup::is_db_file() of taking the last 4 bytes as an int and checking using switch, and
c. the method committed here of checking the last 4 bytes by doing a linear scan against known extensions using memcmp,
method c. generates the smallest object code (only slightly smaller than a, but significantly smaller than b) and runs the fastest (only slightly faster than b, but significantly faster than a). I also feel the gains are not significant enough (in my experiment, less than 100 bytes difference in object code size, duration difference of about 1ms per backup on a data directory with 200 million files, which is in line with what I would expect it to be) to warrant an extended investigation with multiple toolchains and architectures. I don't feel that comparing the last 4 bytes is any more complicated than the other 2 methods.
| #if 1 // FIXME: invoke these only for Aria, MyISAM, CSV but not others | ||
| case BACKUP_PHASE_NO_DML_NON_TRANS: | ||
| /* FIXME: Would be better to selectively purge only the tables we need. | ||
| To be fixed in MDEV-39987. */ | ||
| tc_purge(); | ||
| tdc_purge(true); | ||
| break; | ||
| #endif |
There was a problem hiding this comment.
This is only partly addressing my previous comment about this. MDEV-39987 does not make it clear that this performance issue mainly affects other storage engines, such as ENGINE=InnoDB or ENGINE=RocksDB, which do not need any table handles to be closed.
d23446e to
d4b2048
Compare
9e27d73 to
5235e7a
Compare
d28041f to
8f37343
Compare
On top of the provisional Aria backup solution provisionally incorporated into MDEV-14992, the following improvements have been made: Aria data and index files are copied under DDL-locked lock level instead of commit-locked, making the backup operation less disruptive. Only log files are copied in the commit-locked phase. Writes to non-transactional Aria tables are blocked in the DDL-locked phase, while writes to transactional tables are written to the log file, allowing consistent point-in-time backup at the time of acquiring the commit lock. Data, index and log files are now copied as a "step" action rather than "end phase" action, allowing them to be copied in parallel using the CONCURRENT option. Non-Aria files, including common SQL-layer metadata and files from other storage engines are copied by the SQL layer rather than the Aria plugin. Note these files are at this time not copied concurrently when the concurrent option is used.
8f37343 to
b3645b2
Compare
The BACKUP SERVER functionality supports a number of non-ACID engines by simply copying their datafiles under a lock level which blocks other threads from accessing these tables. For this to work correctly the files must be consistent and up-to-date when copied. Thic is achieved by purging their table caches. This is the change to selectively purge caches only for tables, for which this is necessary. This covers the set of storage engines supported directly from SQL layer (MyISAM, CSV, ARCHIVE, Merge), and additionally the Aria engine, for which only non-transactional tables are purged. Purging of Table Definition Cache is only done for ARCHIVE angine tables, as it is needed to close the archive. Non-standard engine plugins will not have their table caches purged. If necessary, they need to implement the flushing/closing/purging logic in the plugin itself.
An interim solution with some room for optimization: