mirror of
https://github.com/facebook/rocksdb.git
synced 2026-07-07 14:47:40 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1fba33d246 | |||
| 5853adab23 | |||
| b63b4f8c45 | |||
| 7161711e8c |
+41
-4
@@ -6675,6 +6675,13 @@ Status DBImpl::IngestExternalFile(
|
||||
Status DBImpl::IngestExternalFiles(
|
||||
const std::vector<IngestExternalFileArg>& args) {
|
||||
PERF_TIMER_GUARD(file_ingestion_nanos);
|
||||
// Prepare/run ingestion latency, recorded only on success (below).
|
||||
const bool record_ingest_micros =
|
||||
stats_ != nullptr &&
|
||||
stats_->get_stats_level() > StatsLevel::kExceptTimers;
|
||||
const uint64_t ingest_start_micros =
|
||||
record_ingest_micros ? immutable_db_options_.clock->NowMicros() : 0;
|
||||
uint64_t prepare_micros = 0;
|
||||
// TODO: plumb Env::IOActivity, Env::IOPriority
|
||||
const WriteOptions write_options;
|
||||
|
||||
@@ -6701,6 +6708,19 @@ Status DBImpl::IngestExternalFiles(
|
||||
"external_files[" + std::to_string(i) + "] is empty";
|
||||
return Status::InvalidArgument(err_msg);
|
||||
}
|
||||
if (!args[i].file_infos.empty()) {
|
||||
if (args[i].file_infos.size() != args[i].external_files.size()) {
|
||||
return Status::InvalidArgument("file_infos[" + std::to_string(i) +
|
||||
"] size must match external_files[" +
|
||||
std::to_string(i) + "] size");
|
||||
}
|
||||
// file_infos avoids opening the file, so it cannot write a global seqno
|
||||
// back into it. (write_global_seqno is deprecated and defaults to false.)
|
||||
if (args[i].options.write_global_seqno) {
|
||||
return Status::InvalidArgument(
|
||||
"write_global_seqno is not supported when file_infos is set");
|
||||
}
|
||||
}
|
||||
if (i && args[i].options.fill_cache != args[i - 1].options.fill_cache) {
|
||||
return Status::InvalidArgument(
|
||||
"fill_cache should be the same across ingestion options.");
|
||||
@@ -6794,8 +6814,9 @@ Status DBImpl::IngestExternalFiles(
|
||||
this);
|
||||
Status es = ingestion_jobs[i].Prepare(
|
||||
args[i].external_files, args[i].files_checksums,
|
||||
args[i].files_checksum_func_names, args[i].atomic_replace_range,
|
||||
args[i].file_temperature, start_file_number, super_version);
|
||||
args[i].files_checksum_func_names, args[i].file_infos,
|
||||
args[i].atomic_replace_range, args[i].file_temperature,
|
||||
start_file_number, super_version);
|
||||
// capture first error only
|
||||
if (!es.ok() && status.ok()) {
|
||||
status = es;
|
||||
@@ -6810,8 +6831,9 @@ Status DBImpl::IngestExternalFiles(
|
||||
this);
|
||||
Status es = ingestion_jobs[0].Prepare(
|
||||
args[0].external_files, args[0].files_checksums,
|
||||
args[0].files_checksum_func_names, args[0].atomic_replace_range,
|
||||
args[0].file_temperature, next_file_number, super_version);
|
||||
args[0].files_checksum_func_names, args[0].file_infos,
|
||||
args[0].atomic_replace_range, args[0].file_temperature,
|
||||
next_file_number, super_version);
|
||||
if (!es.ok()) {
|
||||
status = es;
|
||||
}
|
||||
@@ -6826,6 +6848,13 @@ Status DBImpl::IngestExternalFiles(
|
||||
return status;
|
||||
}
|
||||
|
||||
// End of prepare phase; run phase starts here.
|
||||
uint64_t run_start_micros = 0;
|
||||
if (record_ingest_micros) {
|
||||
run_start_micros = immutable_db_options_.clock->NowMicros();
|
||||
prepare_micros = run_start_micros - ingest_start_micros;
|
||||
}
|
||||
|
||||
std::vector<SuperVersionContext> sv_ctxs;
|
||||
for (size_t i = 0; i != num_cfs; ++i) {
|
||||
sv_ctxs.emplace_back(true /* create_superversion */);
|
||||
@@ -7067,6 +7096,14 @@ Status DBImpl::IngestExternalFiles(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Record latency only for successful ingestions.
|
||||
if (record_ingest_micros && status.ok()) {
|
||||
RecordTimeToHistogram(stats_, INGEST_EXTERNAL_FILE_PREPARE_TIME,
|
||||
prepare_micros);
|
||||
RecordTimeToHistogram(
|
||||
stats_, INGEST_EXTERNAL_FILE_RUN_TIME,
|
||||
immutable_db_options_.clock->NowMicros() - run_start_micros);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,18 +31,22 @@ Status ExternalSstFileIngestionJob::Prepare(
|
||||
const std::vector<std::string>& external_files_paths,
|
||||
const std::vector<std::string>& files_checksums,
|
||||
const std::vector<std::string>& files_checksum_func_names,
|
||||
const std::vector<ExternalSstFileInfo>& file_infos,
|
||||
const std::optional<RangeOpt>& atomic_replace_range,
|
||||
const Temperature& file_temperature, uint64_t next_file_number,
|
||||
SuperVersion* sv) {
|
||||
Status status;
|
||||
|
||||
// Read the information of files we are ingesting
|
||||
for (const std::string& file_path : external_files_paths) {
|
||||
// Read the information of files we are ingesting.
|
||||
for (size_t i = 0; i < external_files_paths.size(); i++) {
|
||||
const std::string& file_path = external_files_paths[i];
|
||||
IngestedFileInfo file_to_ingest;
|
||||
// For temperature, first assume it matches provided hint
|
||||
file_to_ingest.file_temperature = file_temperature;
|
||||
status =
|
||||
GetIngestedFileInfo(file_path, next_file_number++, &file_to_ingest, sv);
|
||||
const ExternalSstFileInfo* file_info =
|
||||
file_infos.empty() ? nullptr : &file_infos[i];
|
||||
status = GetIngestedFileInfo(file_path, next_file_number++, file_info,
|
||||
&file_to_ingest, sv);
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"Failed to get ingested file info: %s: %s",
|
||||
@@ -970,18 +974,14 @@ Status ExternalSstFileIngestionJob::ResetTableReader(
|
||||
}
|
||||
|
||||
Status ExternalSstFileIngestionJob::SanityCheckTableProperties(
|
||||
const std::string& external_file, uint64_t new_file_number,
|
||||
SuperVersion* sv, IngestedFileInfo* file_to_ingest,
|
||||
std::unique_ptr<TableReader>* table_reader) {
|
||||
// Get the external file properties
|
||||
auto props = table_reader->get()->GetTableProperties();
|
||||
assert(props.get());
|
||||
const auto& uprops = props->user_collected_properties;
|
||||
const std::string& external_file, const TableProperties& props,
|
||||
IngestedFileInfo* file_to_ingest) {
|
||||
const auto& uprops = props.user_collected_properties;
|
||||
|
||||
// Get table version
|
||||
auto version_iter = uprops.find(ExternalSstFilePropertyNames::kVersion);
|
||||
if (version_iter == uprops.end()) {
|
||||
assert(!SstFileWriter::CreatedBySstFileWriter(*props));
|
||||
assert(!SstFileWriter::CreatedBySstFileWriter(props));
|
||||
if (!ingestion_options_.allow_db_generated_files) {
|
||||
return Status::Corruption("External file version not found");
|
||||
} else {
|
||||
@@ -990,7 +990,7 @@ Status ExternalSstFileIngestionJob::SanityCheckTableProperties(
|
||||
file_to_ingest->version = 0;
|
||||
}
|
||||
} else {
|
||||
assert(SstFileWriter::CreatedBySstFileWriter(*props));
|
||||
assert(SstFileWriter::CreatedBySstFileWriter(props));
|
||||
file_to_ingest->version = DecodeFixed32(version_iter->second.c_str());
|
||||
}
|
||||
|
||||
@@ -1004,12 +1004,17 @@ Status ExternalSstFileIngestionJob::SanityCheckTableProperties(
|
||||
|
||||
// Set the global sequence number
|
||||
file_to_ingest->original_seqno = DecodeFixed64(seqno_iter->second.c_str());
|
||||
if (props->external_sst_file_global_seqno_offset == 0) {
|
||||
file_to_ingest->global_seqno_offset = 0;
|
||||
file_to_ingest->global_seqno_offset =
|
||||
static_cast<size_t>(props.external_sst_file_global_seqno_offset);
|
||||
// The on-disk offset is only needed if we will write the global seqno back
|
||||
// into the file (write_global_seqno). The metadata fast-path does not open
|
||||
// the file, and its in-memory table properties do not carry the offset
|
||||
// (it is only computed while reading the file back); that is fine as long
|
||||
// as write_global_seqno is not requested.
|
||||
if (ingestion_options_.write_global_seqno &&
|
||||
file_to_ingest->global_seqno_offset == 0) {
|
||||
return Status::Corruption("Was not able to find file global seqno field");
|
||||
}
|
||||
file_to_ingest->global_seqno_offset =
|
||||
static_cast<size_t>(props->external_sst_file_global_seqno_offset);
|
||||
} else if (file_to_ingest->version == 1) {
|
||||
// SST file V1 should not have global seqno field
|
||||
assert(seqno_iter == uprops.end());
|
||||
@@ -1030,23 +1035,20 @@ Status ExternalSstFileIngestionJob::SanityCheckTableProperties(
|
||||
" is not supported");
|
||||
}
|
||||
|
||||
file_to_ingest->cf_id = static_cast<uint32_t>(props->column_family_id);
|
||||
// This assignment works fine even though `table_reader` may later be reset,
|
||||
// since that will not affect how table properties are parsed, and this
|
||||
// assignment is making a copy.
|
||||
file_to_ingest->table_properties = *props;
|
||||
file_to_ingest->cf_id = static_cast<uint32_t>(props.column_family_id);
|
||||
file_to_ingest->table_properties = props;
|
||||
|
||||
// Get number of entries in table
|
||||
file_to_ingest->num_entries = props->num_entries;
|
||||
file_to_ingest->num_range_deletions = props->num_range_deletions;
|
||||
file_to_ingest->num_entries = props.num_entries;
|
||||
file_to_ingest->num_range_deletions = props.num_range_deletions;
|
||||
|
||||
// Validate table properties related to comparator name and user defined
|
||||
// timestamps persisted flag.
|
||||
file_to_ingest->user_defined_timestamps_persisted =
|
||||
static_cast<bool>(props->user_defined_timestamps_persisted);
|
||||
static_cast<bool>(props.user_defined_timestamps_persisted);
|
||||
bool mark_sst_file_has_no_udt = false;
|
||||
Status s = ValidateUserDefinedTimestampsOptions(
|
||||
cfd_->user_comparator(), props->comparator_name,
|
||||
cfd_->user_comparator(), props.comparator_name,
|
||||
cfd_->ioptions().persist_user_defined_timestamps,
|
||||
file_to_ingest->user_defined_timestamps_persisted,
|
||||
&mark_sst_file_has_no_udt);
|
||||
@@ -1054,7 +1056,9 @@ Status ExternalSstFileIngestionJob::SanityCheckTableProperties(
|
||||
// A column family that enables user-defined timestamps in Memtable only
|
||||
// feature can also ingest external files created by a setting that disables
|
||||
// user-defined timestamps. In that case, we need to re-mark the
|
||||
// user_defined_timestamps_persisted flag for the file.
|
||||
// user_defined_timestamps_persisted flag for the file. The open-and-scan
|
||||
// caller is then responsible for reopening its `TableReader` with the
|
||||
// updated flag.
|
||||
file_to_ingest->user_defined_timestamps_persisted = false;
|
||||
} else if (!s.ok()) {
|
||||
ROCKS_LOG_WARN(
|
||||
@@ -1064,22 +1068,88 @@ Status ExternalSstFileIngestionJob::SanityCheckTableProperties(
|
||||
return s;
|
||||
}
|
||||
|
||||
// `TableReader` is initialized with `user_defined_timestamps_persisted` flag
|
||||
// to be true. If its value changed to false after this sanity check, we
|
||||
// need to reset the `TableReader`.
|
||||
if (ucmp_->timestamp_size() > 0 &&
|
||||
!file_to_ingest->user_defined_timestamps_persisted) {
|
||||
s = ResetTableReader(external_file, new_file_number,
|
||||
file_to_ingest->user_defined_timestamps_persisted, sv,
|
||||
file_to_ingest, table_reader);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
Status ExternalSstFileIngestionJob::GetIngestedFileInfoFromFileInfo(
|
||||
const std::string& external_file, const ExternalSstFileInfo* file_info,
|
||||
IngestedFileInfo* file_to_ingest) {
|
||||
// Boundaries, size, and table properties were obtained without opening the
|
||||
// file (e.g. produced by SstFileWriter::Finish). We reuse them directly.
|
||||
file_to_ingest->file_size = file_info->file_size;
|
||||
file_to_ingest->smallest_internal_key.DecodeFrom(
|
||||
file_info->smallest_internal_key);
|
||||
file_to_ingest->largest_internal_key.DecodeFrom(
|
||||
file_info->largest_internal_key);
|
||||
Status status = SanityCheckTableProperties(
|
||||
external_file, file_info->table_properties, file_to_ingest);
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(
|
||||
db_options_.info_log,
|
||||
"Failed to sanity check table properties for external file %s: %s",
|
||||
external_file.c_str(), status.ToString().c_str());
|
||||
return status;
|
||||
}
|
||||
|
||||
// When the column family enables user-defined timestamps but this file was
|
||||
// written without persisting them (the "UDT in Memtables only" feature), the
|
||||
// supplied boundary keys carry no timestamp (SstFileWriter strips it). Pad
|
||||
// them with the min timestamp so they match the format the open-and-scan path
|
||||
// produces (its reopened `TableReader` pads keys the same way) and the bounds
|
||||
// recorded in the new FileMetaData.
|
||||
const size_t ts_sz = ucmp_->timestamp_size();
|
||||
if (ts_sz > 0 && !file_to_ingest->user_defined_timestamps_persisted) {
|
||||
std::string padded;
|
||||
PadInternalKeyWithMinTimestamp(
|
||||
&padded, file_to_ingest->smallest_internal_key.Encode(), ts_sz);
|
||||
file_to_ingest->smallest_internal_key.DecodeFrom(padded);
|
||||
padded.clear();
|
||||
PadInternalKeyWithMinTimestamp(
|
||||
&padded, file_to_ingest->largest_internal_key.Encode(), ts_sz);
|
||||
file_to_ingest->largest_internal_key.DecodeFrom(padded);
|
||||
}
|
||||
|
||||
// Extend the key bounds to cover any range tombstones. Their bounds are not
|
||||
// part of the point-key internal keys, but the file's recorded range must
|
||||
// include them (the open-and-scan path does this while scanning tombstones).
|
||||
// SstFileWriter writes range tombstones at sequence number 0.
|
||||
if (!file_info->smallest_range_del_key.empty()) {
|
||||
RangeTombstone tombstone(Slice(file_info->smallest_range_del_key),
|
||||
Slice(file_info->largest_range_del_key),
|
||||
/*sn=*/0);
|
||||
file_range_checker_.MaybeUpdateRange(
|
||||
tombstone.SerializeKey(), tombstone.SerializeEndKey(), file_to_ingest);
|
||||
}
|
||||
|
||||
if (ingestion_options_.allow_db_generated_files) {
|
||||
// Sequence numbers are preserved (not reassigned), so the bounds come from
|
||||
// the supplied table properties; the GetSeqnoBoundaryForFile scan is
|
||||
// skipped.
|
||||
file_to_ingest->smallest_seqno =
|
||||
file_to_ingest->table_properties.key_smallest_seqno;
|
||||
file_to_ingest->largest_seqno =
|
||||
file_to_ingest->table_properties.key_largest_seqno;
|
||||
} else {
|
||||
// Normal ingestion reassigns a global sequence number later, so the file's
|
||||
// keys must currently be at seqno 0 (mirror of the open-and-scan check).
|
||||
SequenceNumber largest_seqno =
|
||||
file_to_ingest->table_properties.key_largest_seqno;
|
||||
// UINT64_MAX means unknown and the file is generated before table property
|
||||
// `key_largest_seqno` is introduced.
|
||||
if (largest_seqno != UINT64_MAX && largest_seqno > 0) {
|
||||
return Status::Corruption(
|
||||
"External file has non zero largest sequence number " +
|
||||
std::to_string(largest_seqno));
|
||||
}
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status ExternalSstFileIngestionJob::GetIngestedFileInfoFromFile(
|
||||
const std::string& external_file, uint64_t new_file_number,
|
||||
IngestedFileInfo* file_to_ingest, SuperVersion* sv) {
|
||||
file_to_ingest->external_file_path = external_file;
|
||||
IngestedFileInfo* file_to_ingest, SuperVersion* sv,
|
||||
std::unique_ptr<TableReader>* out_table_reader) {
|
||||
TEST_SYNC_POINT("ExternalSstFileIngestionJob::GetIngestedFileInfo:ReadPath");
|
||||
|
||||
// Get external file size
|
||||
Status status = fs_->GetFileSize(external_file, IOOptions(),
|
||||
@@ -1091,15 +1161,11 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
return status;
|
||||
}
|
||||
|
||||
// Assign FD with number
|
||||
file_to_ingest->fd =
|
||||
FileDescriptor(new_file_number, 0, file_to_ingest->file_size);
|
||||
|
||||
// Create TableReader for external file
|
||||
// Create TableReader for external file.
|
||||
std::unique_ptr<TableReader> table_reader;
|
||||
// Initially create the `TableReader` with flag
|
||||
// `user_defined_timestamps_persisted` to be true since that's the most common
|
||||
// case
|
||||
// `user_defined_timestamps_persisted` to be true since that's the most
|
||||
// common case
|
||||
status = ResetTableReader(external_file, new_file_number,
|
||||
/*user_defined_timestamps_persisted=*/true, sv,
|
||||
file_to_ingest, &table_reader);
|
||||
@@ -1110,8 +1176,8 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
return status;
|
||||
}
|
||||
|
||||
status = SanityCheckTableProperties(external_file, new_file_number, sv,
|
||||
file_to_ingest, &table_reader);
|
||||
status = SanityCheckTableProperties(
|
||||
external_file, *table_reader->GetTableProperties(), file_to_ingest);
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(
|
||||
db_options_.info_log,
|
||||
@@ -1120,6 +1186,23 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
return status;
|
||||
}
|
||||
|
||||
// The `TableReader` above was opened with `user_defined_timestamps_persisted`
|
||||
// assumed true. If the sanity check determined the file has no persisted
|
||||
// timestamps (UDT-in-Memtable-only feature), reopen it with the corrected
|
||||
// flag so keys are parsed properly by the scan below.
|
||||
if (ucmp_->timestamp_size() > 0 &&
|
||||
!file_to_ingest->user_defined_timestamps_persisted) {
|
||||
status = ResetTableReader(external_file, new_file_number,
|
||||
file_to_ingest->user_defined_timestamps_persisted,
|
||||
sv, file_to_ingest, &table_reader);
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"Failed to reset table reader for external file %s: %s",
|
||||
external_file.c_str(), status.ToString().c_str());
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
const bool allow_data_in_errors = db_options_.allow_data_in_errors;
|
||||
ParsedInternalKey key;
|
||||
if (ingestion_options_.allow_db_generated_files) {
|
||||
@@ -1141,8 +1224,8 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
} else {
|
||||
SequenceNumber largest_seqno =
|
||||
table_reader.get()->GetTableProperties()->key_largest_seqno;
|
||||
// UINT64_MAX means unknown and the file is generated before table property
|
||||
// `key_largest_seqno` is introduced.
|
||||
// UINT64_MAX means unknown and the file is generated before table
|
||||
// property `key_largest_seqno` is introduced.
|
||||
if (largest_seqno != UINT64_MAX && largest_seqno > 0) {
|
||||
return Status::Corruption(
|
||||
"External file has non zero largest sequence number " +
|
||||
@@ -1150,23 +1233,6 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
}
|
||||
}
|
||||
|
||||
if (ingestion_options_.verify_checksums_before_ingest) {
|
||||
// If customized readahead size is needed, we can pass a user option
|
||||
// all the way to here. Right now we just rely on the default readahead
|
||||
// to keep things simple.
|
||||
// TODO: plumb Env::IOActivity, Env::IOPriority
|
||||
ReadOptions ro;
|
||||
ro.readahead_size = ingestion_options_.verify_checksums_readahead_size;
|
||||
ro.fill_cache = ingestion_options_.fill_cache;
|
||||
status = table_reader->VerifyChecksum(
|
||||
ro, TableReaderCaller::kExternalSSTIngestion);
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"Failed to verify checksum for table reader: %s",
|
||||
status.ToString().c_str());
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: plumb Env::IOActivity, Env::IOPriority
|
||||
ReadOptions ro;
|
||||
@@ -1254,6 +1320,75 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
}
|
||||
}
|
||||
|
||||
*out_table_reader = std::move(table_reader);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
const std::string& external_file, uint64_t new_file_number,
|
||||
const ExternalSstFileInfo* file_info, IngestedFileInfo* file_to_ingest,
|
||||
SuperVersion* sv) {
|
||||
file_to_ingest->external_file_path = external_file;
|
||||
|
||||
std::unique_ptr<TableReader> table_reader;
|
||||
Status status =
|
||||
file_info != nullptr
|
||||
? GetIngestedFileInfoFromFileInfo(external_file, file_info,
|
||||
file_to_ingest)
|
||||
: GetIngestedFileInfoFromFile(external_file, new_file_number,
|
||||
file_to_ingest, sv, &table_reader);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
assert(file_to_ingest->file_size > 0);
|
||||
assert(!file_to_ingest->unset());
|
||||
assert(file_to_ingest->table_properties.num_entries > 0 ||
|
||||
file_to_ingest->table_properties.num_range_deletions > 0);
|
||||
if (ingestion_options_.allow_db_generated_files) {
|
||||
// These files keep their original sequence numbers (derived, not
|
||||
// reassigned), so the bounds must be valid here.
|
||||
assert(file_to_ingest->smallest_seqno <= file_to_ingest->largest_seqno);
|
||||
assert(file_to_ingest->largest_seqno < kMaxSequenceNumber);
|
||||
}
|
||||
|
||||
// Verify the file checksum if requested. The open-and-scan path already has a
|
||||
// `TableReader` open; the fast-path opens one here only when verification is
|
||||
// requested -- otherwise it performs no file I/O.
|
||||
if (ingestion_options_.verify_checksums_before_ingest) {
|
||||
if (table_reader == nullptr) {
|
||||
status =
|
||||
ResetTableReader(external_file, new_file_number,
|
||||
file_to_ingest->user_defined_timestamps_persisted,
|
||||
sv, file_to_ingest, &table_reader);
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"Failed to reset table reader for external file %s: %s",
|
||||
external_file.c_str(), status.ToString().c_str());
|
||||
return status;
|
||||
}
|
||||
}
|
||||
// If customized readahead size is needed, we can pass a user option all the
|
||||
// way to here. Right now we just rely on the default readahead to keep
|
||||
// things simple.
|
||||
// TODO: plumb Env::IOActivity, Env::IOPriority
|
||||
ReadOptions ro;
|
||||
ro.readahead_size = ingestion_options_.verify_checksums_readahead_size;
|
||||
ro.fill_cache = ingestion_options_.fill_cache;
|
||||
status = table_reader->VerifyChecksum(
|
||||
ro, TableReaderCaller::kExternalSSTIngestion);
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"Failed to verify checksum for external file %s: %s",
|
||||
external_file.c_str(), status.ToString().c_str());
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
// Assign FD with number.
|
||||
file_to_ingest->fd =
|
||||
FileDescriptor(new_file_number, 0, file_to_ingest->file_size);
|
||||
|
||||
const size_t ts_sz = ucmp_->timestamp_size();
|
||||
Slice smallest = file_to_ingest->smallest_internal_key.user_key();
|
||||
Slice largest = file_to_ingest->largest_internal_key.user_key();
|
||||
|
||||
@@ -248,6 +248,7 @@ class ExternalSstFileIngestionJob {
|
||||
Status Prepare(const std::vector<std::string>& external_files_paths,
|
||||
const std::vector<std::string>& files_checksums,
|
||||
const std::vector<std::string>& files_checksum_func_names,
|
||||
const std::vector<ExternalSstFileInfo>& file_infos,
|
||||
const std::optional<RangeOpt>& atomic_replace_range,
|
||||
const Temperature& file_temperature, uint64_t next_file_number,
|
||||
SuperVersion* sv);
|
||||
@@ -315,17 +316,34 @@ class ExternalSstFileIngestionJob {
|
||||
// different options. For example: when external file does not contain
|
||||
// timestamps while column family enables UDT in Memtables only feature.
|
||||
Status SanityCheckTableProperties(const std::string& external_file,
|
||||
uint64_t new_file_number, SuperVersion* sv,
|
||||
IngestedFileInfo* file_to_ingest,
|
||||
std::unique_ptr<TableReader>* table_reader);
|
||||
const TableProperties& props,
|
||||
IngestedFileInfo* file_to_ingest);
|
||||
|
||||
// Open the external file and populate `file_to_ingest` with all the
|
||||
// external information we need to ingest this file.
|
||||
Status GetIngestedFileInfo(const std::string& external_file,
|
||||
uint64_t new_file_number,
|
||||
const ExternalSstFileInfo* file_info,
|
||||
IngestedFileInfo* file_to_ingest,
|
||||
SuperVersion* sv);
|
||||
|
||||
// Acquire the per-file metadata from caller-supplied `file_info` instead
|
||||
// of opening the file (e.g. the ExternalSstFileInfo produced by
|
||||
// SstFileWriter::Finish).
|
||||
Status GetIngestedFileInfoFromFileInfo(const std::string& external_file,
|
||||
const ExternalSstFileInfo* file_info,
|
||||
IngestedFileInfo* file_to_ingest);
|
||||
|
||||
// Acquire the per-file metadata by opening the external file and scanning it
|
||||
// (table properties, sequence number bounds, and boundary keys including any
|
||||
// range-tombstone extensions). Used when no file_info is available. The
|
||||
// opened `TableReader` is returned via `*table_reader` so the caller can
|
||||
// reuse it (e.g. to verify the file checksum) without re-opening the file.
|
||||
Status GetIngestedFileInfoFromFile(
|
||||
const std::string& external_file, uint64_t new_file_number,
|
||||
IngestedFileInfo* file_to_ingest, SuperVersion* sv,
|
||||
std::unique_ptr<TableReader>* out_table_reader);
|
||||
|
||||
// If the input files' key range overlaps themselves, this function divides
|
||||
// them in the user specified order into multiple batches. Where the files
|
||||
// within a batch do not overlap with each other, but key range could overlap
|
||||
|
||||
@@ -152,7 +152,8 @@ class ExternalSSTFileTest
|
||||
bool verify_checksums_before_ingest = true, bool ingest_behind = false,
|
||||
bool sort_data = false,
|
||||
std::map<std::string, std::string>* true_data = nullptr,
|
||||
ColumnFamilyHandle* cfh = nullptr, bool fill_cache = false) {
|
||||
ColumnFamilyHandle* cfh = nullptr, bool fill_cache = false,
|
||||
bool ingest_with_file_info = false) {
|
||||
// Generate a file id if not provided
|
||||
if (file_id == -1) {
|
||||
file_id = last_file_id_ + 1;
|
||||
@@ -188,7 +189,8 @@ class ExternalSSTFileTest
|
||||
return s;
|
||||
}
|
||||
}
|
||||
s = sst_file_writer.Finish();
|
||||
ExternalSstFileInfo file_info;
|
||||
s = sst_file_writer.Finish(&file_info);
|
||||
|
||||
if (s.ok()) {
|
||||
IngestExternalFileOptions ifo;
|
||||
@@ -197,7 +199,14 @@ class ExternalSSTFileTest
|
||||
ifo.verify_checksums_before_ingest = verify_checksums_before_ingest;
|
||||
ifo.ingest_behind = ingest_behind;
|
||||
ifo.fill_cache = fill_cache;
|
||||
if (cfh) {
|
||||
if (ingest_with_file_info) {
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = cfh ? cfh : db_->DefaultColumnFamily();
|
||||
arg.external_files = {file_path};
|
||||
arg.file_infos = {file_info};
|
||||
arg.options = ifo;
|
||||
s = db_->IngestExternalFiles({arg});
|
||||
} else if (cfh) {
|
||||
s = db_->IngestExternalFile(cfh, {file_path}, ifo);
|
||||
} else {
|
||||
s = db_->IngestExternalFile({file_path}, ifo);
|
||||
@@ -298,6 +307,181 @@ class ExternalSSTFileTest
|
||||
int last_file_id_ = 0;
|
||||
};
|
||||
|
||||
TEST_F(ExternalSSTFileTest, IngestionTimingHistogram) {
|
||||
Options options = CurrentOptions();
|
||||
options.statistics = CreateDBStatistics();
|
||||
options.statistics->set_stats_level(StatsLevel::kAll); // enable timers
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(GenerateAndAddExternalFile(
|
||||
options, {{"k1", "v1"}, {"k2", "v2"}, {"k3", "v3"}}, /*file_id=*/-1,
|
||||
/*allow_global_seqno=*/true, /*write_global_seqno=*/false,
|
||||
/*verify_checksums_before_ingest=*/true, /*ingest_behind=*/false,
|
||||
/*sort_data=*/true));
|
||||
|
||||
auto hist = [&](Histograms h) {
|
||||
HistogramData hd;
|
||||
options.statistics->histogramData(h, &hd);
|
||||
return hd;
|
||||
};
|
||||
|
||||
// One sample per call (not per CF) in each phase histogram.
|
||||
HistogramData prepare_hd = hist(INGEST_EXTERNAL_FILE_PREPARE_TIME);
|
||||
HistogramData run_hd = hist(INGEST_EXTERNAL_FILE_RUN_TIME);
|
||||
ASSERT_EQ(1, prepare_hd.count);
|
||||
ASSERT_EQ(1, run_hd.count);
|
||||
ASSERT_GT(prepare_hd.max, 0.0);
|
||||
ASSERT_GT(run_hd.max, 0.0);
|
||||
|
||||
// A second call adds exactly one more sample to each histogram.
|
||||
ASSERT_OK(GenerateAndAddExternalFile(
|
||||
options, {{"k4", "v4"}, {"k5", "v5"}}, /*file_id=*/-1,
|
||||
/*allow_global_seqno=*/true, /*write_global_seqno=*/false,
|
||||
/*verify_checksums_before_ingest=*/true, /*ingest_behind=*/false,
|
||||
/*sort_data=*/true));
|
||||
ASSERT_EQ(2, hist(INGEST_EXTERNAL_FILE_PREPARE_TIME).count);
|
||||
ASSERT_EQ(2, hist(INGEST_EXTERNAL_FILE_RUN_TIME).count);
|
||||
|
||||
// A failed ingestion records nothing (latency is logged only on success).
|
||||
ASSERT_NOK(db_->IngestExternalFile({"/path/does/not/exist.sst"},
|
||||
IngestExternalFileOptions()));
|
||||
ASSERT_EQ(2, hist(INGEST_EXTERNAL_FILE_PREPARE_TIME).count);
|
||||
ASSERT_EQ(2, hist(INGEST_EXTERNAL_FILE_RUN_TIME).count);
|
||||
}
|
||||
|
||||
// Ingestion that reuses the SstFileWriter's metadata via
|
||||
// IngestExternalFileArg::file_infos skips the open-and-scan path and still
|
||||
// produces correct data, including overlapping data that is reassigned a new
|
||||
// global sequence number.
|
||||
TEST_F(ExternalSSTFileTest, IngestWithFileInfo) {
|
||||
Options options = CurrentOptions();
|
||||
DestroyAndReopen(options);
|
||||
std::map<std::string, std::string> true_data;
|
||||
|
||||
// Count entries into the open-and-scan path so we can assert it is skipped.
|
||||
std::atomic<int> read_path_count{0};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"ExternalSstFileIngestionJob::GetIngestedFileInfo:ReadPath",
|
||||
[&](void*) { read_path_count.fetch_add(1); });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Reuse the writer's metadata, with the normal ingestion options: a global
|
||||
// sequence number is assigned (allow_db_generated_files is NOT set) and the
|
||||
// checksum is verified (the fast-path opens the file only to verify it, which
|
||||
// is not a metadata scan).
|
||||
auto ingest_fi = [&](std::vector<std::pair<std::string, std::string>> data) {
|
||||
return GenerateAndAddExternalFile(
|
||||
options, std::move(data), /*file_id=*/-1, /*allow_global_seqno=*/true,
|
||||
/*write_global_seqno=*/false, /*verify_checksums_before_ingest=*/true,
|
||||
/*ingest_behind=*/false, /*sort_data=*/true, &true_data,
|
||||
/*cfh=*/nullptr, /*fill_cache=*/false, /*ingest_with_file_info=*/true);
|
||||
};
|
||||
|
||||
// Non-overlapping ingest: data reads back and the file is not scanned.
|
||||
ASSERT_OK(ingest_fi({{Key(1), "a1"}, {Key(2), "a2"}, {Key(3), "a3"}}));
|
||||
ASSERT_EQ(read_path_count.load(), 0);
|
||||
|
||||
// Overlap existing (flushed) data so a non-zero global seqno is assigned and
|
||||
// the newer values win -- the normal seqno-reassignment path through
|
||||
// file_infos.
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(2), "old2"));
|
||||
true_data[Key(2)] = "old2";
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(ingest_fi({{Key(2), "new2"}, {Key(4), "new4"}}));
|
||||
ASSERT_EQ(read_path_count.load(), 0);
|
||||
|
||||
// Control: ingesting the same way but WITHOUT file_infos does enter the scan.
|
||||
ASSERT_OK(GenerateAndAddExternalFile(
|
||||
options, {{Key(8), "c8"}}, /*file_id=*/-1, /*allow_global_seqno=*/true,
|
||||
/*write_global_seqno=*/false, /*verify_checksums_before_ingest=*/true,
|
||||
/*ingest_behind=*/false, /*sort_data=*/true, &true_data));
|
||||
ASSERT_GT(read_path_count.load(), 0);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
size_t kcnt = 0;
|
||||
VerifyDBFromMap(true_data, &kcnt, false);
|
||||
}
|
||||
|
||||
// Range deletions are honored on the file_infos fast-path: the writer's
|
||||
// range-del bounds are carried in the metadata, so a DeleteRange covering
|
||||
// existing keys still shadows them.
|
||||
TEST_F(ExternalSSTFileTest, IngestWithFileInfoRangeDeletion) {
|
||||
Options options = CurrentOptions();
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(20), "live20"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(25), "live25"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
const std::string f = sst_files_dir_ + "rangedel.sst";
|
||||
SstFileWriter w(EnvOptions(), options);
|
||||
ASSERT_OK(w.Open(f));
|
||||
ASSERT_OK(w.Put(Key(30), "v30"));
|
||||
ASSERT_OK(w.DeleteRange(Key(20), Key(26))); // covers keys 20..25
|
||||
ExternalSstFileInfo info;
|
||||
ASSERT_OK(w.Finish(&info));
|
||||
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = db_->DefaultColumnFamily();
|
||||
arg.external_files = {f};
|
||||
arg.file_infos = {info};
|
||||
arg.options.allow_global_seqno = true;
|
||||
ASSERT_OK(db_->IngestExternalFiles({arg}));
|
||||
|
||||
ReadOptions ro;
|
||||
std::string val;
|
||||
ASSERT_TRUE(db_->Get(ro, Key(20), &val).IsNotFound());
|
||||
ASSERT_TRUE(db_->Get(ro, Key(25), &val).IsNotFound());
|
||||
ASSERT_OK(db_->Get(ro, Key(30), &val));
|
||||
ASSERT_EQ(val, "v30");
|
||||
}
|
||||
|
||||
// verify_checksums_before_ingest is honored on the file_infos fast-path: it
|
||||
// opens the file just to verify, so corruption is still detected.
|
||||
TEST_F(ExternalSSTFileTest, IngestWithFileInfoVerifiesChecksum) {
|
||||
Options options = CurrentOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string f = sst_files_dir_ + "corrupt.sst";
|
||||
SstFileWriter w(EnvOptions(), options);
|
||||
ASSERT_OK(w.Open(f));
|
||||
for (int k = 0; k < 100; k++) {
|
||||
ASSERT_OK(w.Put(Key(k), "v" + Key(k)));
|
||||
}
|
||||
ExternalSstFileInfo info;
|
||||
ASSERT_OK(w.Finish(&info));
|
||||
ASSERT_OK(test::CorruptFile(options.env, f, /*offset=*/64,
|
||||
/*bytes_to_corrupt=*/8));
|
||||
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = db_->DefaultColumnFamily();
|
||||
arg.external_files = {f};
|
||||
arg.file_infos = {info};
|
||||
arg.options.verify_checksums_before_ingest = true;
|
||||
ASSERT_TRUE(db_->IngestExternalFiles({arg}).IsCorruption());
|
||||
}
|
||||
|
||||
// write_global_seqno is incompatible with file_infos: the file is not opened,
|
||||
// so the seqno cannot be written back into it.
|
||||
TEST_F(ExternalSSTFileTest, IngestWithFileInfoRejectsWriteGlobalSeqno) {
|
||||
Options options = CurrentOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string f = sst_files_dir_ + "wgs.sst";
|
||||
SstFileWriter w(EnvOptions(), options);
|
||||
ASSERT_OK(w.Open(f));
|
||||
ASSERT_OK(w.Put(Key(1), "v1"));
|
||||
ExternalSstFileInfo info;
|
||||
ASSERT_OK(w.Finish(&info));
|
||||
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = db_->DefaultColumnFamily();
|
||||
arg.external_files = {f};
|
||||
arg.file_infos = {info};
|
||||
arg.options.write_global_seqno = true;
|
||||
ASSERT_TRUE(db_->IngestExternalFiles({arg}).IsInvalidArgument());
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest, ComparatorMismatch) {
|
||||
Options options = CurrentOptions();
|
||||
Options options_diff_ucmp = options;
|
||||
@@ -1816,7 +2000,8 @@ TEST_P(ExternalSSTFileTest, IngestFileWithGlobalSeqnoRandomized) {
|
||||
for (int i = 0; i < 500; i++) {
|
||||
std::vector<std::pair<std::string, std::string>> random_data;
|
||||
for (int j = 0; j < 100; j++) {
|
||||
std::string k = rnd.RandomString(rnd.Next() % 20);
|
||||
std::string k =
|
||||
rnd.RandomString(rnd.Next() % 20 + 1); // requires non-empty keys
|
||||
std::string v = rnd.RandomString(rnd.Next() % 50);
|
||||
random_data.emplace_back(k, v);
|
||||
}
|
||||
|
||||
@@ -228,6 +228,7 @@ DECLARE_uint64(backup_max_size);
|
||||
DECLARE_int32(checkpoint_one_in);
|
||||
DECLARE_int32(ingest_external_file_one_in);
|
||||
DECLARE_int32(ingest_external_file_width);
|
||||
DECLARE_int32(ingest_external_file_use_file_info_one_in);
|
||||
DECLARE_int32(compact_files_one_in);
|
||||
DECLARE_int32(compact_range_one_in);
|
||||
DECLARE_int32(promote_l0_one_in);
|
||||
|
||||
@@ -862,6 +862,12 @@ DEFINE_int32(ingest_external_file_one_in, 0,
|
||||
DEFINE_int32(ingest_external_file_width, 100,
|
||||
"The width of the ingested external files.");
|
||||
|
||||
DEFINE_int32(ingest_external_file_use_file_info_one_in, 0,
|
||||
"If non-zero, the ingestexternalfile flow reuses each file's "
|
||||
"metadata via IngestExternalFileArg::file_infos (from "
|
||||
"SstFileWriter::Finish) once every N ingestions on average, so "
|
||||
"ingestion skips re-opening and scanning the files.");
|
||||
|
||||
DEFINE_int32(compact_files_one_in, 0,
|
||||
"If non-zero, then CompactFiles() will be called once for every N "
|
||||
"operations on average. 0 indicates CompactFiles() is disabled.");
|
||||
|
||||
@@ -2326,6 +2326,12 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
// deletion file's compaction input optimization.
|
||||
bool test_standalone_range_deletion = thread->rand.OneInOpt(
|
||||
FLAGS_test_ingest_standalone_range_deletion_one_in);
|
||||
// When true, reuse the writer's metadata via IngestExternalFileArg's
|
||||
// file_infos so ingestion skips re-opening and scanning the file. Not
|
||||
// combined with the standalone range deletion mode (a range-del-only file).
|
||||
bool use_file_info =
|
||||
!test_standalone_range_deletion &&
|
||||
thread->rand.OneInOpt(FLAGS_ingest_external_file_use_file_info_one_in);
|
||||
std::vector<std::string> external_files;
|
||||
const std::string sst_filename =
|
||||
GetDbPath() + "/." + std::to_string(thread->tid) + ".sst";
|
||||
@@ -2369,6 +2375,7 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
SstFileWriter sst_file_writer(EnvOptions(options_), options_);
|
||||
SstFileWriter standalone_rangedel_sst_file_writer(EnvOptions(options_),
|
||||
options_);
|
||||
ExternalSstFileInfo sst_file_info;
|
||||
if (s.ok()) {
|
||||
s = sst_file_writer.Open(sst_filename);
|
||||
}
|
||||
@@ -2462,7 +2469,7 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
}
|
||||
}
|
||||
if (s.ok() && !keys.empty()) {
|
||||
s = sst_file_writer.Finish();
|
||||
s = sst_file_writer.Finish(&sst_file_info);
|
||||
}
|
||||
|
||||
if (s.ok() && total_keys != 0 && test_standalone_range_deletion) {
|
||||
@@ -2494,9 +2501,19 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
<< ingest_options.verify_checksums_readahead_size
|
||||
<< ", fill_cache: " << ingest_options.fill_cache
|
||||
<< ", test_standalone_range_deletion: "
|
||||
<< test_standalone_range_deletion;
|
||||
s = db_->IngestExternalFile(column_families_[column_family],
|
||||
external_files, ingest_options);
|
||||
<< test_standalone_range_deletion
|
||||
<< ", use_file_info: " << use_file_info;
|
||||
if (use_file_info) {
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = column_families_[column_family];
|
||||
arg.external_files = external_files;
|
||||
arg.file_infos = {sst_file_info};
|
||||
arg.options = ingest_options;
|
||||
s = db_->IngestExternalFiles({arg});
|
||||
} else {
|
||||
s = db_->IngestExternalFile(column_families_[column_family],
|
||||
external_files, ingest_options);
|
||||
}
|
||||
}
|
||||
if (!s.ok()) {
|
||||
for (PendingExpectedValue& pending_expected_value :
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "rocksdb/file_checksum.h"
|
||||
#include "rocksdb/listener.h"
|
||||
#include "rocksdb/sst_partitioner.h"
|
||||
#include "rocksdb/table_properties.h"
|
||||
#include "rocksdb/types.h"
|
||||
#include "rocksdb/universal_compaction.h"
|
||||
#include "rocksdb/version.h"
|
||||
@@ -2871,6 +2872,69 @@ struct IngestExternalFileOptions {
|
||||
bool fill_cache = true;
|
||||
};
|
||||
|
||||
// ExternalSstFileInfo includes information about an SST file. It is produced by
|
||||
// SstFileWriter. When ExternalSstFileInfo is included in IngestExternalFileArg,
|
||||
// it can significantly improve ingestion performance by avoiding additional IO
|
||||
// on the file.
|
||||
//
|
||||
// TODO: This should be opaque to the user, consider removing the internal
|
||||
// details in next major release.
|
||||
struct ExternalSstFileInfo {
|
||||
ExternalSstFileInfo()
|
||||
: file_path(""),
|
||||
smallest_key(""),
|
||||
largest_key(""),
|
||||
smallest_range_del_key(""),
|
||||
largest_range_del_key(""),
|
||||
file_checksum(""),
|
||||
file_checksum_func_name(""),
|
||||
sequence_number(0),
|
||||
file_size(0),
|
||||
num_entries(0),
|
||||
num_range_del_entries(0),
|
||||
version(0) {}
|
||||
|
||||
ExternalSstFileInfo(const std::string& _file_path,
|
||||
const std::string& _smallest_key,
|
||||
const std::string& _largest_key,
|
||||
SequenceNumber _sequence_number, uint64_t _file_size,
|
||||
uint64_t _num_entries, int32_t _version)
|
||||
: file_path(_file_path),
|
||||
smallest_key(_smallest_key),
|
||||
largest_key(_largest_key),
|
||||
smallest_range_del_key(""),
|
||||
largest_range_del_key(""),
|
||||
file_checksum(""),
|
||||
file_checksum_func_name(""),
|
||||
sequence_number(_sequence_number),
|
||||
file_size(_file_size),
|
||||
num_entries(_num_entries),
|
||||
num_range_del_entries(0),
|
||||
version(_version) {}
|
||||
|
||||
std::string file_path; // external sst file path
|
||||
std::string smallest_key; // smallest user key in file, TODO: remove in favor
|
||||
// of smallest_internal_key
|
||||
std::string largest_key; // largest user key in file, TODO: remove in favor
|
||||
// of largest_internal_key
|
||||
std::string
|
||||
smallest_range_del_key; // smallest range deletion user key in file
|
||||
std::string largest_range_del_key; // largest range deletion user key in file
|
||||
std::string file_checksum; // sst file checksum;
|
||||
std::string file_checksum_func_name; // The name of file checksum function
|
||||
SequenceNumber
|
||||
sequence_number; // smallest sequence number of all keys in file
|
||||
uint64_t file_size; // file size in bytes
|
||||
uint64_t num_entries; // number of entries in file
|
||||
uint64_t num_range_del_entries; // number of range deletion entries in file
|
||||
int32_t version; // file version
|
||||
|
||||
std::string smallest_internal_key; // smallest internal key in file
|
||||
std::string largest_internal_key; // largest internal key in file
|
||||
|
||||
TableProperties table_properties; // other additional table metadata
|
||||
};
|
||||
|
||||
// It is valid that files_checksums and files_checksum_func_names are both
|
||||
// empty (no checksum information is provided for ingestion). Otherwise,
|
||||
// their sizes should be the same as external_files. The file order should
|
||||
@@ -2907,6 +2971,14 @@ struct IngestExternalFileArg {
|
||||
// exclusive, so it is best not to depend on one or the other until it is
|
||||
// sorted out.
|
||||
std::optional<RangeOpt> atomic_replace_range;
|
||||
|
||||
// Optional per-file metadata, parallel to `external_files` (same size and
|
||||
// order). When provided (e.g. the ExternalSstFileInfo returned by
|
||||
// SstFileWriter::Finish), ingestion reuses this metadata instead of
|
||||
// re-opening and scanning each file to recompute it, avoiding that extra I/O.
|
||||
// Not compatible with options.write_global_seqno (the file is not opened, so
|
||||
// a global seqno cannot be written back into it).
|
||||
std::vector<ExternalSstFileInfo> file_infos;
|
||||
};
|
||||
|
||||
enum TraceFilterType : uint64_t {
|
||||
|
||||
@@ -19,56 +19,6 @@ namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class Comparator;
|
||||
|
||||
// ExternalSstFileInfo include information about sst files created
|
||||
// using SstFileWriter.
|
||||
struct ExternalSstFileInfo {
|
||||
ExternalSstFileInfo()
|
||||
: file_path(""),
|
||||
smallest_key(""),
|
||||
largest_key(""),
|
||||
smallest_range_del_key(""),
|
||||
largest_range_del_key(""),
|
||||
file_checksum(""),
|
||||
file_checksum_func_name(""),
|
||||
sequence_number(0),
|
||||
file_size(0),
|
||||
num_entries(0),
|
||||
num_range_del_entries(0),
|
||||
version(0) {}
|
||||
|
||||
ExternalSstFileInfo(const std::string& _file_path,
|
||||
const std::string& _smallest_key,
|
||||
const std::string& _largest_key,
|
||||
SequenceNumber _sequence_number, uint64_t _file_size,
|
||||
uint64_t _num_entries, int32_t _version)
|
||||
: file_path(_file_path),
|
||||
smallest_key(_smallest_key),
|
||||
largest_key(_largest_key),
|
||||
smallest_range_del_key(""),
|
||||
largest_range_del_key(""),
|
||||
file_checksum(""),
|
||||
file_checksum_func_name(""),
|
||||
sequence_number(_sequence_number),
|
||||
file_size(_file_size),
|
||||
num_entries(_num_entries),
|
||||
num_range_del_entries(0),
|
||||
version(_version) {}
|
||||
|
||||
std::string file_path; // external sst file path
|
||||
std::string smallest_key; // smallest user key in file
|
||||
std::string largest_key; // largest user key in file
|
||||
std::string
|
||||
smallest_range_del_key; // smallest range deletion user key in file
|
||||
std::string largest_range_del_key; // largest range deletion user key in file
|
||||
std::string file_checksum; // sst file checksum;
|
||||
std::string file_checksum_func_name; // The name of file checksum function
|
||||
SequenceNumber sequence_number; // sequence number of all keys in file
|
||||
uint64_t file_size; // file size in bytes
|
||||
uint64_t num_entries; // number of entries in file
|
||||
uint64_t num_range_del_entries; // number of range deletion entries in file
|
||||
int32_t version; // file version
|
||||
};
|
||||
|
||||
// SstFileWriter is used to create sst files that can be added to database later
|
||||
// All keys in files generated by SstFileWriter will have sequence number = 0.
|
||||
//
|
||||
|
||||
@@ -765,6 +765,16 @@ enum Histograms : uint32_t {
|
||||
// blocks for uniform key distribution tracking.
|
||||
BLOCK_KEY_DISTRIBUTION_CV,
|
||||
|
||||
// Time (microseconds) in the "prepare" phase of an IngestExternalFile(s) call
|
||||
// (validation and reading/linking the files; does not block writes). One
|
||||
// sample per successful call; requires stats level > kExceptTimers.
|
||||
INGEST_EXTERNAL_FILE_PREPARE_TIME,
|
||||
|
||||
// Time (microseconds) in the "run" phase of an IngestExternalFile(s) call
|
||||
// (work under the DB mutex while writes are blocked). One sample per
|
||||
// successful call; requires stats level > kExceptTimers.
|
||||
INGEST_EXTERNAL_FILE_RUN_TIME,
|
||||
|
||||
HISTOGRAM_ENUM_MAX
|
||||
};
|
||||
|
||||
|
||||
@@ -5998,6 +5998,10 @@ class HistogramTypeJni {
|
||||
return 0x41;
|
||||
case ROCKSDB_NAMESPACE::Histograms::BLOCK_KEY_DISTRIBUTION_CV:
|
||||
return 0x42;
|
||||
case ROCKSDB_NAMESPACE::Histograms::INGEST_EXTERNAL_FILE_PREPARE_TIME:
|
||||
return 0x43;
|
||||
case ROCKSDB_NAMESPACE::Histograms::INGEST_EXTERNAL_FILE_RUN_TIME:
|
||||
return 0x44;
|
||||
case ROCKSDB_NAMESPACE::Histograms::HISTOGRAM_ENUM_MAX:
|
||||
// 0x3E is reserved for backwards compatibility on current minor
|
||||
// version.
|
||||
@@ -6151,6 +6155,10 @@ class HistogramTypeJni {
|
||||
return ROCKSDB_NAMESPACE::Histograms::MULTISCAN_BLOCKS_PER_PREPARE;
|
||||
case 0x42:
|
||||
return ROCKSDB_NAMESPACE::Histograms::BLOCK_KEY_DISTRIBUTION_CV;
|
||||
case 0x43:
|
||||
return ROCKSDB_NAMESPACE::Histograms::INGEST_EXTERNAL_FILE_PREPARE_TIME;
|
||||
case 0x44:
|
||||
return ROCKSDB_NAMESPACE::Histograms::INGEST_EXTERNAL_FILE_RUN_TIME;
|
||||
case 0x3E:
|
||||
// 0x3E is reserved for backwards compatibility on current minor
|
||||
// version.
|
||||
|
||||
@@ -231,6 +231,16 @@ public enum HistogramType {
|
||||
*/
|
||||
BLOCK_KEY_DISTRIBUTION_CV((byte) 0x42),
|
||||
|
||||
/**
|
||||
* Time (us) in the prepare phase of an IngestExternalFile(s) call.
|
||||
*/
|
||||
INGEST_EXTERNAL_FILE_PREPARE_TIME((byte) 0x43),
|
||||
|
||||
/**
|
||||
* Time (us) in the run phase of an IngestExternalFile(s) call.
|
||||
*/
|
||||
INGEST_EXTERNAL_FILE_RUN_TIME((byte) 0x44),
|
||||
|
||||
// 0x3E is reserved for backwards compatibility on current minor version.
|
||||
HISTOGRAM_ENUM_MAX((byte) 0x3E);
|
||||
|
||||
|
||||
@@ -386,6 +386,9 @@ const std::vector<std::pair<Histograms, std::string>> HistogramsNameMap = {
|
||||
{MULTISCAN_PREPARE_MICROS, "rocksdb.multiscan.prepare.micros"},
|
||||
{MULTISCAN_BLOCKS_PER_PREPARE, "rocksdb.multiscan.blocks.per.prepare"},
|
||||
{BLOCK_KEY_DISTRIBUTION_CV, "rocksdb.block.key.distribution.cv"},
|
||||
{INGEST_EXTERNAL_FILE_PREPARE_TIME,
|
||||
"rocksdb.ingest.external.file.prepare.micros"},
|
||||
{INGEST_EXTERNAL_FILE_RUN_TIME, "rocksdb.ingest.external.file.run.micros"},
|
||||
};
|
||||
|
||||
std::shared_ptr<Statistics> CreateDBStatistics() {
|
||||
|
||||
@@ -124,8 +124,17 @@ struct SstFileWriter::Rep {
|
||||
builder->Add(ikey.Encode(), value);
|
||||
|
||||
// update file info
|
||||
Slice encoded_ikey = ikey.Encode();
|
||||
if (file_info.num_entries == 0) {
|
||||
file_info.smallest_internal_key.assign(encoded_ikey.data(),
|
||||
encoded_ikey.size());
|
||||
}
|
||||
file_info.num_entries++;
|
||||
file_info.largest_key.assign(user_key.data(), user_key.size());
|
||||
// Keys are added in strictly increasing order, so the last point key is the
|
||||
// largest.
|
||||
file_info.largest_internal_key.assign(encoded_ikey.data(),
|
||||
encoded_ikey.size());
|
||||
file_info.file_size = builder->FileSize();
|
||||
|
||||
InvalidatePageCache(false /* closing */).PermitUncheckedError();
|
||||
@@ -505,6 +514,7 @@ Status SstFileWriter::Finish(ExternalSstFileInfo* file_info) {
|
||||
r->file_info.file_checksum = r->file_writer->GetFileChecksum();
|
||||
r->file_info.file_checksum_func_name =
|
||||
r->file_writer->GetFileChecksumFuncName();
|
||||
r->file_info.table_properties = r->builder->GetTableProperties();
|
||||
}
|
||||
if (!s.ok()) {
|
||||
Status status = r->ioptions.env->DeleteFile(r->file_info.file_path);
|
||||
@@ -529,6 +539,16 @@ Status SstFileWriter::Finish(ExternalSstFileInfo* file_info) {
|
||||
assert(largest_key.size() >= r->ts_sz);
|
||||
file_info->smallest_key.resize(smallest_key.size() - r->ts_sz);
|
||||
file_info->largest_key.resize(largest_key.size() - r->ts_sz);
|
||||
// Strip the timestamp (located just before the 8-byte internal-key
|
||||
// footer) from the internal-key boundaries as well.
|
||||
file_info->smallest_internal_key.erase(
|
||||
file_info->smallest_internal_key.size() - kNumInternalBytes -
|
||||
r->ts_sz,
|
||||
r->ts_sz);
|
||||
file_info->largest_internal_key.erase(
|
||||
file_info->largest_internal_key.size() - kNumInternalBytes -
|
||||
r->ts_sz,
|
||||
r->ts_sz);
|
||||
}
|
||||
if (!smallest_range_del_key.empty()) {
|
||||
assert(smallest_range_del_key.size() >= r->ts_sz);
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
#include "rocksdb/secondary_cache.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "rocksdb/slice_transform.h"
|
||||
#include "rocksdb/sst_file_writer.h"
|
||||
#include "rocksdb/stats_history.h"
|
||||
#include "rocksdb/table.h"
|
||||
#include "rocksdb/tool_hooks.h"
|
||||
@@ -235,6 +236,8 @@ DEFINE_string(
|
||||
"\tcompact1 -- compact L1 into L2\n"
|
||||
"\twaitforcompaction - pause until compaction is (probably) done\n"
|
||||
"\tflush - flush the memtable\n"
|
||||
"\tingestexternalfile -- Create external SST files and ingest them into "
|
||||
"the DB via IngestExternalFile\n"
|
||||
"\topenandcompact -- Open DB and compact all files to bottommost level, "
|
||||
"writing output to separate directory without modifying source DB. "
|
||||
"Designed for remote compaction service testing\n"
|
||||
@@ -1309,6 +1312,21 @@ DEFINE_string(backup_dir, "",
|
||||
DEFINE_string(restore_dir, "",
|
||||
"If not empty string, use the given dir for restore.");
|
||||
|
||||
DEFINE_int32(ingest_external_file_batch_size, 10,
|
||||
"Number of SST files ingested per IngestExternalFile call (one "
|
||||
"batch) in the ingestexternalfile benchmark.");
|
||||
|
||||
DEFINE_int32(ingest_external_file_num_batches, 1,
|
||||
"Number of IngestExternalFile calls (batches) in the "
|
||||
"ingestexternalfile benchmark. Total files ingested is "
|
||||
"batch_size * num_batches; each file holds --num keys.");
|
||||
|
||||
DEFINE_bool(
|
||||
ingest_external_file_use_file_info, false,
|
||||
"If true, the ingestexternalfile benchmark passes each file's metadata "
|
||||
"(from SstFileWriter::Finish) via IngestExternalFileArg::file_infos, so "
|
||||
"ingestion reuses it instead of re-opening and scanning the files.");
|
||||
|
||||
DEFINE_uint64(
|
||||
initial_auto_readahead_size,
|
||||
ROCKSDB_NAMESPACE::BlockBasedTableOptions().initial_auto_readahead_size,
|
||||
@@ -3972,6 +3990,9 @@ class Benchmark {
|
||||
method = &Benchmark::WriteSeqSeekSeq;
|
||||
} else if (name == "compact") {
|
||||
method = &Benchmark::Compact;
|
||||
} else if (name == "ingestexternalfile") {
|
||||
num_threads = 1;
|
||||
method = &Benchmark::IngestExternalFile;
|
||||
} else if (name == "compactall") {
|
||||
CompactAll();
|
||||
} else if (name == "compact0") {
|
||||
@@ -9288,6 +9309,118 @@ class Benchmark {
|
||||
db->CompactRange(cro, nullptr, nullptr);
|
||||
}
|
||||
|
||||
// Creates SST files with SstFileWriter and ingests them into the DB via
|
||||
// IngestExternalFile, reporting throughput. Batching is used only to group
|
||||
// multiple files into a single IngestExternalFile call:
|
||||
// --ingest_external_file_batch_size files per call,
|
||||
// --ingest_external_file_num_batches calls. Each file holds --num keys over
|
||||
// the same key range, so the files overlap and are assigned new global
|
||||
// sequence numbers. With --ingest_external_file_use_file_info the writer's
|
||||
// metadata is passed via IngestExternalFileArg::file_infos so ingestion
|
||||
// reuses it instead of re-opening and scanning the files. Single threaded.
|
||||
//
|
||||
// NOTE: db_bench's reported micros/op includes file generation; use the
|
||||
// rocksdb.ingest.external.file.micros histogram (--statistics) for the
|
||||
// isolated per-call ingest latency.
|
||||
void IngestExternalFile(ThreadState* thread) {
|
||||
DB* db = SelectDB(thread);
|
||||
|
||||
const int batch_size = std::max(1, FLAGS_ingest_external_file_batch_size);
|
||||
const int num_batches = std::max(1, FLAGS_ingest_external_file_num_batches);
|
||||
const bool use_file_info = FLAGS_ingest_external_file_use_file_info;
|
||||
// --num is the number of keys in each file; all files cover this same key
|
||||
// range, so they overlap (which is fine -- a global seqno is assigned).
|
||||
const int64_t keys_per_file = num_;
|
||||
const int64_t key_space = keys_per_file;
|
||||
|
||||
const std::string tmp_dir = FLAGS_db + "/ingest_tmp";
|
||||
Status s = FLAGS_env->CreateDirIfMissing(tmp_dir);
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "CreateDirIfMissing(%s) failed: %s\n", tmp_dir.c_str(),
|
||||
s.ToString().c_str());
|
||||
db_bench_exit(1);
|
||||
}
|
||||
|
||||
RandomGenerator gen;
|
||||
std::unique_ptr<const char[]> key_guard;
|
||||
Slice key = AllocateKey(&key_guard);
|
||||
int64_t bytes = 0;
|
||||
|
||||
for (int b = 0; b < num_batches; ++b) {
|
||||
// Write batch_size SstFileWriter files (all covering the same key range);
|
||||
// they are batched into a single ingest call below.
|
||||
std::vector<std::string> files;
|
||||
std::vector<ExternalSstFileInfo> file_infos;
|
||||
for (int j = 0; j < batch_size; ++j) {
|
||||
std::string file_path = tmp_dir + "/ingest_" + std::to_string(b) + "_" +
|
||||
std::to_string(j) + ".sst";
|
||||
SstFileWriter sst_file_writer(EnvOptions(), open_options_);
|
||||
s = sst_file_writer.Open(file_path);
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "SstFileWriter::Open(%s) failed: %s\n",
|
||||
file_path.c_str(), s.ToString().c_str());
|
||||
db_bench_exit(1);
|
||||
}
|
||||
for (int64_t i = 0; i < keys_per_file; ++i) {
|
||||
GenerateKeyFromInt(i, key_space, &key);
|
||||
Slice value = gen.Generate();
|
||||
s = sst_file_writer.Put(key, value);
|
||||
if (!s.ok()) {
|
||||
sst_file_writer.Finish().PermitUncheckedError();
|
||||
fprintf(stderr, "SstFileWriter::Put failed: %s\n",
|
||||
s.ToString().c_str());
|
||||
db_bench_exit(1);
|
||||
}
|
||||
bytes += key.size() + value.size();
|
||||
}
|
||||
ExternalSstFileInfo info;
|
||||
s = sst_file_writer.Finish(&info);
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "SstFileWriter::Finish(%s) failed: %s\n",
|
||||
file_path.c_str(), s.ToString().c_str());
|
||||
db_bench_exit(1);
|
||||
}
|
||||
files.emplace_back(file_path);
|
||||
file_infos.emplace_back(std::move(info));
|
||||
}
|
||||
|
||||
// Link the files into the DB instead of copying them, so the benchmark
|
||||
// measures the ingest path itself (which the file_info fast-path speeds
|
||||
// up by skipping the re-open and scan of each file) rather than file-copy
|
||||
// throughput. allow_global_seqno (default) handles the overlap.
|
||||
IngestExternalFileOptions ingest_options;
|
||||
ingest_options.move_files = true;
|
||||
if (use_file_info) {
|
||||
// Reuse the writer's metadata so ingestion skips re-opening/scanning.
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = db->DefaultColumnFamily();
|
||||
arg.external_files = files;
|
||||
arg.file_infos = std::move(file_infos);
|
||||
arg.options = ingest_options;
|
||||
s = db->IngestExternalFiles({arg});
|
||||
} else {
|
||||
s = db->IngestExternalFile(files, ingest_options);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "IngestExternalFile failed: %s\n",
|
||||
s.ToString().c_str());
|
||||
db_bench_exit(1);
|
||||
}
|
||||
thread->stats.FinishedOps(&db_, db, 1, kWrite);
|
||||
|
||||
for (const auto& f : files) {
|
||||
FLAGS_env->DeleteFile(f).PermitUncheckedError();
|
||||
}
|
||||
}
|
||||
|
||||
thread->stats.AddBytes(bytes);
|
||||
char msg[100];
|
||||
snprintf(msg, sizeof(msg),
|
||||
"(%d batches x %d files, %" PRId64 " keys/file%s)", num_batches,
|
||||
batch_size, keys_per_file, use_file_info ? ", file_info" : "");
|
||||
thread->stats.AddMessage(msg);
|
||||
}
|
||||
|
||||
void CompactAll() {
|
||||
CompactRangeOptions cro;
|
||||
cro.max_subcompactions = static_cast<uint32_t>(FLAGS_subcompactions);
|
||||
|
||||
@@ -85,6 +85,21 @@ class DBBenchTest : public testing::Test {
|
||||
ASSERT_EQ(0, db_bench_tool(argc(), argv()));
|
||||
}
|
||||
|
||||
// Every flag is passed explicitly because gflags state persists across
|
||||
// db_bench_tool() calls within the same test process.
|
||||
void RunIngestBench(int batch_size, int num_batches, bool use_file_info) {
|
||||
ResetArgs();
|
||||
AppendArgs(
|
||||
{"./db_bench", "--benchmarks=ingestexternalfile", "--use_existing_db=0",
|
||||
"--num=2000", "--compression_type=none", "--db=" + db_path_,
|
||||
"--wal_dir=" + wal_path_,
|
||||
"--ingest_external_file_batch_size=" + std::to_string(batch_size),
|
||||
"--ingest_external_file_num_batches=" + std::to_string(num_batches),
|
||||
"--ingest_external_file_use_file_info=" +
|
||||
std::string(use_file_info ? "true" : "false")});
|
||||
ASSERT_EQ(0, db_bench_tool(argc(), argv()));
|
||||
}
|
||||
|
||||
void VerifyOptions(const Options& opt) {
|
||||
DBOptions loaded_db_opts;
|
||||
ConfigOptions config_opts;
|
||||
@@ -144,6 +159,16 @@ TEST_F(DBBenchTest, OptionsFile) {
|
||||
VerifyOptions(opt);
|
||||
}
|
||||
|
||||
TEST_F(DBBenchTest, IngestExternalFile) {
|
||||
RunIngestBench(/*batch_size=*/3, /*num_batches=*/2,
|
||||
/*use_file_info=*/false);
|
||||
}
|
||||
|
||||
TEST_F(DBBenchTest, IngestExternalFileWithFileInfo) {
|
||||
RunIngestBench(/*batch_size=*/3, /*num_batches=*/2,
|
||||
/*use_file_info=*/true);
|
||||
}
|
||||
|
||||
TEST_F(DBBenchTest, OptionsFileUniversal) {
|
||||
const std::string kOptionsFileName = test_path_ + "/OPTIONS_test";
|
||||
|
||||
|
||||
@@ -215,6 +215,7 @@ default_params = {
|
||||
"index_block_search_type": lambda: random.choice([0, 1, 2]),
|
||||
"uniform_cv_threshold": lambda: random.choice([-1, 0.2, 1000]),
|
||||
"ingest_external_file_one_in": lambda: random.choice([1000, 1000000]),
|
||||
"ingest_external_file_use_file_info_one_in": lambda: random.choice([0, 2]),
|
||||
"test_ingest_standalone_range_deletion_one_in": lambda: random.choice([0, 5, 10]),
|
||||
"iterpercent": 10,
|
||||
"lock_wal_one_in": lambda: random.choice([10000, 1000000]),
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Added two stats histograms reporting the per-call latency (in microseconds) of each successful `IngestExternalFile`/`IngestExternalFiles` call, split by phase: `rocksdb.ingest.external.file.prepare.micros` (argument validation and reading/validating/linking the external files, which does not block live writes) and `rocksdb.ingest.external.file.run.micros` (the ingestion performed under the DB mutex while live writes are blocked). Failed ingestions are not recorded.
|
||||
@@ -0,0 +1 @@
|
||||
`IngestExternalFileArg` gained a `file_infos` field, a vector of `ExternalSstFileInfo` parallel to `external_files`. When set, `IngestExternalFiles()` reuses the supplied per-file metadata (boundary keys, sequence-number bounds, and table properties) instead of re-opening and scanning each file to recompute it, avoiding that extra I/O.
|
||||
Reference in New Issue
Block a user