-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Overflow watermark #10404
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Overflow watermark #10404
Changes from all commits
a87a40e
478b871
c929f30
ef8367c
b5dbfbc
7a96abf
7fe7911
a392911
921e3f0
3ea380a
33e887d
839bfc7
eb3520b
ef8df87
7ecfd25
96a7594
a14b73d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,32 +7,32 @@ namespace Buffer { | |
|
|
||
| void WatermarkBuffer::add(const void* data, uint64_t size) { | ||
| OwnedImpl::add(data, size); | ||
| checkHighWatermark(); | ||
| checkHighAndOverflowWatermarks(); | ||
| } | ||
|
|
||
| void WatermarkBuffer::add(absl::string_view data) { | ||
| OwnedImpl::add(data); | ||
| checkHighWatermark(); | ||
| checkHighAndOverflowWatermarks(); | ||
| } | ||
|
|
||
| void WatermarkBuffer::add(const Instance& data) { | ||
| OwnedImpl::add(data); | ||
| checkHighWatermark(); | ||
| checkHighAndOverflowWatermarks(); | ||
| } | ||
|
|
||
| void WatermarkBuffer::prepend(absl::string_view data) { | ||
| OwnedImpl::prepend(data); | ||
| checkHighWatermark(); | ||
| checkHighAndOverflowWatermarks(); | ||
| } | ||
|
|
||
| void WatermarkBuffer::prepend(Instance& data) { | ||
| OwnedImpl::prepend(data); | ||
| checkHighWatermark(); | ||
| checkHighAndOverflowWatermarks(); | ||
| } | ||
|
|
||
| void WatermarkBuffer::commit(RawSlice* iovecs, uint64_t num_iovecs) { | ||
| OwnedImpl::commit(iovecs, num_iovecs); | ||
| checkHighWatermark(); | ||
| checkHighAndOverflowWatermarks(); | ||
| } | ||
|
|
||
| void WatermarkBuffer::drain(uint64_t size) { | ||
|
|
@@ -42,23 +42,23 @@ void WatermarkBuffer::drain(uint64_t size) { | |
|
|
||
| void WatermarkBuffer::move(Instance& rhs) { | ||
| OwnedImpl::move(rhs); | ||
| checkHighWatermark(); | ||
| checkHighAndOverflowWatermarks(); | ||
| } | ||
|
|
||
| void WatermarkBuffer::move(Instance& rhs, uint64_t length) { | ||
| OwnedImpl::move(rhs, length); | ||
| checkHighWatermark(); | ||
| checkHighAndOverflowWatermarks(); | ||
| } | ||
|
|
||
| Api::IoCallUint64Result WatermarkBuffer::read(Network::IoHandle& io_handle, uint64_t max_length) { | ||
| Api::IoCallUint64Result result = OwnedImpl::read(io_handle, max_length); | ||
| checkHighWatermark(); | ||
| checkHighAndOverflowWatermarks(); | ||
| return result; | ||
| } | ||
|
|
||
| uint64_t WatermarkBuffer::reserve(uint64_t length, RawSlice* iovecs, uint64_t num_iovecs) { | ||
| uint64_t bytes_reserved = OwnedImpl::reserve(length, iovecs, num_iovecs); | ||
| checkHighWatermark(); | ||
| checkHighAndOverflowWatermarks(); | ||
| return bytes_reserved; | ||
| } | ||
|
|
||
|
|
@@ -72,12 +72,16 @@ void WatermarkBuffer::setWatermarks(uint32_t low_watermark, uint32_t high_waterm | |
| ASSERT(low_watermark < high_watermark || (high_watermark == 0 && low_watermark == 0)); | ||
| low_watermark_ = low_watermark; | ||
| high_watermark_ = high_watermark; | ||
| checkHighWatermark(); | ||
| overflow_watermark_ = high_watermark * overflow_multiplier_; | ||
| // TODO(adip): What should be done if there's an overflow (overflow_watermark_ < | ||
| // overflow_watermark_)? should this be a release_assert? | ||
| ASSERT((high_watermark <= overflow_watermark_) || (overflow_multiplier_ == 0)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. || overflow_watermark_ == 0
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll add a comment here. This checks that there's no overflow due to multiplication: |
||
| checkHighAndOverflowWatermarks(); | ||
| checkLowWatermark(); | ||
| } | ||
|
|
||
| void WatermarkBuffer::checkLowWatermark() { | ||
| if (!above_high_watermark_called_ || | ||
| if (above_overflow_watermark_called_ || !above_high_watermark_called_ || | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we check for above_overflow_watermark_called_ ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At the moment, although an overflow occurs, the buffer can still be used. Once the buffer is read from, the low watermark callback will be called. I'm not sure that in cases where the connection is terminated, the That said, a different buffer design that should be considered is to disable all operations once an overflow has been detected.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's the problem: if the overflow callback is called, the buffer stops functioning properly since low watermark callbacks are no longer called when they should. This PR feels high risk. I think we may be jumping the gun by going straight to closing the connection/resetting stream on overflow. It would be good to gather some data about what can cause the overflow cb to trigger. I know there's a runtime flag to control this behavior, but it defaulting on with a 2x overflow ratio could cause some problems.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed that runtime control over the high watermark constant seems a useful capability.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @antoniovicente I'm unsure we have valid causes for the overflow to trigger at the moment. This feature to me is a safety valve to account for unknown bugs that may trigger buffer to grow way beyond high watermark. As such I would think outright socket closure is appropriate.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @yanavlasov triggering the 2x multipler overflow watermark is very easy. Specially when you factor in that the H2 connection flow control window is not coordinated with high watermarks. Going straight to closing connection is very risky. I'm just arguing for keeping the high/low watermarks in working order even if the overflow one is hit, so we can add overflow implementations that just increment counters. |
||
| (high_watermark_ != 0 && OwnedImpl::length() > low_watermark_)) { | ||
| return; | ||
| } | ||
|
|
@@ -86,14 +90,19 @@ void WatermarkBuffer::checkLowWatermark() { | |
| below_low_watermark_(); | ||
| } | ||
|
|
||
| void WatermarkBuffer::checkHighWatermark() { | ||
| if (above_high_watermark_called_ || high_watermark_ == 0 || | ||
| OwnedImpl::length() <= high_watermark_) { | ||
| void WatermarkBuffer::checkHighAndOverflowWatermarks() { | ||
| if (!above_overflow_watermark_called_ && overflow_watermark_ != 0 && | ||
| OwnedImpl::length() > overflow_watermark_) { | ||
| above_overflow_watermark_called_ = true; | ||
| above_overflow_watermark_(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This early return combined with above_overflow_watermark_called_ means that the second check can end up calling the high watermark cb. Should these checks happen in the opposite order: high watermark first, then overflow?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It really depends whether the normal flow control should be preserved or not, and if the buffer should be usable or not.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any behavior we want to support other than connection shutdown and resource teardown? Presumably initiated within the space of a single epoll event call stack (we would do a hard connection shutdown, with no drain). |
||
| return; | ||
| } | ||
|
|
||
| above_high_watermark_called_ = true; | ||
| above_high_watermark_(); | ||
| if (!above_high_watermark_called_ && high_watermark_ != 0 && | ||
| OwnedImpl::length() > high_watermark_) { | ||
| above_high_watermark_called_ = true; | ||
| above_high_watermark_(); | ||
| } | ||
| } | ||
|
|
||
| } // namespace Buffer | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,8 @@ | |
| #include <functional> | ||
| #include <string> | ||
|
|
||
| #include "envoy/runtime/runtime.h" | ||
|
|
||
| #include "common/buffer/buffer_impl.h" | ||
|
|
||
| namespace Envoy { | ||
|
|
@@ -16,8 +18,12 @@ namespace Buffer { | |
| class WatermarkBuffer : public OwnedImpl { | ||
| public: | ||
| WatermarkBuffer(std::function<void()> below_low_watermark, | ||
| std::function<void()> above_high_watermark) | ||
| : below_low_watermark_(below_low_watermark), above_high_watermark_(above_high_watermark) {} | ||
| std::function<void()> above_high_watermark, | ||
| std::function<void()> above_overflow_watermark) | ||
| : below_low_watermark_(below_low_watermark), above_high_watermark_(above_high_watermark), | ||
| above_overflow_watermark_(above_overflow_watermark), | ||
| overflow_multiplier_(Runtime::LoaderSingleton::get().threadsafeSnapshot()->getInteger( | ||
| "buffer.overflow.high_watermark_multiplier", 2)) {} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is a factor of 2 safe based on the maximum expansion of a request that can happen during proxying due to encoding related issues? Some cases that come to mind:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good points...
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The max header limit excludes framing overhead. Also, you can end up writing headers and body in the same stack, you need space for both.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the goal of high watermark is to deal with highly unusual situations where normal watermarks have failed us. Examples are where a number of connection buffers are close to their watermark, but not over, and a large read occurs, adding a bunch of data before watermarking or overload manager can respond. Or when we forget to do watermark flow control push back due to a missing feedback path (e.g. for proxy generated direct responses). Thus, I'm not super worried about the user having to explicitly size them, they should be sizing the normal watermarks (or knobs implying them) based on an understanding of traffic. Whenever there is compression involved, this might be tricky, e.g. zlib or HPACK. Headers are a special case, in general though compression could be 100x. Presumably the basic watermark limit is set to reflect this. Setting some fixed constant as the fudge factor then makes sense to provide a zero tune capability here IMHO, it probably doesn't matter as long as it's some reasonable distance from the normal operational limit. |
||
|
|
||
| // Override all functions from Instance which can result in changing the size | ||
| // of the underlying buffer. | ||
|
|
@@ -40,20 +46,25 @@ class WatermarkBuffer : public OwnedImpl { | |
| uint32_t highWatermark() const { return high_watermark_; } | ||
|
|
||
| private: | ||
| void checkHighWatermark(); | ||
| void checkHighAndOverflowWatermarks(); | ||
| void checkLowWatermark(); | ||
|
|
||
| std::function<void()> below_low_watermark_; | ||
| std::function<void()> above_high_watermark_; | ||
| std::function<void()> above_overflow_watermark_; | ||
|
|
||
| const uint32_t overflow_multiplier_{0}; | ||
| // Used for enforcing buffer limits (off by default). If these are set to non-zero by a call to | ||
| // setWatermarks() the watermark callbacks will be called as described above. | ||
| uint32_t overflow_watermark_{0}; | ||
| uint32_t high_watermark_{0}; | ||
| uint32_t low_watermark_{0}; | ||
| // Tracks the latest state of watermark callbacks. | ||
| // True between the time above_high_watermark_ has been called until above_high_watermark_ has | ||
| // been called. | ||
| // Set to true after above_high_watermark_ has been called, and reset to false after | ||
| // below_low_watermark_ has been called. | ||
| bool above_high_watermark_called_{false}; | ||
| // Set to true after above_overflow_watermark_ has been called. Never reset, because we expect | ||
| // the associated connection and/or stream will be closed immediately in response to an overflow. | ||
| bool above_overflow_watermark_called_{false}; | ||
| }; | ||
|
|
||
| using WatermarkBufferPtr = std::unique_ptr<WatermarkBuffer>; | ||
|
|
@@ -62,8 +73,10 @@ class WatermarkBufferFactory : public WatermarkFactory { | |
| public: | ||
| // Buffer::WatermarkFactory | ||
| InstancePtr create(std::function<void()> below_low_watermark, | ||
| std::function<void()> above_high_watermark) override { | ||
| return InstancePtr{new WatermarkBuffer(below_low_watermark, above_high_watermark)}; | ||
| std::function<void()> above_high_watermark, | ||
| std::function<void()> above_overflow_watermark) override { | ||
| return InstancePtr{ | ||
| new WatermarkBuffer(below_low_watermark, above_high_watermark, above_overflow_watermark)}; | ||
| } | ||
| }; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if multiplier is the right approach. I think the worst case formula for figuring out how much data can end up in the downstream outgoing buffer is (for H/2 upstream):
min(max_concurrent_streams-downstream, max_concurrent_streams-upstream) * initial_stream_window_size-upstream.
So if the client makes a lot of simultaneous requests which land on distinct upstream servers all responses may end up in the outbound buffer before flow control can do anything about it. In this case low multiplier may cause spurious disconnects.
I wonder if the we should use a number instead of multiplier, or at the very least check if it makes sense given H2 settings for downstream and upstream?