提交 02f2b208 编写于 作者: P Peter Dillinger 提交者: Facebook GitHub Bot

Add BackupEngine feature to exclude files (#11030)

Summary:
We have a request for RocksDB to essentially support
disconnected incremental backup. In other words, if there is limited
or no connectivity to the primary backup dir, we should still be able to
take an incremental backup relative to that primary backup dir,
assuming some metadata about that primary dir is available (and
obviously anticipating primary backup dir will be fully available if
restore is needed).

To support that, this feature allows the API user to "exclude" DB
files from backup. This only applies to files that can be shared
between backups (sst and blob files), and excluded files are
tracked in the backup metadata sufficiently to ensure they are
restored at restore time. At restore time, the user provides
a set of alternate backup directories (as open BackupEngines, which
can be read-only), and excluded files must be found in one of the
backup directories ("included" in some backup).

This feature depends on backup schema version 2 features, though
schema version 2.0 support is not sufficient to read / restore a
backup with exclusions. This change updates the schema version to
2.1 because of this feature, so that it's easy to recognize whether
a RocksDB release supports this feature, while backups not using the
feature are fully compatible with 2.0.

Also in this PR:
* Stacked on https://github.com/facebook/rocksdb/pull/11029
* Allow progress_callback to be empty, not just no-op function, and
recover from exceptions thrown by BackupEngine callbacks.
* The internal-only `AsBackupEngine()` function is working around the
diamond hierarchy of `BackupEngineImplThreadSafe` to get to the
internals, without using confusing features like virtual inheritance.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/11030

Test Plan: unit tests added / updated

Reviewed By: ajkr

Differential Revision: D42004388

Pulled By: pdillinger

fbshipit-source-id: 31b6e533d308a5462e528d9012d650482d974077
上级 bec42648
......@@ -19,6 +19,9 @@
### New Features
* When an SstPartitionerFactory is configured, CompactRange() now automatically selects for compaction any files overlapping a partition boundary that is in the compaction range, even if no actual entries are in the requested compaction range. With this feature, manual compaction can be used to (re-)establish SST partition points when SstPartitioner changes, without a full compaction.
### New Features
* Add BackupEngine feature to exclude files from backup that are known to be backed up elsewhere, using `CreateBackupOptions::exclude_files_callback`. To restore the DB, the excluded files must be provided in alternative backup directories using `RestoreOptions::alternate_dirs`.
## 7.9.0 (11/21/2022)
### Performance Improvements
* Fixed an iterator performance regression for delete range users when scanning through a consecutive sequence of range tombstones (#10877).
......
......@@ -11,6 +11,7 @@
#ifndef ROCKSDB_LITE
#include <cstdint>
#include <forward_list>
#include <functional>
#include <map>
#include <string>
......@@ -23,6 +24,8 @@
#include "rocksdb/status.h"
namespace ROCKSDB_NAMESPACE {
class BackupEngineReadOnlyBase;
class BackupEngine;
// The default DB file checksum function name.
constexpr char kDbFileChecksumFuncName[] = "FileChecksumCrc32c";
......@@ -270,6 +273,28 @@ inline BackupEngineOptions::ShareFilesNaming operator|(
return static_cast<BackupEngineOptions::ShareFilesNaming>(l | r);
}
// Identifying information about a backup shared file that is (or might be)
// excluded from a backup using exclude_files_callback.
struct BackupExcludedFileInfo {
explicit BackupExcludedFileInfo(const std::string& _relative_file)
: relative_file(_relative_file) {}
// File name and path relative to the backup dir.
std::string relative_file;
};
// An auxiliary structure for exclude_files_callback
struct MaybeExcludeBackupFile {
explicit MaybeExcludeBackupFile(BackupExcludedFileInfo&& _info)
: info(std::move(_info)) {}
// Identifying information about a backup shared file that could be excluded
const BackupExcludedFileInfo info;
// API user sets to true if the file should be excluded from this backup
bool exclude_decision = false;
};
struct CreateBackupOptions {
// Flush will always trigger if 2PC is enabled.
// If write-ahead logs are disabled, set flush_before_backup=true to
......@@ -278,10 +303,31 @@ struct CreateBackupOptions {
// Callback for reporting progress, based on callback_trigger_interval_size.
//
// RocksDB callbacks are NOT exception-safe. A callback completing with an
// exception can lead to undefined behavior in RocksDB, including data loss,
// unreported corruption, deadlocks, and more.
std::function<void()> progress_callback = []() {};
// An exception thrown from the callback will result in Status::Aborted from
// the operation.
std::function<void()> progress_callback = {};
// A callback that allows the API user to select files for exclusion, such
// as if the files are known to exist in an alternate backup directory.
// Only "shared" files can be excluded from backups. This is an advanced
// feature because the BackupEngine user is trusted to keep track of files
// such that the DB can be restored.
//
// Input to the callback is a [begin,end) range of sharable files live in
// the DB being backed up, and the callback implementation sets
// exclude_decision=true for files to exclude. A callback offers maximum
// flexibility, e.g. if remote files are unavailable at backup time but
// whose existence has been recorded somewhere. In case of an empty or
// no-op callback, all files are included in the backup .
//
// To restore the DB, RestoreOptions::alternate_dirs must be used to provide
// the excluded files.
//
// An exception thrown from the callback will result in Status::Aborted from
// the operation.
std::function<void(MaybeExcludeBackupFile* files_begin,
MaybeExcludeBackupFile* files_end)>
exclude_files_callback = {};
// If false, background_thread_cpu_priority is ignored.
// Otherwise, the cpu priority can be decreased,
......@@ -300,6 +346,11 @@ struct RestoreOptions {
// Default: false
bool keep_log_files;
// For backups that were created using exclude_files_callback, this
// option enables restoring those backups by providing BackupEngines on
// directories known to contain the required files.
std::forward_list<BackupEngineReadOnlyBase*> alternate_dirs;
explicit RestoreOptions(bool _keep_log_files = false)
: keep_log_files(_keep_log_files) {}
};
......@@ -324,9 +375,15 @@ struct BackupInfo {
// Backup API user metadata
std::string app_metadata;
// Backup file details, if requested with include_file_details=true
// Backup file details, if requested with include_file_details=true.
// Does not include excluded_files.
std::vector<BackupFileInfo> file_details;
// Identifying information about shared files that were excluded from the
// created backup. See exclude_files_callback and alternate_dirs.
// This information is only provided if include_file_details=true.
std::vector<BackupExcludedFileInfo> excluded_files;
// DB "name" (a directory in the backup_env) for opening this backup as a
// read-only DB. This should also be used as the DBOptions::wal_dir, such
// as by default setting wal_dir="". See also env_for_open.
......@@ -348,8 +405,8 @@ struct BackupInfo {
BackupInfo() {}
BackupInfo(BackupID _backup_id, int64_t _timestamp, uint64_t _size,
uint32_t _number_files, const std::string& _app_metadata)
explicit BackupInfo(BackupID _backup_id, int64_t _timestamp, uint64_t _size,
uint32_t _number_files, const std::string& _app_metadata)
: backup_id(_backup_id),
timestamp(_timestamp),
size(_size),
......@@ -364,8 +421,8 @@ class BackupStatistics {
number_fail_backup = 0;
}
BackupStatistics(uint32_t _number_success_backup,
uint32_t _number_fail_backup)
explicit BackupStatistics(uint32_t _number_success_backup,
uint32_t _number_fail_backup)
: number_success_backup(_number_success_backup),
number_fail_backup(_number_fail_backup) {}
......@@ -462,6 +519,9 @@ class BackupEngineReadOnlyBase {
// Returns Status::OK() if all checks are good
virtual IOStatus VerifyBackup(BackupID backup_id,
bool verify_with_checksum = false) const = 0;
// Internal use only
virtual BackupEngine* AsBackupEngine() = 0;
};
// Append-only functions of a BackupEngine. See BackupEngine comment for
......
此差异已折叠。
......@@ -16,9 +16,11 @@
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <limits>
#include <memory>
#include <random>
#include <stdexcept>
#include <string>
#include <utility>
......@@ -754,7 +756,7 @@ class BackupEngineTest : public testing::Test {
void CloseBackupEngine() { backup_engine_.reset(nullptr); }
// cross-cutting test of GetBackupInfo
void AssertBackupInfoConsistency() {
void AssertBackupInfoConsistency(bool allow_excluded = false) {
std::vector<BackupInfo> backup_info;
backup_engine_->GetBackupInfo(&backup_info, /*with file details*/ true);
std::map<std::string, uint64_t> file_sizes;
......@@ -774,6 +776,9 @@ class BackupEngineTest : public testing::Test {
sum_for_backup += file.size;
}
ASSERT_EQ(backup.size, sum_for_backup);
if (!allow_excluded) {
ASSERT_EQ(backup.excluded_files.size(), 0);
}
}
std::vector<BackupID> corrupt_backup_ids;
......@@ -3094,10 +3099,25 @@ TEST_F(BackupEngineTest, OpenBackupAsReadOnlyDB) {
TEST_F(BackupEngineTest, ProgressCallbackDuringBackup) {
DestroyDBWithoutCheck(dbname_, options_);
// Too big for this small DB
engine_options_->callback_trigger_interval_size = 100000;
OpenDBAndBackupEngine(true);
FillDB(db_.get(), 0, 100);
// First test exception handling
// Easily small enough for this small DB
engine_options_->callback_trigger_interval_size = 1000;
OpenBackupEngine();
ASSERT_TRUE(
backup_engine_->CreateNewBackup(db_.get(), true, []() { throw 42; })
.IsAborted());
ASSERT_TRUE(backup_engine_
->CreateNewBackup(db_.get(), true,
[]() { throw std::out_of_range("blah"); })
.IsAborted());
// Too big for this small DB
engine_options_->callback_trigger_interval_size = 100000;
OpenBackupEngine();
bool is_callback_invoked = false;
ASSERT_OK(backup_engine_->CreateNewBackup(
db_.get(), true,
......@@ -4180,7 +4200,7 @@ TEST_F(BackupEngineTest, FileTemperatures) {
&info, /*include_file_details*/ true));
ASSERT_GT(info.file_details.size(), 2);
for (auto& e : info.file_details) {
ASSERT_EQ(expected_temps[e.file_number], e.temperature);
EXPECT_EQ(expected_temps[e.file_number], e.temperature);
}
// Restore backup to another virtual (tiered) dir
......@@ -4202,6 +4222,175 @@ TEST_F(BackupEngineTest, FileTemperatures) {
}
}
TEST_F(BackupEngineTest, ExcludeFiles) {
// Required for excluding files
engine_options_->schema_version = 2;
// Need a sufficent set of file numbers
options_.level0_file_num_compaction_trigger = 100;
OpenDBAndBackupEngine(true, false, kShareWithChecksum);
// Need a sufficent set of file numbers
const int keys_iteration = 5000;
FillDB(db_.get(), 0, keys_iteration / 3);
FillDB(db_.get(), keys_iteration / 3, keys_iteration * 2 / 3);
FillDB(db_.get(), keys_iteration * 2 / 3, keys_iteration);
CloseAndReopenDB();
BackupEngine* alt_backup_engine;
BackupEngineOptions alt_engine_options{*engine_options_};
// Use an alternate Env to test that support
std::string backup_alt_chroot = test::PerThreadDBPath("db_alt_backups");
EXPECT_OK(Env::Default()->CreateDirIfMissing(backup_alt_chroot));
alt_engine_options.backup_dir = "/altbk";
std::shared_ptr<FileSystem> alt_fs{
NewChrootFileSystem(FileSystem::Default(), backup_alt_chroot)};
std::unique_ptr<Env> alt_env{new CompositeEnvWrapper(Env::Default(), alt_fs)};
alt_engine_options.backup_env = alt_env.get();
ASSERT_OK(BackupEngine::Open(test_db_env_.get(), alt_engine_options,
&alt_backup_engine));
// Ensure each backup is same set of files
db_.reset();
DB* db = nullptr;
ASSERT_OK(DB::OpenForReadOnly(options_, dbname_, &db));
// A callback that throws should cleanly fail the backup creation.
// Do this early to ensure later operations still work.
CreateBackupOptions cbo;
cbo.exclude_files_callback = [](MaybeExcludeBackupFile* /*files_begin*/,
MaybeExcludeBackupFile* /*files_end*/) {
throw 42;
};
ASSERT_TRUE(backup_engine_->CreateNewBackup(cbo, db).IsAborted());
cbo.exclude_files_callback = [](MaybeExcludeBackupFile* /*files_begin*/,
MaybeExcludeBackupFile* /*files_end*/) {
throw std::out_of_range("blah");
};
ASSERT_TRUE(backup_engine_->CreateNewBackup(cbo, db).IsAborted());
// Include files only in given bucket, based on modulus and remainder
constexpr int modulus = 4;
int remainder = 0;
cbo.exclude_files_callback = [&remainder](MaybeExcludeBackupFile* files_begin,
MaybeExcludeBackupFile* files_end) {
for (auto* f = files_begin; f != files_end; ++f) {
std::string s = StringSplit(f->info.relative_file, '/').back();
s = s.substr(0, s.find("_"));
int64_t num = std::strtoll(s.c_str(), nullptr, /*base*/ 10);
// Exclude if not a match
f->exclude_decision = (num % modulus) != remainder;
}
};
BackupID first_id{};
BackupID last_alt_id{};
remainder = 0;
ASSERT_OK(backup_engine_->CreateNewBackup(cbo, db, &first_id));
AssertBackupInfoConsistency(/*allow excluded*/ true);
remainder = 1;
ASSERT_OK(alt_backup_engine->CreateNewBackup(cbo, db));
AssertBackupInfoConsistency(/*allow excluded*/ true);
remainder = 2;
ASSERT_OK(backup_engine_->CreateNewBackup(cbo, db));
AssertBackupInfoConsistency(/*allow excluded*/ true);
remainder = 3;
ASSERT_OK(alt_backup_engine->CreateNewBackup(cbo, db, &last_alt_id));
AssertBackupInfoConsistency(/*allow excluded*/ true);
// Close DB
ASSERT_OK(db->Close());
delete db;
db = nullptr;
for (auto be_pair :
{std::make_pair(backup_engine_.get(), alt_backup_engine),
std::make_pair(alt_backup_engine, backup_engine_.get())}) {
DestroyDB(dbname_, options_);
RestoreOptions ro;
// Fails without alternate dir
ASSERT_TRUE(be_pair.first->RestoreDBFromLatestBackup(dbname_, dbname_, ro)
.IsInvalidArgument());
DestroyDB(dbname_, options_);
// Works with alternate dir
ro.alternate_dirs.push_front(be_pair.second);
ASSERT_OK(be_pair.first->RestoreDBFromLatestBackup(dbname_, dbname_, ro));
// Check DB contents
db = OpenDB();
AssertExists(db, 0, keys_iteration);
delete db;
}
// Should still work after close and re-open
CloseBackupEngine();
OpenBackupEngine();
for (auto be_pair :
{std::make_pair(backup_engine_.get(), alt_backup_engine),
std::make_pair(alt_backup_engine, backup_engine_.get())}) {
DestroyDB(dbname_, options_);
RestoreOptions ro;
ro.alternate_dirs.push_front(be_pair.second);
ASSERT_OK(be_pair.first->RestoreDBFromLatestBackup(dbname_, dbname_, ro));
}
// Deletion semantics are tricky when within a single backup dir one backup
// includes a file and the other backup excluded the file. The excluded one
// does not have a persistent record of metadata like file checksum, etc.
// Although it would be possible to amend the backup with the excluded file,
// that is not currently supported (unless you open the backup as read-only
// DB and take another backup of it). The "excluded" reference to the file
// is like a weak reference: it doesn't prevent the file from being deleted
// if all the backups with "included" references to it are deleted.
CloseBackupEngine();
OpenBackupEngine();
AssertBackupInfoConsistency(/*allow excluded*/ true);
ASSERT_OK(backup_engine_->DeleteBackup(first_id));
ASSERT_OK(alt_backup_engine->DeleteBackup(last_alt_id));
// Includes check for any leaked backup files
AssertBackupInfoConsistency(/*allow excluded*/ true);
// Excluded file(s) deleted, unable to restore
for (auto be_pair :
{std::make_pair(backup_engine_.get(), alt_backup_engine),
std::make_pair(alt_backup_engine, backup_engine_.get())}) {
RestoreOptions ro;
ro.alternate_dirs.push_front(be_pair.second);
ASSERT_TRUE(be_pair.first->RestoreDBFromLatestBackup(dbname_, dbname_, ro)
.IsInvalidArgument());
}
// Close & Re-open (no crash, etc.)
CloseBackupEngine();
OpenBackupEngine();
AssertBackupInfoConsistency(/*allow excluded*/ true);
// Excluded file(s) deleted, unable to restore
for (auto be_pair :
{std::make_pair(backup_engine_.get(), alt_backup_engine),
std::make_pair(alt_backup_engine, backup_engine_.get())}) {
RestoreOptions ro;
ro.alternate_dirs.push_front(be_pair.second);
ASSERT_TRUE(be_pair.first->RestoreDBFromLatestBackup(dbname_, dbname_, ro)
.IsInvalidArgument());
}
// Ensure files are not leaked after removing everything.
ASSERT_OK(backup_engine_->DeleteBackup(first_id + 1));
ASSERT_OK(alt_backup_engine->DeleteBackup(last_alt_id - 1));
// Includes check for leaked backups files
AssertBackupInfoConsistency(/*allow excluded*/ false);
}
} // namespace
} // namespace ROCKSDB_NAMESPACE
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册