diff --git a/api/envoy/config/bootstrap/v3/bootstrap.proto b/api/envoy/config/bootstrap/v3/bootstrap.proto index 43e3a33a3f158..30d05dac50c8e 100644 --- a/api/envoy/config/bootstrap/v3/bootstrap.proto +++ b/api/envoy/config/bootstrap/v3/bootstrap.proto @@ -41,7 +41,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // ` for more detail. // Bootstrap :ref:`configuration overview `. -// [#next-free-field: 39] +// [#next-free-field: 40] message Bootstrap { option (udpa.annotations.versioning).previous_message_type = "envoy.config.bootstrap.v2.Bootstrap"; @@ -124,6 +124,18 @@ message Bootstrap { LogFormat log_format = 1; } + message DeferredStatOptions { + // When the flag is enabled, Envoy will lazily initialize a subset of the stats (see below). + // This will save memory and CPU cycles when creating the objects that own these stats, if those + // stats are never referenced throughout the lifetime of the process. However, it will incur additional + // memory overhead for these objects, and a small increase of CPU usage when a at least one of the stats + // is updated for the first time. + // Groups of stats that will be lazily initialized: + // - Cluster traffic stats: a subgroup of the :ref:`cluster statistics ` + // that are used when requests are routed to the cluster. + bool enable_deferred_creation_stats = 1; + } + reserved 10, 11; reserved "runtime"; @@ -186,6 +198,10 @@ message Bootstrap { // Optional set of stats sinks. repeated metrics.v3.StatsSink stats_sinks = 6; + // Options to control behaviors of deferred creation compatible stats. + // [#not-implemented-hide:] + DeferredStatOptions deferred_stat_options = 39; + // Configuration for internal processing of stats. metrics.v3.StatsConfig stats_config = 13; diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 44a909319a4e5..529d4b8f5d3de 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -239,6 +239,11 @@ new_features: added new configuration field :ref:`key_formatter ` to format redis key. The field supports using %KEY% as a formatter command for substituting the redis key as part of the substitution formatter expression. +- area: stats + change: | + added config :ref:`enable_deferred_creation_stats + `. + When set to true, enables deferred instantiation on supported stats structures. - area: ratelimit change: | added new configuration field :ref:`domain diff --git a/envoy/server/configuration.h b/envoy/server/configuration.h index c716ca7393a04..945a175f66509 100644 --- a/envoy/server/configuration.h +++ b/envoy/server/configuration.h @@ -84,6 +84,11 @@ class StatsConfig { * @return bool indicator to flush stats on-demand via the admin interface instead of on a timer. */ virtual bool flushOnAdmin() const PURE; + + /** + * @return true if deferred creation of stats is enabled. + */ + virtual bool enableDeferredCreationStats() const PURE; }; /** diff --git a/envoy/stats/stats.h b/envoy/stats/stats.h index b2768144c60f7..040dc392d1fb2 100644 --- a/envoy/stats/stats.h +++ b/envoy/stats/stats.h @@ -217,5 +217,40 @@ using SizeFn = std::function; */ template using StatFn = std::function; +/** + * Interface for stats lazy initialization. + * To save memory and CPU consumption on blocks of stats that are never referenced throughout the + * process lifetime, they can be encapsulated in a DeferredCreationCompatibleInterface. Then the +Envoy + * bootstrap configuration can be set to defer the instantiation of those block. Note that when the + * blocks of stats are created, they carry an extra 60~100 byte overhead (depending on worker thread + * count) due to internal bookkeeping data structures. The overhead when deferred stats are disabled + * is just 8 bytes. +* See more context: https://github.com/envoyproxy/envoy/issues/23575 + */ +template class DeferredCreationCompatibleInterface { +public: + // Helper function to get-or-create and return the StatsStructType object. + virtual StatsStructType& getOrCreate() PURE; + + virtual ~DeferredCreationCompatibleInterface() = default; +}; + +// A helper class for a lazy compatible stats struct type. +template class DeferredCreationCompatibleStats { +public: + explicit DeferredCreationCompatibleStats( + std::unique_ptr> d) + : data_(std::move(d)) {} + // Allows move construct and assign. + DeferredCreationCompatibleStats& operator=(DeferredCreationCompatibleStats&&) noexcept = default; + DeferredCreationCompatibleStats(DeferredCreationCompatibleStats&&) noexcept = default; + + inline StatsStructType* operator->() { return &data_->getOrCreate(); }; + inline StatsStructType& operator*() { return data_->getOrCreate(); }; + +private: + std::unique_ptr> data_; +}; } // namespace Stats } // namespace Envoy diff --git a/envoy/stats/stats_macros.h b/envoy/stats/stats_macros.h index bec6b6bf722d0..47caffb36d951 100644 --- a/envoy/stats/stats_macros.h +++ b/envoy/stats/stats_macros.h @@ -158,6 +158,9 @@ static inline std::string statPrefixJoin(absl::string_view prefix, absl::string_ */ #define MAKE_STATS_STRUCT(StatsStruct, StatNamesStruct, ALL_STATS) \ struct StatsStruct { \ + /* Also referenced in Stats::createDeferredCompatibleStats. */ \ + using StatNameType = StatNamesStruct; \ + static const absl::string_view typeName() { return #StatsStruct; } \ StatsStruct(const StatNamesStruct& stat_names, Envoy::Stats::Scope& scope, \ Envoy::Stats::StatName prefix = Envoy::Stats::StatName()) \ : stat_names_(stat_names) \ @@ -165,9 +168,8 @@ static inline std::string statPrefixJoin(absl::string_view prefix, absl::string_ MAKE_STATS_STRUCT_HISTOGRAM_HELPER_, \ MAKE_STATS_STRUCT_TEXT_READOUT_HELPER_, \ MAKE_STATS_STRUCT_STATNAME_HELPER_) {} \ - const StatNamesStruct& stat_names_; \ + const StatNameType& stat_names_; \ ALL_STATS(GENERATE_COUNTER_STRUCT, GENERATE_GAUGE_STRUCT, GENERATE_HISTOGRAM_STRUCT, \ GENERATE_TEXT_READOUT_STRUCT, GENERATE_STATNAME_STRUCT) \ } - } // namespace Envoy diff --git a/source/common/stats/BUILD b/source/common/stats/BUILD index d49ad82d7fc18..b795283f77185 100644 --- a/source/common/stats/BUILD +++ b/source/common/stats/BUILD @@ -36,6 +36,18 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "deferred_creation", + hdrs = ["deferred_creation.h"], + deps = [ + "//envoy/stats:stats_interface", + "//source/common/common:cleanup_lib", + "//source/common/common:thread_lib", + "//source/common/stats:symbol_table_lib", + "//source/common/stats:utility_lib", + ], +) + envoy_cc_library( name = "histogram_lib", srcs = ["histogram_impl.cc"], diff --git a/source/common/stats/deferred_creation.h b/source/common/stats/deferred_creation.h new file mode 100644 index 0000000000000..4e2732728355d --- /dev/null +++ b/source/common/stats/deferred_creation.h @@ -0,0 +1,119 @@ +#pragma once + +#include "envoy/common/pure.h" +#include "envoy/stats/scope.h" +#include "envoy/stats/stats.h" + +#include "source/common/common/cleanup.h" +#include "source/common/common/thread.h" +#include "source/common/stats/symbol_table.h" +#include "source/common/stats/utility.h" + +namespace Envoy { +namespace Stats { + +/** + * Lazy-initialization wrapper for StatsStructType, intended for deferred instantiation of a block + * of stats that might not be needed in a given Envoy process. + * + * This class is thread-safe -- instantiations can occur on multiple concurrent threads. + * This is used when + * :ref:`enable_deferred_creation_stats + * ` is enabled. + */ +template +class DeferredStats : public DeferredCreationCompatibleInterface { +public: + // Capture the stat names object and the scope with a ctor, that can be used to instantiate a + // StatsStructType object later. + // Caller should make sure scope and stat_names outlive this object. + DeferredStats(const typename StatsStructType::StatNameType& stat_names, + Stats::ScopeSharedPtr scope) + : initialized_( + // A lambda is used as we need to register the name into the symbol table. + // Note: there is no issue to capture a reference of the scope here as this lambda is + // only used to initialize the 'initialized_' Gauge. + [&scope]() -> Gauge& { + Stats::StatNamePool pool(scope->symbolTable()); + return Stats::Utility::gaugeFromElements( + *scope, {pool.add(StatsStructType::typeName()), pool.add("initialized")}, + Stats::Gauge::ImportMode::HiddenAccumulate); + }()), + ctor_([this, &stat_names, stats_scope = std::move(scope)]() -> StatsStructType* { + initialized_.inc(); + // Reset ctor_ to save some RAM. + Cleanup reset_ctor([this] { ctor_ = nullptr; }); + return new StatsStructType(stat_names, *stats_scope); + }) { + if (initialized_.value() > 0) { + getOrCreateHelper(); + } + } + ~DeferredStats() { + if (ctor_ == nullptr) { + initialized_.dec(); + } + } + inline StatsStructType& getOrCreate() override { return getOrCreateHelper(); } + +private: + // We can't call getOrCreate directly from constructor, otherwise the compiler complains about + // bypassing virtual dispatch even though it's fine. + inline StatsStructType& getOrCreateHelper() { return *internal_stats_.get(ctor_); } + + // In order to preserve stat value continuity across a config reload, we need to automatically + // re-instantiate lazy stats when they are constructed, if there is already a live instantiation + // to the same stats. Consider the following alternate scenarios: + + // Scenario 1: a cluster is instantiated but receives no requests, so its traffic-related stats + // are never instantiated. When this cluster gets reloaded on a config update, a new lazy-init + // block is created, but the stats should again not be instantiated. + + // Scenario 2: a cluster is instantiated and receives traffic, so its traffic-related stats are + // instantiated. We must ensure that a new instance for the same cluster gets its lazy-stats + // instantiated before the previous cluster of the same name is destructed. + + // To do that we keep an "initialized" gauge in the cluster's scope, which will be associated by + // name to the previous generation's cluster's lazy-init block. We use the value in this shared + // gauge to determine whether to instantiate the lazy block on construction. + Gauge& initialized_; + // TODO(#26957): Clean up this ctor_ by moving its ownership to AtomicPtr, and drop + // the setter lambda when the nested object is created. + std::function ctor_; + Thread::AtomicPtr internal_stats_; +}; + +// Non-deferred wrapper over StatsStructType. This is used when +// :ref:`enable_deferred_creation_stats +// ` is not enabled. +template +class DirectStats : public DeferredCreationCompatibleInterface { +public: + DirectStats(const typename StatsStructType::StatNameType& stat_names, Stats::Scope& scope) + : stats_(stat_names, scope) {} + inline StatsStructType& getOrCreate() override { return stats_; } + +private: + StatsStructType stats_; +}; + +// Template that lazily initializes a StatsStruct. +// The bootstrap config :ref:`enable_deferred_creation_stats +// ` decides if +// stats lazy initialization is enabled or not. +template +DeferredCreationCompatibleStats +createDeferredCompatibleStats(Stats::ScopeSharedPtr scope, + const typename StatsStructType::StatNameType& stat_names, + bool deferred_creation) { + if (deferred_creation) { + return DeferredCreationCompatibleStats( + std::make_unique>(stat_names, scope)); + } else { + return DeferredCreationCompatibleStats( + std::make_unique>(stat_names, *scope)); + } +} + +} // namespace Stats +} // namespace Envoy diff --git a/source/docs/stats.md b/source/docs/stats.md index 97e42e5bcd4c7..1616558b9afcd 100644 --- a/source/docs/stats.md +++ b/source/docs/stats.md @@ -295,3 +295,11 @@ from the same symbol table. To facilitate this, a test-only global singleton can be instantiated, via either `Stats::TestUtil::TestSymbolTable` or `Stats::TestUtil::TestStore`. All such structures use a singleton symbol-table whose lifetime is a single test method. This should resolve the assertion. + + +Deferred Initialization of Stats +================================ + +When :ref:`enable_deferred_creation_stats ` +is enabled in Bootstrap, for stats that are deferred creation compatible, the actual stats struct creation +is deferred to first access of any member of that stats, i.e. instantiation only happens when an invocation on operator "*" or "->" happens. diff --git a/source/server/configuration_impl.cc b/source/server/configuration_impl.cc index 51e78065f7188..987e536a6ec32 100644 --- a/source/server/configuration_impl.cc +++ b/source/server/configuration_impl.cc @@ -60,7 +60,8 @@ void FilterChainUtility::buildUdpFilterChain( } } -StatsConfigImpl::StatsConfigImpl(const envoy::config::bootstrap::v3::Bootstrap& bootstrap) { +StatsConfigImpl::StatsConfigImpl(const envoy::config::bootstrap::v3::Bootstrap& bootstrap) + : deferred_stat_options_(bootstrap.deferred_stat_options()) { if (bootstrap.has_stats_flush_interval() && bootstrap.stats_flush_case() != envoy::config::bootstrap::v3::Bootstrap::STATS_FLUSH_NOT_SET) { @@ -87,6 +88,11 @@ void MainImpl::initialize(const envoy::config::bootstrap::v3::Bootstrap& bootstr // tracing configuration is missing from the bootstrap config. initializeTracers(bootstrap.tracing(), server); + // stats_config_ should be set before creating the ClusterManagers so that it is available + // from the ServerFactoryContext when creating the static clusters and stats sinks, where + // stats deferred instantiation setting is read. + stats_config_ = std::make_unique(bootstrap); + const auto& secrets = bootstrap.static_resources().secrets(); ENVOY_LOG(info, "loading {} static secret(s)", secrets.size()); for (ssize_t i = 0; i < secrets.size(); i++) { @@ -103,8 +109,9 @@ void MainImpl::initialize(const envoy::config::bootstrap::v3::Bootstrap& bootstr ENVOY_LOG(debug, "listener #{}:", i); server.listenerManager().addOrUpdateListener(listeners[i], "", false); } - initializeWatchdogs(bootstrap, server); + // This has to happen after ClusterManager initialization, as it depends on config from + // ClusterManager. initializeStatsConfig(bootstrap, server); } @@ -112,10 +119,6 @@ void MainImpl::initializeStatsConfig(const envoy::config::bootstrap::v3::Bootstr Instance& server) { ENVOY_LOG(info, "loading stats configuration"); - // stats_config_ should be set before populating the sinks so that it is available - // from the ServerFactoryContext when creating the stats sinks. - stats_config_ = std::make_unique(bootstrap); - for (const envoy::config::metrics::v3::StatsSink& sink_object : bootstrap.stats_sinks()) { // Generate factory and translate stats sink custom config. auto& factory = Config::Utility::getAndCheckFactory(sink_object); diff --git a/source/server/configuration_impl.h b/source/server/configuration_impl.h index 503c396ffce31..5b5b81cdbf121 100644 --- a/source/server/configuration_impl.h +++ b/source/server/configuration_impl.h @@ -56,11 +56,15 @@ class StatsConfigImpl : public StatsConfig { bool flushOnAdmin() const override { return flush_on_admin_; } void addSink(Stats::SinkPtr sink) { sinks_.emplace_back(std::move(sink)); } + bool enableDeferredCreationStats() const override { + return deferred_stat_options_.enable_deferred_creation_stats(); + } private: std::list sinks_; std::chrono::milliseconds flush_interval_; bool flush_on_admin_{false}; + const envoy::config::bootstrap::v3::Bootstrap::DeferredStatOptions deferred_stat_options_; }; /** diff --git a/test/common/stats/BUILD b/test/common/stats/BUILD index 6b8dd9689e629..92883c81c8988 100644 --- a/test/common/stats/BUILD +++ b/test/common/stats/BUILD @@ -226,6 +226,42 @@ envoy_cc_benchmark_binary( ], ) +envoy_cc_test( + name = "deferred_creation_stats_test", + srcs = ["deferred_creation_stats_test.cc"], + deps = [ + "//envoy/stats:stats_interface", + "//source/common/memory:stats_lib", + "//source/common/stats:deferred_creation", + "//source/common/stats:thread_local_store_lib", + "//test/test_common:utility_lib", + ], +) + +envoy_cc_benchmark_binary( + name = "deferred_creation_stats_benchmark", + srcs = ["deferred_creation_stats_speed_test.cc"], + external_deps = [ + "benchmark", + ], + deps = [ + ":real_thread_test_base", + "//source/common/common:random_generator_lib", + "//source/common/common:utility_lib", + "//source/common/runtime:runtime_lib", + "//source/common/stats:deferred_creation", + "//source/common/stats:isolated_store_lib", + "//source/common/stats:symbol_table_lib", + "//source/exe:process_wide_lib", + ], +) + +envoy_benchmark_test( + name = "deferred_creation_stats_benchmark_test", + size = "large", + benchmark_binary = "deferred_creation_stats_benchmark", +) + envoy_benchmark_test( name = "symbol_table_benchmark_test", benchmark_binary = "symbol_table_benchmark", diff --git a/test/common/stats/deferred_creation_stats_speed_test.cc b/test/common/stats/deferred_creation_stats_speed_test.cc new file mode 100644 index 0000000000000..e3358254e7441 --- /dev/null +++ b/test/common/stats/deferred_creation_stats_speed_test.cc @@ -0,0 +1,240 @@ +#include "source/common/common/random_generator.h" +#include "source/common/stats/deferred_creation.h" +#include "source/common/stats/isolated_store_impl.h" +#include "source/common/stats/symbol_table.h" +#include "source/common/stats/thread_local_store.h" +#include "source/common/thread_local/thread_local_impl.h" +#include "source/exe/process_wide.h" + +#include "test/benchmark/main.h" +#include "test/common/stats/real_thread_test_base.h" +#include "test/test_common/real_threads_test_helper.h" + +#include "benchmark/benchmark.h" + +namespace Envoy { +namespace Stats { + +// Creates a copy of Upstream::ALL_CLUSTER_TRAFFIC_STATS, such that we have a stable +// set of stats for performance test. +#define AWESOME_STATS(COUNTER, GAUGE, HISTOGRAM, TEXT_READOUT, STATNAME) \ + COUNTER(bind_errors) \ + COUNTER(original_dst_host_invalid) \ + COUNTER(retry_or_shadow_abandoned) \ + COUNTER(upstream_cx_close_notify) \ + COUNTER(upstream_cx_connect_attempts_exceeded) \ + COUNTER(upstream_cx_connect_fail) \ + COUNTER(upstream_cx_connect_timeout) \ + COUNTER(upstream_cx_connect_with_0_rtt) \ + COUNTER(upstream_cx_destroy) \ + COUNTER(upstream_cx_destroy_local) \ + COUNTER(upstream_cx_destroy_local_with_active_rq) \ + COUNTER(upstream_cx_destroy_remote) \ + COUNTER(upstream_cx_destroy_remote_with_active_rq) \ + COUNTER(upstream_cx_destroy_with_active_rq) \ + COUNTER(upstream_cx_http1_total) \ + COUNTER(upstream_cx_http2_total) \ + COUNTER(upstream_cx_http3_total) \ + COUNTER(upstream_cx_idle_timeout) \ + COUNTER(upstream_cx_max_duration_reached) \ + COUNTER(upstream_cx_max_requests) \ + COUNTER(upstream_cx_none_healthy) \ + COUNTER(upstream_cx_overflow) \ + COUNTER(upstream_cx_pool_overflow) \ + COUNTER(upstream_cx_protocol_error) \ + COUNTER(upstream_cx_rx_bytes_total) \ + COUNTER(upstream_cx_total) \ + COUNTER(upstream_cx_tx_bytes_total) \ + COUNTER(upstream_flow_control_backed_up_total) \ + COUNTER(upstream_flow_control_drained_total) \ + COUNTER(upstream_flow_control_paused_reading_total) \ + COUNTER(upstream_flow_control_resumed_reading_total) \ + COUNTER(upstream_internal_redirect_failed_total) \ + COUNTER(upstream_internal_redirect_succeeded_total) \ + COUNTER(upstream_rq_cancelled) \ + COUNTER(upstream_rq_completed) \ + COUNTER(upstream_rq_maintenance_mode) \ + COUNTER(upstream_rq_max_duration_reached) \ + COUNTER(upstream_rq_pending_failure_eject) \ + COUNTER(upstream_rq_pending_overflow) \ + COUNTER(upstream_rq_pending_total) \ + COUNTER(upstream_rq_0rtt) \ + COUNTER(upstream_rq_per_try_timeout) \ + COUNTER(upstream_rq_per_try_idle_timeout) \ + COUNTER(upstream_rq_retry) \ + COUNTER(upstream_rq_retry_backoff_exponential) \ + COUNTER(upstream_rq_retry_backoff_ratelimited) \ + COUNTER(upstream_rq_retry_limit_exceeded) \ + COUNTER(upstream_rq_retry_overflow) \ + COUNTER(upstream_rq_retry_success) \ + COUNTER(upstream_rq_rx_reset) \ + COUNTER(upstream_rq_timeout) \ + COUNTER(upstream_rq_total) \ + COUNTER(upstream_rq_tx_reset) \ + COUNTER(upstream_http3_broken) \ + GAUGE(upstream_cx_active, Accumulate) \ + GAUGE(upstream_cx_rx_bytes_buffered, Accumulate) \ + GAUGE(upstream_cx_tx_bytes_buffered, Accumulate) \ + GAUGE(upstream_rq_active, Accumulate) \ + GAUGE(upstream_rq_pending_active, Accumulate) \ + HISTOGRAM(upstream_cx_connect_ms, Milliseconds) \ + HISTOGRAM(upstream_cx_length_ms, Milliseconds) + +MAKE_STAT_NAMES_STRUCT(AwesomeStatNames, AWESOME_STATS); +MAKE_STATS_STRUCT(AwesomeStats, AwesomeStatNames, AWESOME_STATS); + +class DeferredCreationStatsBenchmarkBase { +public: + DeferredCreationStatsBenchmarkBase(bool lazy, const uint64_t n_clusters, Store& s) + : deferred_creation_(lazy), num_clusters_(n_clusters), stat_store_(s), + stat_names_(stat_store_.symbolTable()) {} + + void createStats(bool defer_init) { + for (uint64_t i = 0; i < num_clusters_; ++i) { + std::string new_cluster_name = absl::StrCat("cluster_", i); + ScopeSharedPtr scope = stat_store_.createScope(new_cluster_name); + scopes_.push_back(scope); + auto lazy_stat = std::make_shared>( + createDeferredCompatibleStats(scope, stat_names_, deferred_creation_)); + lazy_stats_.push_back(lazy_stat); + if (!defer_init) { + *(*lazy_stat); + } + } + } + + const bool deferred_creation_; + const uint64_t num_clusters_; + Store& stat_store_; + std::vector scopes_; + std::vector>> lazy_stats_; + AwesomeStatNames stat_names_; +}; + +// Benchmark no-lazy-init on stats, the lazy init version is much faster since no allocation. +void benchmarkDeferredCreationCreation(::benchmark::State& state) { + if (benchmark::skipExpensiveBenchmarks() && state.range(1) > 2000) { + state.SkipWithError("Skipping expensive benchmark"); + return; + } + + IsolatedStoreImpl stats_store; + DeferredCreationStatsBenchmarkBase base(state.range(0) == 1, state.range(1), stats_store); + + for (auto _ : state) { // NOLINT: Silences warning about dead store + base.createStats(/*defer_init=*/true); + } +} + +BENCHMARK(benchmarkDeferredCreationCreation) + ->ArgsProduct({{0, 1}, {1000, 2000, 5000, 10000, 20000}}) + ->Unit(::benchmark::kMillisecond); + +// Benchmark lazy-init of stats in same thread, mimics main thread creation. +void benchmarkDeferredCreationCreationInstantiateSameThread(::benchmark::State& state) { + if (benchmark::skipExpensiveBenchmarks() && state.range(1) > 2000) { + state.SkipWithError("Skipping expensive benchmark"); + return; + } + + IsolatedStoreImpl stats_store; + DeferredCreationStatsBenchmarkBase base(state.range(0) == 1, state.range(1), stats_store); + + for (auto _ : state) { // NOLINT: Silences warning about dead store + base.createStats(/*defer_init=*/false); + } +} + +BENCHMARK(benchmarkDeferredCreationCreationInstantiateSameThread) + ->ArgsProduct({{0, 1}, {1000, 2000, 5000, 10000, 20000}}) + ->Unit(::benchmark::kMillisecond); + +class MultiThreadDeferredCreationStatsTest : public ThreadLocalRealThreadsMixin, + public DeferredCreationStatsBenchmarkBase { +public: + MultiThreadDeferredCreationStatsTest(bool lazy, const uint64_t n_clusters) + : ThreadLocalRealThreadsMixin(5), + DeferredCreationStatsBenchmarkBase(lazy, n_clusters, *ThreadLocalRealThreadsMixin::store_) { + } + + ~MultiThreadDeferredCreationStatsTest() { + shutdownThreading(); + // First, wait for the main-dispatcher to initiate the cross-thread TLS cleanup. + mainDispatchBlock(); + + // Next, wait for all the worker threads to complete their TLS cleanup. + tlsBlock(); + + // Finally, wait for the final central-cache cleanup, which occurs on the main thread. + mainDispatchBlock(); + } +}; + +// Benchmark lazy-init stats in different worker threads, mimics worker threads creation. +void benchmarkDeferredCreationCreationInstantiateOnWorkerThreads(::benchmark::State& state) { + if (benchmark::skipExpensiveBenchmarks() && state.range(1) > 2000) { + state.SkipWithError("Skipping expensive benchmark"); + return; + } + + ProcessWide process_wide_; // Process-wide state setup/teardown (excluding grpc). + MultiThreadDeferredCreationStatsTest test(state.range(0) == 1, state.range(1)); + + for (auto _ : state) { // NOLINT: Silences warning about dead store + test.runOnMainBlocking([&]() { // Create stats on main-thread. + test.createStats(/*defer_init=*/true); + }); + + std::atomic thread_idx = 0; + test.runOnAllWorkersBlocking([&]() { + int32_t batch_size = test.num_clusters_ / 5; + int t_idx = thread_idx++; + uint64_t begin = t_idx * batch_size; + uint64_t end = std::min(begin + batch_size, test.num_clusters_); + for (uint64_t idx = begin; idx < end; ++idx) { + // Instantiate the actual AwesomeStats objects in worker threads, in batches to avoid + // possible contention. + if (test.deferred_creation_) { + // Lazy-init on workers happen when the "index"-th stat instance is not created. + *(*test.lazy_stats_[idx]); + } + } + }); + } +} + +BENCHMARK(benchmarkDeferredCreationCreationInstantiateOnWorkerThreads) + ->ArgsProduct({{0, 1}, {1000, 2000, 5000, 10000, 20000}}) + ->Unit(::benchmark::kMillisecond); + +// Benchmark mimics that worker threads inc the stats. +void benchmarkDeferredCreationStatsAccess(::benchmark::State& state) { + if (benchmark::skipExpensiveBenchmarks() && state.range(1) > 2000) { + state.SkipWithError("Skipping expensive benchmark"); + return; + } + + ProcessWide process_wide_; // Process-wide state setup/teardown (excluding grpc). + MultiThreadDeferredCreationStatsTest test(state.range(0) == 1, state.range(1)); + + for (auto _ : state) { // NOLINT: Silences warning about dead store + test.runOnMainBlocking([&]() { // Create stats on main-thread. + test.createStats(/*defer_init=*/false); + }); + test.runOnAllWorkersBlocking([&]() { + // 50 x num_clusters_ inc() calls. + for (uint64_t idx = 0; idx < 10 * test.num_clusters_; ++idx) { + AwesomeStats& stats = *(*test.lazy_stats_[idx % test.num_clusters_]); + stats.upstream_cx_active_.inc(); + } + }); + } +} + +BENCHMARK(benchmarkDeferredCreationStatsAccess) + ->ArgsProduct({{0, 1}, {1000, 2000, 5000, 10000, 20000}}) + ->Unit(::benchmark::kMillisecond); + +} // namespace Stats + +} // namespace Envoy diff --git a/test/common/stats/deferred_creation_stats_test.cc b/test/common/stats/deferred_creation_stats_test.cc new file mode 100644 index 0000000000000..f067f0ebbb5a7 --- /dev/null +++ b/test/common/stats/deferred_creation_stats_test.cc @@ -0,0 +1,200 @@ +#include "envoy/stats/scope.h" +#include "envoy/stats/stats_macros.h" + +#include "source/common/common/thread.h" +#include "source/common/stats/deferred_creation.h" +#include "source/common/stats/thread_local_store.h" + +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Stats { +namespace { + +#define AWESOME_STATS(COUNTER, GAUGE, HISTOGRAM, TEXT_READOUT, STATNAME) COUNTER(foo) + +MAKE_STAT_NAMES_STRUCT(AwesomeStatNames, AWESOME_STATS); +MAKE_STATS_STRUCT(AwesomeStats, AwesomeStatNames, AWESOME_STATS); + +class DeferredCreationStatsTest : public testing::Test { +public: + SymbolTableImpl symbol_table_; + AllocatorImpl allocator_{symbol_table_}; + ThreadLocalStoreImpl store_{allocator_}; + AwesomeStatNames stats_names_{symbol_table_}; +}; + +using MyStats = DeferredCreationCompatibleStats; + +// Tests that non-lazy stats has no "AwesomeStats.initialized" gauge. +TEST_F(DeferredCreationStatsTest, NonLazyNoInitializedGauge) { + { + ScopeSharedPtr scope = store_.createScope("bluh"); + MyStats non_lazy_y = createDeferredCompatibleStats(scope, stats_names_, false); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized"), nullptr); + EXPECT_EQ(TestUtility::findCounter(store_, "bluh.foo")->value(), 0); + non_lazy_y->foo_.inc(); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized"), nullptr); + EXPECT_EQ(TestUtility::findCounter(store_, "bluh.foo")->value(), 1); + } + // Scope gone, stats deleted. + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized"), nullptr); + EXPECT_EQ(TestUtility::findCounter(store_, "bluh.foo"), nullptr); +} + +// Tests that "AwesomeStats.initialized" gauge equals the number of initiated MyStats instances. +TEST_F(DeferredCreationStatsTest, StatsGoneWithScope) { + { + ScopeSharedPtr scope = store_.createScope("bluh"); + // No such gauge when there is no lazy init stats instances. + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized"), nullptr); + MyStats x = createDeferredCompatibleStats(scope, stats_names_, true); + MyStats y = createDeferredCompatibleStats(scope, stats_names_, true); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 0); + x->foo_.inc(); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 1); + EXPECT_EQ(x->foo_.value(), 1); + EXPECT_EQ(y->foo_.value(), 1); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 2); + } + // Deleted as scope deleted. + EXPECT_EQ(TestUtility::findCounter(store_, "bluh.foo"), nullptr); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized"), nullptr); + { + // Recreate scope "bluh". + ScopeSharedPtr scope = store_.createScope("bluh"); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized"), nullptr); + MyStats x = createDeferredCompatibleStats(scope, stats_names_, true); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 0); + // Previous data is gone, as scope_v2 and scope_1's lifecycle do not overlap. + EXPECT_EQ(x->foo_.value(), 0); + // Initialized now. + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 1); + } +} + +// Tests that multiple stats struct instances within the same scope has no issue to keep the +// stats, with removals. +TEST_F(DeferredCreationStatsTest, MultipleInstancesSameScopeDynamicallyDestructed) { + { + ScopeSharedPtr scope_1 = store_.createScope("bluh"); + auto x = std::make_unique( + createDeferredCompatibleStats(scope_1, stats_names_, true)); + auto y = std::make_unique( + createDeferredCompatibleStats(scope_1, stats_names_, true)); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 0); + // Only instantiate x, and then delete it. + (*x)->foo_.inc(); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 1); + EXPECT_EQ((*x)->foo_.value(), 1); + x.reset(); + // y is not instantiated before x was deleted, no AwesomeStats instance, but stats are not + // lost. + EXPECT_EQ(TestUtility::findCounter(store_, "bluh.foo")->value(), 1); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 0); + // Instantiate y now. + EXPECT_EQ((*y)->foo_.value(), 1); + (*y)->foo_.inc(); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 1); + EXPECT_EQ((*y)->foo_.value(), 2); + } + // Deleted as scope deleted. + EXPECT_EQ(TestUtility::findCounter(store_, "bluh.foo"), nullptr); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized"), nullptr); + { + ScopeSharedPtr scope_v2 = store_.createScope("bluh"); + MyStats x = createDeferredCompatibleStats(scope_v2, stats_names_, true); + // Previous data is gone, as scope_v2 and scope_1's lifecycle do not overlap. + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 0); + EXPECT_EQ(x->foo_.value(), 0); + // Initialized now. + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 1); + } + // Deleted as scope deleted. + EXPECT_EQ(TestUtility::findCounter(store_, "bluh.foo"), nullptr); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized"), nullptr); +} + +// Tests that as long as scope lives, stats under the scope won't be lost. +TEST_F(DeferredCreationStatsTest, ScopeOutlivesLazyStats) { + ScopeSharedPtr scope_1 = store_.createScope("bluh"); + { + auto x = std::make_unique( + createDeferredCompatibleStats(scope_1, stats_names_, true)); + auto y = std::make_unique( + createDeferredCompatibleStats(scope_1, stats_names_, true)); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 0); + (*x)->foo_.inc(); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 1); + EXPECT_EQ((*x)->foo_.value(), 1); + EXPECT_EQ((*y)->foo_.value(), 1); + // x,y instantiated. + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 2); + // Only x instantiated. + y.reset(); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 1); + (*x)->foo_.inc(); + EXPECT_EQ((*x)->foo_.value(), 2); + } + // Both MyStats deleted. + EXPECT_EQ(TestUtility::findCounter(store_, "bluh.foo")->value(), 2); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 0); + { + // scope_1 overlaps with scope_v2. + ScopeSharedPtr scope_v2 = store_.createScope("bluh"); + + MyStats x_v2 = createDeferredCompatibleStats(scope_v2, stats_names_, true); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 0); + // Previous data is NOT gone, as scope_v2 and scope_1's lifecycle overlap. + EXPECT_EQ(x_v2->foo_.value(), 2); + + x_v2->foo_.inc(); + EXPECT_EQ(TestUtility::findCounter(store_, "bluh.foo")->value(), 3); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 1); + } + // scope_v2 is gone, but stat value kept since scope_1 is alive. + EXPECT_EQ(TestUtility::findCounter(store_, "bluh.foo")->value(), 3); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 0); +} + +// Tests that as two AwesomeStats instances of different scope, as long as the scope life-cycle +// overlaps, still got data kept when the earlier scope got deleted. +TEST_F(DeferredCreationStatsTest, WhenScopesOverlapStatsAreAliveAsLongAsThereAre) { + + ScopeSharedPtr scope_v1 = store_.createScope("bluh"); + auto x = std::make_unique( + createDeferredCompatibleStats(scope_v1, stats_names_, true)); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 0); + (*x)->foo_.inc(); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 1); + + EXPECT_EQ(TestUtility::findCounter(store_, "bluh.foo")->value(), 1); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 1); + + // Now scope_v2 gets created, but no action on any stats. + ScopeSharedPtr scope_v2 = store_.createScope("bluh"); + auto y = std::make_unique( + createDeferredCompatibleStats(scope_v2, stats_names_, true)); + // NOTE: since x was instantiated, y is instantiated on creation. + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 2); + + // Now remove scope_v1, stats won't be lost. + x.reset(); + scope_v1.reset(); + EXPECT_EQ(TestUtility::findCounter(store_, "bluh.foo")->value(), 1); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized")->value(), 1); + + // remove scope_v2, stats will be gone. + y.reset(); + scope_v2.reset(); + + EXPECT_EQ(TestUtility::findCounter(store_, "bluh.foo"), nullptr); + EXPECT_EQ(TestUtility::findGauge(store_, "bluh.AwesomeStats.initialized"), nullptr); +} + +} // namespace +} // namespace Stats +} // namespace Envoy diff --git a/test/mocks/server/server_factory_context.h b/test/mocks/server/server_factory_context.h index 77a2610680c0c..d7bd11f4719f1 100644 --- a/test/mocks/server/server_factory_context.h +++ b/test/mocks/server/server_factory_context.h @@ -47,6 +47,7 @@ class MockStatsConfig : public virtual StatsConfig { MOCK_METHOD(std::chrono::milliseconds, flushInterval, (), (const)); MOCK_METHOD(bool, flushOnAdmin, (), (const)); MOCK_METHOD(const Stats::SinkPredicates*, sinkPredicates, (), (const)); + MOCK_METHOD(bool, enableDeferredCreationStats, (), (const)); }; class MockServerFactoryContext : public virtual ServerFactoryContext { diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index 429f6a848cd33..a7a578de18d27 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -925,6 +925,7 @@ metatable microbenchmarks midp milli +mimics misconfiguration misconfigured mixin