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

问题描述 投票:0回答:1

我想尽快发送相当大数量(几千)的 HTTP 请求,而不会给 CDN 带来太多负载(有一个 https: URL,并且 ALPN 在 TLS 阶段选择 HTTP/2)所以,令人震惊(即时间)转移)请求是一种选择,但我不想等待太久(最大限度地减少错误和总往返时间)并且我不会受到我正在操作的规模的服务器的速率限制还没有。

我看到的问题源自

h2_bundle.go
,特别是在
writeFrame
onWriteTimeout
中,当大约 500-1k 请求正在进行时,这在
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 在单位时间内进行大量往返,但我能找到的大多数建议都是从服务器的角度来看的,而不是从客户端的角度来看。我已经尝试指定我能找到的所有相关超时,并提高连接池最大大小。

go http2
1个回答
1
投票

这是我对正在发生的事情的最佳猜测:

请求的速率压垮了连接队列或 HTTP/2 内部的某些其他资源。也许这通常是可以修复的,或者可以针对我的特定用例进行微调,但克服此类问题的最快方法是完全依赖 HTTP/1.1,然后实现有限形式的重试 + 速率 -限制机制。

此外,除了禁用 HTTP/ 的“丑陋黑客”之外,我现在还使用单次重试和来自

https://pkg.go.dev/golang.org/x/time/rate#Limiter
rate.Limiter 2,以便出站请求能够发送 M 个请求的初始“突发”,然后以 N/秒的给定速率“逐渐泄漏”。最终,来自
h2_bundle.go
的错误对于最终用户来说太难解析了。以我的拙见,任何预期/意外的 EOF 都可能导致客户端“再试一次”或两次,无论如何,这更务实。

根据 docs,在运行时在 Go 的

http.Client
中禁用 h2 的最简单方法是
env GODEBUG=http2client=0 ...
,我也可以通过下面的其他方式实现。理解这一点尤其重要:“下一个协议”是在 TLS 期间“尽早”预先协商的,因此 Go 的
http.Transport
必须与单例缓存/备忘录一起管理该配置,以便以高性能的方式提供该功能。因此,请使用您自己的
httpClient
.Do(req)
(并且不要忘记为您的请求提供
context.Context
,这样“取消”它就很简单),使用自定义
http.RoundTripper
进行传输。这是一些示例代码:

import (
    "net/http"
    "time"
)

type forwardRoundTripper struct {
    rt http.RoundTripper // e.g. an *http.Transport
}

func (my *forwardRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
    // set r = r.WithContext(ctx) for some `ctx` if you desire
    // adjust URL, or this transport (as necessary, per-request)
    // NOTE: A very common thing is to add HTTP headers here
    return my.rt.RoundTrip(r)
}

// httpClient has an http.RoundTripper given for general Transport!
// (don't forget to choose a reasonable CheckRedirect and Jar/etc.)
var httpClient = &http.Client{
    Timeout:   time.Second * 10, // or whatever you prefer here
    Transport: &forwardRoundTripper{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() {
    h := httpClient
    h2ok := ... // e.g. cmp.Or(os.Getenv("IS_H2_OK"), "yes") == "yes"
    if t, ok := h.Transport.(*forwardRoundTripper); ok && !h2ok {
        h2t, _ := t.rt.(*http.Transport) // ok
        h.Transport = h2Disabled(h2t.Clone())
        // recommended: log at warn (h2 disabled)
    }
    // log at info about Client
    // tweak rate limits here
}

这使我们能够发出所需的请求量,或者在我们之前遇到的边缘情况下获得更合理的错误。

最新问题
© www.soinside.com 2019 - 2025. All rights reserved.