Golang:启用 idleConn 时,http2 客户端立即通过代理关闭连接

Golang: http2 client immediately close connections via proxy while idleConn was enabled

0x00 TL;DR

我正在使用 Go 实现一个 http 客户端,并使用 squid 作为转发代理向远程服务器发送请求。当通过代理使用 http/1.1 或 http/1.1, 没有代理的 http2 时一切顺利,但是,当通过代理使用 http2 客户端时,大多数连接立即关闭,只有一两个连接被保留.

不确定是我的错误代码还是什么。在 http 传输上启用了 idleConn 配置。提前致谢。

0x01 环境

代码

package main

import (
    "context"
    "crypto/tls"
    "fmt"
    "io"
    "io/ioutil"
    "net"
    "net/http"
    "net/url"
    "sync"
    "time"
)

const (
    proxyURL = "http://{{proxy-ip}}:3128"
)

func main() {
    wg := &sync.WaitGroup{}

    wg.Add(1)
    go func() {
        doSendRequests()
        wg.Done()
    }()

    wg.Wait()
}

func doSendRequests() {
    //client := newHTTPClient(nil)
    client := newHTTPClient(proxy)

    round := 0
    for {
        round += 1
        for i := 0; i < 5; i++ {
            go func(r int) {
                start := time.Now()
                var err error
                defer func() {
                    duration := time.Since(start)
                    fmt.Printf("Round: %d, A: %v, duration: %v\n", r, err, duration)
                }()
                req := newRequest("https://www.google.com")
                resp, err := client.Do(req)
                if err == nil {
                    io.Copy(ioutil.Discard, resp.Body)
                    resp.Body.Close()
                } else {
                    fmt.Printf("A: %v\n", err)
                }
            }(round)
        }

        time.Sleep(100 * time.Second)
    }
}

type TLSDialer struct {
    net.Dialer
}

var (
    config = &tls.Config{InsecureSkipVerify: true}
)

func (dialer *TLSDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
    return tls.DialWithDialer(&dialer.Dialer, network, addr, config)
}

var (
    DefaultDialer = net.Dialer{
        Timeout:   30 * time.Second,
        KeepAlive: 30 * time.Second,
        DualStack: true,
    }

    DefaultTLSDialer = TLSDialer{DefaultDialer}
)

func proxy(req *http.Request) (*url.URL, error) {
    uri, err := url.Parse(proxyURL)
    if err != nil {
        return nil, nil
    }
    return uri, nil
}

func newHTTPClient(proxy func(*http.Request) (*url.URL, error)) *http.Client {
    t := &http.Transport{
        DialContext:           (&DefaultDialer).DialContext,
        DisableKeepAlives:     false,
        DisableCompression:    false,
        ForceAttemptHTTP2:     true,
        MaxIdleConns:          5000,
        MaxIdleConnsPerHost:   1000,
        IdleConnTimeout:       0 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
    }

    if proxy == nil {
        t.DialTLSContext = (&DefaultTLSDialer).DialContext
    } else {
        t.Proxy = proxy
        t.TLSClientConfig = &tls.Config{
            InsecureSkipVerify: true,
        }
    }

    return &http.Client{Transport: t}
}

func newRequest(uri string) *http.Request {
    ctx, _ := context.WithTimeout(context.Background(), 60*time.Second)
    req, _ := http.NewRequestWithContext(ctx, "GET", uri, nil)

    return req
}

环境

进入 1.15.3

GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/ec2-user/.cache/go-build"
GOENV="/home/ec2-user/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/ec2-user/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/ec2-user/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build200120630=/tmp/go-build -gno-record-gcc-switches"

uname -a

Linux ip-10-53-74-64.ec2.internal 4.14.243-185.433.amzn2.x86_64 #1 SMP Mon Aug 9 05:55:52 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

鱿鱼4.15

0x02现象

正如我在摘要中提到的,当我通过代理使用 http/1.1 或不使用代理的 http/1.1, http2 时一切顺利。但是,当通过代理使用 http2 客户端时,将创建与请求一样多的连接,然后关闭其他连接,只留下一两个连接。我用 Wireshark 抓包,发现连接最初是从客户端关闭的,所以它可能与代理端无关,对此不确定。

没有代理

// www.google.com
Every 0.5s: netstat -anop | grep '172.217'

Sat Nov 27 04:26:22 2021

tcp        0      0 10.53.74.64:44106       172.217.0.36:443        ESTABLISHED 3110/main            keepalive (25.35/0/0)
tcp        0      0 10.53.74.64:44378       172.217.0.36:443        ESTABLISHED 3110/main            keepalive (25.44/0/0)
tcp        0      0 10.53.74.64:44084       172.217.0.36:443        ESTABLISHED 3110/main            keepalive (25.60/0/0)
tcp        0      0 10.53.74.64:44374       172.217.0.36:443        ESTABLISHED 3110/main            keepalive (25.43/0/0)
tcp        0      0 10.53.74.64:44220       172.217.0.36:443        ESTABLISHED 3110/main            keepalive (25.52/0/0)

使用代理和 http/1.1

Every 0.5s: netstat -anop | grep ":3128"

Sun Dec  5 13:09:44 2021

tcp        0      0 10.53.74.64:58370       {{proxy-ip}}:3128     ESTABLISHED 26322/main           keepalive (23.70/0/0)
tcp        0      0 10.53.74.64:58366       {{proxy-ip}}:3128     ESTABLISHED 26322/main           keepalive (23.70/0/0)
tcp        0      0 10.53.74.64:58374       {{proxy-ip}}:3128     ESTABLISHED 26322/main           keepalive (23.70/0/0)
tcp        0      0 10.53.74.64:58368       {{proxy-ip}}:3128     ESTABLISHED 26322/main           keepalive (23.69/0/0)
tcp        0      0 10.53.74.64:58372       {{proxy-ip}}:3128     ESTABLISHED 26322/main           keepalive (23.70/0/0)

使用代理和 http2

如下所示,其他连接在 ESTABLISHED 后不久关闭,并且在 http 传输上启用了 idleConn 配置。

Sun Dec  5 13:19:35 UTC 2021
tcp        0    284 10.53.74.64:58414       {{proxy-ip}}:3128     ESTABLISHED 34301/main           on (0.20/0/0)
tcp        0    284 10.53.74.64:58416       {{proxy-ip}}:3128     ESTABLISHED 34301/main           on (0.19/0/0)
tcp        0    284 10.53.74.64:58410       {{proxy-ip}}:3128     ESTABLISHED 34301/main           on (0.19/0/0)
tcp        0    284 10.53.74.64:58412       {{proxy-ip}}:3128     ESTABLISHED 34301/main           on (0.19/0/0)
tcp        0    284 10.53.74.64:58418       {{proxy-ip}}:3128     ESTABLISHED 34301/main           on (0.19/0/0)

Sun Dec  5 13:19:35 UTC 2021
tcp        0     89 10.53.74.64:58414       {{proxy-ip}}:3128     FIN_WAIT1   -                    on (0.14/0/0)
tcp        0     89 10.53.74.64:58416       {{proxy-ip}}:3128     FIN_WAIT1   -                    on (0.14/0/0)
tcp        0    365 10.53.74.64:58410       {{proxy-ip}}:3128     ESTABLISHED 34301/main           on (0.14/0/0)
tcp        0     89 10.53.74.64:58412       {{proxy-ip}}:3128     FIN_WAIT1   -                    on (0.14/0/0)
tcp        0     89 10.53.74.64:58418       {{proxy-ip}}:3128     FIN_WAIT1   -                    on (0.14/0/0)

Sun Dec  5 13:19:35 UTC 2021
tcp        0      0 10.53.74.64:58414       {{proxy-ip}}:3128     TIME_WAIT   -                    timewait (59.99/0/0)
tcp        0      0 10.53.74.64:58416       {{proxy-ip}}:3128     TIME_WAIT   -                    timewait (59.99/0/0)
tcp        0     31 10.53.74.64:58410       {{proxy-ip}}:3128     ESTABLISHED 34301/main           on (0.26/0/0)
tcp        0      0 10.53.74.64:58412       {{proxy-ip}}:3128     TIME_WAIT   -                    timewait (59.99/0/0)
tcp        0      0 10.53.74.64:58418       {{proxy-ip}}:3128     TIME_WAIT   -                    timewait (59.99/0/0)

最新的golang 1.17.4(目前:2021-12-05)可以复现。有没有人可以提供一种方法来弄清楚为什么会发生这种情况?谢谢。

EDIT1:所有请求均无误完成。

EDIT2:我在 Golang 回购上打开了一个问题,以防有人感兴趣: https://github.com/golang/go/issues/50000

最后,我弄明白了,一切都按预期工作,所以这与 net/http2 无关。

更多详情请参考本期:https://github.com/golang/go/issues/50000.