go x/net/http2: do not flush immediately after write headers

axr492tv  于 5个月前  发布在  Go
关注(0)|答案(7)|浏览(45)

http2客户端在写入头部信息后立即刷新,因此有两个TCP数据包被分开。
https://github.com/golang/net/blob/master/http2/transport.go#L1617
当有大量请求时,这可能会导致性能损失。
那么是否应该将头部和主体数据包组合在一起,并在写入头部信息后不要立即刷新?

r8xiu3jd

r8xiu3jd1#

我创建了一个discussion,似乎没有专业的答案。

1dkrff03

1dkrff032#

这看起来不像是一个API变更,所以将其从提案流程中移除。
CC @neild@bradfitz

t98cgbkg

t98cgbkg3#

不确定为什么有这个刷新。
在尝试更改此情况时的一个担忧是,请求在读取响应的某些部分之前不会发送正文。这不是常见情况,但当将HTTP/2请求视为双向流时,这是允许的。只是删除这个刷新意味着请求永远不会被发送。
在尝试更改这里的任何内容之前,我希望有一个可靠的答案来说明这种情况是如何工作的,或者为什么不重要。

lstz6jyr

lstz6jyr4#

你好,我进行了一个测试。
服务器代码:

package main

import (
	"net/http"

	"golang.org/x/net/http2"
	"golang.org/x/net/http2/h2c"
)

func main() {
	h2s := &http2.Server{}
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

	})
	server := &http.Server{
		Addr:    "0.0.0.0:28080",
		Handler: h2c.NewHandler(handler, h2s),
	}
	if err := server.ListenAndServe(); err != nil {
		panic(err)
	}
}

客户端代码:

package main

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"net/http"
	"strings"
	"sync"
	"time"

	"golang.org/x/net/http2"
)

func main() {
	client := http.Client{
		Transport: &http2.Transport{
			AllowHTTP: true,
			DialTLSContext: func(_ context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
				return net.Dial(network, addr)
			},
		},
	}

	var (
		startTime   = time.Now()
		wg          sync.WaitGroup
		concurrency = 10
		count       = 100000
	)
	for i := 0; i < concurrency; i++ {
		wg.Add(1)
		go func() {
			worker(client, count)
			wg.Done()
		}()
	}
	wg.Wait()
	dur := time.Since(startTime)
	fmt.Printf("%s %d %.2f\n", dur, 10*100000, float64(10*100000)/dur.Seconds())
}

func worker(client http.Client, n int) {
	for i := 0; i < n; i++ {
		_, err := client.Post(
			"http://192.168.0.4:28080",
			"application/json",
			strings.NewReader(`{"key":"value"}`),
		)
		if err != nil {
			panic(err)
		}
	}
}

当我使用原始的 golang.org/x/net/http2 包时,测试结果如下:

➜  go run ./test-h2c/client/client.go
42.616582576s 1000000 23465.04
➜  go run ./test-h2c/client/client.go
43.251295758s 1000000 23120.69

在我注解掉 cc.bw.Flush() 之后,再次进行测试,结果如下:

diff --git a/http2/transport.go b/http2/transport.go
index 05ba23d..a09df11 100644
--- a/http2/transport.go
+++ b/http2/transport.go
@@ -1614,7 +1614,7 @@ func (cc *ClientConn) writeHeaders(streamID uint32, endStream bool, maxFrameSize
                        cc.fr.WriteContinuation(streamID, endHeaders, chunk)
                }
        }
-       cc.bw.Flush()
+       // cc.bw.Flush()
        return cc.werr
 }

从测试情况来看,有超过 40% 的改进。
此外,还可以通过 tcpdump 捕获看到修改前有两个数据包,修改后它们被合并成一个。

rur96b6h

rur96b6h5#

我同意这个改变降低了POST延迟,这是没有疑问的。
我的担忧是它改变了用户可见的行为:一个POST请求现在不会在从响应体中读取第一个字节之前发送。我不知道这是否会破坏任何现有的用户。如果有人确实想在发送第一个请求体字节之前从响应体中读取,那么在这个改变之后他们应该如何做?

bmvo0sr5

bmvo0sr56#

我认为没有必要单独发送头部。当然,你可以评估在发送头部之前等待读取正文是否存在问题?

x3naxklr

x3naxklr7#

另外,如果有问题。那么是否可以为发送头部添加等待超时?
它应该支持以下情况:

  • 立即发送,这是兼容性的默认设置。
  • 等待超时
  • 等待直到读取到正文返回

相关问题