diff --git a/kvrocks.conf b/kvrocks.conf index 13b1fb6c7f4..4c19504e5cd 100644 --- a/kvrocks.conf +++ b/kvrocks.conf @@ -127,6 +127,13 @@ db-name change.me.db # Note that you must specify a directory here, not a file name. dir /tmp/kvrocks +# Optional: A snapshot directory +# +# If specified, Kvrocks will snapshot the DB at [dir] to the specified [snapshot-dir], +# and start a read-only server from the snapshot. +# +# snapshot-dir /tmp/kvrocks-snapshot-changeme + # You can configure where to store your server logs by the log-dir. # If you don't specify one, we will use the above `dir` as our default log directory. # We also can send logs to stdout/stderr is as simple as: diff --git a/src/config/config.cc b/src/config/config.cc index c2491844253..63e5c215818 100644 --- a/src/config/config.cc +++ b/src/config/config.cc @@ -24,13 +24,11 @@ #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -190,6 +188,7 @@ Config::Config() { new IntField(&force_compact_file_min_deleted_percentage, 10, 1, 100)}, {"db-name", true, new StringField(&db_name, "change.me.db")}, {"dir", true, new StringField(&dir, kDefaultDir)}, + {"snapshot-dir", true, new StringField(&snapshot_dir, "")}, {"backup-dir", false, new StringField(&backup_dir, kDefaultBackupDir)}, {"log-dir", true, new StringField(&log_dir, "")}, {"log-level", false, new EnumField(&log_level, log_levels, google::INFO)}, diff --git a/src/config/config.h b/src/config/config.h index 3dcc8d87002..7307e1f9ddf 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -142,6 +142,10 @@ struct Config { std::string replica_announce_ip; uint32_t replica_announce_port = 0; + // The following option exists so users can spawn a read-only server from a snapshot + // of a running server. + std::string snapshot_dir; + bool persist_cluster_nodes_enabled = true; bool slot_id_encoded = false; bool cluster_enabled = false; diff --git a/src/storage/storage.cc b/src/storage/storage.cc index 36b027a6694..1d95dad2810 100644 --- a/src/storage/storage.cc +++ b/src/storage/storage.cc @@ -44,6 +44,7 @@ #include "redis_metadata.h" #include "rocksdb/cache.h" #include "rocksdb_crc32c.h" +#include "scope_exit.h" #include "server/server.h" #include "storage/batch_indexer.h" #include "table_properties_collector.h" @@ -75,6 +76,37 @@ const int64_t kIORateLimitMaxMb = 1024000; using rocksdb::Slice; +static Status CreateSnapshot(Config &config) { + // The Storage destructor deletes anything at the checkpoint_dir, so we need to make + // sure it's empty in case the user happens to use a snapshot name which matches the + // default (checkpoint/) + const std::string old_checkpoint_dir = std::exchange(config.checkpoint_dir, ""); + const auto checkpoint_dir_guard = + MakeScopeExit([&config, &old_checkpoint_dir] { config.checkpoint_dir = old_checkpoint_dir; }); + + // Since .Open() will call `CreateSnapshot` if `snapshot_dir` is set, we need to + // clear it, and reset it after the snapshot is created to preserve symmetry. + const std::string snapshot_dir = std::exchange(config.snapshot_dir, ""); + const auto snapshot_dir_guard = MakeScopeExit([&config, &snapshot_dir] { config.snapshot_dir = snapshot_dir; }); + + engine::Storage storage(&config); + if (const auto s = storage.Open(kDBOpenModeForReadOnly); !s.IsOK()) { + return {Status::NotOK, fmt::format("failed to open DB in read-only mode: {}", s.Msg())}; + } + + rocksdb::Checkpoint *snapshot = nullptr; + if (const auto s = rocksdb::Checkpoint::Create(storage.GetDB(), &snapshot); !s.ok()) { + return {Status::NotOK, s.ToString()}; + } + + std::unique_ptr snapshot_guard(snapshot); + if (const auto s = snapshot->CreateCheckpoint(snapshot_dir + "/db"); !s.ok()) { + return {Status::NotOK, s.ToString()}; + } + + return Status::OK(); +} + Storage::Storage(Config *config) : backup_creating_time_secs_(util::GetTimeStamp()), env_(rocksdb::Env::Default()), @@ -267,6 +299,18 @@ Status Storage::CreateColumnFamilies(const rocksdb::Options &options) { } Status Storage::Open(DBOpenMode mode) { + // If a snapshot directory was specified, create a snapshot and open the database + // there in read-only mode. + if (config_->snapshot_dir != "") { + if (auto s = CreateSnapshot(*config_); !s.IsOK()) { + return s; + } + LOG(INFO) << "Starting server in read-only mode with snapshot dir: " << config_->snapshot_dir; + config_->dir = config_->snapshot_dir; + config_->db_dir = config_->snapshot_dir + "/db"; + mode = DBOpenMode::kDBOpenModeForReadOnly; + } + auto guard = WriteLockGuard(); db_closing_ = false; diff --git a/src/storage/storage.h b/src/storage/storage.h index 3a45ccd43c1..1075b3c7a51 100644 --- a/src/storage/storage.h +++ b/src/storage/storage.h @@ -28,7 +28,6 @@ #include #include -#include #include #include #include