欢迎光临
我们一直在努力

为什么你的 HTTP 请求比别人慢 3 倍?Session 复用、连接池、Keep-Alive 一次讲透

为什么你的 HTTP 请求比别人慢 3 倍?Session 复用、连接池、Keep-Alive 一次讲透

同一个接口,同事的脚本 1 秒跑完 20 个请求,你跑了 3 秒多。代码看起来差不多,差在哪?

大概率就一件事:他用 Session,你直接 requests.get()。

这一篇把连接复用这件事讲透,看完你就能判断自己的代码该不该改。


一、每次 requests.get() 背后发生了什么

很多人以为 requests.get(url) 就是"发一个请求"。其实它做了一整套动作:

  • 建一个 TCP 连接(三次握手)
  • 如果是 HTTPS,还要 TLS 握手(TLS 1.3 至少 1 个 RTT,TLS 1.2 要 2 个)
  • 发 HTTP 请求
  • 收响应
  • 关闭连接
  • 关键在第 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 协议及相关法律法规,勿将技术用于任何违规场景。

    赞(0)
    未经允许不得转载:171主机测评 » 为什么你的 HTTP 请求比别人慢 3 倍?Session 复用、连接池、Keep-Alive 一次讲透
    分享到: 更多 (0)

    评论 抢沙发

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