Overload: Reset expensive streams using byte accounting#17702
Overload: Reset expensive streams using byte accounting#17702snowp merged 21 commits intoenvoyproxy:mainfrom
Conversation
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>
|
/assign @yanavlasov |
|
cc @alyssawilk |
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
|
/retest |
|
Retrying Azure Pipelines: |
|
/retest |
|
Retrying Azure Pipelines: |
alyssawilk
left a comment
There was a problem hiding this comment.
Nice addition! Some thoughts below :-)
| testing::Bool()), | ||
| protocolTestParamsAndBoolToString); | ||
|
|
||
| TEST_P(Http2OverloadManagerIntegrationTest, ResetsExpensiveStreamsWhenOverloaded) { |
There was a problem hiding this comment.
can we make sure to have an integration test where upstream buffers take too much space and another where downstream buffers do?
There was a problem hiding this comment.
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.
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
KBaichoo
left a comment
There was a problem hiding this comment.
Thanks for the review @alyssawilk
| testing::Bool()), | ||
| protocolTestParamsAndBoolToString); | ||
|
|
||
| TEST_P(Http2OverloadManagerIntegrationTest, ResetsExpensiveStreamsWhenOverloaded) { |
There was a problem hiding this comment.
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.
|
CI failing for unrelated fuzz tests fixed here: #17767 |
…-rework Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
alyssawilk
left a comment
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
can we simply reject the config if we only include half the config needed?
There was a problem hiding this comment.
Will do. Good idea.
| 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 |
There was a problem hiding this comment.
super nitty optional (since it passed spellcheck =P) dtor -> destuctor?
| // 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()); |
There was a problem hiding this comment.
can we check reset reason or response code details here to make sure the accounting works?
There was a problem hiding this comment.
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.
envoy/source/common/http/http2/codec_impl.cc
Line 1082 in 744b7bf
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?
There was a problem hiding this comment.
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 :-)
There was a problem hiding this comment.
Added an explicit stat for number of streams the action called resetStream on.
KBaichoo
left a comment
There was a problem hiding this comment.
Will push these changes soon, thanks for the feedback.
| 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 |
| // 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()); |
There was a problem hiding this comment.
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.
envoy/source/common/http/http2/codec_impl.cc
Line 1082 in 744b7bf
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. |
There was a problem hiding this comment.
Will do. Good idea.
|
/wait |
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
|
/retest |
|
Retrying Azure Pipelines: |
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
…-rework Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
alyssawilk
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Done it's now envoy.overload_actions.reset_high_memory_stream
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
snowp
left a comment
There was a problem hiding this comment.
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 |
| Reset Streams | ||
| ^^^^^^^^^^^^^^^^^ |
There was a problem hiding this comment.
This seems off, the squibbles should line up with the header
|
|
||
| .. warning:: | ||
|
|
||
| Reset Streams only currently works with HTTP2. |
There was a problem hiding this comment.
nit: I'd say Resetting streams via an overload action currently only works with HTTP2.
| 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 |
| <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 |
| there's something seriously wrong e.g. the existence of streams using :math:`>= | ||
| 128 * minimum_threshold_for_tracking`. |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
should this be new_class.has_value()?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Should this class have its own logger instead of just using misc?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
nit: end comments with proper punctuation, here and elsewhere
| namespace Server { | ||
| namespace { | ||
| // TODO(kbaichoo): refactor into a utility. | ||
| Stats::Counter& getCounter(Stats::Scope& scope, absl::string_view name_of_stat) { |
There was a problem hiding this comment.
@jmarantz Do we have something like this already, or is there a nicer way to do this?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
sgtm, will leverage the standard pattern
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
snowp
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
| 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 |
There was a problem hiding this comment.
Is it 8 regardless of the value of minimum_account_to_track_power_of_two ? Not clear from the docs
| 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 |
There was a problem hiding this comment.
Instead of "certain buckets" I would say something like "reset buckets according to the strategy described below"
Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
| (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 |
| 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 |
| 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 |
|
/retest |
|
Retrying Azure Pipelines: |
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