如何在Golang的标准http.Client中禁用HTTP/2,或者避免流ID=N的大量INTERNAL_ERROR?

vq8itlhq  于 2023-01-03  发布在  Go
关注(0)|答案(1)|浏览(366)

我想尽快发送相当大数量(几千)的HTTP请求,而不给CDN(有https:URL,ALPN在TLS阶段选择HTTP/2)因此,交错(即时移)请求是一个选项,但我不想等待太长时间(最小化错误和总往返时间),而且我还没有受到服务器在我运营的规模上的速率限制。
我看到的问题源于h2_bundle.go,特别是在writeFrameonWriteTimeout中,此时大约有500- 1 k个请求正在传输,这在io.Copy(fileWriter, response.Body)期间表现为:

http2ErrCodeInternal = "INTERNAL_ERROR" // also IDs a Stream number
// ^ then io.Copy observes the reader encountering "unexpected EOF"

现在我很好地坚持使用HTTP/1.x,但我希望得到一个解释:发生了什么。很明显,人们确实使用Go语言在单位时间内进行了很多次往返,但我能找到的大多数建议都是从服务器的Angular 出发的,而不是从客户端的Angular 。我已经尝试过指定所有相关的超时,并提高连接池的最大大小。

atmip9wb

atmip9wb1#

以下是我对这一切的最佳猜测:
HTTP/2内部的请求速率超过了连接队列或其他一些资源。也许这在一般情况下是可以修复的,或者可以针对我的特定用例进行微调,但克服这类问题的最快方法是完全依赖HTTP/1.1,以及实现有限的重试+速率限制机制。
另外,除了禁用HTTP/2的“丑陋的攻击”之外,我现在还使用一次重试和www.example.com中的rate.Limiterhttps://pkg.go.dev/golang.org/x/time/rate#Limiter,这样出站请求能够发送M个请求的初始“突发”,然后以给定的N/秒速率“逐渐泄漏”。来自h2_bundle.go的错误对于最终用户来说太难解析了。2一个预期的/意外的EOF应该导致客户端“再试一次”或两次,无论如何这是更实用的。
根据文档,在运行时禁用Go语言http.Client中h2的最简单方法是env GODEBUG=http2client=0 ...,我也可以通过其他方法来实现。尤其需要理解的是,“下一个协议”是在TLS过程中“早期”预先协商的,因此Go语言的http.Transport必须管理该配置沿着缓存/备忘录,以高效的方式提供其功能。因此,使用您自己的httpClient.Do(req)(别忘了给予您的请求一个context.Context,这样很容易取消),使用自定义的http.RoundTripper进行传输。

type forwardRoundTripper struct {
    rt http.RoundTripper
}

func (my *forwardRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
    return my.rt.RoundTrip(r) // adjust URLs, or transport as necessary per-request
}

// httpTransport is the http.RoundTripper given to a Client as Transport
// (don't forget to set up a reasonable Timeout and other behavior as desired)
var httpTransport = &customRoundTripper{rt: http.DefaultTransport}

func h2Disabled(rt *http.Transport) *http.Transport {
    log.Println("--- only using HTTP/1.x ...")
    rt.ForceAttemptHTTP2 = false // not good enough
    // at least one of the following is ALSO required:
    rt.TLSClientConfig.NextProtos = []string{"http/1.1"}
    // need to Clone() or replace the TLSClientConfig if a request already occurred
    // - Why? Because the first time the transport is used, it caches certain structures.
    // (if you do this replacement, don't forget to set a minimum TLS version)

    rt.TLSHandshakeTimeout = longTimeout // not related to h2, but necessary for stability
    rt.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
    // ^ some sources seem to think this is necessary, but not in all cases
    // (it WILL be required if an "h2" key is already present in this map)
    return rt
}

func init() {
    h2ok := ...
    if t, ok := httpTransport.rt.(*http.Transport); ok && !h2ok {
        httpTransport.rt = h2Disabled(t.Clone())
    }
    // tweak rate limits here
}

这使我能够生成所需的请求量,或者在边缘情况下获得更合理的错误。

相关问题