mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
695 lines
25 KiB
C++
695 lines
25 KiB
C++
#include <StorageRunner.h>
|
|
|
|
#include <chrono>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
|
|
#include <Common/Exception.h>
|
|
#include <Common/Logger.h>
|
|
#include <Common/MemoryStatisticsOS.h>
|
|
#include <Common/ZooKeeper/ZooKeeperCommon.h>
|
|
#include <Coordination/CoordinationSettings.h>
|
|
#include <IO/Operators.h>
|
|
#include <Poco/ConsoleChannel.h>
|
|
#include <Poco/Logger.h>
|
|
#include <Poco/Util/AbstractConfiguration.h>
|
|
|
|
namespace DB::ErrorCodes
|
|
{
|
|
extern const int BAD_ARGUMENTS;
|
|
extern const int LOGICAL_ERROR;
|
|
}
|
|
|
|
namespace
|
|
{
|
|
constexpr std::chrono::microseconds POLL_SLEEP{100};
|
|
|
|
bool isListResponseOp(Coordination::OpNum op)
|
|
{
|
|
using enum Coordination::OpNum;
|
|
return op == List || op == SimpleList || op == FilteredList || op == FilteredListWithStatsAndData
|
|
|| op == ListRecursive;
|
|
}
|
|
|
|
size_t countListEntries(const Coordination::ZooKeeperResponse & response, Coordination::OpNum op_num)
|
|
{
|
|
if (!isListResponseOp(op_num))
|
|
return 0;
|
|
if (const auto * list = dynamic_cast<const Coordination::ZooKeeperListResponse *>(&response))
|
|
return list->names.size();
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
StorageRunner::StorageRunner(
|
|
const std::string & config_path_,
|
|
std::optional<size_t> concurrency_,
|
|
std::optional<double> max_time_,
|
|
std::optional<double> report_delay_,
|
|
std::optional<size_t> max_iterations_,
|
|
std::optional<bool> continue_on_error_)
|
|
: config_path(config_path_)
|
|
{
|
|
if (config_path.empty())
|
|
throw DB::Exception(DB::ErrorCodes::BAD_ARGUMENTS, "--config is required in --storage mode");
|
|
|
|
auto log_channel = Poco::AutoPtr<Poco::ConsoleChannel>(new Poco::ConsoleChannel(std::cerr));
|
|
Poco::Logger::root().setChannel(log_channel);
|
|
Poco::Logger::root().setLevel("error");
|
|
|
|
DB::ConfigProcessor config_processor(config_path, true, false);
|
|
config_ptr = config_processor.loadConfig().configuration;
|
|
|
|
parseConfig(*config_ptr);
|
|
|
|
if (concurrency_)
|
|
concurrency = *concurrency_;
|
|
if (max_time_)
|
|
max_time = *max_time_;
|
|
if (report_delay_)
|
|
report_delay = *report_delay_;
|
|
if (max_iterations_)
|
|
max_iterations = *max_iterations_;
|
|
if (continue_on_error_)
|
|
continue_on_error = *continue_on_error_;
|
|
}
|
|
|
|
StorageRunner::~StorageRunner()
|
|
{
|
|
shutdown = true;
|
|
for (auto & t : generator_threads)
|
|
if (t.joinable())
|
|
t.join();
|
|
generators_done = true;
|
|
if (preprocess_thread_handle && preprocess_thread_handle->joinable())
|
|
preprocess_thread_handle->join();
|
|
preprocess_done = true;
|
|
if (commit_thread_handle && commit_thread_handle->joinable())
|
|
commit_thread_handle->join();
|
|
}
|
|
|
|
void StorageRunner::parseConfig(const Poco::Util::AbstractConfiguration & config)
|
|
{
|
|
concurrency = config.getUInt("concurrency", 4);
|
|
max_time = config.getDouble("timelimit", 0.0);
|
|
report_delay = config.getDouble("report_delay", 1.0);
|
|
max_iterations = config.getUInt64("iterations", 0);
|
|
continue_on_error = config.getBool("continue_on_error", false);
|
|
|
|
writes_per_read_batch = config.getUInt64("storage.writes_per_read_batch", 10);
|
|
snapshot_toggle_periods = config.getUInt64("storage.snapshot_toggle_periods", 0);
|
|
preprocess_commit_queue_size = config.getUInt64("storage.preprocess_commit_queue_size", 32);
|
|
input_queue_size = config.getUInt64("storage.input_queue_size", 2048);
|
|
tick_time_ms = config.getInt64("storage.tick_time_ms", 500);
|
|
|
|
if (writes_per_read_batch == 0)
|
|
throw DB::Exception(DB::ErrorCodes::BAD_ARGUMENTS, "storage.writes_per_read_batch must be >= 1");
|
|
if (preprocess_commit_queue_size < 2)
|
|
throw DB::Exception(DB::ErrorCodes::BAD_ARGUMENTS, "storage.preprocess_commit_queue_size must be >= 2");
|
|
if (input_queue_size < 2)
|
|
throw DB::Exception(DB::ErrorCodes::BAD_ARGUMENTS, "storage.input_queue_size must be >= 2");
|
|
|
|
benchmark_context.initializeFromConfig(config);
|
|
}
|
|
|
|
size_t StorageRunner::opIndex(Coordination::OpNum op_num)
|
|
{
|
|
/// Dense mapping for the ops we expect in keeper-bench workloads.
|
|
using enum Coordination::OpNum;
|
|
switch (op_num)
|
|
{
|
|
case Create: return 1;
|
|
case Remove: return 2;
|
|
case Exists: return 3;
|
|
case Get: return 4;
|
|
case Set: return 5;
|
|
case SimpleList: return 6;
|
|
case List: return 7;
|
|
case Check: return 8;
|
|
case Multi: return 9;
|
|
case MultiRead: return 10;
|
|
case FilteredList: return 11;
|
|
case CheckNotExists: return 12;
|
|
case CreateIfNotExists: return 13;
|
|
case RemoveRecursive: return 14;
|
|
case CheckStat: return 15;
|
|
case TryRemove: return 16;
|
|
case FilteredListWithStatsAndData: return 17;
|
|
case ListRecursive: return 18;
|
|
case Sync: return 19;
|
|
case Heartbeat: return 20;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
std::string_view opSlotName(size_t slot)
|
|
{
|
|
using namespace std::string_view_literals;
|
|
switch (slot)
|
|
{
|
|
case 1: return "create"sv;
|
|
case 2: return "remove"sv;
|
|
case 3: return "exists"sv;
|
|
case 4: return "get"sv;
|
|
case 5: return "set"sv;
|
|
case 6: return "simplelist"sv;
|
|
case 7: return "list"sv;
|
|
case 8: return "check"sv;
|
|
case 9: return "multi"sv;
|
|
case 10: return "multiread"sv;
|
|
case 11: return "filteredlist"sv;
|
|
case 12: return "checknotexists"sv;
|
|
case 13: return "createifnotexists"sv;
|
|
case 14: return "removerecursive"sv;
|
|
case 15: return "checkstat"sv;
|
|
case 16: return "tryremove"sv;
|
|
case 17: return "filteredliststatsdata"sv;
|
|
case 18: return "listrecursive"sv;
|
|
case 19: return "sync"sv;
|
|
case 20: return "heartbeat"sv;
|
|
default: return "other"sv;
|
|
}
|
|
}
|
|
}
|
|
|
|
void StorageRunner::setupStorage()
|
|
{
|
|
auto settings = std::make_shared<DB::CoordinationSettings>();
|
|
settings->loadFromConfig("storage.coordination_settings", *config_ptr);
|
|
keeper_context = std::make_shared<DB::KeeperContext>(/*standalone_keeper=*/true, settings);
|
|
keeper_context->setLocalLogsPreprocessed();
|
|
keeper_context->setServerState(DB::KeeperContext::Phase::RUNNING);
|
|
|
|
storage = Storage::create(tick_time_ms, /*superdigest=*/"", keeper_context);
|
|
|
|
/// Allocate one session for setup and one per generator thread.
|
|
/// All subsequent requests from a generator use its dedicated session.
|
|
const int64_t setup_timeout_ms = std::max<int64_t>(60'000, tick_time_ms * 1000);
|
|
setup_session_id = storage->getSessionID(setup_timeout_ms);
|
|
for (size_t i = 0; i < concurrency; ++i)
|
|
generator_session_ids.push_back(storage->getSessionID(setup_timeout_ms));
|
|
|
|
/// Recursively create the setup tree. The real `BenchmarkContext::startup` uses a
|
|
/// ZooKeeper client, so we walk the node tree here and issue preprocess+commit
|
|
/// directly against the storage, populating `tagged_paths` as we go.
|
|
std::function<void(const BenchmarkContext::Node &, const std::string &)> create_subtree;
|
|
auto & tagged = benchmark_context.getTaggedPaths();
|
|
const Coordination::ACLs & default_acls = benchmark_context.getDefaultAcls();
|
|
|
|
create_subtree = [&](const BenchmarkContext::Node & node, const std::string & parent_path)
|
|
{
|
|
std::string path = parent_path == "/" ? "/" + node.name.getString() : parent_path + "/" + node.name.getString();
|
|
|
|
auto request = std::make_shared<Coordination::ZooKeeperCreateRequest>();
|
|
request->path = path;
|
|
request->data = node.data ? node.data->getString() : "";
|
|
request->acls = default_acls;
|
|
|
|
int64_t zxid = next_zxid.fetch_add(1);
|
|
try
|
|
{
|
|
storage->preprocessRequest(request, setup_session_id, 0, zxid, /*check_acl=*/true, /*digest=*/std::nullopt, /*log_idx=*/0);
|
|
auto responses = storage->processRequest(request, setup_session_id, zxid);
|
|
for (const auto & response : responses)
|
|
{
|
|
if (response.response->error != Coordination::Error::ZOK)
|
|
throw zkutil::KeeperException::fromPath(response.response->error, path);
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
std::cerr << "Setup request failed for path " << path << ": "
|
|
<< DB::getCurrentExceptionMessage(false) << std::endl;
|
|
throw;
|
|
}
|
|
|
|
if (node.tag)
|
|
tagged[*node.tag].push_back(path);
|
|
|
|
for (const auto & child : node.children)
|
|
create_subtree(*child, path);
|
|
};
|
|
|
|
std::cerr << "---- Populating storage for benchmark ----" << std::endl;
|
|
for (const auto & root_node : benchmark_context.getRootNodes())
|
|
create_subtree(*root_node, "/");
|
|
|
|
if (!benchmark_context.getTaggedPaths().empty())
|
|
{
|
|
std::cerr << "Tagged paths:" << std::endl;
|
|
for (const auto & [tag_name, paths] : benchmark_context.getTaggedPaths())
|
|
std::cerr << " \"" << tag_name << "\": " << paths.size() << " paths" << std::endl;
|
|
}
|
|
std::cerr << "Populated " << storage->getStorageStats().nodes_count << " znodes.\n" << std::endl;
|
|
}
|
|
|
|
void StorageRunner::startGenerators()
|
|
{
|
|
const auto * tagged_paths = benchmark_context.getTaggedPaths().empty() ? nullptr : &benchmark_context.getTaggedPaths();
|
|
|
|
auto list_children = [this](const std::string & parent_path) -> std::vector<std::string>
|
|
{
|
|
Coordination::KeeperRequestsForSessions requests;
|
|
DB::KeeperRequestForSession rfs;
|
|
rfs.session_id = setup_session_id;
|
|
auto list_req = std::make_shared<Coordination::ZooKeeperListRequest>();
|
|
list_req->path = parent_path;
|
|
rfs.request = list_req;
|
|
requests.push_back(std::move(rfs));
|
|
|
|
auto responses = storage->processLocalRequests(requests, /*check_acl=*/false);
|
|
if (responses.empty())
|
|
throw DB::Exception(DB::ErrorCodes::LOGICAL_ERROR, "Empty response to list of {}", parent_path);
|
|
const auto & resp = *responses[0].response;
|
|
if (resp.error != Coordination::Error::ZOK)
|
|
throw zkutil::KeeperException::fromPath(resp.error, parent_path);
|
|
const auto & list = dynamic_cast<const Coordination::ZooKeeperListResponse &>(resp);
|
|
return list.names;
|
|
};
|
|
|
|
generators.resize(concurrency);
|
|
for (size_t i = 0; i < concurrency; ++i)
|
|
{
|
|
generators[i] = std::make_shared<Generator>();
|
|
generators[i]->startup(*config_ptr, list_children, i, tagged_paths);
|
|
generators[i]->setWatchCallback(std::make_shared<Coordination::WatchCallback>(
|
|
[](const Coordination::WatchResponse &) {}));
|
|
}
|
|
}
|
|
|
|
template <typename QueueT>
|
|
void StorageRunner::pushBlocking(QueueT & queue, QueueItem & item)
|
|
{
|
|
while (!shutdown.load(std::memory_order_relaxed))
|
|
{
|
|
if (queue.tryPush(item))
|
|
return;
|
|
std::this_thread::sleep_for(POLL_SLEEP);
|
|
}
|
|
}
|
|
|
|
void StorageRunner::generatorThread(size_t idx)
|
|
{
|
|
auto & generator = *generators[idx];
|
|
const int64_t session_id = generator_session_ids[idx];
|
|
|
|
while (!shutdown.load(std::memory_order_relaxed))
|
|
{
|
|
if (max_iterations != 0)
|
|
{
|
|
size_t started = requests_started.fetch_add(1);
|
|
if (started >= max_iterations)
|
|
{
|
|
shutdown = true;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
requests_started.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
|
|
ZooKeeperRequestWithCallbacks request_with_callbacks;
|
|
try
|
|
{
|
|
request_with_callbacks = generator.generate();
|
|
}
|
|
catch (...)
|
|
{
|
|
std::cerr << "Generator exception: " << DB::getCurrentExceptionMessage(false) << std::endl;
|
|
if (!continue_on_error)
|
|
shutdown = true;
|
|
continue;
|
|
}
|
|
|
|
QueueItem item;
|
|
item.request = std::move(request_with_callbacks.request);
|
|
item.session_id = session_id;
|
|
item.op_num = item.request->getOpNum();
|
|
item.is_write = !item.request->isReadRequest();
|
|
item.callback = std::move(request_with_callbacks.callback);
|
|
|
|
if (item.is_write)
|
|
pushBlocking(*preprocess_queue, item);
|
|
else
|
|
pushBlocking(*read_queue, item);
|
|
}
|
|
}
|
|
|
|
void StorageRunner::preprocessThread()
|
|
{
|
|
Stopwatch watch;
|
|
QueueItem item;
|
|
while (true)
|
|
{
|
|
if (!preprocess_queue->tryPop(item))
|
|
{
|
|
if (generators_done.load() && preprocess_queue->size() == 0)
|
|
break;
|
|
std::this_thread::sleep_for(POLL_SLEEP);
|
|
continue;
|
|
}
|
|
|
|
item.zxid = next_zxid.fetch_add(1, std::memory_order_relaxed);
|
|
|
|
watch.restart();
|
|
try
|
|
{
|
|
std::shared_lock lock(state_machine_storage_mutex);
|
|
storage->preprocessRequest(item.request, item.session_id, /*time=*/0, item.zxid, /*check_acl=*/true, /*digest=*/std::nullopt, /*log_idx=*/0);
|
|
}
|
|
catch (...)
|
|
{
|
|
std::cerr << "preprocessRequest exception: " << DB::getCurrentExceptionMessage(false) << std::endl;
|
|
if (!continue_on_error)
|
|
shutdown = true;
|
|
continue;
|
|
}
|
|
uint64_t elapsed = watch.elapsedNanoseconds();
|
|
period_stats.preprocess_busy_ns.fetch_add(elapsed, std::memory_order_relaxed);
|
|
period_stats.per_op[opIndex(item.op_num)].preprocess_ns.fetch_add(elapsed, std::memory_order_relaxed);
|
|
|
|
pushBlocking(*commit_queue, item);
|
|
}
|
|
}
|
|
|
|
void StorageRunner::commitThread()
|
|
{
|
|
pcg64 rng{randomSeed()};
|
|
std::uniform_int_distribution<size_t> read_trigger_dist(0, writes_per_read_batch - 1);
|
|
|
|
Stopwatch watch;
|
|
|
|
auto drain_reads = [&]()
|
|
{
|
|
Coordination::KeeperRequestsForSessions batch;
|
|
std::vector<QueueItem> batch_items;
|
|
|
|
QueueItem read_item;
|
|
while (read_queue->tryPop(read_item))
|
|
{
|
|
DB::KeeperRequestForSession rfs;
|
|
rfs.session_id = read_item.session_id;
|
|
rfs.request = read_item.request;
|
|
batch.push_back(std::move(rfs));
|
|
batch_items.push_back(std::move(read_item));
|
|
}
|
|
if (batch.empty())
|
|
return;
|
|
|
|
watch.restart();
|
|
Coordination::KeeperResponsesForSessions responses;
|
|
try
|
|
{
|
|
std::shared_lock lock(state_machine_storage_mutex);
|
|
responses = storage->processLocalRequests(batch, /*check_acl=*/false);
|
|
}
|
|
catch (...)
|
|
{
|
|
std::cerr << "processLocalRequests exception: " << DB::getCurrentExceptionMessage(false) << std::endl;
|
|
if (!continue_on_error)
|
|
shutdown = true;
|
|
period_stats.commit_read_busy_ns.fetch_add(watch.elapsedNanoseconds(), std::memory_order_relaxed);
|
|
return;
|
|
}
|
|
uint64_t total_ns = watch.elapsedNanoseconds();
|
|
period_stats.commit_read_busy_ns.fetch_add(total_ns, std::memory_order_relaxed);
|
|
|
|
/// Attribute the batch time uniformly to individual requests.
|
|
uint64_t per_request_ns = batch_items.empty() ? 0 : total_ns / batch_items.size();
|
|
|
|
for (size_t i = 0; i < batch_items.size(); ++i)
|
|
{
|
|
auto & resp_item = responses[i];
|
|
auto slot = opIndex(batch_items[i].op_num);
|
|
auto & s = period_stats.per_op[slot];
|
|
s.count.fetch_add(1, std::memory_order_relaxed);
|
|
s.process_ns.fetch_add(per_request_ns, std::memory_order_relaxed);
|
|
if (isListResponseOp(batch_items[i].op_num) && resp_item.response)
|
|
s.list_entries.fetch_add(countListEntries(*resp_item.response, batch_items[i].op_num), std::memory_order_relaxed);
|
|
|
|
period_stats.reads_committed.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
};
|
|
|
|
QueueItem write_item;
|
|
while (true)
|
|
{
|
|
if (!commit_queue->tryPop(write_item))
|
|
{
|
|
if (preprocess_done.load() && commit_queue->size() == 0)
|
|
{
|
|
drain_reads();
|
|
if (read_queue->size() == 0)
|
|
break;
|
|
continue;
|
|
}
|
|
|
|
if (read_trigger_dist(rng) == 0)
|
|
drain_reads();
|
|
|
|
std::this_thread::sleep_for(POLL_SLEEP);
|
|
continue;
|
|
}
|
|
|
|
watch.restart();
|
|
Coordination::KeeperResponsesForSessions responses;
|
|
try
|
|
{
|
|
std::shared_lock lock(state_machine_storage_mutex);
|
|
responses = storage->processRequest(write_item.request, write_item.session_id, write_item.zxid);
|
|
}
|
|
catch (...)
|
|
{
|
|
std::cerr << "processRequest exception: " << DB::getCurrentExceptionMessage(false) << std::endl;
|
|
if (!continue_on_error)
|
|
shutdown = true;
|
|
continue;
|
|
}
|
|
uint64_t elapsed = watch.elapsedNanoseconds();
|
|
period_stats.commit_write_busy_ns.fetch_add(elapsed, std::memory_order_relaxed);
|
|
|
|
auto slot = opIndex(write_item.op_num);
|
|
auto & s = period_stats.per_op[slot];
|
|
s.count.fetch_add(1, std::memory_order_relaxed);
|
|
s.process_ns.fetch_add(elapsed, std::memory_order_relaxed);
|
|
|
|
period_stats.writes_committed.fetch_add(1, std::memory_order_relaxed);
|
|
|
|
/// processRequest produces one response for the request + any number of WatchResponse-s
|
|
/// for triggered watches.
|
|
const Coordination::Response * main_response = nullptr;
|
|
for (const auto & response : responses)
|
|
{
|
|
if (dynamic_cast<const Coordination::WatchResponse *>(response.response.get()))
|
|
continue;
|
|
chassert(!main_response);
|
|
main_response = response.response.get();
|
|
}
|
|
chassert(main_response);
|
|
|
|
if (write_item.callback)
|
|
write_item.callback(main_response);
|
|
|
|
/// With probability 1/writes_per_read_batch, drain and process reads.
|
|
if (read_trigger_dist(rng) == 0)
|
|
drain_reads();
|
|
}
|
|
}
|
|
|
|
void StorageRunner::StatsSnapshot::add(const StatsSnapshot & other)
|
|
{
|
|
writes += other.writes;
|
|
reads += other.reads;
|
|
preprocess_busy_ns += other.preprocess_busy_ns;
|
|
commit_write_busy_ns += other.commit_write_busy_ns;
|
|
commit_read_busy_ns += other.commit_read_busy_ns;
|
|
for (size_t i = 0; i < NUM_OP_SLOTS; ++i)
|
|
{
|
|
per_op[i].count += other.per_op[i].count;
|
|
per_op[i].process_ns += other.per_op[i].process_ns;
|
|
per_op[i].preprocess_ns += other.per_op[i].preprocess_ns;
|
|
per_op[i].list_entries += other.per_op[i].list_entries;
|
|
}
|
|
}
|
|
|
|
StorageRunner::StatsSnapshot StorageRunner::takePeriodStats()
|
|
{
|
|
/// Swap/reset atomic counters for this period.
|
|
auto take = [](std::atomic<uint64_t> & v) { return v.exchange(0, std::memory_order_relaxed); };
|
|
|
|
StatsSnapshot snapshot;
|
|
snapshot.writes = take(period_stats.writes_committed);
|
|
snapshot.reads = take(period_stats.reads_committed);
|
|
snapshot.preprocess_busy_ns = take(period_stats.preprocess_busy_ns);
|
|
snapshot.commit_write_busy_ns = take(period_stats.commit_write_busy_ns);
|
|
snapshot.commit_read_busy_ns = take(period_stats.commit_read_busy_ns);
|
|
for (size_t i = 0; i < NUM_OP_SLOTS; ++i)
|
|
{
|
|
snapshot.per_op[i].count = take(period_stats.per_op[i].count);
|
|
snapshot.per_op[i].process_ns = take(period_stats.per_op[i].process_ns);
|
|
snapshot.per_op[i].preprocess_ns = take(period_stats.per_op[i].preprocess_ns);
|
|
snapshot.per_op[i].list_entries = take(period_stats.per_op[i].list_entries);
|
|
}
|
|
return snapshot;
|
|
}
|
|
|
|
void StorageRunner::printStats(const std::string & header, double seconds, const StatsSnapshot & stats, std::optional<bool> snapshot_mode)
|
|
{
|
|
double period_ns = seconds * 1e9;
|
|
auto util = [period_ns](uint64_t ns) { return period_ns > 0 ? 100.0 * static_cast<double>(ns) / period_ns : 0.0; };
|
|
|
|
uint64_t rss_mb = 0;
|
|
#if defined(OS_LINUX) || defined(OS_FREEBSD)
|
|
try
|
|
{
|
|
DB::MemoryStatisticsOS mem;
|
|
rss_mb = mem.get().resident / (1024 * 1024);
|
|
}
|
|
catch (...) // NOLINT(bugprone-empty-catch): best-effort memory readout, 'Ok' to ignore if /proc/self/statm is unavailable
|
|
{
|
|
}
|
|
#endif
|
|
|
|
uint64_t znode_count = 0;
|
|
{
|
|
std::lock_guard lock(state_machine_storage_mutex);
|
|
znode_count = storage ? storage->getStorageStats().nodes_count : 0;
|
|
}
|
|
|
|
std::stringstream out; // STYLE_CHECK_ALLOW_STD_STRING_STREAM
|
|
out << std::fixed << std::setprecision(1);
|
|
out << "\n==== " << header << " " << seconds << "s ====\n";
|
|
out << "writes/s: " << static_cast<double>(stats.writes) / seconds
|
|
<< ", reads/s: " << static_cast<double>(stats.reads) / seconds << "\n";
|
|
out << "util: preprocess " << util(stats.preprocess_busy_ns)
|
|
<< "%, commit(writes) " << util(stats.commit_write_busy_ns)
|
|
<< "%, commit(reads) " << util(stats.commit_read_busy_ns) << "%\n";
|
|
out << "znodes: " << znode_count
|
|
<< ", rss: " << rss_mb << " MiB";
|
|
if (snapshot_mode)
|
|
out << ", snapshot_mode: " << (*snapshot_mode ? "yes" : "no");
|
|
out << "\n";
|
|
out << "per-op avg ns per request:\n";
|
|
for (size_t i = 1; i < NUM_OP_SLOTS; ++i)
|
|
{
|
|
const auto & s = stats.per_op[i];
|
|
if (s.count == 0)
|
|
continue;
|
|
uint64_t avg_process = s.process_ns / s.count;
|
|
out << " " << opSlotName(i) << ": count=" << s.count
|
|
<< " process=" << avg_process << " ns";
|
|
if (s.preprocess_ns > 0)
|
|
{
|
|
uint64_t avg_preprocess = s.preprocess_ns / s.count;
|
|
out << " preprocess=" << avg_preprocess << " ns";
|
|
}
|
|
if (s.list_entries > 0)
|
|
{
|
|
uint64_t ns_per_entry = s.process_ns / s.list_entries;
|
|
out << " (" << s.list_entries << " names, " << ns_per_entry << " ns/name)";
|
|
}
|
|
out << "\n";
|
|
}
|
|
|
|
std::cerr << out.str() << std::flush;
|
|
}
|
|
|
|
void StorageRunner::report(double period_seconds, bool snapshot_mode_during_period)
|
|
{
|
|
StatsSnapshot period = takePeriodStats();
|
|
total_stats.add(period);
|
|
printStats("period", period_seconds, period, snapshot_mode_during_period);
|
|
}
|
|
|
|
void StorageRunner::runBenchmark()
|
|
{
|
|
shared_context = DB::Context::createShared();
|
|
global_context = DB::Context::createGlobal(shared_context.get());
|
|
global_context->makeGlobalContext();
|
|
/// Set the config before setApplicationType: it loads server settings from the context's
|
|
/// config, and keeper-bench has no Poco::Util::Application to provide a global one.
|
|
global_context->setConfig(config_ptr);
|
|
global_context->setApplicationType(DB::Context::ApplicationType::KEEPER);
|
|
|
|
setupStorage();
|
|
startGenerators();
|
|
|
|
preprocess_queue = std::make_unique<NonblockingBoundedQueue<QueueItem>>(input_queue_size);
|
|
read_queue = std::make_unique<NonblockingBoundedQueue<QueueItem>>(input_queue_size);
|
|
commit_queue = std::make_unique<NonblockingBoundedQueue<QueueItem>>(preprocess_commit_queue_size);
|
|
|
|
total_watch.restart();
|
|
|
|
preprocess_thread_handle = std::make_unique<std::thread>([this] { preprocessThread(); });
|
|
commit_thread_handle = std::make_unique<std::thread>([this] { commitThread(); });
|
|
for (size_t i = 0; i < concurrency; ++i)
|
|
generator_threads.emplace_back([this, i] { generatorThread(i); });
|
|
|
|
/// Main thread: periodic reporting and snapshot-mode toggles.
|
|
Stopwatch period_watch;
|
|
size_t period_idx = 0;
|
|
bool period_had_snapshot = false;
|
|
std::unique_ptr<DB::KeeperNodeStreamForSnapshot> stream_for_snapshot;
|
|
while (!shutdown.load(std::memory_order_relaxed))
|
|
{
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
|
|
if (max_time > 0 && total_watch.elapsedSeconds() >= max_time)
|
|
shutdown = true;
|
|
|
|
double elapsed_period = period_watch.elapsedSeconds();
|
|
if ((report_delay > 0 && elapsed_period >= report_delay) || shutdown.load())
|
|
{
|
|
period_idx++;
|
|
|
|
/// Toggle snapshot mode at period boundaries.
|
|
bool want_snapshot_now = snapshot_enabled.load(std::memory_order_relaxed);
|
|
if (snapshot_toggle_periods > 0 && (period_idx % snapshot_toggle_periods == 0))
|
|
{
|
|
std::lock_guard lock(state_machine_storage_mutex);
|
|
if (snapshot_enabled.load())
|
|
{
|
|
storage->nodes_storage->finishWritingSnapshot(std::move(stream_for_snapshot));
|
|
snapshot_enabled.store(false);
|
|
}
|
|
else
|
|
{
|
|
stream_for_snapshot = storage->nodes_storage->beginWritingSnapshot();
|
|
snapshot_enabled.store(true);
|
|
}
|
|
}
|
|
|
|
report(elapsed_period, period_had_snapshot || want_snapshot_now);
|
|
period_watch.restart();
|
|
period_had_snapshot = snapshot_enabled.load();
|
|
}
|
|
}
|
|
|
|
shutdown = true;
|
|
for (auto & t : generator_threads)
|
|
if (t.joinable())
|
|
t.join();
|
|
generators_done = true;
|
|
if (preprocess_thread_handle->joinable())
|
|
preprocess_thread_handle->join();
|
|
preprocess_done = true;
|
|
if (commit_thread_handle->joinable())
|
|
commit_thread_handle->join();
|
|
|
|
/// Disable snapshot mode before the storage is destroyed: SnapshotableHashTable's
|
|
/// destructor asserts !snapshot_mode via clearOutdatedNodes.
|
|
if (snapshot_enabled.load())
|
|
{
|
|
storage->nodes_storage->finishWritingSnapshot(std::move(stream_for_snapshot));
|
|
snapshot_enabled.store(false);
|
|
}
|
|
|
|
/// Account for the work done while draining the queues after the last period report.
|
|
total_stats.add(takePeriodStats());
|
|
printStats("total", total_watch.elapsedSeconds(), total_stats, std::nullopt);
|
|
}
|