Skip to content

Commit 4de3ce3

Browse files
committed
Add option to verify block checksums of output files (facebook#14103)
Summary: For all compactions, RocksDB performs a lightweight sanity check on output SST files before installation (in `CompactionJob::VerifyOutputFiles()`). However, this lightweight check may not catch corruption that is small enough to allow the SST files to still be opened. There is an existing feature, `paranoid_file_check`, which opens the SST file, iterates through all keys, and checks the hash of each key. While this provides the ultimate level of data integrity checking, it comes at a high computational cost. In this PR, we introduce a new mutable CF option, `verify_output_flags`. The `verify_output_flags` is a bitmask enum that allows users to select various verification types, including block checksum verification, full key iteration, and file checksum verification (to be added in subsequent PRs). Note that the existing `paranoid_file_check` option is equivalent to a full key iteration check. Block-level checksum verification is much lighter than the full key iteration check. Please note that the previously deprecated `verify_checksums_in_compaction` option (removed in version 5.3.0) was for verifying the checksum of **input SST files**. RocksDB continues to perform this verification for both local and remote compactions, and this behavior remains unchanged. In contrast, this PR focuses on verifying the **output SST files**. ## To follow up - File-level Checksum verification for output SST files - Deprecate `paranoid_file_checks` option in favor of the new option - Add to stress test / db_bench Pull Request resolved: facebook#14103 Test Plan: New Unit Test added. The corruption is both detected by `paranoid_file_check` and various types of verification set by this new option, `verify_output_flags` ``` ./compaction_service_test --gtest_filter="*CompactionServiceTest.CorruptedOutput*" ``` Reviewed By: pdillinger Differential Revision: D86357924 Pulled By: jaykorean fbshipit-source-id: a9e04798f249c7e977231e179622a0830d6675fe
1 parent 92ede49 commit 4de3ce3

12 files changed

Lines changed: 254 additions & 52 deletions

db/compaction/compaction_job.cc

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -812,20 +812,19 @@ Status CompactionJob::SyncOutputDirectories() {
812812
Status CompactionJob::VerifyOutputFiles() {
813813
Status status;
814814
std::vector<port::Thread> thread_pool;
815-
std::vector<const CompactionOutputs::Output*> files_output;
816-
for (const auto& state : compact_->sub_compact_states) {
817-
for (const auto& output : state.GetOutputs()) {
818-
files_output.emplace_back(&output);
819-
}
820-
}
821815
ColumnFamilyData* cfd = compact_->compaction->column_family_data();
822-
std::atomic<size_t> next_file_idx(0);
823-
auto verify_table = [&](Status& output_status) {
824-
while (true) {
825-
size_t file_idx = next_file_idx.fetch_add(1);
826-
if (file_idx >= files_output.size()) {
827-
break;
828-
}
816+
VerifyOutputFlags verify_output_flags =
817+
compact_->compaction->mutable_cf_options().verify_output_flags;
818+
819+
// For backward compatibility
820+
if (paranoid_file_checks_) {
821+
verify_output_flags |= VerifyOutputFlags::kVerifyIteration;
822+
verify_output_flags |= VerifyOutputFlags::kEnableForLocalCompaction;
823+
verify_output_flags |= VerifyOutputFlags::kEnableForRemoteCompaction;
824+
}
825+
826+
auto verify_table = [&](SubcompactionState& subcompaction_state) {
827+
for (const auto& output_file : subcompaction_state.GetOutputs()) {
829828
// Verify that the table is usable
830829
// We set for_compaction to false and don't
831830
// OptimizeForCompactionTableRead here because this is a special case
@@ -834,13 +833,19 @@ Status CompactionJob::VerifyOutputFiles() {
834833
// verification as user reads since the goal is to cache it here for
835834
// further user reads
836835
ReadOptions verify_table_read_options(Env::IOActivity::kCompaction);
836+
verify_table_read_options.verify_checksums = true;
837+
verify_table_read_options.readahead_size =
838+
file_options_for_read_.compaction_readahead_size;
839+
840+
std::unique_ptr<TableReader> table_reader_guard;
841+
TableReader* table_reader_ptr = table_reader_guard.get();
837842
verify_table_read_options.rate_limiter_priority =
838843
GetRateLimiterPriority();
839844
InternalIterator* iter = cfd->table_cache()->NewIterator(
840845
verify_table_read_options, file_options_, cfd->internal_comparator(),
841-
files_output[file_idx]->meta,
846+
output_file.meta,
842847
/*range_del_agg=*/nullptr, compact_->compaction->mutable_cf_options(),
843-
/*table_reader_ptr=*/nullptr,
848+
/*table_reader_ptr=*/&table_reader_ptr,
844849
cfd->internal_stats()->GetFileReadHist(
845850
compact_->compaction->output_level()),
846851
TableReaderCaller::kCompactionRefill, /*arena=*/nullptr,
@@ -850,38 +855,63 @@ Status CompactionJob::VerifyOutputFiles() {
850855
/*largest_compaction_key=*/nullptr,
851856
/*allow_unprepared_value=*/false);
852857
auto s = iter->status();
853-
854-
if (s.ok() && paranoid_file_checks_) {
855-
OutputValidator validator(cfd->internal_comparator(),
856-
/*_enable_hash=*/true);
857-
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
858-
s = validator.Add(iter->key(), iter->value());
859-
if (!s.ok()) {
860-
break;
858+
if (s.ok()) {
859+
// Check for remote/local compaction and verify_output_flags flags
860+
const bool should_verify =
861+
(subcompaction_state.compaction_job_stats.is_remote_compaction &&
862+
!!(verify_output_flags &
863+
VerifyOutputFlags::kEnableForRemoteCompaction)) ||
864+
(!subcompaction_state.compaction_job_stats.is_remote_compaction &&
865+
!!(verify_output_flags &
866+
VerifyOutputFlags::kEnableForLocalCompaction));
867+
868+
if (should_verify) {
869+
const bool should_verify_block_checksum =
870+
!!(verify_output_flags & VerifyOutputFlags::kVerifyBlockChecksum);
871+
const bool should_verify_iteration =
872+
!!(verify_output_flags & VerifyOutputFlags::kVerifyIteration);
873+
if (should_verify_block_checksum) {
874+
assert(table_reader_ptr != nullptr);
875+
// If verifying iteration as well, verify meta blocks here only to
876+
// avoid redundant checks on data blocks
877+
s = table_reader_ptr->VerifyChecksum(
878+
verify_table_read_options, TableReaderCaller::kCompaction,
879+
/*meta_blocks_only=*/should_verify_iteration);
880+
}
881+
if (s.ok() && should_verify_iteration) {
882+
OutputValidator validator(cfd->internal_comparator(),
883+
/*_enable_hash=*/true);
884+
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
885+
s = validator.Add(iter->key(), iter->value());
886+
if (!s.ok()) {
887+
break;
888+
}
889+
}
890+
if (s.ok()) {
891+
s = iter->status();
892+
}
893+
if (s.ok() && !validator.CompareValidator(output_file.validator)) {
894+
s = Status::Corruption(
895+
"Key-value checksum of compaction output doesn't match what "
896+
"was computed when written");
897+
}
861898
}
862-
}
863-
if (s.ok()) {
864-
s = iter->status();
865-
}
866-
if (s.ok() &&
867-
!validator.CompareValidator(files_output[file_idx]->validator)) {
868-
s = Status::Corruption("Paranoid checksums do not match");
869899
}
870900
}
871901

872902
delete iter;
873903

874904
if (!s.ok()) {
875-
output_status = s;
905+
subcompaction_state.status = s;
876906
break;
877907
}
878908
}
879909
};
880910
for (size_t i = 1; i < compact_->sub_compact_states.size(); i++) {
881911
thread_pool.emplace_back(verify_table,
882-
std::ref(compact_->sub_compact_states[i].status));
912+
std::ref(compact_->sub_compact_states[i]));
883913
}
884-
verify_table(compact_->sub_compact_states[0].status);
914+
verify_table(compact_->sub_compact_states[0]);
885915
for (auto& thread : thread_pool) {
886916
thread.join();
887917
}

db/compaction/compaction_service_test.cc

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,7 @@ TEST_F(CompactionServiceTest, CorruptedOutputParanoidFileCheck) {
10471047
Destroy(options);
10481048
options.disable_auto_compactions = true;
10491049
options.paranoid_file_checks = paranoid_file_check_enabled;
1050+
options.verify_output_flags = VerifyOutputFlags::kVerifyNone;
10501051
ReopenWithCompactionService(&options);
10511052
GenerateTestData();
10521053

@@ -1101,6 +1102,87 @@ TEST_F(CompactionServiceTest, CorruptedOutputParanoidFileCheck) {
11011102
}
11021103
}
11031104

1105+
TEST_F(CompactionServiceTest, CorruptedOutputVerifyOutputFlags) {
1106+
for (VerifyOutputFlags verify_output_flags :
1107+
{VerifyOutputFlags::kVerifyNone,
1108+
VerifyOutputFlags::kEnableForLocalCompaction |
1109+
VerifyOutputFlags::kVerifyBlockChecksum,
1110+
VerifyOutputFlags::kEnableForRemoteCompaction |
1111+
VerifyOutputFlags::kVerifyBlockChecksum,
1112+
VerifyOutputFlags::kEnableForRemoteCompaction |
1113+
VerifyOutputFlags::kVerifyIteration,
1114+
VerifyOutputFlags::kVerifyAll}) {
1115+
SCOPED_TRACE(
1116+
"verify_output_flags=" +
1117+
std::to_string(static_cast<std::underlying_type_t<VerifyOutputFlags>>(
1118+
verify_output_flags)));
1119+
1120+
Options options = CurrentOptions();
1121+
Destroy(options);
1122+
options.disable_auto_compactions = true;
1123+
options.paranoid_file_checks = false;
1124+
options.verify_output_flags = verify_output_flags;
1125+
ReopenWithCompactionService(&options);
1126+
GenerateTestData();
1127+
1128+
auto my_cs = GetCompactionService();
1129+
1130+
std::string start_str = Key(15);
1131+
std::string end_str = Key(45);
1132+
Slice start(start_str);
1133+
Slice end(end_str);
1134+
uint64_t comp_num = my_cs->GetCompactionNum();
1135+
1136+
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
1137+
"CompactionServiceCompactionJob::Run:0", [&](void* arg) {
1138+
CompactionServiceResult* compaction_result =
1139+
*(static_cast<CompactionServiceResult**>(arg));
1140+
ASSERT_TRUE(compaction_result != nullptr &&
1141+
!compaction_result->output_files.empty());
1142+
// Corrupt files here
1143+
for (const auto& output_file : compaction_result->output_files) {
1144+
std::string file_name =
1145+
compaction_result->output_path + "/" + output_file.file_name;
1146+
1147+
// Corrupt very small range of bytes. This corruption is so small
1148+
// that this isn't caught by default light-weight check
1149+
ASSERT_OK(test::CorruptFile(env_, file_name, 0, 1,
1150+
false /* verifyChecksum */));
1151+
}
1152+
});
1153+
SyncPoint::GetInstance()->EnableProcessing();
1154+
const bool is_enabled_for_remote_compaction =
1155+
!!(verify_output_flags & VerifyOutputFlags::kEnableForRemoteCompaction);
1156+
const bool should_verify_block_checksum =
1157+
!!(verify_output_flags & VerifyOutputFlags::kVerifyBlockChecksum);
1158+
const bool should_verify_iteration =
1159+
!!(verify_output_flags & VerifyOutputFlags::kVerifyIteration);
1160+
1161+
Status s = db_->CompactRange(CompactRangeOptions(), &start, &end);
1162+
if (is_enabled_for_remote_compaction &&
1163+
(should_verify_block_checksum || should_verify_iteration)) {
1164+
ASSERT_NOK(s);
1165+
ASSERT_TRUE(s.IsCorruption());
1166+
} else {
1167+
// CompactRange() goes through if block checksum wasn't verified
1168+
ASSERT_OK(s);
1169+
}
1170+
1171+
ASSERT_GE(my_cs->GetCompactionNum(), comp_num + 1);
1172+
1173+
SyncPoint::GetInstance()->DisableProcessing();
1174+
SyncPoint::GetInstance()->ClearAllCallBacks();
1175+
1176+
// On the worker side, the compaction is considered success
1177+
// Verification is done on the primary side
1178+
CompactionServiceResult result;
1179+
my_cs->GetResult(&result);
1180+
ASSERT_OK(result.status);
1181+
ASSERT_TRUE(result.stats.is_manual_compaction);
1182+
ASSERT_TRUE(result.stats.is_remote_compaction);
1183+
}
1184+
}
1185+
11041186
TEST_F(CompactionServiceTest, TruncatedOutput) {
11051187
Options options = CurrentOptions();
11061188
options.disable_auto_compactions = true;

include/rocksdb/advanced_options.h

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,61 @@ enum class PrepopulateBlobCache : uint8_t {
156156
kFlushOnly = 0x1, // Prepopulate blobs during flush only
157157
};
158158

159+
// Bitmask enum for verify output flags during compaction.
160+
// This allows fine-grained control over what verification is performed
161+
// on compaction output files and when it's enabled.
162+
enum class VerifyOutputFlags : uint32_t {
163+
kVerifyNone = 0x0, // No verification
164+
165+
// First set of bits: type of verifications
166+
kVerifyBlockChecksum = 1 << 0, // Verify block checksums
167+
kVerifyIteration = 1 << 1, // Verify iteration and full key/value hash
168+
// by comparing the one inserted into a
169+
// file, and what is read back.
170+
171+
// TODO - Implement
172+
// kVerifyFileChecksum = 1 << 2, // Verify file-level checksum
173+
174+
// Second set of bits: when to enable verification
175+
kEnableForLocalCompaction = 1 << 10, // Enable for local compaction
176+
kEnableForRemoteCompaction = 1 << 11, // Enable for remote compaction
177+
178+
// TODO - Implement
179+
// kEnableForFlush = 1 << 12, // Enable for flush
180+
181+
kVerifyAll = 0xFFFFFFFF,
182+
};
183+
184+
inline VerifyOutputFlags operator|(VerifyOutputFlags lhs,
185+
VerifyOutputFlags rhs) {
186+
using T = std::underlying_type_t<VerifyOutputFlags>;
187+
return static_cast<VerifyOutputFlags>(static_cast<T>(lhs) |
188+
static_cast<T>(rhs));
189+
}
190+
191+
inline VerifyOutputFlags& operator|=(VerifyOutputFlags& lhs,
192+
VerifyOutputFlags rhs) {
193+
lhs = lhs | rhs;
194+
return lhs;
195+
}
196+
197+
inline VerifyOutputFlags operator&(VerifyOutputFlags lhs,
198+
VerifyOutputFlags rhs) {
199+
using T = std::underlying_type_t<VerifyOutputFlags>;
200+
return static_cast<VerifyOutputFlags>(static_cast<T>(lhs) &
201+
static_cast<T>(rhs));
202+
}
203+
204+
inline VerifyOutputFlags& operator&=(VerifyOutputFlags& lhs,
205+
VerifyOutputFlags rhs) {
206+
lhs = lhs & rhs;
207+
return lhs;
208+
}
209+
210+
inline bool operator!(VerifyOutputFlags flag) {
211+
return flag == VerifyOutputFlags::kVerifyNone;
212+
}
213+
159214
struct AdvancedColumnFamilyOptions {
160215
// The maximum number of write buffers that are built up in memory.
161216
// The default and the minimum number is 2, so that when 1 write buffer
@@ -704,6 +759,13 @@ struct AdvancedColumnFamilyOptions {
704759
// Dynamically changeable through SetOptions() API
705760
bool paranoid_file_checks = false;
706761

762+
// Bitmask enum for output verification option.
763+
//
764+
// Default: 0 (kVerifyNone)
765+
//
766+
// Dynamically changeable (as a uint32_t) through SetOptions() API.
767+
VerifyOutputFlags verify_output_flags = VerifyOutputFlags::kVerifyNone;
768+
707769
// In debug mode, RocksDB runs consistency checks on the LSM every time the
708770
// LSM changes (Flush, Compaction, AddFile). When this option is true, these
709771
// checks are also enabled in release mode. These checks were historically

options/cf_options.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,10 @@ static std::unordered_map<std::string, OptionTypeInfo>
395395
{offsetof(struct MutableCFOptions, paranoid_file_checks),
396396
OptionType::kBoolean, OptionVerificationType::kNormal,
397397
OptionTypeFlags::kMutable}},
398+
{"verify_output_flags",
399+
{offsetof(struct MutableCFOptions, verify_output_flags),
400+
OptionType::kUInt32T, OptionVerificationType::kNormal,
401+
OptionTypeFlags::kMutable}},
398402
{"verify_checksums_in_compaction",
399403
{0, OptionType::kBoolean, OptionVerificationType::kDeprecated,
400404
OptionTypeFlags::kMutable}},

options/cf_options.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ struct MutableCFOptions {
143143
preclude_last_level_data_seconds(
144144
options.preclude_last_level_data_seconds),
145145
preserve_internal_time_seconds(options.preserve_internal_time_seconds),
146+
verify_output_flags(options.verify_output_flags),
146147
enable_blob_files(options.enable_blob_files),
147148
min_blob_size(options.min_blob_size),
148149
blob_file_size(options.blob_file_size),
@@ -213,6 +214,7 @@ struct MutableCFOptions {
213214
compaction_options_fifo(),
214215
preclude_last_level_data_seconds(0),
215216
preserve_internal_time_seconds(0),
217+
verify_output_flags(VerifyOutputFlags::kVerifyNone),
216218
enable_blob_files(false),
217219
min_blob_size(0),
218220
blob_file_size(0),
@@ -313,6 +315,7 @@ struct MutableCFOptions {
313315
CompactionOptionsUniversal compaction_options_universal;
314316
uint64_t preclude_last_level_data_seconds;
315317
uint64_t preserve_internal_time_seconds;
318+
VerifyOutputFlags verify_output_flags;
316319

317320
// Blob file related options
318321
bool enable_blob_files;

options/options_helper.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@ void UpdateColumnFamilyOptions(const MutableCFOptions& moptions,
270270
cf_opts->compaction_options_fifo = moptions.compaction_options_fifo;
271271
cf_opts->compaction_options_universal = moptions.compaction_options_universal;
272272

273+
cf_opts->verify_output_flags = moptions.verify_output_flags;
274+
273275
// Blob file related options
274276
cf_opts->enable_blob_files = moptions.enable_blob_files;
275277
cf_opts->min_blob_size = moptions.min_blob_size;

options/options_settable_test.cc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,8 @@ TEST_F(OptionsSettableTest, ColumnFamilyOptionsAllFieldsSettable) {
687687
"memtable_veirfy_per_key_checksum_on_seek=1;"
688688
"memtable_op_scan_flush_trigger=123;"
689689
"memtable_avg_op_scan_flush_trigger=12;"
690-
"cf_allow_ingest_behind=1;",
690+
"cf_allow_ingest_behind=1;"
691+
"verify_output_flags=2049;",
691692
new_options));
692693

693694
ASSERT_NE(new_options->blob_cache.get(), nullptr);

options/options_test.cc

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1720,13 +1720,24 @@ TEST_F(OptionsTest, MutableCFOptions) {
17201720

17211721
ASSERT_OK(GetColumnFamilyOptionsFromString(
17221722
config_options, cf_opts,
1723-
"paranoid_file_checks=true; block_based_table_factory.block_align=false; "
1723+
"paranoid_file_checks=true; "
1724+
"verify_output_flags=2049; "
1725+
"block_based_table_factory.block_align=false; "
17241726
"block_based_table_factory.super_block_alignment_size=65536; "
17251727
"block_based_table_factory.super_block_alignment_space_overhead_ratio="
17261728
"4096; "
17271729
"block_based_table_factory.block_size=8192;",
17281730
&cf_opts));
17291731
ASSERT_TRUE(cf_opts.paranoid_file_checks);
1732+
ASSERT_NE(
1733+
(cf_opts.verify_output_flags & VerifyOutputFlags::kVerifyBlockChecksum),
1734+
VerifyOutputFlags::kVerifyNone);
1735+
ASSERT_NE((cf_opts.verify_output_flags &
1736+
VerifyOutputFlags::kEnableForRemoteCompaction),
1737+
VerifyOutputFlags::kVerifyNone);
1738+
ASSERT_EQ((cf_opts.verify_output_flags &
1739+
VerifyOutputFlags::kEnableForLocalCompaction),
1740+
VerifyOutputFlags::kVerifyNone);
17301741
ASSERT_NE(cf_opts.table_factory.get(), nullptr);
17311742
auto* bbto = cf_opts.table_factory->GetOptions<BlockBasedTableOptions>();
17321743
ASSERT_NE(bbto, nullptr);
@@ -2581,6 +2592,7 @@ TEST_F(OptionsOldApiTest, GetOptionsFromMapTest) {
25812592
ASSERT_EQ(new_cf_opt.optimize_filters_for_hits, true);
25822593
ASSERT_EQ(new_cf_opt.prefix_extractor->AsString(), "rocksdb.FixedPrefix.31");
25832594
ASSERT_EQ(new_cf_opt.experimental_mempurge_threshold, 0.003);
2595+
ASSERT_EQ(new_cf_opt.verify_output_flags, VerifyOutputFlags::kVerifyNone);
25842596
ASSERT_EQ(new_cf_opt.enable_blob_files, true);
25852597
ASSERT_EQ(new_cf_opt.min_blob_size, 1ULL << 10);
25862598
ASSERT_EQ(new_cf_opt.blob_file_size, 1ULL << 30);

0 commit comments

Comments
 (0)