Fangliding 报告 Go 1.27 新版 net/x/http2 缺少单连接复用机制
在 golang/go 的一个问题中,Fangliding 报告 Go 1.27 起新的包装式 net/x/http2 实现缺少旧实现的单连接复用(single flight)机制:以 100 个并发请求测试,新版会为每个请求各发起一次 TCP 拨号(共 100 次),而加 `-tags http2legacy` 的旧实现只拨号 1 次。他认为 net/http 因为事先不知道服务端 HTTP 版本才需要多次拨号,而 x/net/http2 已有前置信息,建议增加一个开关启用单连接复用,或在 ALPN 只包含 h2 时自动启用。该问题目前仍为 open,尚无官方结论。
作者原文@FanglidingGo version
go 1.27 linux/amd64
Output of
go envin your module/workspace:AR='ar' CC='gcc' CGO_CFLAGS='-O2 -g' CGO_CPPFLAGS='' CGO_CXXFLAGS='-O2 -g' CGO_ENABLED='1' CGO_FFLAGS='-O2 -g' CGO_LDFLAGS='-O2 -g' CXX='g++' GCCGO='gccgo' GO111MODULE='' GOAMD64='v1' GOARCH='amd64' GOAUTH='netrc' GOBIN='' GOCACHE='/home/codespace/.cache/go-build' GOCACHEPROG='' GODEBUG='' GOENV='/home/codespace/.config/go/env' GOEXE='' GOEXPERIMENT='' GOFIPS140='off' GOFLAGS='' GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build2735046389=/tmp/go-build -gno-record-gcc-switches' GOHOSTARCH='amd64' GOHOSTOS='linux' GOINSECURE='' GOMOD='/workspaces/Xray-core/go.mod' GOMODCACHE='/go/pkg/mod' GONOPROXY='' GONOSUMDB='' GOOS='linux' GOPATH='/go' GOPRIVATE='' GOPROXY='https://proxy.golang.org,direct' GOROOT='/usr/local/go' GOSUMDB='sum.golang.org' GOTELEMETRY='local' GOTELEMETRYDIR='/home/codespace/.config/go/telemetry' GOTMPDIR='' GOTOOLCHAIN='auto' GOTOOLDIR='/usr/local/go/pkg/tool/linux_amd64' GOVCS='' GOVERSION='go1.26.1' GOWORK='' PKG_CONFIG='pkg-config'What did you do?
run this code to count dial calls in http2
package main import ( "context" "crypto/tls" "fmt" "io" "net" "net/http" "sync" "sync/atomic" "golang.org/x/net/http2" ) func main() { totalRequests := 100 targetURL := "https://cp.cloudflare.com/cdn-cgi/trace" var dialCount atomic.Int64 client := &http.Client{ Transport: &http2.Transport{ DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (c net.Conn, err error) { dialCount.Add(1) dialer := &tls.Dialer{Config: cfg} return dialer.DialContext(ctx, network, addr) }, }, } var wg sync.WaitGroup start := make(chan struct{}) for range totalRequests { wg.Add(1) go func() { defer wg.Done() <-start resp, err := client.Get(targetURL) if err == nil { io.Copy(io.Discard, resp.Body) resp.Body.Close() } }() } close(start) wg.Wait() fmt.Printf("Total Dial Calls: %d\n", dialCount.Load()) }What did you see happen?
It made 100 tcp dial calls for every http request
Total Dial Calls: 100What did you expect to see?
In old http2 implementation, instantly sending multiple requests will only make one TCP dial, this is the expected behavior of http2
run code above with thisgo run -tags http2legacy .And it will return
Total Dial Calls: 1I understand that HTTP package needs to dial multiple connections because the server HTTP version is unknown, but we have prior knowledge in net/x/http2, and perhaps we can add a config to enable the single flight mechanism in http package (or automatically use when alpn only contains h2?)