Skip to content
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

fix too many flush #13244

Open
wants to merge 3 commits into
base: 4.1
Choose a base branch
from
Open

fix too many flush #13244

wants to merge 3 commits into from

Conversation

icodening
Copy link

Motivation:
fix too many flush.
when I call ctx.write to send end frame, it will be auto flush.
image

Modification:
check StreamChannel state before flush.
When stream is closed, it may not need auto flush

Result:
reduce the number of flush
image

Fixes #13232

@normanmaurer
Copy link
Member

A unit test would be nice

@icodening
Copy link
Author

icodening commented Mar 2, 2023

In the current version,AbstractHttp2StreamChannel.doBeginRead will auto flush when write end header frame.

@Test
public void closeStreamWithoutFlush() {
    Http2StreamChannel childChannel3 = newInboundStream(3, true, new LastInboundHandler());
    Http2StreamChannel childChannel5 = newInboundStream(5, true, new LastInboundHandler());
    FlushSniffer flushSniffer = new FlushSniffer();
    parentChannel.pipeline().addFirst(flushSniffer);

    //1. write data and batch flush
    byte[] dataBuf = "test".getBytes();
    childChannel5.write(new DefaultHttp2DataFrame(Unpooled.wrappedBuffer(dataBuf), false));
    childChannel3.write(new DefaultHttp2DataFrame(Unpooled.wrappedBuffer(dataBuf), false));
    childChannel3.flush();
    assertTrue(flushSniffer.checkFlush());

    //2. write end header frame
    Http2Headers headers = new DefaultHttp2Headers();
    headers.set("test", "test");
    childChannel3.write(new DefaultHttp2HeadersFrame(headers, true));
    childChannel5.write(new DefaultHttp2HeadersFrame(headers, true));

    assertFalse(flushSniffer.checkFlush()); // fails. write end frame will auto 'flush'
    //batch flush
    childChannel5.flush();
    assertTrue(flushSniffer.checkFlush());
}

@normanmaurer
Copy link
Member

@icodening hmm I am not convinced this change is "safe". If we not flush we could miss to update the connection related flow control.

@normanmaurer
Copy link
Member

@icodening I did make some small changes which I think are more correct and safe.

@normanmaurer
Copy link
Member

@bryce-anderson you might want to check this as well

@@ -838,6 +842,9 @@ private void updateLocalWindowIfNeeded() {
int bytes = flowControlledBytes;
flowControlledBytes = 0;
ChannelFuture future = write0(parentContext(), new DefaultHttp2WindowUpdateFrame(bytes).stream(stream));

windowUpdateFrameWritten = true;
Copy link
Author

@icodening icodening Mar 3, 2023

Choose a reason for hiding this comment

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

io.netty.handler.codec.http2.Http2FrameCodec#consumeBytes may return false, but windowUpdateFrameWritten is always true, this will cause unnecessary flush

Copy link
Member

Choose a reason for hiding this comment

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

There might be some more improvements we can make but I would say we can do another PR for this.

Copy link
Member

Choose a reason for hiding this comment

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

General question not related to this PR: if we write window_update frame to the parentContext() can we rely on the parent channel to manage flushes instead of triggering flush from the stream channel?

@normanmaurer
Copy link
Member

@icodening one question... Why are you concerned by flushes at all ? If there is nothing to write it should be relative cheap.

@icodening
Copy link
Author

@icodening one question... Why are you concerned by flushes at all ? If there is nothing to write it should be relative cheap.

I want to batch flush, when I call ctx.flush once, it will actually call flush multiple times.

The following code is from io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController.FlowState#writeAllocatedBytes, it seems that flush is called once for each end stream.

while (!cancelled && (frame = peek()) != null) {
    int maxBytes = min(allocated, writableWindow());
    if (maxBytes <= 0 && frame.size() > 0) {
        // The frame still has data, but the amount of allocated bytes has been exhausted.
        // Don't write needless empty frames.
        break;
    }
    writeOccurred = true;
    int initialFrameSize = frame.size();
    try {
        frame.write(ctx, max(0, maxBytes));
        if (frame.size() == 0) {
            // This frame has been fully written, remove this frame and notify it.
            // Since we remove this frame first, we're guaranteed that its error
            // method will not be called when we call cancel.
            pendingWriteQueue.remove();
            frame.writeComplete();
        }
    } finally {
        // Decrement allocated by how much was actually written.
        allocated -= initialFrameSize - frame.size();
    }
}     

image

@normanmaurer normanmaurer added this to the 4.1.90.Final milestone Mar 3, 2023
@@ -806,7 +808,9 @@ void doBeginRead() {
}
// We need to double check that there is nothing left to flush such as a
// window update frame.
flush();
if (windowUpdateFrameWritten) {
flush();
Copy link
Author

Choose a reason for hiding this comment

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

if stream state is CLOSE, it should not need to be flush, right?

if (!Http2Stream.State.CLOSED.equals(stream.state())) {
    flush();
}

Copy link
Member

Choose a reason for hiding this comment

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

well I think it might still need to do to ensure connection window updates are flushed ?

Copy link
Member

Choose a reason for hiding this comment

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

correct, spec allows writing window_update frame after endStream=true

Copy link
Member

Choose a reason for hiding this comment

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

@normanmaurer for ST this change won't make any difference because we manage flushes externally. But I can imagine that there is a risk to break users who rely on the current behavior (flush before closing the stream). Maybe it's worth adding an opt-in property to control this change.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this checking of stream state is sufficient. The flow controller does at least profess to free any remaining bytes on stream closure and thus ignores any more stream flow window updates here.

@@ -838,6 +842,9 @@ private void updateLocalWindowIfNeeded() {
int bytes = flowControlledBytes;
flowControlledBytes = 0;
ChannelFuture future = write0(parentContext(), new DefaultHttp2WindowUpdateFrame(bytes).stream(stream));

windowUpdateFrameWritten = true;
Copy link
Member

Choose a reason for hiding this comment

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

General question not related to this PR: if we write window_update frame to the parentContext() can we rely on the parent channel to manage flushes instead of triggering flush from the stream channel?

@@ -806,7 +808,9 @@ void doBeginRead() {
}
// We need to double check that there is nothing left to flush such as a
// window update frame.
flush();
if (windowUpdateFrameWritten) {
flush();
Copy link
Member

Choose a reason for hiding this comment

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

correct, spec allows writing window_update frame after endStream=true

//do not auto flush, it will be 'false'
assertFalse(flushSniffer.checkFlush());
//batch flush
childChannel5.flush();
Copy link
Member

Choose a reason for hiding this comment

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

To clarify: the flush automatically invoked right now only if the stream is closed, meaning that both inbound and outbound side signaled endStream=true. If the inbound side is still open, you won't see the flush.

From StreamChannel point of view, it's not active anymore if the stream is closed. There will be channelInactive() event sent as part of processing write(endStream=true) (line 1388), and the closeFuture() will be notified. There is nothing that users can write after that point.

@normanmaurer it sounds like it's incorrect to invoke childChannel5.flush() here, because childChannel5.isActive() returns false at this point. However, there is no other easy way for users of StreamChannel API to flush after response is written except using writeAndFlush, which complicates usability. In ST, we also invoke flush() after the last write(...) because we have a separate logic to control flushes.

How does it work for the main connection Channel (let's say HTTP/1.1)? If someone writes and closes the channel, I would expect Netty to flush everything before closing. Seems reasonable for StreamChannel to behave consistently with real Channel, at least by default.

Also, multiplexed streams run in parallel if there is > 1 in-flight requests. It complicates how "batch flush" can be implemented if writes from multiple streams intersect with each other.

I want to batch flush, when I call ctx.flush once, it will actually call flush multiple times.

@icodening taking the above description into account, can you clarify the exact flow of how you plan to "batch flush" on a closed stream?

Copy link
Member

Choose a reason for hiding this comment

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

Btw, have you considered adding FlushConsolidationHandler between Http2ConnectionHandler and Http2MultiplexHandler?

Copy link
Author

Choose a reason for hiding this comment

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

Btw, have you considered adding FlushConsolidationHandler between Http2ConnectionHandler and Http2MultiplexHandler?

I tried, this way can reduce flush. thks

Copy link
Author

Choose a reason for hiding this comment

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

@icodening taking the above description into account, can you clarify the exact flow of how you plan to "batch flush" on a closed stream?

I will write the message to the queue and schedule the eventloop to execute.
ctx.write will be called when the number of messages is less than the threshold, and ctx.flush will be called when the number of messages is more than the threshold.

Copy link
Author

Choose a reason for hiding this comment

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

@normanmaurer it sounds like it's incorrect to invoke childChannel5.flush() here, because childChannel5.isActive() returns false at this point. However, there is no other easy way for users of StreamChannel API to flush after response is written except using writeAndFlush, which complicates usability. In ST, we also invoke flush() after the last write(...) because we have a separate logic to control flushes.

It seems more reasonable to use the parent channel flush

@@ -806,7 +808,9 @@ void doBeginRead() {
}
// We need to double check that there is nothing left to flush such as a
// window update frame.
flush();
if (windowUpdateFrameWritten) {
flush();
Copy link
Member

Choose a reason for hiding this comment

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

@normanmaurer for ST this change won't make any difference because we manage flushes externally. But I can imagine that there is a risk to break users who rely on the current behavior (flush before closing the stream). Maybe it's worth adding an opt-in property to control this change.

Copy link
Contributor

@bryce-anderson bryce-anderson left a comment

Choose a reason for hiding this comment

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

The modifications look safe to me, but could use a few more tests.

It's been a while since I've looked at this code, but it's not clear to me that it's necessary to flush the flow control frames, otherwise flow control for data buffered when a stream is closed would appear to be broken. Specifically, we only write an explicit flow control frame via updateLocalWindowIfNeeded(), which is only called at the beginning of the HttpChannelUnsafe.beginRead(). However, when a stream is closed it attempts to drain its inbound buffer but does so by going directly to Unsafe.doBeginRead(), skipping the generation of flow control messages for anything that is still in the inboundBuffer. Since those may be data frames and it appears we wouldn't send flow control for them on a stream close they must be accounted for elsewhere, which looks like in the sessions flow controller when the stream is removed here, which includes a flush.
That said, it's been a while so don't be surprised if this isn't an accurate assessment of things. 😅

Comment on lines +1372 to +1373
Http2StreamChannel childChannel3 = newInboundStream(3, true, new LastInboundHandler());
Http2StreamChannel childChannel5 = newInboundStream(5, true, new LastInboundHandler());
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be good to ensure that we do flush any window update frames. Can we add a third stream that tests that, or if you prefer, another test?

@@ -1037,6 +1044,7 @@ public void flush() {
// We need to set this to false before we call flush0(...) as ChannelFutureListener may produce more data
// that are explicit flushed.
writeDoneAndNoFlush = false;
windowUpdateFrameWritten = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

There are strong parallels with the writeDoneAndNoFlush field. It would be good to align the names and make clear the distinction, which is only that writeDoneAndNoFlush includes all frame types (including window updates) whereas windowUpdateFrameWritten is only for window update frames.

@normanmaurer normanmaurer added this to the 4.1.97.Final milestone Jul 27, 2023
@He-Pin
Copy link
Contributor

He-Pin commented Sep 13, 2023

@icodening Anyupdate on this?

@icodening
Copy link
Author

@icodening Anyupdate on this?

No updates yet

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.

too many flush in http2 write end header
5 participants