Feature requests for BackupableDB
Summary: This diff introduces some features that were requested by two internal customers: * Ability for backups not to share table files, because we can't guarantee that equal filename means equal content accross replicas * Ability for two threads to call EnableFileDeletions() and DisableFileDeletions() * Ability to stop backup from another thread and not slow down the DB close * Copy the files to the temporary folder first and then atomically rename Test Plan: Added some tests to backupable_db_test Reviewers: dhruba, sanketh, muthu, sdong, haobo Reviewed By: haobo CC: leveldb, sanketh, muthu Differential Revision: https://reviews.facebook.net/D14769
This commit is contained in:
parent
d0406675c2
commit
cb37ddf229
@ -31,6 +31,14 @@ struct BackupableDBOptions {
|
||||
// Default: nullptr
|
||||
Env* backup_env;
|
||||
|
||||
// If share_table_files == true, backup will assume that table files with
|
||||
// same name have the same contents. This enables incremental backups and
|
||||
// avoids unnecessary data copies.
|
||||
// If share_table_files == false, each backup will be on its own and will
|
||||
// not share any data with other backups.
|
||||
// default: true
|
||||
bool share_table_files;
|
||||
|
||||
// Backup info and error messages will be written to info_log
|
||||
// if non-nullptr.
|
||||
// Default: nullptr
|
||||
@ -49,6 +57,7 @@ struct BackupableDBOptions {
|
||||
|
||||
explicit BackupableDBOptions(const std::string& _backup_dir,
|
||||
Env* _backup_env = nullptr,
|
||||
bool _share_table_files = true,
|
||||
Logger* _info_log = nullptr,
|
||||
bool _sync = true,
|
||||
bool _destroy_old_data = false) :
|
||||
@ -93,6 +102,14 @@ class BackupableDB : public StackableDB {
|
||||
Status PurgeOldBackups(uint32_t num_backups_to_keep);
|
||||
// deletes a specific backup
|
||||
Status DeleteBackup(BackupID backup_id);
|
||||
// Call this from another thread if you want to stop the backup
|
||||
// that is currently happening. It will return immediatelly, will
|
||||
// not wait for the backup to stop.
|
||||
// The backup will stop ASAP and the call to CreateNewBackup will
|
||||
// return Status::Incomplete(). It will not clean up after itself, but
|
||||
// the state will remain consistent. The state will be cleaned up
|
||||
// next time you create BackupableDB or RestoreBackupableDB.
|
||||
void StopBackup();
|
||||
|
||||
private:
|
||||
BackupEngine* backup_engine_;
|
||||
@ -108,9 +125,10 @@ class RestoreBackupableDB {
|
||||
void GetBackupInfo(std::vector<BackupInfo>* backup_info);
|
||||
|
||||
// restore from backup with backup_id
|
||||
// IMPORTANT -- if you restore from some backup that is not the latest,
|
||||
// and you start creating new backups from the new DB, all the backups
|
||||
// that were newer than the backup you restored from will be deleted
|
||||
// IMPORTANT -- if options_.share_table_files == true and you restore DB
|
||||
// from some backup that is not the latest, and you start creating new
|
||||
// backups from the new DB, all the backups that were newer than the
|
||||
// backup you restored from will be deleted
|
||||
//
|
||||
// Example: Let's say you have backups 1, 2, 3, 4, 5 and you restore 3.
|
||||
// If you try creating a new backup now, old backups 4 and 5 will be deleted
|
||||
|
@ -20,6 +20,7 @@
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <limits>
|
||||
#include <atomic>
|
||||
|
||||
namespace rocksdb {
|
||||
|
||||
@ -31,6 +32,9 @@ class BackupEngine {
|
||||
Status CreateNewBackup(DB* db, bool flush_before_backup = false);
|
||||
Status PurgeOldBackups(uint32_t num_backups_to_keep);
|
||||
Status DeleteBackup(BackupID backup_id);
|
||||
void StopBackup() {
|
||||
stop_backup_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
void GetBackupInfo(std::vector<BackupInfo>* backup_info);
|
||||
Status RestoreDBFromBackup(BackupID backup_id, const std::string &db_dir,
|
||||
@ -106,13 +110,16 @@ class BackupEngine {
|
||||
return "private";
|
||||
}
|
||||
inline std::string GetPrivateFileRel(BackupID backup_id,
|
||||
const std::string &file = "") const {
|
||||
bool tmp = false,
|
||||
const std::string& file = "") const {
|
||||
assert(file.size() == 0 || file[0] != '/');
|
||||
return GetPrivateDirRel() + "/" + std::to_string(backup_id) + "/" + file;
|
||||
return GetPrivateDirRel() + "/" + std::to_string(backup_id) +
|
||||
(tmp ? ".tmp" : "") + "/" + file;
|
||||
}
|
||||
inline std::string GetSharedFileRel(const std::string& file = "") const {
|
||||
inline std::string GetSharedFileRel(const std::string& file = "",
|
||||
bool tmp = false) const {
|
||||
assert(file.size() == 0 || file[0] != '/');
|
||||
return "shared/" + file;
|
||||
return "shared/" + file + (tmp ? ".tmp" : "");
|
||||
}
|
||||
inline std::string GetLatestBackupFile(bool tmp = false) const {
|
||||
return GetAbsolutePath(std::string("LATEST_BACKUP") + (tmp ? ".tmp" : ""));
|
||||
@ -151,6 +158,7 @@ class BackupEngine {
|
||||
std::map<BackupID, BackupMeta> backups_;
|
||||
std::unordered_map<std::string, int> backuped_file_refs_;
|
||||
std::vector<BackupID> obsolete_backups_;
|
||||
std::atomic<bool> stop_backup_;
|
||||
|
||||
// options data
|
||||
BackupableDBOptions options_;
|
||||
@ -161,13 +169,17 @@ class BackupEngine {
|
||||
};
|
||||
|
||||
BackupEngine::BackupEngine(Env* db_env, const BackupableDBOptions& options)
|
||||
: options_(options),
|
||||
db_env_(db_env),
|
||||
backup_env_(options.backup_env != nullptr ? options.backup_env : db_env_) {
|
||||
: stop_backup_(false),
|
||||
options_(options),
|
||||
db_env_(db_env),
|
||||
backup_env_(options.backup_env != nullptr ? options.backup_env
|
||||
: db_env_) {
|
||||
|
||||
// create all the dirs we need
|
||||
backup_env_->CreateDirIfMissing(GetAbsolutePath());
|
||||
backup_env_->CreateDirIfMissing(GetAbsolutePath(GetSharedFileRel()));
|
||||
if (!options_.share_table_files) {
|
||||
backup_env_->CreateDirIfMissing(GetAbsolutePath(GetSharedFileRel()));
|
||||
}
|
||||
backup_env_->CreateDirIfMissing(GetAbsolutePath(GetPrivateDirRel()));
|
||||
backup_env_->CreateDirIfMissing(GetBackupMetaDir());
|
||||
|
||||
@ -298,8 +310,9 @@ Status BackupEngine::CreateNewBackup(DB* db, bool flush_before_backup) {
|
||||
Log(options_.info_log, "Started the backup process -- creating backup %u",
|
||||
new_backup_id);
|
||||
|
||||
// create private dir
|
||||
s = backup_env_->CreateDir(GetAbsolutePath(GetPrivateFileRel(new_backup_id)));
|
||||
// create temporary private dir
|
||||
s = backup_env_->CreateDir(
|
||||
GetAbsolutePath(GetPrivateFileRel(new_backup_id, true)));
|
||||
|
||||
// copy live_files
|
||||
for (size_t i = 0; s.ok() && i < live_files.size(); ++i) {
|
||||
@ -320,7 +333,7 @@ Status BackupEngine::CreateNewBackup(DB* db, bool flush_before_backup) {
|
||||
// * if it's kDescriptorFile, limit the size to manifest_file_size
|
||||
s = BackupFile(new_backup_id,
|
||||
&new_backup,
|
||||
type == kTableFile, /* shared */
|
||||
options_.share_table_files && type == kTableFile,
|
||||
db->GetName(), /* src_dir */
|
||||
live_files[i], /* src_fname */
|
||||
(type == kDescriptorFile) ? manifest_file_size : 0);
|
||||
@ -342,6 +355,13 @@ Status BackupEngine::CreateNewBackup(DB* db, bool flush_before_backup) {
|
||||
// we copied all the files, enable file deletions
|
||||
db->EnableFileDeletions();
|
||||
|
||||
if (s.ok()) {
|
||||
// move tmp private backup to real backup folder
|
||||
s = backup_env_->RenameFile(
|
||||
GetAbsolutePath(GetPrivateFileRel(new_backup_id, true)), // tmp
|
||||
GetAbsolutePath(GetPrivateFileRel(new_backup_id, false)));
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
// persist the backup metadata on the disk
|
||||
s = new_backup.StoreToFile(options_.sync);
|
||||
@ -561,6 +581,9 @@ Status BackupEngine::CopyFile(const std::string& src,
|
||||
Slice data;
|
||||
|
||||
do {
|
||||
if (stop_backup_.load(std::memory_order_acquire)) {
|
||||
return Status::Incomplete("Backup stopped");
|
||||
}
|
||||
size_t buffer_to_read = (copy_file_buffer_size_ < size_limit) ?
|
||||
copy_file_buffer_size_ : size_limit;
|
||||
s = src_file->Read(buffer_to_read, &data, buf.get());
|
||||
@ -590,12 +613,16 @@ Status BackupEngine::BackupFile(BackupID backup_id,
|
||||
|
||||
assert(src_fname.size() > 0 && src_fname[0] == '/');
|
||||
std::string dst_relative = src_fname.substr(1);
|
||||
std::string dst_relative_tmp;
|
||||
if (shared) {
|
||||
dst_relative = GetSharedFileRel(dst_relative);
|
||||
dst_relative_tmp = GetSharedFileRel(dst_relative, true);
|
||||
dst_relative = GetSharedFileRel(dst_relative, false);
|
||||
} else {
|
||||
dst_relative = GetPrivateFileRel(backup_id, dst_relative);
|
||||
dst_relative_tmp = GetPrivateFileRel(backup_id, true, dst_relative);
|
||||
dst_relative = GetPrivateFileRel(backup_id, false, dst_relative);
|
||||
}
|
||||
std::string dst_path = GetAbsolutePath(dst_relative);
|
||||
std::string dst_path_tmp = GetAbsolutePath(dst_relative_tmp);
|
||||
Status s;
|
||||
uint64_t size;
|
||||
|
||||
@ -607,12 +634,15 @@ Status BackupEngine::BackupFile(BackupID backup_id,
|
||||
} else {
|
||||
Log(options_.info_log, "Copying %s", src_fname.c_str());
|
||||
s = CopyFile(src_dir + src_fname,
|
||||
dst_path,
|
||||
dst_path_tmp,
|
||||
db_env_,
|
||||
backup_env_,
|
||||
options_.sync,
|
||||
&size,
|
||||
size_limit);
|
||||
if (s.ok() && shared) {
|
||||
s = backup_env_->RenameFile(dst_path_tmp, dst_path);
|
||||
}
|
||||
}
|
||||
if (s.ok()) {
|
||||
backup->AddFile(dst_relative, size);
|
||||
@ -671,14 +701,16 @@ void BackupEngine::GarbageCollection(bool full_scan) {
|
||||
&private_children);
|
||||
for (auto& child : private_children) {
|
||||
BackupID backup_id = 0;
|
||||
bool tmp_dir = child.find(".tmp") != std::string::npos;
|
||||
sscanf(child.c_str(), "%u", &backup_id);
|
||||
if (backup_id == 0 || backups_.find(backup_id) != backups_.end()) {
|
||||
if (!tmp_dir && // if it's tmp_dir, delete it
|
||||
(backup_id == 0 || backups_.find(backup_id) != backups_.end())) {
|
||||
// it's either not a number or it's still alive. continue
|
||||
continue;
|
||||
}
|
||||
// here we have to delete the dir and all its children
|
||||
std::string full_private_path =
|
||||
GetAbsolutePath(GetPrivateFileRel(backup_id));
|
||||
GetAbsolutePath(GetPrivateFileRel(backup_id, tmp_dir));
|
||||
std::vector<std::string> subchildren;
|
||||
backup_env_->GetChildren(full_private_path, &subchildren);
|
||||
for (auto& subchild : subchildren) {
|
||||
@ -813,7 +845,9 @@ Status BackupEngine::BackupMeta::StoreToFile(bool sync) {
|
||||
|
||||
BackupableDB::BackupableDB(DB* db, const BackupableDBOptions& options)
|
||||
: StackableDB(db), backup_engine_(new BackupEngine(db->GetEnv(), options)) {
|
||||
backup_engine_->DeleteBackupsNewerThan(GetLatestSequenceNumber());
|
||||
if (options.share_table_files) {
|
||||
backup_engine_->DeleteBackupsNewerThan(GetLatestSequenceNumber());
|
||||
}
|
||||
}
|
||||
|
||||
BackupableDB::~BackupableDB() {
|
||||
@ -836,6 +870,10 @@ Status BackupableDB::DeleteBackup(BackupID backup_id) {
|
||||
return backup_engine_->DeleteBackup(backup_id);
|
||||
}
|
||||
|
||||
void BackupableDB::StopBackup() {
|
||||
backup_engine_->StopBackup();
|
||||
}
|
||||
|
||||
// --- RestoreBackupableDB methods ------
|
||||
|
||||
RestoreBackupableDB::RestoreBackupableDB(Env* db_env,
|
||||
|
@ -305,7 +305,7 @@ class BackupableDBTest {
|
||||
CreateLoggerFromOptions(dbname_, backupdir_, env_,
|
||||
Options(), &logger_);
|
||||
backupable_options_.reset(new BackupableDBOptions(
|
||||
backupdir_, test_backup_env_.get(), logger_.get(), true));
|
||||
backupdir_, test_backup_env_.get(), true, logger_.get(), true));
|
||||
|
||||
// delete old files in db
|
||||
DestroyDB(dbname_, Options());
|
||||
@ -317,7 +317,8 @@ class BackupableDBTest {
|
||||
return db;
|
||||
}
|
||||
|
||||
void OpenBackupableDB(bool destroy_old_data = false, bool dummy = false) {
|
||||
void OpenBackupableDB(bool destroy_old_data = false, bool dummy = false,
|
||||
bool share_table_files = true) {
|
||||
// reset all the defaults
|
||||
test_backup_env_->SetLimitWrittenFiles(1000000);
|
||||
test_db_env_->SetLimitWrittenFiles(1000000);
|
||||
@ -331,6 +332,7 @@ class BackupableDBTest {
|
||||
ASSERT_OK(DB::Open(options_, dbname_, &db));
|
||||
}
|
||||
backupable_options_->destroy_old_data = destroy_old_data;
|
||||
backupable_options_->share_table_files = share_table_files;
|
||||
db_.reset(new BackupableDB(db, *backupable_options_));
|
||||
}
|
||||
|
||||
@ -659,6 +661,38 @@ TEST(BackupableDBTest, DeleteNewerBackups) {
|
||||
CloseRestoreDB();
|
||||
}
|
||||
|
||||
TEST(BackupableDBTest, NoShareTableFiles) {
|
||||
const int keys_iteration = 5000;
|
||||
OpenBackupableDB(true, false, false);
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
FillDB(db_.get(), keys_iteration * i, keys_iteration * (i + 1));
|
||||
ASSERT_OK(db_->CreateNewBackup(!!(i % 2)));
|
||||
}
|
||||
CloseBackupableDB();
|
||||
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
AssertBackupConsistency(i + 1, 0, keys_iteration * (i + 1),
|
||||
keys_iteration * 6);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(BackupableDBTest, DeleteTmpFiles) {
|
||||
OpenBackupableDB();
|
||||
CloseBackupableDB();
|
||||
std::string shared_tmp = backupdir_ + "/shared/00006.sst.tmp";
|
||||
std::string private_tmp_dir = backupdir_ + "/private/10.tmp";
|
||||
std::string private_tmp_file = private_tmp_dir + "/00003.sst";
|
||||
file_manager_->WriteToFile(shared_tmp, "tmp");
|
||||
file_manager_->CreateDir(private_tmp_dir);
|
||||
file_manager_->WriteToFile(private_tmp_file, "tmp");
|
||||
ASSERT_EQ(true, file_manager_->FileExists(private_tmp_dir));
|
||||
OpenBackupableDB();
|
||||
CloseBackupableDB();
|
||||
ASSERT_EQ(false, file_manager_->FileExists(shared_tmp));
|
||||
ASSERT_EQ(false, file_manager_->FileExists(private_tmp_file));
|
||||
ASSERT_EQ(false, file_manager_->FileExists(private_tmp_dir));
|
||||
}
|
||||
|
||||
} // anon namespace
|
||||
|
||||
} // namespace rocksdb
|
||||
|
Loading…
Reference in New Issue
Block a user