Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6b8b489
add deferred_creation util into stats
stevenzzzz Jun 9, 2023
8f3119a
add missed files
stevenzzzz Jun 9, 2023
17cfb10
update with fixed
stevenzzzz Jun 9, 2023
f8ba76a
move the API changes into to this PR as well
stevenzzzz Jun 9, 2023
68d3c6b
josh comments
stevenzzzz Jun 9, 2023
19d75d8
update comment in deferred_creation.h
stevenzzzz Jun 9, 2023
2bbace4
save proto comment update
stevenzzzz Jun 9, 2023
7d69cbf
update DeferredCreation comment
stevenzzzz Jun 9, 2023
8318935
add comment for why the scope is taken by reference
stevenzzzz Jun 9, 2023
91e47e3
update the extra cost estimation on deferred_creation stats
stevenzzzz Jun 9, 2023
cfc9859
sad no local typo check tool
stevenzzzz Jun 9, 2023
f6bd430
more nits fixing
stevenzzzz Jun 12, 2023
97c2b86
fix typo
stevenzzzz Jun 13, 2023
8e9dfe5
Update source/common/stats/deferred_creation.h
stevenzzzz Jun 13, 2023
bca128a
Update source/common/stats/deferred_creation.h
stevenzzzz Jun 13, 2023
d1ee36c
fix Greg comments
stevenzzzz Jun 13, 2023
448e474
Merge branch 'main' into deferred-1
stevenzzzz Jun 13, 2023
1fa16e7
fix format
stevenzzzz Jun 13, 2023
6e9a861
Create a DeferredStatOptions submessage in bootstrap for possible fut…
stevenzzzz Jun 13, 2023
c1d2aa4
fix the placement issue in bootstrap proto
stevenzzzz Jun 14, 2023
4e72f1e
fixes for format error and nits
stevenzzzz Jun 14, 2023
35e7cb1
make the method public
stevenzzzz Jun 14, 2023
0a11cb5
hmm, fix grammar errors around explicit
stevenzzzz Jun 14, 2023
8a80f36
remove trailing whitespace
stevenzzzz Jun 14, 2023
448074d
Adi comment fix
stevenzzzz Jun 16, 2023
28db9d3
add not-implemented-hide tag
stevenzzzz Jun 20, 2023
8134314
fix doc format: broken ref
stevenzzzz Jun 20, 2023
c22db02
gix doc format
stevenzzzz Jun 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion api/envoy/config/bootstrap/v3/bootstrap.proto
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
// <config_overview_bootstrap>` for more detail.

// Bootstrap :ref:`configuration overview <config_overview_bootstrap>`.
// [#next-free-field: 39]
// [#next-free-field: 40]
message Bootstrap {
option (udpa.annotations.versioning).previous_message_type =
"envoy.config.bootstrap.v2.Bootstrap";
Expand Down Expand Up @@ -181,6 +181,15 @@ message Bootstrap {
// Optional set of stats sinks.
repeated metrics.v3.StatsSink stats_sinks = 6;

// 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 blocks. Note that
// when the blocks of stats are created, they carry an extra overhead (around 60-100 bytes depending
// on worker thread count) due to internal bookkeeping data structures.
//
// When set to true, Envoy will defer initializing selected blocks of stats until the first time they are updated.
bool enable_deferred_creation_stats = 39;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will we ever want to free the deferred-creation stats if they aren't used for some period of time? If so, would it change how we structure this config? Or would there be any other related config for this setting?

Would it be worth having an empty message here, which when set enables this feature? Then we could put additional config for it in that message if/when we need it, such as options for selecting which stats are deferred-creation, timer for freeing them after non-use, etc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jmarantz as well.

My thinking is that this is a all-or-none setting that controls the deferred creation characteristics for all stats that's supports deferred creation, but I'd let Josh to chime in here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like Greg's suggestion: by making this a message with a bool in it, rather than just a bool, you present an API that can be extended in the future to maybe include a timeout.

I think a timeout would be useful in situations like the one you have in mind, but where a cluster might get a request, bringing in all its stats, and then not get any for many days after that. We might prefer to drop those stats then have them consume memory until process exit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, will change it to a submessage field.
Is there any preference on how to name the new sub-message?

one QQ: is the timeout idea also applicable to the "current " non-deferred-created stats, that are always populated, but not used?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like SparseStatAllocation for the name?

I don't think the timeout is applicable to current stats, unless we wanted to change the definition of used() to be used within last X duration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks ALL!

@jmarantz proposed to use DeferredStatOptions in the bootstrap config.
PR updated, PTAL.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a slight preference for DeferredStatOptions but I'm OK with SparseStatAllocation as well. I thought we had gone with Deferred in the code in lieu of Lazy, and it would be preferable to be consistent. But I don't feel too strongly about it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// Configuration for internal processing of stats.
metrics.v3.StatsConfig stats_config = 13;

Expand Down
4 changes: 4 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,10 @@ new_features:
added new configuration field :ref:`key_formatter
<envoy_v3_api_field_extensions.filters.network.redis_proxy.v3.RedisProxy.PrefixRoutes.Route.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 <envoy_v3_api_field_config.bootstrap.v3.Bootstrap.enable_deferred_creation_stats>`.
When set to true, enables deferred instantiation on supported stats structures.
- area: ratelimit
change: |
added new configuration field :ref:`domain
Expand Down
5 changes: 5 additions & 0 deletions envoy/server/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/**
Expand Down
35 changes: 35 additions & 0 deletions envoy/stats/stats.h
Original file line number Diff line number Diff line change
Expand Up @@ -218,5 +218,40 @@ using SizeFn = std::function<void(std::size_t)>;
*/
template <typename Stat> using StatFn = std::function<void(Stat&)>;

/**
* 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 <typename StatsStructType> class DeferredCreationCompatibleInterface {
public:
// Helper function to get-or-create and return the StatsStructType object.
virtual StatsStructType& instantiate() PURE;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As noted in the comment, I think getOrCreate() may be a better name if that's what this does.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


virtual ~DeferredCreationCompatibleInterface() = default;
};

// A helper class for a lazy compatible stats struct type.
template <typename StatsStructType> class DeferredCreationCompatibleStats {
public:
DeferredCreationCompatibleStats(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: explicit

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

std::unique_ptr<DeferredCreationCompatibleInterface<StatsStructType>> d)
: data_(std::move(d)) {}
// Allows move construct and assign.
DeferredCreationCompatibleStats& operator=(DeferredCreationCompatibleStats&&) noexcept = default;
DeferredCreationCompatibleStats(DeferredCreationCompatibleStats&&) noexcept = default;

inline StatsStructType* operator->() { return &data_->instantiate(); };
inline StatsStructType& operator*() { return data_->instantiate(); };

private:
std::unique_ptr<DeferredCreationCompatibleInterface<StatsStructType>> data_;
};
} // namespace Stats
} // namespace Envoy
6 changes: 4 additions & 2 deletions envoy/stats/stats_macros.h
Original file line number Diff line number Diff line change
Expand Up @@ -158,16 +158,18 @@ 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; \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I understand why you need to create this alias. But can you add a comment explaining that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, added a line comment.

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) \
ALL_STATS(MAKE_STATS_STRUCT_COUNTER_HELPER_, MAKE_STATS_STRUCT_GAUGE_HELPER_, \
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
12 changes: 12 additions & 0 deletions source/common/stats/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
119 changes: 119 additions & 0 deletions source/common/stats/deferred_creation.h
Original file line number Diff line number Diff line change
@@ -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
* <envoy_v3_api_field_config.bootstrap.v3.Bootstrap.enable_deferred_creation_stats>` is enabled.
*/
template <typename StatsStructType>
class DeferredStats : public DeferredCreationCompatibleInterface<StatsStructType> {
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.
*/
Comment thread
stevenzzzz marked this conversation as resolved.
Outdated
[&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) {
getOrCreate();
}
}
~DeferredStats() {
if (ctor_ == nullptr) {
initialized_.dec();
}
}

private:
// We can't call instantiate directly from constructor, otherwise the compiler complains about
// bypassing virtual dispatch even tho it's fine.
Comment thread
stevenzzzz marked this conversation as resolved.
Outdated
inline StatsStructType& getOrCreate() { return *internal_stats_.get(ctor_); }
inline StatsStructType& instantiate() override { return getOrCreate(); }

// 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<StatsStructType*()> ctor_;
Thread::AtomicPtr<StatsStructType, Thread::AtomicPtrAllocMode::DeleteOnDestruct> internal_stats_;
};

// Non-deferred wrapper over StatsStructType. This is used when
// :ref:`enable_deferred_creation_stats
// <envoy_v3_api_field_config.bootstrap.v3.Bootstrap.enable_deferred_creation_stats>` is not
// enabled.
template <typename StatsStructType>
class DirectStats : public DeferredCreationCompatibleInterface<StatsStructType> {
public:
DirectStats(const typename StatsStructType::StatNameType& stat_names, Stats::Scope& scope)
: stats_(stat_names, scope) {}

private:
inline StatsStructType& instantiate() override { return stats_; }
StatsStructType stats_;
};

// Template that lazily initializes a StatsStruct.
// The bootstrap config :ref:`enable_deferred_creation_stats
// <envoy_v3_api_field_config.bootstrap.v3.Bootstrap.enable_deferred_creation_stats>` decides if
// stats lazy initialzation is enabled or not.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/initialzation/initialization/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the catch.

template <typename StatsStructType>
DeferredCreationCompatibleStats<StatsStructType>
createDeferredCompatibleStats(Stats::ScopeSharedPtr scope,
const typename StatsStructType::StatNameType& stat_names,
bool deferred_creation) {
if (deferred_creation) {
return {std::make_unique<DeferredStats<StatsStructType>>(stat_names, scope)};
} else {
return {std::make_unique<DirectStats<StatsStructType>>(stat_names, *scope)};
}
}

} // namespace Stats
} // namespace Envoy
8 changes: 8 additions & 0 deletions source/docs/stats.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <envoy_v3_api_field_config.bootstrap.v3.Bootstrap.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.
15 changes: 9 additions & 6 deletions source/server/configuration_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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)
: enable_deferred_creation_stats_(bootstrap.enable_deferred_creation_stats()) {
if (bootstrap.has_stats_flush_interval() &&
bootstrap.stats_flush_case() !=
envoy::config::bootstrap::v3::Bootstrap::STATS_FLUSH_NOT_SET) {
Expand All @@ -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<StatsConfigImpl>(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++) {
Expand All @@ -103,19 +109,16 @@ 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);
}

void MainImpl::initializeStatsConfig(const envoy::config::bootstrap::v3::Bootstrap& bootstrap,
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<StatsConfigImpl>(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<StatsSinkFactory>(sink_object);
Expand Down
2 changes: 2 additions & 0 deletions source/server/configuration_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,13 @@ 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 enable_deferred_creation_stats_; }

private:
std::list<Stats::SinkPtr> sinks_;
std::chrono::milliseconds flush_interval_;
bool flush_on_admin_{false};
bool enable_deferred_creation_stats_{false};
};

/**
Expand Down
36 changes: 36 additions & 0 deletions test/common/stats/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading