Backup API

对于C++ APi,参考include/rocksdb/utilities/backupable_db.h。主要的抽象是备份引擎,会暴露一些简单的接口用于创建备份,获取备份信息,以及从备份中恢复。有两个不同的备份引擎实现:(1)BackupEngine用于创建新的备份,以及(2)BackupEngineReadOnly用于从备份恢复数据。他们都可以用于获取备份相关的信息。

创建并校验一份备份

在RocksDB,我们实现了一个简单的方法来备份db,以及校验其正确性。这里是一个简单的例子:

  1. #include "rocksdb/db.h"
  2. #include "rocksdb/utilities/backupable_db.h"
  3. #include <vector>
  4. using namespace rocksdb;
  5. int main() {
  6. Options options;
  7. options.create_if_missing = true;
  8. DB* db;
  9. Status s = DB::Open(options, "/tmp/rocksdb", &db);
  10. assert(s.ok());
  11. db->Put(...); // do your thing
  12. BackupEngine* backup_engine;
  13. s = BackupEngine::Open(Env::Default(), BackupableDBOptions("/tmp/rocksdb_backup"), &backup_engine);
  14. assert(s.ok());
  15. s = backup_engine->CreateNewBackup(db);
  16. assert(s.ok());
  17. db->Put(...); // make some more changes
  18. s = backup_engine->CreateNewBackup(db);
  19. assert(s.ok());
  20. std::vector<BackupInfo> backup_info;
  21. backup_engine->GetBackupInfo(&backup_info);
  22. // you can get IDs from backup_info if there are more than two
  23. s = backup_engine->VerifyBackup(1 /* ID */);
  24. assert(s.ok());
  25. s = backup_engine->VerifyBackup(2 /* ID */);
  26. assert(s.ok());
  27. delete db;
  28. delete backup_engine;
  29. }

这个简单的例子会创建一对备份到/tmp/rocksdb_backup。注意你可以在同一个引擎里创建并校验多个备份。

备份正常来说是增量的(参考BackupableDBOptions::share_table_files)。你可以使用BackupEngine::CreateNewBackup()创建一个新的备份,并且只有最新的数据会被拷贝到备份目录(更多细节,参考底层实现

一旦你有一些保存好的备份,你可以调用BackupEngine::GetBackupInfo()来获取一个备份列表以及时间戳对应的备份信息大小信息(注意,所有备份的和大于实际的备份目录的大小,因为有些数据会被多个备份共享)。备份可以通过自增ID做唯一标志。

BackupEngine::VerifyBackups()被调用,他会检查备份目录的文件大小和db目录下原始文件的大小。然而,我们不检查校验和,因为这需要读取所有数据。注意BackupEngine::VerifyBackups()唯一的合理用途是 同一个引擎在因为在备份过程中,状态被捕获,被用于创建(多个)备份后,在一个备份引擎上调用他。

从备份恢复

恢复备份也很简单:

  1. #include "rocksdb/db.h"
  2. #include "rocksdb/utilities/backupable_db.h"
  3. using namespace rocksdb;
  4. int main() {
  5. BackupEngineReadOnly* backup_engine;
  6. Status s = BackupEngineReadOnly::Open(Env::Default(), BackupableDBOptions("/tmp/rocksdb_backup"), &backup_engine);
  7. assert(s.ok());
  8. backup_engine->RestoreDBFromBackup(1, "/tmp/rocksdb", "/tmp/rocksdb");
  9. delete backup_engine;
  10. }

这段代码会读取/tmp/rocksdb里的第一个备份。BackupEngineReadOnly::RestoreDBFromBackup()的第一个参数是备份ID,第二个参数是目标DB目录,第三个是日志文件的目标地址(在某些DB,他们可能跟DB目录不同,但是大多数时候他们是同一个目录,参考Options::wal_dir了解更多信息)。BackupEngineReadOnly::RestoreDBFromLatestBackup()会从最新的备份里恢复DB,也就是说,ID最高的那个备份。

每个恢复的文件的校验和都要被计算,然后与备份的文件进行比较。如果校验和不一致,恢复过程会终止,并且会返回Status::Corruption

你必须重新打开任何线上的数据库,才能看到恢复的数据。

备份目录结构

  1. /tmp/rocksdb_backup/
  2. ├── LATEST_BACKUP
  3. ├── meta
  4. └── 1
  5. ├── private
  6. └── 1
  7. ├── CURRENT
  8. ├── MANIFEST-000008
  9. | └── OPTIONS-000009
  10. └── shared_checksum
  11. └── 000007_1498774076_590.sst

LATEST_BACKUP是一个包含最高备份ID的文件。在我们上面的例子里,它里面有一个”1”。他被用于获取最新的备份编号,但是现在不再需要他了,因为有一个更简单的通过META文件获取的方式。这个文件会在Rocksdb5.0被移除。

meta 目录包含一个“meta文件”,描述每一个备份,他的名字为备份ID。例如,一个meta文件包含一个包含该备份的所有文件的列表。格式在其实现文件中被描述(utilities/backupable/backupable_db.cc)。

private目录总是包含非SST文件(options, current, manifest, 以及WAL)。如果Options::share_table_files被置空,他还会包含SST文件。

shared目录(未展示)在设置了Options::share_table_files而没有设置Options::share_files_with_checksum的时候包含SST文件。在这个目录,文件直接用他们在原DB里的名称命名。所以他只应该被用于备份一个单一RocksDB实例;否则,文件名会冲突。

shared_checksum目录在置了Options::share_table_filesOptions::share_files_with_checksum的时候包含SST文件。在这个目录,文件用他们在原DB里的名称,大小和校验和命名。这些属性能帮助来自多个RocksDB实例的文件获得唯一标志。

备份性能

请注意,备份引擎的Open()的时间开销 跟已经存在的备份数量成正比,因为我们需要初始化已经存在的每个备份的文件信息。所以如果你面向一个远程文件系统(如HDFS),而你可能有很多备份,那么初始化备份引擎可能需要花点时间,因为有网络交互的时间。我们推荐你保持备份引擎存活,并且不要每次都重新创建。

另一个加快备份引擎初始化的方法是删除不用的备份。为了删除不用的备份,只需要调用PurgeOldBackups(N),N是你希望保留的备份数量。除了最新的N份备份,所有备份都会被清理。你可以通通过调用DeleteBackup(id)来删除任意备份。

同时注意性能也受从本地db读取然后拷贝到备份这个过程的影响。由于你可能使用不同的环境来读取和拷贝,并行瓶颈可能在任意一边出现。例如,如果本地db是HDD,使用更多的线程来备份(参考高级使用)不会有效果,因为这个场景的瓶颈是磁盘度性能,可能已经饱和了。同事,一个很小的HDFS集群可能无法获得好的并发性。如果本地db在SSD而备份目标在一个大容量HDFS,就比较好了。在我们的压测下,使用16线程会减小备份时间到单线程的1/3。

底层实现

当你调用BackupEngine::CreateNewBackup(),他执行一下动作:

  1. 关闭文件删除
  2. 获取存活文件(包括表文件,current,选线和manifest文件)
  3. 拷贝存货文件到备份目录。由于表文件是不可修改,并且文件名唯一,如果备份目录下已经有这个文件了,我们就不拷贝他了。例如,如果有一个文件叫00050.sst在备份目录里,然后GetLiveFiles()返回了00050.sst,我们不会拷贝这个文件到备份目录。然而,不管文件是否被拷贝,所有文件的校验和都需要被计算。如果一个文件已经存在,算出来的校验和会跟之前算出来的校验和做比较,以确保没有意料外的事情发生。如果校验和不一致,备份会终止,系统恢复到BackupEngine::CreateNewBackup()调用前的状态。值得注意的是,一个备份终止可能意味着当前db中的文件出错,也可能是备份的文件出错。另外,manifest和current文件总是被拷贝到private目录,因为他们不是不可变得。
  4. 如果flush_before_backup为false,我们还需要把WAL拷贝到备份目录。我们调用GetSortedWalFiles()然后拷贝所有存活文件到备份目录。
  5. 重新打开文件删除。

高级使用

我们可以恢复用户定义的元数据备份。把你的元数据传给BackupEngine::CreateNewBackupWithMetadata()然后后续通过BackupEngine::GetBackupInfo()读取。例如,这个可以使用不同于我们的自增id的自定义id,以用于区分备份。

我们现在也备份和恢复option文件了。回复后,你可以从db目录使用rocksdb::LoadLatestOptions()或者rocksdb:: LoadOptionsFromFile()加载option。限制是,并不是options里的所有内容都可以转换成一个文件中的text。加载并恢复后,你仍旧需要一些步骤来手工设置一些没有设置的信息。好消息是,你比以前需要做的事情少了许多。

你需要初始化一些env,然后给backup_target初始化BackupableDBOptions::backup_env。把你的备份根目录写入BackupableDBOptions::backup_dir在该目录下,文件会按照上述说明组织。

BackupableDBOptions::share_table_files控制备份是否增量完成。如果为真,SST文件会去到”shared”目录。如果不同的SST文件使用同一个名称,就会造成冲突(比如说,如果多个数据库使用同一个备份目标文件夹)。

BackupableDBOptions::share_files_with_checksum控制共享文件如何被区分。如果为真,共享SST文件会通过校验和,大小,以及序列号进行区分。这可以防止上述多个数据库使用同一个目录带来的冲突。

BackupableDBOptions::max_background_operations控制备份和恢复的时候用于拷贝文件的线程数。对于分布式文件系统,如HDFS,增加拷贝并发数可以获得很好的收益。

BackupableDBOptions::info_log是一个Logger对象,非空时,用于打印LOG日志。参考Logger

如果BackupableDBOptions::sync为真,每次文件写,我们都会使用fsync(2)来同步文件数据和元数据到磁盘上,保证重启或者机器崩溃后的备份持久化。设置为false会加快一点速度,但是一些(更新的)备份可能不能持久化。尽管在大多数情况下,一切都相安无事。

如果你设置BackupableDBOptions::destroy_old_data为true,创建新的BackupEngin会删除目标备份目录下的所有旧的备份。

BackupEngine::CreateNewBackup()方法需要一个参数flush_before_backup,默认为false。如果flush_before_backup为true,BackupEngine会先发起一个memtable落盘,之后才开始拷贝DB文件到备份目录。这样会不再拷贝WAL文件到备份目录(因为落盘后会删除他们)。如果flush_before_backup为false,备份不会发起落盘。这时,备份会包括memtable对应的WAL文件。不管flush_before_backup为何,备份总是与当前状态一致。

进一步阅读

对于具体实现,参考utilities/backupable/backupable_db.cc