421 lines
16 KiB
C++
421 lines
16 KiB
C++
/*
|
|
* Throughput.actor.cpp
|
|
*
|
|
* This source file is part of the FoundationDB open source project
|
|
*
|
|
* Copyright 2013-2022 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 "fdbrpc/ContinuousSample.h"
|
|
#include "fdbclient/NativeAPI.actor.h"
|
|
#include "fdbserver/TesterInterface.actor.h"
|
|
#include "fdbserver/WorkerInterface.actor.h"
|
|
#include "fdbserver/workloads/workloads.actor.h"
|
|
#include "flow/ActorCollection.h"
|
|
#include "fdbrpc/Smoother.h"
|
|
#include "flow/actorcompiler.h" // This must be the last #include.
|
|
|
|
struct ITransactor : ReferenceCounted<ITransactor> {
|
|
struct Stats {
|
|
int64_t reads, writes, retries, transactions;
|
|
double totalLatency, grvLatency, rowReadLatency, commitLatency;
|
|
Stats()
|
|
: reads(0), writes(0), retries(0), transactions(0), totalLatency(0), grvLatency(0), rowReadLatency(0),
|
|
commitLatency(0) {}
|
|
void operator+=(Stats const& s) {
|
|
reads += s.reads;
|
|
writes += s.writes;
|
|
retries += s.retries;
|
|
transactions += s.transactions;
|
|
totalLatency += s.totalLatency;
|
|
grvLatency += s.grvLatency;
|
|
rowReadLatency += s.rowReadLatency;
|
|
commitLatency += s.commitLatency;
|
|
}
|
|
};
|
|
|
|
virtual Future<Void> doTransaction(Database const&, Stats* stats) = 0;
|
|
virtual ~ITransactor() {}
|
|
};
|
|
|
|
struct RWTransactor : ITransactor {
|
|
int reads, writes;
|
|
int minValueBytes, maxValueBytes;
|
|
std::string valueString;
|
|
int keyCount, keyBytes;
|
|
|
|
RWTransactor(int reads, int writes, int keyCount, int keyBytes, int minValueBytes, int maxValueBytes)
|
|
: reads(reads), writes(writes), minValueBytes(minValueBytes), maxValueBytes(maxValueBytes), keyCount(keyCount),
|
|
keyBytes(keyBytes) {
|
|
ASSERT(minValueBytes <= maxValueBytes);
|
|
valueString = std::string(maxValueBytes, '.');
|
|
}
|
|
|
|
Key randomKey() {
|
|
Key result = makeString(keyBytes);
|
|
uint8_t* data = mutateString(result);
|
|
memset(data, '.', keyBytes);
|
|
|
|
double d = double(deterministicRandom()->randomInt(0, keyCount)) / keyCount;
|
|
emplaceIndex(data, 0, *(int64_t*)&d);
|
|
|
|
return result;
|
|
}
|
|
|
|
Value randomValue() {
|
|
return StringRef((const uint8_t*)valueString.c_str(),
|
|
deterministicRandom()->randomInt(minValueBytes, maxValueBytes + 1));
|
|
};
|
|
|
|
Future<Void> doTransaction(Database const& db, Stats* stats) override {
|
|
return rwTransaction(db, Reference<RWTransactor>::addRef(this), stats);
|
|
}
|
|
|
|
ACTOR static Future<Optional<Value>> getLatency(Future<Optional<Value>> f, double* t) {
|
|
Optional<Value> v = wait(f);
|
|
*t += now();
|
|
return v;
|
|
}
|
|
|
|
ACTOR static Future<Void> rwTransaction(Database db, Reference<RWTransactor> self, Stats* stats) {
|
|
state std::vector<Key> keys;
|
|
state std::vector<Value> values;
|
|
state Transaction tr(db);
|
|
|
|
for (int op = 0; op < self->reads || op < self->writes; op++)
|
|
keys.push_back(self->randomKey());
|
|
values.reserve(self->writes);
|
|
for (int op = 0; op < self->writes; op++)
|
|
values.push_back(self->randomValue());
|
|
|
|
loop {
|
|
try {
|
|
state double t_start = now();
|
|
wait(success(tr.getReadVersion()));
|
|
state double t_rv = now();
|
|
state double rrLatency = -t_rv * self->reads;
|
|
|
|
state std::vector<Future<Optional<Value>>> reads;
|
|
reads.reserve(self->reads);
|
|
for (int i = 0; i < self->reads; i++)
|
|
reads.push_back(getLatency(tr.get(keys[i]), &rrLatency));
|
|
wait(waitForAll(reads));
|
|
for (int i = 0; i < self->writes; i++)
|
|
tr.set(keys[i], values[i]);
|
|
state double t_beforeCommit = now();
|
|
wait(tr.commit());
|
|
|
|
stats->transactions++;
|
|
stats->reads += self->reads;
|
|
stats->writes += self->writes;
|
|
stats->grvLatency += t_rv - t_start;
|
|
stats->commitLatency += now() - t_beforeCommit;
|
|
stats->rowReadLatency += rrLatency / self->reads;
|
|
break;
|
|
} catch (Error& e) {
|
|
wait(tr.onError(e));
|
|
stats->retries++;
|
|
}
|
|
}
|
|
|
|
return Void();
|
|
}
|
|
};
|
|
|
|
struct ABTransactor : ITransactor {
|
|
Reference<ITransactor> a, b;
|
|
double alpha; // 0.0 = all a, 1.0 = all b
|
|
|
|
ABTransactor(double alpha, Reference<ITransactor> a, Reference<ITransactor> b) : a(a), b(b), alpha(alpha) {}
|
|
|
|
Future<Void> doTransaction(Database const& db, Stats* stats) override {
|
|
return deterministicRandom()->random01() >= alpha ? a->doTransaction(db, stats) : b->doTransaction(db, stats);
|
|
}
|
|
};
|
|
|
|
struct SweepTransactor : ITransactor {
|
|
// Runs a linearly-changing workload that changes from A-type to B-type over
|
|
// the specified duration--the timer starts at the first transaction.
|
|
Reference<ITransactor> a, b;
|
|
double startTime;
|
|
double startDelay;
|
|
double duration;
|
|
|
|
SweepTransactor(double duration, double startDelay, Reference<ITransactor> a, Reference<ITransactor> b)
|
|
: a(a), b(b), startTime(-1), startDelay(startDelay), duration(duration) {}
|
|
|
|
Future<Void> doTransaction(Database const& db, Stats* stats) override {
|
|
if (startTime == -1)
|
|
startTime = now() + startDelay;
|
|
|
|
double alpha;
|
|
double n = now();
|
|
if (n < startTime)
|
|
alpha = 0;
|
|
else if (n > startTime + duration)
|
|
alpha = 1;
|
|
else
|
|
alpha = (n - startTime) / duration;
|
|
|
|
return deterministicRandom()->random01() >= alpha ? a->doTransaction(db, stats) : b->doTransaction(db, stats);
|
|
}
|
|
};
|
|
|
|
struct IMeasurer : ReferenceCounted<IMeasurer> {
|
|
// This could be an ITransactor, but then it needs an actor to wait for the transaction to actually finish
|
|
virtual Future<Void> start() { return Void(); }
|
|
virtual void addTransaction(ITransactor::Stats* stats, double now) = 0;
|
|
virtual void getMetrics(std::vector<PerfMetric>& m) = 0;
|
|
IMeasurer& operator=(IMeasurer const&) {
|
|
return *this;
|
|
} // allow copy operator for non-reference counted instances of subclasses
|
|
|
|
virtual ~IMeasurer() {}
|
|
};
|
|
|
|
struct MeasureSinglePeriod : IMeasurer {
|
|
double delay, duration;
|
|
double startT;
|
|
|
|
ContinuousSample<double> totalLatency, grvLatency, rowReadLatency, commitLatency;
|
|
ITransactor::Stats stats; // totalled over the period
|
|
|
|
MeasureSinglePeriod(double delay, double duration)
|
|
: delay(delay), duration(duration), totalLatency(2000), grvLatency(2000), rowReadLatency(2000),
|
|
commitLatency(2000) {}
|
|
|
|
Future<Void> start() override {
|
|
startT = now();
|
|
return Void();
|
|
}
|
|
void addTransaction(ITransactor::Stats* st, double now) override {
|
|
if (!(now >= startT + delay && now < startT + delay + duration))
|
|
return;
|
|
|
|
totalLatency.addSample(st->totalLatency);
|
|
grvLatency.addSample(st->grvLatency);
|
|
rowReadLatency.addSample(st->rowReadLatency);
|
|
|
|
if (st->commitLatency > 0) {
|
|
commitLatency.addSample(st->commitLatency);
|
|
}
|
|
|
|
stats += *st;
|
|
}
|
|
void getMetrics(std::vector<PerfMetric>& m) override {
|
|
double measureDuration = duration;
|
|
m.emplace_back("Transactions/sec", stats.transactions / measureDuration, Averaged::False);
|
|
m.emplace_back("Retries/sec", stats.retries / measureDuration, Averaged::False);
|
|
m.emplace_back("Operations/sec", (stats.reads + stats.writes) / measureDuration, Averaged::False);
|
|
m.emplace_back("Read rows/sec", stats.reads / measureDuration, Averaged::False);
|
|
m.emplace_back("Write rows/sec", stats.writes / measureDuration, Averaged::False);
|
|
|
|
m.emplace_back("Mean Latency (ms)", 1000 * totalLatency.mean(), Averaged::True);
|
|
m.emplace_back("Median Latency (ms, averaged)", 1000 * totalLatency.median(), Averaged::True);
|
|
m.emplace_back("90% Latency (ms, averaged)", 1000 * totalLatency.percentile(0.90), Averaged::True);
|
|
m.emplace_back("98% Latency (ms, averaged)", 1000 * totalLatency.percentile(0.98), Averaged::True);
|
|
|
|
m.emplace_back("Mean Row Read Latency (ms)", 1000 * rowReadLatency.mean(), Averaged::True);
|
|
m.emplace_back("Median Row Read Latency (ms, averaged)", 1000 * rowReadLatency.median(), Averaged::True);
|
|
m.emplace_back("Mean GRV Latency (ms)", 1000 * grvLatency.mean(), Averaged::True);
|
|
m.emplace_back("Median GRV Latency (ms, averaged)", 1000 * grvLatency.median(), Averaged::True);
|
|
m.emplace_back("Mean Commit Latency (ms)", 1000 * commitLatency.mean(), Averaged::True);
|
|
m.emplace_back("Median Commit Latency (ms, averaged)", 1000 * commitLatency.median(), Averaged::True);
|
|
}
|
|
};
|
|
|
|
struct MeasurePeriodically : IMeasurer {
|
|
double period;
|
|
std::set<std::string> includeMetrics;
|
|
MeasureSinglePeriod msp, msp0;
|
|
std::vector<PerfMetric> accumulatedMetrics;
|
|
|
|
MeasurePeriodically(double period, std::set<std::string> includeMetrics)
|
|
: period(period), includeMetrics(includeMetrics), msp(0, period), msp0(0, period) {}
|
|
|
|
Future<Void> start() override {
|
|
msp.start();
|
|
return periodicActor(this);
|
|
}
|
|
void addTransaction(ITransactor::Stats* st, double now) override { msp.addTransaction(st, now); }
|
|
void getMetrics(std::vector<PerfMetric>& m) override {
|
|
m.insert(m.end(), accumulatedMetrics.begin(), accumulatedMetrics.end());
|
|
}
|
|
void nextPeriod(double t) {
|
|
// output stats
|
|
std::string prefix = format("T=%04.0fs:", t);
|
|
std::vector<PerfMetric> m;
|
|
msp.getMetrics(m);
|
|
for (auto i = m.begin(); i != m.end(); ++i)
|
|
if (includeMetrics.count(i->name())) {
|
|
accumulatedMetrics.push_back(i->withPrefix(prefix));
|
|
}
|
|
|
|
// reset stats
|
|
msp = msp0;
|
|
msp.start();
|
|
}
|
|
|
|
ACTOR static Future<Void> periodicActor(MeasurePeriodically* self) {
|
|
state double startT = now();
|
|
state double elapsed = 0;
|
|
loop {
|
|
elapsed += self->period;
|
|
wait(delayUntil(startT + elapsed));
|
|
self->nextPeriod(elapsed);
|
|
}
|
|
}
|
|
};
|
|
|
|
struct MeasureMulti : IMeasurer {
|
|
std::vector<Reference<IMeasurer>> ms;
|
|
Future<Void> start() override {
|
|
std::vector<Future<Void>> s;
|
|
for (auto m = ms.begin(); m != ms.end(); ++m)
|
|
s.push_back((*m)->start());
|
|
return waitForAll(s);
|
|
}
|
|
void addTransaction(ITransactor::Stats* stats, double now) override {
|
|
for (auto m = ms.begin(); m != ms.end(); ++m)
|
|
(*m)->addTransaction(stats, now);
|
|
}
|
|
void getMetrics(std::vector<PerfMetric>& metrics) override {
|
|
for (auto m = ms.begin(); m != ms.end(); ++m)
|
|
(*m)->getMetrics(metrics);
|
|
}
|
|
};
|
|
|
|
struct ThroughputWorkload : TestWorkload {
|
|
double targetLatency, testDuration, Pgain, Igain;
|
|
Reference<ITransactor> op;
|
|
Reference<IMeasurer> measurer;
|
|
|
|
int activeActors;
|
|
double totalLatencyIntegral, totalTransactionsIntegral, startT;
|
|
|
|
ThroughputWorkload(WorkloadContext const& wcx)
|
|
: TestWorkload(wcx), activeActors(0), totalLatencyIntegral(0), totalTransactionsIntegral(0) {
|
|
auto multi = makeReference<MeasureMulti>();
|
|
measurer = multi;
|
|
|
|
targetLatency = getOption(options, "targetLatency"_sr, 0.05);
|
|
|
|
int keyCount = getOption(options, "nodeCount"_sr, (uint64_t)100000);
|
|
int keyBytes = std::max(getOption(options, "keyBytes"_sr, 16), 16);
|
|
int maxValueBytes = getOption(options, "valueBytes"_sr, 100);
|
|
int minValueBytes = getOption(options, "minValueBytes"_sr, maxValueBytes);
|
|
double sweepDuration = getOption(options, "sweepDuration"_sr, 0);
|
|
double sweepDelay = getOption(options, "sweepDelay"_sr, 0);
|
|
|
|
auto AType = Reference<ITransactor>(new RWTransactor(getOption(options, "readsPerTransactionA"_sr, 10),
|
|
getOption(options, "writesPerTransactionA"_sr, 0),
|
|
keyCount,
|
|
keyBytes,
|
|
minValueBytes,
|
|
maxValueBytes));
|
|
auto BType = Reference<ITransactor>(new RWTransactor(getOption(options, "readsPerTransactionB"_sr, 5),
|
|
getOption(options, "writesPerTransactionB"_sr, 5),
|
|
keyCount,
|
|
keyBytes,
|
|
minValueBytes,
|
|
maxValueBytes));
|
|
|
|
if (sweepDuration > 0) {
|
|
op = Reference<ITransactor>(new SweepTransactor(sweepDuration, sweepDelay, AType, BType));
|
|
} else {
|
|
op = Reference<ITransactor>(new ABTransactor(getOption(options, "alpha"_sr, 0.1), AType, BType));
|
|
}
|
|
|
|
double measureDelay = getOption(options, "measureDelay"_sr, 50.0);
|
|
double measureDuration = getOption(options, "measureDuration"_sr, 10.0);
|
|
multi->ms.push_back(Reference<IMeasurer>(new MeasureSinglePeriod(measureDelay, measureDuration)));
|
|
|
|
double measurePeriod = getOption(options, "measurePeriod"_sr, 0.0);
|
|
std::vector<std::string> periodicMetrics =
|
|
getOption(options, "measurePeriodicMetrics"_sr, std::vector<std::string>());
|
|
if (measurePeriod) {
|
|
ASSERT(periodicMetrics.size() != 0);
|
|
multi->ms.push_back(Reference<IMeasurer>(new MeasurePeriodically(
|
|
measurePeriod, std::set<std::string>(periodicMetrics.begin(), periodicMetrics.end()))));
|
|
}
|
|
|
|
Pgain = getOption(options, "ProportionalGain"_sr, 0.1);
|
|
Igain = getOption(options, "IntegralGain"_sr, 0.005);
|
|
|
|
testDuration = measureDelay + measureDuration;
|
|
// testDuration = getOption( options, "testDuration"_sr, measureDelay + measureDuration );
|
|
}
|
|
|
|
std::string description() const override { return "Throughput"; }
|
|
|
|
Future<Void> setup(Database const& cx) override {
|
|
return Void(); // No setup for now - use a separate workload to do setup
|
|
}
|
|
|
|
Future<Void> start(Database const& cx) override {
|
|
startT = now();
|
|
PromiseStream<Future<Void>> add;
|
|
Future<Void> ac = actorCollection(add.getFuture(), &activeActors);
|
|
Future<Void> r = timeout(measurer->start() && ac, testDuration, Void());
|
|
ASSERT(!ac.isReady()); // ... because else the following line would create an unbreakable reference cycle
|
|
add.send(throughputActor(cx, this, add));
|
|
return r;
|
|
}
|
|
|
|
Future<bool> check(Database const& cx) override { return true; }
|
|
|
|
ACTOR static Future<Void> throughputActor(Database db, ThroughputWorkload* self, PromiseStream<Future<Void>> add) {
|
|
state double before = now();
|
|
state ITransactor::Stats stats;
|
|
wait(self->op->doTransaction(db, &stats));
|
|
state double after = now();
|
|
|
|
wait(delay(0.0));
|
|
stats.totalLatency = after - before;
|
|
self->measurer->addTransaction(&stats, after);
|
|
|
|
self->totalLatencyIntegral += after - before;
|
|
self->totalTransactionsIntegral += 1;
|
|
|
|
double error = after - before - self->targetLatency;
|
|
// Ideally ierror would be integral [avg. transaction latency - targetLatency] dt.
|
|
// Actually we calculate integral[ transaction latency - targetLatency ] dtransaction and change units.
|
|
double ierror = (self->totalLatencyIntegral - self->totalTransactionsIntegral * self->targetLatency) /
|
|
self->totalTransactionsIntegral * (after - self->startT);
|
|
|
|
double desiredSuccessors = 1 - (error * self->Pgain + ierror * self->Igain) / self->targetLatency;
|
|
|
|
// if (deterministicRandom()->random01() < .001) TraceEvent("ThroughputControl").detail("Error",
|
|
// error).detail("IError", ierror).detail("DesiredSuccessors", desiredSuccessors).detail("ActiveActors",
|
|
// self->activeActors);
|
|
|
|
desiredSuccessors = std::min(desiredSuccessors, 2.0);
|
|
|
|
// SOMEDAY: How can we prevent the number of actors on different clients from diverging?
|
|
|
|
int successors = deterministicRandom()->random01() + desiredSuccessors;
|
|
if (successors < 1 && self->activeActors <= 1)
|
|
successors = 1;
|
|
if (successors > 1 && self->activeActors >= 200000)
|
|
successors = 1;
|
|
for (int s = 0; s < successors; s++)
|
|
add.send(throughputActor(db, self, add));
|
|
return Void();
|
|
}
|
|
|
|
void getMetrics(std::vector<PerfMetric>& m) override { measurer->getMetrics(m); }
|
|
};
|
|
WorkloadFactory<ThroughputWorkload> ThroughputWorkloadFactory("Throughput");
|