How to disable HTTP/2 in Golang's standard http.Client, or avoid tons of INTERNAL_ERRORs from Stream ID=N?

huangapple go评论75阅读模式
英文:

How to disable HTTP/2 in Golang's standard http.Client, or avoid tons of INTERNAL_ERRORs from Stream ID=N?

问题

我想尽快发送大量的HTTP请求(几千个),而不会给CDN(具有https:URL,并且在TLS阶段选择HTTP/2)带来太大的负载。因此,错开(即时间偏移)请求是一个选项,但我不想等待太长时间(最小化错误和总往返时间),而且在我目前的操作规模下,服务器没有对我进行速率限制。

我遇到的问题源于h2_bundle.go,具体来说是在writeFrameonWriteTimeout中,当大约有500-1k个请求正在进行时,io.Copy(fileWriter, response.Body)会出现以下错误:

http2ErrCodeInternal = "INTERNAL_ERROR" // also IDs a Stream number
// ^ 然后io.Copy观察到读取器遇到"unexpected EOF"

目前我可以使用HTTP/1.x,但我很想知道发生了什么。显然,人们确实使用Go来实现每单位时间内进行大量往返,但我能找到的大多数建议都是从服务器的角度而不是客户端的角度。我已经尝试指定所有相关的超时时间,并增加连接池的最大大小。

英文:

I want to send a fairly large number (several thousand) of HTTP requests ASAP, without putting too much load on the CDN (has an https: URL, and ALPN selects HTTP/2 during the TLS phase) So, staggering (i.e. time shifting) the requests is an option, but I don't want to wait TOO long (minimize errors AND total round-trip time) and I'm not being rate limited by the server at the scale I'm operating yet.

The problem I'm seeing originates from h2_bundle.go and specifically in either writeFrame or onWriteTimeout when about 500-1k requests are in-flight, which manifests during io.Copy(fileWriter, response.Body) as:

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

I'm fine sticking with HTTP/1.x for now, but I would love an explanation re: what's going on. Clearly, people DO use Go to make a lot of round-trips happen per unit time, but most advice I can find is from the perspective of the server, not clients. I've already tried specifying all the relevant time-outs I can find, and cranking up connection pool max sizes.

答案1

得分: 1

这是我对情况的最佳猜测:

请求的速率超过了HTTP/2内部的连接队列或其他资源的处理能力。也许这个问题在一般情况下是可以修复的,或者可以根据我的具体用例进行微调,但是克服这种问题的最快方法是完全依赖HTTP/1.1,并实施有限的重试和速率限制机制。

此外,我现在使用了单个重试和rate.Limiter(来自https://pkg.go.dev/golang.org/x/time/rate#Limiter),除了禁用HTTP/2的“丑陋的hack”之外,出站请求能够发送初始的M个“突发”请求,然后以每秒N个的速率“逐渐泄漏”。最终,h2_bundle.go中的错误对于最终用户来说太难以解析了。预期/意外的EOF应该导致客户端“再试一次”或两次,这更加实用。

根据文档,在Go的http.Client中在运行时禁用h2的最简单方法是env GODEBUG=http2client=0 ...,我也可以通过其他方式实现。特别重要的是要理解,“下一个协议”在TLS期间“早期”进行了预协商,因此Go的http.Transport必须管理该配置以及缓存/备忘录,以便以高性能的方式提供其功能。因此,使用自己的httpClient.Do(req)(不要忘记给请求一个context.Context,以便轻松取消),并为Transport使用自定义的http.RoundTripper。以下是一些示例代码:

type forwardRoundTripper struct {
	rt http.RoundTripper
}

func (my *forwardRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
	return my.rt.RoundTrip(r) // 根据需要调整URL或传输
}

// httpTransport是作为Transport给Client的http.RoundTripper
// (不要忘记根据需要设置合理的Timeout和其他行为)
var httpTransport = &customRoundTripper{rt: http.DefaultTransport}

func h2Disabled(rt *http.Transport) *http.Transport {
	log.Println("--- 仅使用HTTP/1.x ...")
	rt.ForceAttemptHTTP2 = false // 这样不够好
	// 下面的至少一个也是必需的:
	rt.TLSClientConfig.NextProtos = []string{"http/1.1"}
	// 如果已经发生了请求,需要克隆(Clone())或替换TLSClientConfig
	// - 为什么?因为在第一次使用传输时,它会缓存某些结构。
	// (如果进行了此替换,请不要忘记设置最低TLS版本)

	rt.TLSHandshakeTimeout = longTimeout // 与h2无关,但对于稳定性是必需的
	rt.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
	// ^ 一些来源似乎认为这是必需的,但并非在所有情况下都是如此
	// (如果此映射中已经存在“h2”键,则将需要它)
	return rt
}

func init() {
	h2ok := ...
	if t, ok := httpTransport.rt.(*http.Transport); ok && !h2ok {
		httpTransport.rt = h2Disabled(t.Clone())
	}
	// 在这里调整速率限制
}

这样我就可以进行所需数量的请求,或者在边缘情况下获得更合理的错误。

英文:

Here's my best guess at what's going on:

The rate of requests is overwhelming a queue of connections or some other resource in the HTTP/2 internals. Maybe this is fix-able in general or possible to fine tune for my specific use case, but the fastest way to overcome this kind of problem is to rely on HTTP/1.1 entirely, as well as implement limited retry + rate limiting mechanisms.

> Aside, I am now using a single retry and rate.Limiter from https://pkg.go.dev/golang.org/x/time/rate#Limiter in addition to the "ugly hack" of disabled HTTP/2, so that outbound requests are able send an initial "burst" of M requests, and then "leak more gradually" at a given rate of N/sec. Ultimately, the errors from h2_bundle.go are just too ugly for end-users to parse. An expected/unexpected EOF should result in the client "giving it another try" or two, which is more pragmatic anyway.

As per the docs, the easiest way to disable h2 in Go's http.Client at runtime is env GODEBUG=http2client=0 ... which I can also achieve in other ways as well. Especially important to understand is that the "next protocol" is pre-negotiated "early" during TLS, so Go's http.Transport must manage that configuration along with a cache/memo to provide its functionality in a performant way. Therefore, use your own httpClient to .Do(req) (and don't forget to give your Request a context.Context so that it's easy to cancel) using a custom http.RoundTripper for Transport. Here's some example code:

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
}

This lets me make the volume of requests that I need to OR get more-reasonable errors in edge cases.

huangapple
  • 本文由 发表于 2023年1月1日 05:56:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/74972233.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定