欢迎光临
我们一直在努力

同样 50 次请求,连接数从 50 到 1

在这里插入图片描述

文章目录

    • 一、连接数是对端先说的算
    • 二、客户端的三层,层层都能省
    • 三、还有一个隐藏层:代理
    • 四、怎么用
    • 五、30 秒带走版

上一篇讲 urllib3 的 Retry,有位读者问:requests.Session 和直接 requests.get 到底差在哪?当时我说「差在连接复用」,答应补一篇实测。这篇就是。

不绕圈子,先给数字:**同样向同一个对端发 50 次 GET 请求,对端 accept 到的 TCP 连接数,最差 50 条,最好 1 条。差 50 倍。**而决定这个数字的,一半在客户端,另一半在对端——这是这篇最想讲清的事。

一、连接数是对端先说的算

测试方法:起一个本地程序,在 accept 层计数(数的是 TCP 连接建立次数,不是请求数),然后用不同姿势打 50 次请求。

第一组结果就很反直觉:

客户端姿势对端 accept 到的连接数
HTTP/1.0 对端 + 池化客户端 50
HTTP/1.1 对端 + 同一个 PoolManager 1
HTTP/1.1 对端 + Session 1
HTTP/1.1 对端 + 无 Session 50

在这里插入图片描述

注意第一行:客户端这边什么都没做错——PoolManager 是标准池化姿势,但它对面的对端是 HTTP/1.0,每次响应完就关连接。客户端把连接放回池里,下一次取出来发现是条死连接,只能重新握手。50 次请求,50 次握手,池化努力全部归零。

而第二行:同样的客户端,对端换成 HTTP/1.1 且响应带 Content-Length——50 次请求,对端只 accept 到 1 条连接。剩下 49 次全在这条连接上复用。

这就是连接复用的完整真相:**它是双边协议,不是单方设置。**对端不保活,客户端的池子就是个每次必换的储物柜。

二、客户端的三层,层层都能省

把客户端的姿势拆开,是三层,每层管一段:

在这里插入图片描述

第一层,对端保活。 HTTP/1.1 + Content-Length(或 chunked)才具备保活条件。HTTP/1.0、或者每次响应都带关闭连接的头部,客户端再怎么池化都白费。用 Python 自带 http.server 的同学注意:默认协议版本是 HTTP/1.0,很多人拿它当压测靶机,测出来的「客户端性能」其实是对端先关的连接——我这次就先在它上面栽了一次(见文末复盘)。

第二层,客户端池化。 一个 PoolManager(或 requests.Session)走完全部请求。反过来,每次请求新建一个 PoolManager:20 次请求就是 20 条连接——连接建了用一次就扔。requests.get() 本质上就是后者:它内部每次新建 Session。

第三层,容量配置。 PoolManager(maxsize=N) 卡并发连接上限,block=True/False 决定超额时的行为。这层的实测对照最有意思:

在这里插入图片描述

4 个并发慢请求(每个对端 sleep 0.25 秒)+ maxsize=2:

  • block=True:只用 2 条连接,但 4 个请求全部完成要 0.53 秒——后 2 个请求在池边等了 0.27 秒,等前面的连接腾出来复用;
  • block=False:建了 4 条连接,全部并行,0.26 秒完成。

一个用延迟换连接数,一个用连接数换延迟。没有对错,取决于你在哪个资源上更紧张:容器里文件描述数有限、对端有连接数限制,选 block=True;追求吞吐且对端扛得住,选 block=False。默认是 block=False——所以默认行为是「宁可多建连接」。

三、还有一个隐藏层:代理

这次实测还顺手验证了上一篇的坑 4。本机环境变量里有 http_proxy,requests.get(不设 trust_env=False)打本地程序,50 次请求 50 条连接,而且最慢——请求全被送去代理转发,代理每跳一次建一条连接,本地对端的池化对象根本不在链路上。

想复现连接池收益,两条路:urllib3.PoolManager 直连(默认不读环境变量),或者 requests.Session(trust_env=False)。后者实测 50 次请求 1 条连接,50/50 成功。

四、怎么用

import requests

s = requests.Session()
s.trust_env = False # 内网/本地调用必须关,否则走代理(1018① 坑 4)
s.headers.update({"Connection": "keep-alive"})

# 整个程序复用一个 Session;连接池默认 maxsize=10,按 host 分池
for url in urls:
r = s.get(url, timeout=(3.05, 10))

三个默认值值得知道:HTTPAdapter 默认 pool_connections=10(按 host 的池子数)、pool_maxsize=10(每个池的连接上限)、pool_block=False(超额新建)。打同一个 host 的并发超过 10 时,第 11 个起就在静默新建连接——想卡住就显式设 pool_maxsize 并把 pool_block 设 True。

五、30 秒带走版

  • 连接复用是双边协议:对端 HTTP/1.0 或每次 close,客户端池化再努力也归零(50 请求 50 连接);
  • 对端 HTTP/1.1 + Content-Length,同一个 PoolManager:50 请求只建 1 条连接;
  • 每次新建 PoolManager = 每次重新握手,20 请求 20 连接;
  • maxsize 卡并发连接上限;block=True 等待复用(0.53s/2 连接)、block=False 超额新建(0.26s/4 连接);
  • pool_block 默认 False——默认宁可多建连接;
  • http_proxy 环境变量会让 Session 池化对象整个绕路(50 连接且最慢),内网调用 trust_env=False。

  • 在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 同样 50 次请求,连接数从 50 到 1
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址