为什么你的 HTTP 请求比别人慢 3 倍?Session 复用、连接池、Keep-Alive 一次讲透
同一个接口,同事的脚本 1 秒跑完 20 个请求,你跑了 3 秒多。代码看起来差不多,差在哪?
大概率就一件事:他用 Session,你直接 requests.get()。
这一篇把连接复用这件事讲透,看完你就能判断自己的代码该不该改。
一、每次 requests.get() 背后发生了什么
很多人以为 requests.get(url) 就是"发一个请求"。其实它做了一整套动作:
关键在第 5 步:连接用完就关了。
下一次 requests.get(),上面 1~5 再来一遍。20 个请求 = 20 次 TCP + TLS 握手。
对同一个域名、同一个服务的 20 个请求,这些握手全是重复劳动。
握手到底有多贵?跨公网一次 TCP 三次握手约 1 个 RTT,TLS 1.3 握手再加 1 个 RTT。假设 RTT 是 100ms,那么一个请求光建连就花了 200ms,还没算真正发数据的时间。
20 个请求,就是 4 秒白白花在握手和建连上。
二、Session 做了什么
requests.Session() 的核心价值就一句话:把连接留住,下次接着用。
具体来说:
- 连接复用:底层维护一个连接池,请求完不关连接,放回池里。下次同域名的请求直接拿现成的。
- Cookie 持久化:Session 自动保存响应里的 Cookie,下次请求自动带上。
- 默认头复用:你给 Session 设的 headers,之后每个请求都带上,不用重复写。
用起来也简单:
import requests
# 不用 Session:每次新建连接
for i in range(20):
requests.get(f"https://example.com/api/{i}")
# 用 Session:连接复用
with requests.Session() as s:
for i in range(20):
s.get(f"https://example.com/api/{i}")
代码只差一点,性能差一大截。
再补一个容易忽略的点:Cookie 的连续性。不用 Session 的话,每次 requests.get() 都是独立上下文,上一步响应 Set-Cookie 下发的 Cookie,不会自动带到下一步——登录、分页、校验类接口全得自己手动维护。Session 帮你把这条链串起来了。
三、实测对比
我这边拿同一个服务测了 20 个请求(模拟 30ms 网络延迟):
| 裸 requests.get() | ~3.1s | 每次重连 + 重握手 |
| Session 复用 | ~0.9s | 只在第一次建连 |
3 倍多的差距,全在握手上。
如果服务在内网、延迟极低,差距会小一些;但只要是跨公网 HTTPS,Session 的收益都很明显。
差距为什么这么大?因为 Session 把 20 次握手压缩成了 1 次。第 2 到第 20 个请求都在同一条已经建好的连接上跑,完整握手一次都不用再做了。
顺带提一句,TLS 会话复用(session ticket)也能减少重复握手。但它和连接复用是两回事——前者省掉的是握手动作,后者省掉的是整条连接。两者叠在一起,效果才最好。
怎么自己验证连接复用了
想知道自己的代码到底有没有复用连接,跑个小实验:
import time
import requests
s = requests.Session()
for i in range(3):
t = time.perf_counter()
s.get("https://example.com/")
print(f"第 {i+1} 次: {(time.perf_counter()–t)*1000:.0f} ms")
第一次会明显慢(包含了建连和握手),第二次、第三次快一大截——说明连接确实复用了。如果三次耗时差不多,那就是没复用,多半是服务端返回了 Connection: close。
四、连接池怎么配
默认情况下,Session 底层的连接池大小是 10。并发一高就不够用:
import requests
from requests.adapters import HTTPAdapter
s = requests.Session()
adapter = HTTPAdapter(
pool_connections=20, # 连接池数量
pool_maxsize=50, # 单个池最大连接数
)
s.mount("https://", adapter)
s.mount("http://", adapter)
两个参数的区别:
- pool_connections:缓存多少个不同主机的连接池
- pool_maxsize:每个主机池最多保持多少条连接
单域名高频请求,重点看 pool_maxsize,调到接近你的并发数;多域名场景才需要关注 pool_connections。
调太大也不好:连接占着不放,服务端和本机都可能被拖累。
五、Keep-Alive 的坑
连接复用依赖 HTTP 的 Keep-Alive 机制。但有两个坑,我都踩过。
坑 1:服务端可能主动关连接
服务端会设一个 Keep-Alive: timeout=xx。如果两次请求间隔超过这个时间,连接被服务端关了,你下次用的时候会拿到一个"死连接",直接报 ConnectionResetError。
生产环境常见的解法是配重试:
from urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter
retry = Retry(total=3, backoff_factor=0.5,
status_forcelist=[500, 502, 503, 504])
adapter = HTTPAdapter(max_retries=retry,
pool_connections=20, pool_maxsize=50)
我一开始没想到是连接被服务端关了,查了半天以为是网络抖动,其实是 Keep-Alive 超时。
坑 2:响应没读完,连接没法复用
如果一个请求的响应体你没读完(比如只读了 resp.headers 就不管了),底层连接没法干净地放回池里,复用就失效了。
用 stream=True 的时候尤其要注意,必须显式 resp.close() 或者把 resp.content 读完。
坑 3:Session 忘了关,连接泄漏
Session 用完记得 close(),或者直接上 with。否则在常驻服务里,池子里的连接可能长期占着不放,最后被耗尽。
# 推荐:用 with,自动关闭
with requests.Session() as s:
...
# 或者显式关闭
s = requests.Session()
try:
...
finally:
s.close()
单次脚本里忘了关一般没事(进程退出就释放了),但常驻服务里这是隐患。
六、什么时候 Session 反而没必要
- 单次请求,用完就退出的脚本
- 每次请求目标域名都不同(比如一次性遍历 1000 个不同站点),连接复用无从谈起
- 明确需要每次全新上下文(比如要隔离 Cookie)
这三种场景,用不用 Session 差别不大,别为了"规范"硬套。
还有一种情况是短生命周期的任务:比如一个定时任务每次只发 1 个请求就退出,建 Session 的开销和收益基本抵消,怎么写都行。优化的前提是"同一个连接会被重复用",用不上就别纠结。
七、Session 不等于并发
有个常见误解:用了 Session 就会快。其实 Session 只解决"连接复用",不解决"同时发多个请求"。
上面所有例子都是串行发的——一个请求回来才发下一个。要真正提速,还得加上并发:
from concurrent.futures import ThreadPoolExecutor
import requests
def fetch(i: int) –> int:
with requests.Session() as s: # 每个线程一个 Session
return s.get(f"https://example.com/api/{i}").status_code
with ThreadPoolExecutor(max_workers=10) as pool:
results = list(pool.map(fetch, range(100)))
几个要点:
- 每个线程用自己的 Session,Session 本身不是线程安全的
- max_workers 别开太大,50~100 已经能测出服务端的承受极限了
- 想更高并发、更省资源,用 aiohttp 或 httpx 的异步模式
连接复用 + 并发,才是完整的优化组合。
八、三个常见误区
误区一:每次请求都新建 Session。 最常见的错误——把 requests.Session() 写进循环里,每个请求一个 Session,等于没复用连接。Session 要建一次、用多次。
误区二:以为 Session 能解决所有慢。 DNS 慢、服务端处理慢、带宽小,这些 Session 都救不了。它只优化"建连"这一段。
误区三:把 Session 当登录态保险箱。 Session 会自动持久化 Cookie,但如果服务端是靠请求头里的 token 鉴权,Session 不会自动带,还得手动设。
九、小结
把今天的内容收一下:
| 同域名多次请求慢 | 用 Session 复用连接 |
| 并发不够 | 连接池 + 线程池 / 协程 |
| 报 ConnectionResetError | 配 Retry 重试 |
| 常驻服务连接泄漏 | 用 with 或显式 close |
核心就一句:别让每次请求都从零开始建连接。 这一条改对,性能立刻上一个台阶。
声明:本文为原创技术分享,涉及的采集思路仅供学习与合法用途参考。请遵守目标网站的 robots 协议及相关法律法规,勿将技术用于任何违规场景。





