Skip to content

Overload: Reset expensive streams using byte accounting#17702

Merged
snowp merged 21 commits intoenvoyproxy:mainfrom
KBaichoo:overload-rst-stream-rework
Sep 1, 2021
Merged

Overload: Reset expensive streams using byte accounting#17702
snowp merged 21 commits intoenvoyproxy:mainfrom
KBaichoo:overload-rst-stream-rework

Conversation

@KBaichoo
Copy link
Copy Markdown
Contributor

Commit Message: Overload Manager action to reset expensive streams using byte accounting
Additional Description:
Risk Level: Medium
Testing: unit and integration tests
Docs Changes: included
Release Notes: included
Platform Specific Features: NA
Optional Runtime guard: N/A (guarded by configuration of the overload manager)
Related Issue #15791

Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
absl::optional instead of specific sentential.

Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
into resetStreamsInBucket using adapter.

Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
Manager unneeded bits.

Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
instead. Update documentation.

Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
@KBaichoo
Copy link
Copy Markdown
Contributor Author

/assign @yanavlasov

@KBaichoo
Copy link
Copy Markdown
Contributor Author

cc @alyssawilk

Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
@KBaichoo
Copy link
Copy Markdown
Contributor Author

/retest

@repokitteh-read-only
Copy link
Copy Markdown

Retrying Azure Pipelines:
Retried failed jobs in: envoy-presubmit

🐱

Caused by: a #17702 (comment) was created by @KBaichoo.

see: more, trace.

@KBaichoo
Copy link
Copy Markdown
Contributor Author

/retest

@repokitteh-read-only
Copy link
Copy Markdown

Retrying Azure Pipelines:
Retried failed jobs in: envoy-presubmit

🐱

Caused by: a #17702 (comment) was created by @KBaichoo.

see: more, trace.

Copy link
Copy Markdown
Contributor

@alyssawilk alyssawilk left a comment

Choose a reason for hiding this comment

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

Nice addition! Some thoughts below :-)

Comment thread source/common/buffer/watermark_buffer.cc
Comment thread source/common/buffer/watermark_buffer.cc
testing::Bool()),
protocolTestParamsAndBoolToString);

TEST_P(Http2OverloadManagerIntegrationTest, ResetsExpensiveStreamsWhenOverloaded) {
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.

can we make sure to have an integration test where upstream buffers take too much space and another where downstream buffers do?

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.

Good idea. This original test had the upstream buffers hold data (sending to upstream was blocked). Added a test for the response path where the data will be in the downstream buffers to the client.

@alyssawilk alyssawilk self-assigned this Aug 16, 2021
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
Copy link
Copy Markdown
Contributor Author

@KBaichoo KBaichoo left a comment

Choose a reason for hiding this comment

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

Thanks for the review @alyssawilk

Comment thread source/common/buffer/watermark_buffer.cc
Comment thread source/common/buffer/watermark_buffer.cc
testing::Bool()),
protocolTestParamsAndBoolToString);

TEST_P(Http2OverloadManagerIntegrationTest, ResetsExpensiveStreamsWhenOverloaded) {
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.

Good idea. This original test had the upstream buffers hold data (sending to upstream was blocked). Added a test for the response path where the data will be in the downstream buffers to the client.

@KBaichoo
Copy link
Copy Markdown
Contributor Author

CI failing for unrelated fuzz tests fixed here: #17767

…-rework

Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
Copy link
Copy Markdown
Contributor

@alyssawilk alyssawilk left a comment

Choose a reason for hiding this comment

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

Looking great! Here's next round of comments :-)

:ref:`buffer_factory_config
<envoy_v3_api_field_config.overload.v3.OverloadManager.buffer_factory_config>`.
If the `minimum_threshold_for_tracking` isn't configured, Envoy *won't* track
per stream allocated bytes which is needed for this action to work.
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.

can we simply reject the config if we only include half the config needed?

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.

Will do. Good idea.

Comment thread source/common/buffer/watermark_buffer.h Outdated
Http::StreamResetHandler& reset_handler);
~BufferMemoryAccountImpl() override {
// The buffer_memory_allocated_ should always be zero on destruction, even if we
// triggered a reset of the downstream. This is because the dtor only will
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.

super nitty optional (since it passed spellcheck =P) dtor -> destuctor?

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.

// Wait for the proxy to notice and take action for the overload.
if (streamBufferAccounting()) {
test_server_->waitForCounterGe("http.config_test.downstream_rq_rx_reset", 2);
EXPECT_TRUE(medium_request_response->waitForReset());
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.

can we check reset reason or response code details here to make sure the accounting works?

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.

For reset reason: We get HTTP::StreamResetReason::RemoteReset from the endpoint (client). I think that's because it's the catch-all in ConnectionImpl::onStreamClose.

reason = StreamResetReason::RemoteReset;

We could add additional plumbing to better tune this e.g. have the RST_REASON sent to downstream be INTERNAL_ERROR (0x2)

as per https://www.rfc-editor.org/rfc/rfc7540.html#section-7

and have the client (e.g. onStreamClose) check for this to guess the reason... though there might be multiple reasons client might send internal error? e.g. Internal Error RST -> Overload might be a mapping, but there might be more?

WDYT?

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.

if the client in this test gets remote reset I think that's fine - internal error might be a bit more accurate but I don't mind the catchall.
The real request here is that we have a way to debug what's going on in production if we see a bunch of streams being reset, and want to verify the cause. Normally my catchall is "check the stream response code details" but I think the way we reset this through the stream interface we don't have a way to send details, or stick them directly in the stream info. Alternately there stats which were added somewhere? I'm game for stats or details but I think we should have a way to test Envoy is triggering the path that (reading the test) I believe it's triggering since that lets SRE do the same verification live :-)

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.

Added an explicit stat for number of streams the action called resetStream on.

Copy link
Copy Markdown
Contributor Author

@KBaichoo KBaichoo left a comment

Choose a reason for hiding this comment

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

Will push these changes soon, thanks for the feedback.

Comment thread source/common/buffer/watermark_buffer.h Outdated
Http::StreamResetHandler& reset_handler);
~BufferMemoryAccountImpl() override {
// The buffer_memory_allocated_ should always be zero on destruction, even if we
// triggered a reset of the downstream. This is because the dtor only will
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.

// Wait for the proxy to notice and take action for the overload.
if (streamBufferAccounting()) {
test_server_->waitForCounterGe("http.config_test.downstream_rq_rx_reset", 2);
EXPECT_TRUE(medium_request_response->waitForReset());
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.

For reset reason: We get HTTP::StreamResetReason::RemoteReset from the endpoint (client). I think that's because it's the catch-all in ConnectionImpl::onStreamClose.

reason = StreamResetReason::RemoteReset;

We could add additional plumbing to better tune this e.g. have the RST_REASON sent to downstream be INTERNAL_ERROR (0x2)

as per https://www.rfc-editor.org/rfc/rfc7540.html#section-7

and have the client (e.g. onStreamClose) check for this to guess the reason... though there might be multiple reasons client might send internal error? e.g. Internal Error RST -> Overload might be a mapping, but there might be more?

WDYT?

:ref:`buffer_factory_config
<envoy_v3_api_field_config.overload.v3.OverloadManager.buffer_factory_config>`.
If the `minimum_threshold_for_tracking` isn't configured, Envoy *won't* track
per stream allocated bytes which is needed for this action to work.
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.

Will do. Good idea.

@KBaichoo
Copy link
Copy Markdown
Contributor Author

/wait

Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
@KBaichoo
Copy link
Copy Markdown
Contributor Author

/retest

@repokitteh-read-only
Copy link
Copy Markdown

Retrying Azure Pipelines:
Retried failed jobs in: envoy-presubmit

🐱

Caused by: a #17702 (comment) was created by @KBaichoo.

see: more, trace.

Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
…-rework

Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
Copy link
Copy Markdown
Contributor

@alyssawilk alyssawilk left a comment

Choose a reason for hiding this comment

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

This is looking great! one more hit offhand but I think it's ready for a non-googler pass (especially as I'm out until Tuesday :-) )

- Envoy will reduce the waiting period for a configured set of timeouts. See
:ref:`below <config_overload_manager_reducing_timeouts>` for details on configuration.

* - envoy.overload_actions.reset_streams
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.

how about a specific name here as I suspect there are or will be other reasons we reset streams. reset_high_memory_stream or expensive_streams or something more terse but still specific?

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.

+1

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 it's now envoy.overload_actions.reset_high_memory_stream

Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
Copy link
Copy Markdown
Contributor

@snowp snowp left a comment

Choose a reason for hiding this comment

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

Thanks for working on this! Took a pass at the docs and implementation for now, looks pretty good!

- Envoy will reduce the waiting period for a configured set of timeouts. See
:ref:`below <config_overload_manager_reducing_timeouts>` for details on configuration.

* - envoy.overload_actions.reset_streams
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.

+1

Comment on lines +172 to +173
Reset Streams
^^^^^^^^^^^^^^^^^
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.

This seems off, the squibbles should line up with the header

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.


.. warning::

Reset Streams only currently works with HTTP2.
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: I'd say Resetting streams via an overload action currently only works with HTTP2.

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.

configured via :ref:`buffer_factory_config
<envoy_v3_api_field_config.overload.v3.OverloadManager.buffer_factory_config>`.

As an example, here is partial Overload Manager configuration with minimum
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.

a partial

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

<envoy_v3_api_field_config.overload.v3.OverloadManager.buffer_factory_config>`.

As an example, here is partial Overload Manager configuration with minimum
threshold for tracking and a single overload action entry that enables reset
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.

that resets streams

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

Comment on lines +215 to +216
there's something seriously wrong e.g. the existence of streams using :math:`>=
128 * minimum_threshold_for_tracking`.
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.

Same, should probably say "streams using X amount of heap" or something to that effect

ASSERT(current_class != new_class, "Expected the current_class and new_class to be different");

if (current_class == -1 && new_class >= 0) {
if (!current_class.has_value() && new_class >= 0u) {
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.

should this be new_class.has_value()?

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.

Good point, it's actually redundant since current_class != new_class

e.g. if no current class => new class has value. If no new class => current class.

std::floor(pressure * BufferMemoryAccountImpl::NUM_MEMORY_CLASSES_) + 1, 8);
uint32_t bucket_idx = BufferMemoryAccountImpl::NUM_MEMORY_CLASSES_ - buckets_to_clear;

ENVOY_LOG_MISC(warn, "resetting streams in buckets >= {}", bucket_idx);
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.

Should this class have its own logger instead of just using misc?

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.

Think it's consistent with other Overload manager actions this way such as scaled timers.

uint64_t WatermarkBufferFactory::resetAccountsGivenPressure(float pressure) {
ASSERT(pressure >= 0.0 && pressure <= 1.0, "Provided pressure is out of range [0, 1].");

// Compute buckets to clear
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: end comments with proper punctuation, here and elsewhere

Comment thread source/server/worker_impl.cc Outdated
namespace Server {
namespace {
// TODO(kbaichoo): refactor into a utility.
Stats::Counter& getCounter(Stats::Scope& scope, absl::string_view name_of_stat) {
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.

@jmarantz Do we have something like this already, or is there a nicer way to do this?

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.

There is no such helper because this pattern takes a global symbol-table lock, so we don't really want to make it look easy :)

What should be done instead is the names that are known at compile-time should be saved as StatNames in the factory object. This pattern happens a lot. A StatNamePool is the easiest way to create a handful of known-at-compile-time names.

In this case it looks like ResetStreamsCount is known at compile-time so we can make a StatName for that in the ProdWorkerFactory constructor.

If there's a case where the name is not known at compile-time, that's a candidate for a DynamicStatName, which uses more bytes, but can be constructed without a symbol table lock.

See https://github.com/envoyproxy/envoy/blob/main/source/docs/stats.md for more details.

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.

sgtm, will leverage the standard pattern

@snowp snowp added the waiting label Aug 27, 2021
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
Copy link
Copy Markdown
Contributor

@snowp snowp left a comment

Choose a reason for hiding this comment

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

Thanks for the docs cleanup, much easier to follow! A couple more comments and then I think we're good to go

(e.g. streams using memory within a power of two range). There are 8 buckets,
with the last bucket capturing all of the streams using :math:`>= 128 *
minimum_threshold_for_tracking`. In this example the `minimum_threshold_for_tracking` is 1MB.
By setting the `minimum_account_to_track_power_of_two` to `20`, we will only track
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.

Could you spell out how 20 translates to 1MB? I assume it's due to 2^20 but would be helpful to be explicit in the docs

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

minimum_threshold_for_tracking`. In this example the `minimum_threshold_for_tracking` is 1MB.
By setting the `minimum_account_to_track_power_of_two` to `20`, we will only track
streams using >= 1MB worth of allocated memory in buffers. Streams using >= 1MB
will be classified into 8 power of two sized buckets. For this example, the
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.

Is it 8 regardless of the value of minimum_account_to_track_power_of_two ? Not clear from the docs

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

The above configuration also configures the overload manager to reset our tracked
streams based on heap usage as a trigger. When the heap usage is less than 85%,
no streams will be reset. When heap usage is at or above 85%, we start to
reset certain buckets. When the heap usage is at 95% all streams using >= 1MB memory
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.

Instead of "certain buckets" I would say something like "reset buckets according to the strategy described below"

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

@snowp snowp added the waiting label Aug 31, 2021
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
Copy link
Copy Markdown
Contributor Author

@KBaichoo KBaichoo left a comment

Choose a reason for hiding this comment

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

Thanks for the review @snowp

(e.g. streams using memory within a power of two range). There are 8 buckets,
with the last bucket capturing all of the streams using :math:`>= 128 *
minimum_threshold_for_tracking`. In this example the `minimum_threshold_for_tracking` is 1MB.
By setting the `minimum_account_to_track_power_of_two` to `20`, we will only track
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

minimum_threshold_for_tracking`. In this example the `minimum_threshold_for_tracking` is 1MB.
By setting the `minimum_account_to_track_power_of_two` to `20`, we will only track
streams using >= 1MB worth of allocated memory in buffers. Streams using >= 1MB
will be classified into 8 power of two sized buckets. For this example, the
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

The above configuration also configures the overload manager to reset our tracked
streams based on heap usage as a trigger. When the heap usage is less than 85%,
no streams will be reset. When heap usage is at or above 85%, we start to
reset certain buckets. When the heap usage is at 95% all streams using >= 1MB memory
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

@KBaichoo
Copy link
Copy Markdown
Contributor Author

/retest

@repokitteh-read-only
Copy link
Copy Markdown

Retrying Azure Pipelines:
Retried failed jobs in: envoy-presubmit

🐱

Caused by: a #17702 (comment) was created by @KBaichoo.

see: more, trace.

Copy link
Copy Markdown
Contributor

@snowp snowp left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

@snowp snowp merged commit 7bf466c into envoyproxy:main Sep 1, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants