371 lines
14 KiB
C++
371 lines
14 KiB
C++
/*
|
|
* ConsistencyScanInterface.h
|
|
*
|
|
* This source file is part of the FoundationDB open source project
|
|
*
|
|
* Copyright 2013-2024 Apple Inc. and the FoundationDB project authors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include "fdbclient/SystemData.h"
|
|
#include "fdbclient/json_spirit/json_spirit_value.h"
|
|
#include "flow/serialize.h"
|
|
#include "fmt/core.h"
|
|
#if defined(NO_INTELLISENSE) && !defined(FDBCLIENT_CONSISTENCYSCANINTERFACE_ACTOR_G_H)
|
|
#define FDBCLIENT_CONSISTENCYSCANINTERFACE_ACTOR_G_H
|
|
#include "fdbclient/ConsistencyScanInterface.actor.g.h"
|
|
#elif !defined(FDBCLIENT_CONSISTENCYSCANINTERFACE_ACTOR_H)
|
|
#define FDBCLIENT_CONSISTENCYSCANINTERFACE_ACTOR_H
|
|
|
|
#include "fdbclient/CommitProxyInterface.h"
|
|
#include "fdbclient/DatabaseConfiguration.h"
|
|
#include "fdbclient/FDBTypes.h"
|
|
#include "fdbclient/RunRYWTransaction.actor.h"
|
|
#include "fdbrpc/fdbrpc.h"
|
|
#include "fdbrpc/Locality.h"
|
|
#include "fdbclient/KeyBackedTypes.actor.h"
|
|
#include "fdbclient/KeyBackedRangeMap.actor.h"
|
|
|
|
#include "flow/actorcompiler.h" // must be last include
|
|
|
|
struct ConsistencyScanInterface {
|
|
constexpr static FileIdentifier file_identifier = 4983265;
|
|
RequestStream<ReplyPromise<Void>> waitFailure;
|
|
RequestStream<struct HaltConsistencyScanRequest> haltConsistencyScan;
|
|
struct LocalityData locality;
|
|
UID myId;
|
|
|
|
ConsistencyScanInterface() {}
|
|
explicit ConsistencyScanInterface(const struct LocalityData& l, UID id) : locality(l), myId(id) {}
|
|
|
|
void initEndpoints() {}
|
|
UID id() const { return myId; }
|
|
NetworkAddress address() const { return waitFailure.getEndpoint().getPrimaryAddress(); }
|
|
bool operator==(const ConsistencyScanInterface& r) const { return id() == r.id(); }
|
|
bool operator!=(const ConsistencyScanInterface& r) const { return !(*this == r); }
|
|
|
|
template <class Archive>
|
|
void serialize(Archive& ar) {
|
|
serializer(ar, waitFailure, haltConsistencyScan, locality, myId);
|
|
}
|
|
};
|
|
|
|
struct HaltConsistencyScanRequest {
|
|
constexpr static FileIdentifier file_identifier = 2323417;
|
|
UID requesterID;
|
|
ReplyPromise<Void> reply;
|
|
|
|
HaltConsistencyScanRequest() {}
|
|
explicit HaltConsistencyScanRequest(UID uid) : requesterID(uid) {}
|
|
|
|
template <class Ar>
|
|
void serialize(Ar& ar) {
|
|
serializer(ar, requesterID, reply);
|
|
}
|
|
};
|
|
|
|
// Consistency Scan State
|
|
// This class provides access to the Consistency Scan's state stored in the database.
|
|
// The state is divided into these components
|
|
// Config - Tells the scan whether and how to run. Written by user, read by scan.
|
|
// RangeConfig - Tells the scan what ranges to operate on or ignore
|
|
//
|
|
// CurrentRoundStats - Execution state and stats for the current round
|
|
// RoundHistory - History of RoundInfo's by start version
|
|
// LifetimeStats - Accumulated lifetime counts for the cluster
|
|
//
|
|
// The class trigger will only be fired by changes to Config or RangeConfig
|
|
struct ConsistencyScanState : public KeyBackedClass {
|
|
ConsistencyScanState(Key prefix = SystemKey("\xff/consistencyScanState"_sr)) : KeyBackedClass(prefix) {}
|
|
|
|
struct Config {
|
|
constexpr static FileIdentifier file_identifier = 23123;
|
|
|
|
bool enabled = false;
|
|
|
|
// The values below are NOT being initialized from knobs because once the scan is enabled
|
|
// changing the knobs does nothing. The consistency check knobs are for the consistency
|
|
// check workload, which is different from the Consistency Scan feature
|
|
|
|
// Max byte read bandwidth allowed, default 50 MB/s
|
|
int64_t maxReadByteRate = 50e6;
|
|
// Target time in seconds for completion of one full scan of the database, default 30 days.
|
|
int64_t targetRoundTimeSeconds = 60 * 60 * 24 * 30;
|
|
// Minimum time in seconds a round should take.
|
|
|
|
// If a round completes faster than this, the scanner will delay afterwards, though if the
|
|
// scan role is restarted it will start a new scan.
|
|
int64_t minRoundTimeSeconds = 60 * 60 * 24 * 30;
|
|
|
|
// The minimum start version allowed for the current round. If the round started before this
|
|
// it will be ended without completion, moved to history, and a new round will begin.
|
|
Version minStartVersion = 0;
|
|
|
|
// Number of days of history to keep, this is enforced using CORE_VERSIONSPERSECOND
|
|
int64_t roundHistoryDays = 90;
|
|
|
|
json_spirit::mObject toJSON() const {
|
|
json_spirit::mObject doc;
|
|
doc["enabled"] = enabled;
|
|
doc["max_rate_bytes_per_second"] = maxReadByteRate;
|
|
doc["target_interval_seconds"] = targetRoundTimeSeconds;
|
|
doc["min_interval_seconds"] = minRoundTimeSeconds;
|
|
doc["min_start_version"] = minStartVersion;
|
|
doc["round_history_days"] = roundHistoryDays;
|
|
return doc;
|
|
}
|
|
|
|
template <class Ar>
|
|
void serialize(Ar& ar) {
|
|
serializer(ar,
|
|
enabled,
|
|
maxReadByteRate,
|
|
targetRoundTimeSeconds,
|
|
minRoundTimeSeconds,
|
|
minStartVersion,
|
|
roundHistoryDays);
|
|
}
|
|
};
|
|
|
|
// Configuration value in a range map for a key range
|
|
struct RangeConfig {
|
|
constexpr static FileIdentifier file_identifier = 846323;
|
|
|
|
// Whether the range is included as a configured target for the scan
|
|
// This should normally be set by the user
|
|
Optional<bool> included;
|
|
|
|
// Whether the range should be currently even though it remains a scan target
|
|
// This should be set by operations on the cluster that would make shards temporarily inconsistent.
|
|
Optional<bool> skip;
|
|
|
|
RangeConfig apply(RangeConfig const& rhs) const {
|
|
RangeConfig result = *this;
|
|
if (rhs.included.present()) {
|
|
result.included = rhs.included;
|
|
}
|
|
if (rhs.skip.present()) {
|
|
result.skip = rhs.skip;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
template <class Ar>
|
|
void serialize(Ar& ar) {
|
|
serializer(ar, included, skip);
|
|
}
|
|
|
|
std::string toString() const { return fmt::format("included={} skip={}", included, skip); }
|
|
json_spirit::mObject toJSON() const {
|
|
json_spirit::mObject doc;
|
|
if (included.present()) {
|
|
doc["included"] = *included;
|
|
}
|
|
if (skip.present()) {
|
|
doc["skip"] = *skip;
|
|
}
|
|
return doc;
|
|
}
|
|
};
|
|
|
|
struct LifetimeStats {
|
|
constexpr static FileIdentifier file_identifier = 7897646;
|
|
|
|
// Amount of FDB keyspace read, regardless of replication
|
|
int64_t logicalBytesScanned = 0;
|
|
// Actual amount of data read from shard replicas
|
|
int64_t replicatedBytesRead = 0;
|
|
int64_t errorCount = 0;
|
|
|
|
template <class Ar>
|
|
void serialize(Ar& ar) {
|
|
serializer(ar, logicalBytesScanned, replicatedBytesRead, errorCount);
|
|
}
|
|
|
|
json_spirit::mObject toJSON() const {
|
|
json_spirit::mObject doc;
|
|
doc["logical_bytes_scanned"] = logicalBytesScanned;
|
|
doc["replicated_bytes_scanned"] = replicatedBytesRead;
|
|
doc["errors"] = errorCount;
|
|
return doc;
|
|
}
|
|
};
|
|
|
|
struct RoundStats {
|
|
constexpr static FileIdentifier file_identifier = 23126;
|
|
|
|
Version startVersion = 0;
|
|
double startTime = 0;
|
|
Version endVersion = 0;
|
|
double endTime = 0;
|
|
Version lastProgressVersion = 0;
|
|
double lastProgressTime = 0;
|
|
|
|
// Whether the scan finished, useful for history round stats.
|
|
bool complete = false;
|
|
|
|
// Amount of FDB keyspace read, regardless of replication
|
|
int64_t logicalBytesScanned = 0;
|
|
// Actual amount of data read from shard replicas
|
|
int64_t replicatedBytesRead = 0;
|
|
int64_t errorCount = 0;
|
|
int64_t skippedRanges = 0;
|
|
// FIXME: add failed request count that we periodically save even if no progress too?
|
|
|
|
Key lastEndKey = ""_sr;
|
|
|
|
/*
|
|
// TODO: Ideas of more things to track:
|
|
int64_t shardsScanned = 0;
|
|
int64_t replicasScanned = 0;
|
|
// Shards or replicas can be skipped due to being offline or locked
|
|
int64_t shardsSkipped = 0;
|
|
int64_t replicasSkipped = 0;
|
|
*/
|
|
|
|
template <class Ar>
|
|
void serialize(Ar& ar) {
|
|
serializer(ar,
|
|
startVersion,
|
|
startTime,
|
|
endVersion,
|
|
endTime,
|
|
lastProgressVersion,
|
|
lastProgressTime,
|
|
complete,
|
|
logicalBytesScanned,
|
|
replicatedBytesRead,
|
|
errorCount,
|
|
skippedRanges,
|
|
lastEndKey);
|
|
}
|
|
|
|
json_spirit::mObject toJSON() const {
|
|
json_spirit::mObject doc;
|
|
doc["complete"] = complete;
|
|
doc["start_version"] = startVersion;
|
|
if (startTime != 0) {
|
|
doc["start_timestamp"] = startTime;
|
|
doc["start_datetime"] = epochsToGMTString(startTime);
|
|
}
|
|
|
|
doc["end_version"] = endVersion;
|
|
if (endTime != 0) {
|
|
doc["end_timestamp"] = endTime;
|
|
doc["end_datetime"] = epochsToGMTString(endTime);
|
|
}
|
|
|
|
doc["last_progress_version"] = lastProgressVersion;
|
|
if (lastProgressTime != 0) {
|
|
doc["last_progress_timestamp"] = lastProgressTime;
|
|
doc["last_progress_datetime"] = epochsToGMTString(lastProgressTime);
|
|
}
|
|
|
|
doc["logical_bytes_scanned"] = logicalBytesScanned;
|
|
doc["replicated_bytes_scanned"] = replicatedBytesRead;
|
|
doc["errors"] = errorCount;
|
|
doc["last_end_key"] = lastEndKey.toString();
|
|
doc["skippedRanges"] = skippedRanges;
|
|
return doc;
|
|
}
|
|
};
|
|
|
|
// Range map for configuring key range options. By default, all ranges are scanned.
|
|
typedef KeyBackedRangeMap<Key, RangeConfig, TupleCodec<Key>, ObjectCodec<RangeConfig, _IncludeVersion>>
|
|
RangeConfigMap;
|
|
|
|
// Map of scan start version to its stats so a history can be maintained.
|
|
typedef KeyBackedObjectMap<Version, RoundStats, _IncludeVersion> StatsHistoryMap;
|
|
|
|
RangeConfigMap rangeConfig() {
|
|
// Updating rangeConfig updates the class trigger
|
|
return { subspace.pack(__FUNCTION__sr), trigger, IncludeVersion() };
|
|
}
|
|
|
|
KeyBackedObjectProperty<Config, _IncludeVersion> config() {
|
|
// Updating rangeConfig updates the class trigger
|
|
return { subspace.pack(__FUNCTION__sr), IncludeVersion(), trigger };
|
|
}
|
|
|
|
// Updating the lifetime stats does not update the class trigger because the stats are constantly updated, but when
|
|
// resetting them the same transaction that sets the stats value must also call trigger.update(tr) so that the scan
|
|
// loop will restart and not overwrite the reset value with a stale copy.
|
|
KeyBackedObjectProperty<LifetimeStats, _IncludeVersion> lifetimeStats() {
|
|
return { subspace.pack(__FUNCTION__sr), IncludeVersion() };
|
|
}
|
|
|
|
KeyBackedObjectProperty<RoundStats, _IncludeVersion> currentRoundStats() {
|
|
return { subspace.pack(__FUNCTION__sr), IncludeVersion() };
|
|
}
|
|
|
|
// History of scan round stats stored by their start version
|
|
StatsHistoryMap roundStatsHistory() { return { subspace.pack(__FUNCTION__sr), IncludeVersion() }; }
|
|
|
|
ACTOR static Future<Void> clearStatsActor(ConsistencyScanState* self, Reference<ReadYourWritesTransaction> tr) {
|
|
// read the keyspaces so the transaction conflicts on write (key-backed properties don't expose conflict ranges,
|
|
// and the extra work here is negligible because this is a rare manual command so performance is not a huge
|
|
// concern)
|
|
wait(success(self->currentRoundStats().getD(tr)) && success(self->lifetimeStats().getD(tr)) &&
|
|
success(self->roundStatsHistory().getRange(tr, {}, {}, 1, Snapshot::False, Reverse::False)));
|
|
|
|
// update each of the stats keyspaces to empty
|
|
self->currentRoundStats().set(tr, ConsistencyScanState::RoundStats());
|
|
self->lifetimeStats().set(tr, ConsistencyScanState::LifetimeStats());
|
|
self->roundStatsHistory().erase(tr, 0, MAX_VERSION);
|
|
return Void();
|
|
}
|
|
|
|
Future<Void> clearStats(Reference<ReadYourWritesTransaction> tr) { return clearStatsActor(this, tr); }
|
|
};
|
|
|
|
/////////////////////
|
|
// Code below this line is not used by the Consistency Scan Role, only the ConsistencyCheck Workload.
|
|
|
|
ACTOR Future<Version> getVersion(Database cx);
|
|
ACTOR Future<bool> getKeyServers(
|
|
Database cx,
|
|
Promise<std::vector<std::pair<KeyRange, std::vector<StorageServerInterface>>>> keyServersPromise,
|
|
KeyRangeRef kr,
|
|
bool performQuiescentChecks,
|
|
bool failureIsError,
|
|
bool* success);
|
|
ACTOR Future<bool> getKeyLocations(Database cx,
|
|
std::vector<std::pair<KeyRange, std::vector<StorageServerInterface>>> shards,
|
|
Promise<Standalone<VectorRef<KeyValueRef>>> keyLocationPromise,
|
|
bool performQuiescentChecks,
|
|
bool* success);
|
|
ACTOR Future<Void> checkDataConsistency(Database cx,
|
|
VectorRef<KeyValueRef> keyLocations,
|
|
DatabaseConfiguration configuration,
|
|
std::map<UID, StorageServerInterface> tssMapping,
|
|
bool performQuiescentChecks,
|
|
bool performTSSCheck,
|
|
bool firstClient,
|
|
bool failureIsError,
|
|
int clientId,
|
|
int clientCount,
|
|
bool distributed,
|
|
bool shuffleShards,
|
|
int shardSampleFactor,
|
|
int64_t sharedRandomNumber,
|
|
int64_t repetitions,
|
|
int64_t* bytesReadInPreviousRound,
|
|
int restart,
|
|
int64_t maxRate,
|
|
int64_t targetInterval,
|
|
bool* success);
|
|
|
|
#include "flow/unactorcompiler.h"
|
|
|
|
#endif // FDBCLIENT_CONSISTENCYSCANINTERFACE_H
|