http1.1 长连接与 golang 并发请求的疑问

2022-02-13 22:00:14 +08:00
 daoqiongsi1101

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

问题

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

谢谢。

2878 次点击
所在节点    Go 编程语言
17 条回复
sagaxu
2022-02-13 22:09:50 +08:00
10 个请求并发就是 10 个连接
daoqiongsi1101
2022-02-13 22:10:15 +08:00
@sagaxu 跟浏览器的机制不一样么
sagaxu
2022-02-13 22:14:54 +08:00
@daoqiongsi1101 浏览器只服务一个用户,同一个域名做个排队,对体验影响不大。服务端不一样,100 个请求也许是 100 个不同用户发起的,如果排队,后面的用户只能超时了。

服务端也有连接池大小限制,一般几百到几千,超出大小了,要么排队等,要么临时创建不受限制的连接但用完就关。
qwq11
2022-02-13 22:29:17 +08:00
10 个 goroutine 就是同时(分时复用)调用 10 次创建 socket ,自然就是会是 10 个连接,分别占用 10 个端口。

浏览器的实现是为了性能考虑,如果加载 100 个资源,从并行角度来讲,那当然是开 100 个连接好。但是如果我开了 10 个窗口,就要有 1000 个连接,每次连接都做一次 TCP TLS handshake ,就太非资源了
daoqiongsi1101
2022-02-13 22:43:06 +08:00
@sagaxu 这个程序是本地运行的,go run main.go 启动,用 net/http 包发请求,默认配置也有连接池,既然有连接池,那应该会复用连接
jinliming2
2022-02-13 23:16:32 +08:00
等下,HTTP/1.1 的请求,并行不是复用同一个 TCP 啊?串行才是复用同一个 TCP 的啊!队头阻塞也是复用同一个 TCP 连接的串行 HTTP ,后面的请求要等待前面的请求响应啊。
Chrome 并发连接数限制是指创建的 TCP 的连接数啊,并且仅限 HTTP/1 。

HTTP/1 还没有分帧并发的技术,所有数据都是串行发送的,不存在你说的“并发请求、顺序响应”。要实现并发,就得建立多条 TCP ,在多个 TCP 连接上同时请求。
HTTP/1 的 keep-alive 是指一轮 请求-响应 结束后,不关闭 TCP 连接,可以直接在当前 TCP 连接上进行新的 HTTP 请求-响应。
Aoang
2022-02-13 23:22:49 +08:00
具体的情况得结合源码及你具体的代码才能知道。

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

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

对于调用非常频繁的服务,即使连接复用也还是可能出现问题。对于这种还是用 rpc 比较好,http 不适合。
yin1999
2022-02-14 00:13:07 +08:00
http.Client 默认使用的 transport 默认会复用 tcp 连接,但需要在每次请求后 io.ReadAll 一下 response.Body 并 close ,这样能够保证连接复用。当然,这个是串行复用的,就是在一次请求结束后,不立刻关闭 tcp 连接,在后面连接有效且有相应请求时,不再需要重新建立 tcp 连接。
daoqiongsi1101
2022-02-14 00:19:37 +08:00
@jinliming2 没错没错,这 6 个是 6 个不同的 tcp 连接,后面串行的才是复用前面的连接。
wangyu17455
2022-02-14 00:38:46 +08:00
浏览器的复用指的是:假如从同一个域名下加载 100 个资源,如果用的是 http1.1 ,同一时刻只会存在 6 个到这个域名的 tcp 连接,最初的 6 个资源请求完毕空闲出的连接才会被复用进行下一次请求。
jinliming2
2022-02-14 01:49:14 +08:00
@daoqiongsi1101 所以啊,你只要想一想,你 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 )来关闭连接。
rrfeng
2022-02-14 08:41:09 +08:00
问题之前:浏览器的描述不对,你并不知道浏览器建立了几个链接。最多 6 个是连接池的限制。

问题本身:net/http 默认 client 有连接池的,所以你也并不知道用了几个。除非每个 goroutine 里自己创建了独立的 client 。
NeoZephyr
2022-02-14 11:08:18 +08:00
所以说,有些时候 http1.1 是不是会比 http2.0 要好。
daoqiongsi1101
2022-02-14 14:10:17 +08:00
@NeoZephyr Nginx upstream 不支持 http2 ,就是说某些情况下 http1.1 更好,但没想到具体说明场景。
@jinliming2 请问你如何理解这个呢? https://trac.nginx.org/nginx/ticket/923
daoqiongsi1101
2022-02-14 14:31:31 +08:00
@jinliming2 很详尽,感谢!
shyling
2022-02-14 21:03:37 +08:00
你写代码请求时跟浏览器关系不大。。。

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

直接新连接那就是系统的限制了
jinliming2
2022-02-14 21:30:53 +08:00
@daoqiongsi1101 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 的时代已经快过去了?(瞎说的)

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/833608

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX