Golang Go语言中http1.1 长连接与并发请求的疑问

发布于 1周前 作者 itying888 来自 Go语言

Golang Go语言中http1.1 长连接与并发请求的疑问

浏览器对于同一个域名的请求,比如 Chrome 并发连接数是 6 个,假设是 http1.1 协议,那么这 6 个请求是复用一个 tcp 连接并发请求(响应还是按顺序),超出了就会有“队头阻塞”的问题。

问题

使用 golang 写一个程序,开 10 个 goroutine 并发请求同一个域名的资源,那么这 10 个请求是用一个 tcp 连接吗?如果是,是否也存在“队头阻塞”问题?那这样跟浏览器并发请求有何区别?

谢谢。


更多关于Golang Go语言中http1.1 长连接与并发请求的疑问的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

18 回复

10 个请求并发就是 10 个连接

更多关于Golang Go语言中http1.1 长连接与并发请求的疑问的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


跟浏览器的机制不一样么

浏览器只服务一个用户,同一个域名做个排队,对体验影响不大。服务端不一样,100 个请求也许是 100 个不同用户发起的,如果排队,后面的用户只能超时了。

服务端也有连接池大小限制,一般几百到几千,超出大小了,要么排队等,要么临时创建不受限制的连接但用完就关。

10 个 goroutine 就是同时(分时复用)调用 10 次创建 socket ,自然就是会是 10 个连接,分别占用 10 个端口。

浏览器的实现是为了性能考虑,如果加载 100 个资源,从并行角度来讲,那当然是开 100 个连接好。但是如果我开了 10 个窗口,就要有 1000 个连接,每次连接都做一次 TCP TLS handshake ,就太非资源了

这个程序是本地运行的,go run main.go 启动,用 net/http 包发请求,默认配置也有连接池,既然有连接池,那应该会复用连接

等下,HTTP/1.1 的请求,并行不是复用同一个 TCP 啊?串行才是复用同一个 TCP 的啊!队头阻塞也是复用同一个 TCP 连接的串行 HTTP ,后面的请求要等待前面的请求响应啊。
Chrome 并发连接数限制是指创建的 TCP 的连接数啊,并且仅限 HTTP/1 。

HTTP/1 还没有分帧并发的技术,所有数据都是串行发送的,不存在你说的“并发请求、顺序响应”。要实现并发,就得建立多条 TCP ,在多个 TCP 连接上同时请求。
HTTP/1 的 keep-alive 是指一轮 请求-响应 结束后,不关闭 TCP 连接,可以直接在当前 TCP 连接上进行新的 HTTP 请求-响应。

具体的情况得结合源码及你具体的代码才能知道。

http.Client 是可以配置最大连接数及其他的一些配置的。

默认配置下,如果多个协程共用的一个 http.Client ,那么可能是会出现连接复用的。
如果不想连接复用,可以每个协程使用单独的 http.Client ,不过不建议这么做。

对于调用非常频繁的服务,即使连接复用也还是可能出现问题。对于这种还是用 rpc 比较好,http 不适合。

http.Client 默认使用的 transport 默认会复用 tcp 连接,但需要在每次请求后 io.ReadAll 一下 response.Body 并 close ,这样能够保证连接复用。当然,这个是串行复用的,就是在一次请求结束后,不立刻关闭 tcp 连接,在后面连接有效且有相应请求时,不再需要重新建立 tcp 连接。

没错没错,这 6 个是 6 个不同的 tcp 连接,后面串行的才是复用前面的连接。

浏览器的复用指的是:假如从同一个域名下加载 100 个资源,如果用的是 http1.1 ,同一时刻只会存在 6 个到这个域名的 tcp 连接,最初的 6 个资源请求完毕空闲出的连接才会被复用进行下一次请求。

所以啊,你只要想一想,你 10 个 goroutine 请求是同时请求的,还是有先后顺序的?肯定是同时的啊。

实际上,同时并发请求数受到操作系统的限制(端口数、ulimit 、maxfiles 之类的),为了避免达到操作系统限制,同时降低建立 TCP 的开销,所以浏览器才会实现较小的并发限制。
实际上,具体是看你 golang 代码怎么写的。多个 goroutine 是用的同一个 http.Client 还是不同的,单域名连接数配置的多少。
如果是使用默认的 http.Client 来请求的话,那么走的就是默认的 http.Transport 配置,里面指定的单域名连接数 MaxConnsPerHost 默认是 0 ,也就是不限制。那么你只要没有达到操作系统限制,并发就是能建立多少 TCP 就建立多少 TCP 来并发请求,所以 10 个 goroutine 就是 10 个 TCP 。
而另外,可以针对单个 http.Client 对象来限制连接数,那么使用同一个实例来请求就会受到限制,达到限制就会阻塞等待。

另外注意,这个连接数跟连接池关系不大。默认的 http.Transport 的连接池有两个配置:全部连接池大小 MaxIdleConns 默认是 100 、单域名连接池大小 DefaultMaxIdleConnsPerHost 默认是 2 。这两个配置不影响并发请求的 TCP 连接数。

假设现在是单域名,单域名连接池大小 DefaultMaxIdleConnsPerHost 是 2 ,MaxConnsPerHost 并发数设置了 5 。那么此时你开了 10 个 goroutine 并发,并且每个请求都是 1 秒响应,则会发生这样的情况(实际情况可能受各种竞争因素影响):
1 ,建立 5 个 TCP 连接并发,剩余 5 个连接阻塞等待。
2 ,5 个连接请求结束,只有 2 个 TCP 连接进入连接池复用,剩余 3 个 TCP 连接被 close 。
3 ,剩下的 5 个连接开始请求,其中 2 个复用之前连接池中的 TCP 连接,另外 3 个则建立新的 TCP 连接进行请求。
4 ,所有请求都结束,连接池中持有 2 个连接等待复用,其余连接都被 close 。
5 ,如果没有更多请求了,则连接池中的连接在超时( IdleConnTimeout )后被自动 close 。或者等待 TCP 自己的 keepalive 超时规则( tcp_keepalive_time 、tcp_keepalive_intvl 、tcp_keepalive_probes )来关闭连接。

问题之前:浏览器的描述不对,你并不知道浏览器建立了几个链接。最多 6 个是连接池的限制。

问题本身:net/http 默认 client 有连接池的,所以你也并不知道用了几个。除非每个 goroutine 里自己创建了独立的 client 。

所以说,有些时候 http1.1 是不是会比 http2.0 要好。

Nginx upstream 不支持 http2 ,就是说某些情况下 http1.1 更好,但没想到具体说明场景。
请问你如何理解这个呢? https://trac.nginx.org/nginx/ticket/923

很详尽,感谢!

你写代码请求时跟浏览器关系不大。。。

如果用了连接池就是受限于连接池,连接用完后再请求会等前面连接释放

直接新连接那就是系统的限制了

http://nginx.org/en/docs/http/ngx_http_upstream_module.html#server 对于 HTTP/1 upstream ,可以通过 max_conns 限制连接数、keepalive 限制连接池。

先说一下 HTTP/1 、HTTP/2 、HTTP/3:
HTTP/1 在单个 TCP 上以 keep-alive 的形式复用,按照“请求-响应-请求-响应”的顺序进行,后面的请求会受到前面请求的阻塞,叫做“线头阻塞 Head-of-line blocking” https://en.wikipedia.org/wiki/Head-of-line_blocking
HTTP/2 在单个 TCP 上分帧复用连接,多个请求可以同时发送、同时接收。但这个“同时”只是在应用层的 HTTP 协议眼中看起来是“同时”的,而在传输层的 TCP 眼中来看还是串行的。就是将多个 HTTP 请求响应拆成小片,然后在单个 TCP 连接中交替(也不一定是交替)传输。所以仍然存在“线头阻塞”,只不过是 TCP 层面的线头阻塞。
HTTP/3 将传输层协议给替换掉了,改成了基于 UDP 的 QUIC ,由于 UDP 属于无状态的,所以传输层的包也是同时发送了,这就消除了传输层的线头阻塞。

回到你这个链接里讨论的不支持 HTTP/2 ,总结一下原因:
1 ,支持 HTTP/2 作为 upstream ,那么所有连接复用同一个 TCP ,会受到 TCP 线头阻塞和拥塞控制问题的影响,对 nginx 来说,会使得事情变得复杂
2 ,用单个连接传输,几乎消除了连接数的限制,但是实际上 nginx 本身也没有主动做这样的限制(除非你自己指定 max_conns ),所以这个没啥意义
3 ,(感觉也是最主要的一点)实现 HTTP/2 进行单路复用,需要对 upstream 模块做重大修改,风险比较高
If you still think that talking to backends via HTTP/2 is something needed - feel free to provide patches.(翻译:老子嫌麻烦不想做,你要觉得有必要,你做好了把 patch 提交上来)

所以,风险大于收益,他们觉得没有实现的必要。
这也是 2015 、2016 年的帖子了,那时候 HTTP/2 才刚起步。
现在 HTTP/3 的时代快来了,云厂商都在部署 h3 了,不知道是不是 nginx 的时代已经快过去了?(瞎说的)

在Go语言中处理HTTP/1.1长连接与并发请求时,有几点关键概念和技术需要注意:

HTTP/1.1默认支持长连接(也称为持久连接),即TCP连接在发送一个请求并收到响应后不会被立即关闭,而是会保持一段时间,以便客户端可以发送更多的请求。这减少了建立和关闭TCP连接的开销,提高了性能。

Go语言的net/http包对HTTP/1.1长连接提供了良好的支持。在客户端,你可以通过创建一个http.Client实例并设置其Transport字段来配置连接池、超时等参数,从而控制长连接的行为。

对于并发请求,Go语言具有天然的优势。你可以使用goroutine来并发地发起HTTP请求,并利用channel或其他同步机制来收集和处理这些请求的响应。由于goroutine非常轻量,你可以轻松地创建成千上万个goroutine来并发处理请求。

然而,需要注意的是,虽然HTTP/1.1长连接和并发请求可以提高性能,但也可能带来一些问题,如连接泄露、服务器过载等。因此,在实际应用中,你需要根据具体情况来配置连接池的大小、请求的并发数等参数,以确保系统的稳定性和性能。

总的来说,Go语言提供了强大的工具来处理HTTP/1.1长连接和并发请求,但开发者需要合理地配置和使用这些工具,才能充分发挥Go语言的性能优势。

回到顶部