aiohttp 请求生命周期

下面是你要的图 —— aiohttp 请求生命周期 + 各 timeout 对应阶段

我用流程图形式标出来每个超时参数作用的位置。


🧠 aiohttp 请求生命周期图

┌───────────────────────────────────────────────┐
│                 total 超时范围                │
│      (整个请求从开始到结束的最大时间)       │
└───────────────────────────────────────────────┘
        │
        ▼
┌──────────────────┐
│  DNS 解析阶段     │
└──────────────────┘
        │
        │  ← connect 超时包含
        ▼
┌──────────────────┐
│  TCP 建立连接     │
└──────────────────┘
        │
        │  ← sock_connect 超时只管这一段
        │
        │  ← connect 超时也包含这一段
        ▼
┌──────────────────┐
│  SSL 握手 (HTTPS) │
└──────────────────┘
        │
        │  ← connect 超时包含
        ▼
┌──────────────────┐
│  发送 HTTP 请求   │
└──────────────────┘
        │
        ▼
┌──────────────────┐
│  等待服务器响应   │
│  (TTFB)          │
└──────────────────┘
        │
        ▼
┌────────────────────────────────┐
│  读取响应数据(可能是流式)     │
│                                │
│  每次读取数据块之间的间隔        │
│  受 sock_read 控制              │
└────────────────────────────────┘
        │
        ▼
┌──────────────────┐
│  请求完成         │
└──────────────────┘

🔎 每个 timeout 的作用总结

🟢 total

包住整个流程:

DNS → TCP → SSL → 发送 → 等待 → 读取

只要整体超过这个时间就超时。


🟢 connect

控制:

DNS + TCP + SSL

如果在“建连阶段”卡住,就触发。


🟢 sock_connect

只控制:

TCP 三次握手阶段

一般用于更精细控制。


🟢 sock_read(流式最重要)

控制:

两次数据块读取之间的最大间隔

⚠️ 不是总下载时间
⚠️ 不是整个响应时间

而是:

“服务器多久没发新数据就断”

对于 LLM streaming 非常关键。


🔥 再给你一个 LLM Streaming 场景例子

假设:

ClientTimeout(
    total=None,
    connect=10,
    sock_read=30
)

发生情况:

场景 会不会超时
服务器 5 秒内连上 ✅ 正常
建连 15 秒 ❌ connect 超时
正常开始流式输出
输出一半,40 秒没 token ❌ sock_read 超时
一直有 token 但总共 5 分钟 ✅ 不超时(因为 total=None)

🎯 一句话理解图

connect = 建连阶段
sock_connect = 纯TCP阶段
sock_read = 数据读取阶段
total = 整个生命周期

如果你愿意,我还能再给你画一张:

👉 「连接池(TCPConnector)内部结构图」
👉 或 「流式读取时的时间轴示意图」

那两张图会让你彻底搞懂 aiohttp 在高并发 LLM 场景下是怎么运作的。

好,这张是你要的——

🧠 LLM 流式读取(Streaming)时间轴示意图

我们用“时间向右流动”的方式来看 sock_read 是怎么工作的。


✅ 正常流式情况(不会超时)

假设:

ClientTimeout(
    total=None,
    connect=10,
    sock_read=30
)

时间轴:

时间  ───────────────────────────────────────────────▶

连接阶段
│
├── DNS/TCP/SSL (2s)
│
└── 建连完成

服务器开始返回流式数据(SSE)

Token1      Token2      Token3      Token4      Token5
  ↓           ↓           ↓           ↓           ↓
  |----5s-----|----3s-----|----8s-----|----6s-----|

          每两个 token 之间都 < 30 秒

✅ 不会触发超时

因为:

sock_read = “两次数据块之间最大允许间隔”

只要持续有数据,就不会断。


❌ 触发 sock_read 超时的情况

时间  ───────────────────────────────────────────────▶

Token1        Token2
  ↓             ↓
  |-----4s------|

        ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

        40 秒没有任何数据返回

        ↑
        sock_read = 30 秒

结果:

asyncio.TimeoutError

原因:

超过 30 秒没有收到新的数据块

注意:

  • 即使之前已经收到很多 token
  • 只要“间隔太久”
  • 就会断开

🔥 对比:如果设置 total=60

total = 60
sock_read = 30

情况 1:持续有 token,但总耗时 70 秒

Token持续不断
但总时间到 60 秒

❌ 会触发 total 超时


情况 2:total=None

Token 持续 5 分钟
只要每 30 秒内有新 token

✅ 永远不会因为时间总长而断

这才是大模型 streaming 推荐设置。


🎯 更直观理解:计时器是怎么“重置”的?

sock_read 的计时方式:

收到一个 chunk
    ↓
启动 30 秒倒计时
    ↓
如果 30 秒内收到新 chunk
    ↓
重置计时器
    ↓
继续

所以它不是“累计时间”,而是“空闲时间”。


🧩 再给你一个“真实 LLM 卡死场景”

有些模型:

  • 生成到一半
  • 上游卡住
  • 连接没断
  • 但不再发送 token

如果你没有设置 sock_read

程序会一直挂住

如果你设置:

sock_read=30
30 秒后自动断开

这就是生产环境必须控制的点。


🏗 总结这张图核心

sock_read = 控制“空闲时间”
total     = 控制“总时间”
connect   = 控制“建连时间”

如果你愿意,我可以再画一张:

👉 「流式 + 重试 + 指数退避」完整时间轴图
👉 或 「高并发下连接池复用时间线图」

那两张会更偏生产级架构理解。

好,这张是你要的——

🏗 高并发下连接池复用时间线图(aiohttp)

我们来看:

  • 多个协程并发请求
  • TCPConnector 如何分配连接
  • keep-alive 如何复用
  • limit / limit_per_host 如何生效

🧠 场景设定

TCPConnector(
    limit=4,
    limit_per_host=2,
    keepalive_timeout=10
)

并发 6 个请求:

Task A
Task B
Task C
Task D
Task E
Task F

目标:

https://api.llm.com

🕒 时间线图

时间向右 →

时间 ─────────────────────────────────────────────▶

第 1 阶段:前两个请求建立连接

Task A     ──TCP1───────使用中──────────────┐
Task B     ──TCP2───────使用中──────────────┘

连接池状态:
[ TCP1*, TCP2* ]
* 表示使用中

⚠️ 因为:

limit_per_host = 2

所以同一个 host 只能 2 个连接。


第 2 阶段:Task C / D / E / F 到来

Task C   等待连接
Task D   等待连接
Task E   等待连接
Task F   等待连接

此时:

连接池已满(2/2)

这些任务进入:

连接池等待队列


第 3 阶段:Task A 完成

Task A 完成
TCP1 进入空闲状态(keep-alive)

连接池:

[ TCP1 (空闲), TCP2* ]

此时:

Task C 立刻复用 TCP1

时间线:

Task C     ──复用TCP1───────────────

注意:

没有重新建 TCP

没有 DNS

没有 SSL

👉 直接发 HTTP 请求


第 4 阶段:Task B 完成

TCP2 变为空闲
Task D 复用 TCP2

连接池:

[ TCP1*, TCP2* ]

🔥 关键理解:连接复用发生在这里

        建立一次 TCP
              ↓
       多个请求复用
              ↓
    直到 keepalive_timeout 到期

⏳ keepalive_timeout 生效图

假设 10 秒内没有新请求:

TCP1  空闲 10 秒
TCP2  空闲 10 秒

时间线:

空闲 ─────────10秒─────────▶ 关闭连接

连接池变成:

[ ]

下次请求会重新建 TCP。


🚦 limit 和 limit_per_host 同时作用

假设我们再加一个 host:

api.llm.com
api.another.com

如果:

limit=4
limit_per_host=2

最大情况:

api.llm.com     2 连接
api.another.com 2 连接

超过 4 个总连接就会排队。


🧠 高并发真实效果总结

在 LLM 场景中:

1000 并发请求

但如果你设置:

limit_per_host=50

实际同时存在的 TCP 连接只有 50 个。

其他请求:

  • 要么排队
  • 要么复用已完成的连接

📊 把整个过程浓缩成一张逻辑图

请求进入
    ↓
是否有空闲连接?
    ↓         ↓
   有         没有
   ↓          ↓
复用TCP     是否达到limit?
                ↓        ↓
               否        是
               ↓         ↓
            建立新TCP    排队等待

🎯 生产级理解重点

1️⃣ limit 控制资源上限
2️⃣ limit_per_host 防止打爆单个上游
3️⃣ keepalive 减少 TCP/SSL 开销
4️⃣ 高并发时大多数请求在“排队 + 复用”


如果你愿意,我可以再画一张更底层的:

👉 「单个 TCP 连接内 HTTP 请求复用 vs 多 TCP 并发对比图」
👉 或 「连接池 + 重试 + 限流 组合时间线图(真实生产模型调用场景)」

那会更贴近真实大模型网关架构。

posted @ 2026-03-03 16:26  X1OO  阅读(1)  评论(0)    收藏  举报