【aiohttp】 使用说明
更可控”到底控制什么?(LLM 调用里最关键的 6 件事)
不管用哪个库,真正决定可控性的通常是你有没有把这些做好:
-
超时策略:connect / read(流) / total 分开(流式一般 total 不宜太小)
-
重试策略:只对“可重试”的错误重试(网络错误、429、部分 5xx),并且指数退避+抖动
-
并发与连接池:每个 host 的并发上限、全局并发上限、keep-alive
-
限流与排队:令牌桶/漏桶;避免瞬时把 429 打爆
-
流式处理背压:消费者慢时别把内存堆爆(逐块处理、超时与断线恢复)
-
观测与脱敏:记录耗时拆分、状态码分布、token 用量;日志别泄露 prompt/PII
aiohttp 的“可控”主要体现在:你能把 连接生命周期、并发/连接池、超时、TLS/代理、流式读取、重试与观测 这些网络层细节都掌握在自己手里,而不是交给 SDK 的黑盒。
下面按“可控点 → 具体怎么做(代码)”给你一套比较生产化的写法(async 场景)。
1) 连接池与并发控制(TCPConnector + Semaphore)
可控点
- 全局并发、单 host 并发
- keep-alive 行为与空闲连接回收
- DNS 缓存、IPv4/IPv6、强制关闭连接等
具体实现
import asyncio
import aiohttp
# 全局并发(业务层):避免把上游/本机打爆
SEM = asyncio.Semaphore(200)
connector = aiohttp.TCPConnector(
limit=500, # 总连接上限(所有host)
limit_per_host=100, # 单host连接上限
keepalive_timeout=30, # 空闲连接保活
ttl_dns_cache=300, # DNS缓存TTL
enable_cleanup_closed=True # 清理关闭的ssl连接(某些环境有用)
)
timeout = aiohttp.ClientTimeout(
total=60,
connect=10,
sock_connect=10,
sock_read=30,
)
session = aiohttp.ClientSession(connector=connector, timeout=timeout)
要点
Semaphore控制的是“业务并发”(包括排队),TCPConnector.limit*控制的是“连接并发”- LLM 调用常见瓶颈是 429/限流,不做并发治理很容易雪崩
2) 超时细分(ClientTimeout)
可控点
connect:包含 DNS/TCP/SSL 等“建连链路”sock_read:流式时尤其关键(多久没新数据就断)total:整体上限(流式通常别设太死)
具体实现(流式 vs 非流式两套)
# 非流式:总时长通常可控
timeout_json = aiohttp.ClientTimeout(total=30, connect=10, sock_read=20)
# 流式:total 可能很长甚至不可预期,重点控 sock_read
timeout_stream = aiohttp.ClientTimeout(total=None, connect=10, sock_read=30)
3) 重试策略(自己写:更透明、更可控)
aiohttp 本身不强制给你重试语义(这也是可控的一部分:你决定“什么该重试、重试几次、怎么退避”)。
可控点
- 只重试“幂等/可安全重试”的调用,或你明确允许的场景
- 区分:网络异常、429、5xx、超时
- 指数退避 + jitter,最大重试窗口
- 读取
Retry-After(如果上游提供)
具体实现(可直接复用)
import asyncio
import random
from typing import Optional
RETRY_STATUS = {429, 500, 502, 503, 504}
def _parse_retry_after(headers) -> Optional[float]:
ra = headers.get("Retry-After")
if not ra:
return None
try:
return float(ra)
except ValueError:
return None
async def request_with_retry(session: aiohttp.ClientSession, method: str, url: str, **kwargs):
max_attempts = 5
base = 0.5 # 初始退避
cap = 8.0 # 单次睡眠上限
last_exc = None
for attempt in range(1, max_attempts + 1):
try:
async with session.request(method, url, **kwargs) as resp:
if resp.status in RETRY_STATUS:
ra = _parse_retry_after(resp.headers)
# 读取一点响应体用于日志(避免占用过多内存)
body_preview = (await resp.content.read(512)).decode("utf-8", "ignore")
if attempt == max_attempts:
raise aiohttp.ClientResponseError(
request_info=resp.request_info,
history=resp.history,
status=resp.status,
message=f"retryable status, body_preview={body_preview!r}",
headers=resp.headers,
)
# 退避:优先 Retry-After,否则指数退避+抖动
sleep_s = ra if ra is not None else min(cap, base * (2 ** (attempt - 1)))
sleep_s *= random.uniform(0.8, 1.2)
await asyncio.sleep(sleep_s)
continue
# 正常返回:你可以在这里统一做 json / text
return resp
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
last_exc = e
if attempt == max_attempts:
raise
sleep_s = min(cap, base * (2 ** (attempt - 1))) * random.uniform(0.8, 1.2)
await asyncio.sleep(sleep_s)
raise last_exc # 理论不会到
生产里还会加:预算控制(总重试耗时不能超过某个上限)、以及对 POST 是否允许重试 的严格约束。
4) 代理、TLS 与证书(企业环境最常见)
可控点
- HTTP/HTTPS 代理、透明网关
- 自定义 CA、客户端证书(mTLS)、禁用不安全 TLS
- SNI / hostname 校验策略(一般别关)
具体实现
import ssl
import aiohttp
ssl_ctx = ssl.create_default_context(cafile="/etc/ssl/certs/your_ca.pem")
# mTLS(如果需要)
# ssl_ctx.load_cert_chain(certfile="client.crt", keyfile="client.key")
connector = aiohttp.TCPConnector(ssl=ssl_ctx)
async with aiohttp.ClientSession(connector=connector) as session:
async with session.post(
"https://api.example.com/v1/chat",
proxy="http://proxy.corp:8080", # 或 https proxy
json={"foo": "bar"},
) as resp:
data = await resp.json()
也可以用环境变量自动代理(trust_env=True):
aiohttp.ClientSession(trust_env=True)
5) 流式响应(SSE / chunked)与背压控制
LLM 的 stream 常见是 SSE(data: ...\n\n)。aiohttp 让你控制“读多大、多久读一次、读到哪就停、消费者慢怎么办”。
可控点
- 每次读取 chunk 的大小(间接影响延迟/CPU)
sock_read控制“多久没新 token 断开”- 消费者慢:用队列/背压,不把内存堆爆
- 断线重连:从你自己的状态恢复(例如 last_event_id / 已收 token)
一个简化的 SSE 读取示例
async def iter_sse_lines(resp: aiohttp.ClientResponse):
# 按行读,SSE 一般以 \n\n 分隔事件
buffer = b""
async for chunk in resp.content.iter_chunked(1024):
buffer += chunk
while b"\n" in buffer:
line, buffer = buffer.split(b"\n", 1)
yield line.decode("utf-8", "ignore")
async def call_stream(session, url, payload):
async with session.post(url, json=payload) as resp:
resp.raise_for_status()
async for line in iter_sse_lines(resp):
if not line:
continue
if line.startswith("data:"):
data = line[5:].strip()
if data == "[DONE]":
break
# 在这里处理 token 增量
yield data
6) 可观测性(TraceConfig:DNS/TCP/TTFB/总耗时)
这是 aiohttp 很“可控”的一个点:它提供 TraceConfig 钩子,你可以打点:
- 请求开始/结束
- DNS 解析开始/结束
- 连接创建、连接复用
- 请求异常
具体实现(示例:打印耗时与连接复用)
import time
import aiohttp
trace = aiohttp.TraceConfig()
@trace.on_request_start.append
async def on_request_start(session, context, params):
context.start = time.perf_counter()
@trace.on_connection_reuseconn.append
async def on_reuse(session, context, params):
context.reused = True
@trace.on_request_end.append
async def on_request_end(session, context, params):
cost = (time.perf_counter() - context.start) * 1000
reused = getattr(context, "reused", False)
print(f"{params.method} {params.url} cost_ms={cost:.1f} reused={reused}")
session = aiohttp.ClientSession(trace_configs=[trace])
生产里通常把这些打到 metrics(Prometheus / OpenTelemetry)。
7) 你最终会怎么“组织代码”(推荐结构)
为了可控 + 可维护,建议做一个薄封装(不要把业务代码散落着 session.post()):
LLMHttpClient:管理ClientSession(连接池、timeout、trace)request_with_retry():统一重试、错误映射、日志脱敏call_json()/call_stream():分别处理非流式/流式RateLimiter(可选):全局限流,避免 429 风暴
一句话总结
aiohttp 的可控性主要来自:你可以精细配置连接池/超时/SSL/代理,并且能“手写并统一”重试、流式读取与观测;实现上就是围绕 TCPConnector、ClientTimeout、TraceConfig、以及你自己的 retry/stream 逻辑来搭一个稳定的 client。
如果你告诉我你是 SSE 流式 还是 一次性 JSON、以及大概 并发/QPS,我可以把上面代码整理成一个可直接拷贝的 LLMHttpClient 类(带限流、重试、trace、stream)。

浙公网安备 33010602011771号