【aiohttp】 使用说明

更可控”到底控制什么?(LLM 调用里最关键的 6 件事)

不管用哪个库,真正决定可控性的通常是你有没有把这些做好:

  1. 超时策略:connect / read(流) / total 分开(流式一般 total 不宜太小)

  2. 重试策略:只对“可重试”的错误重试(网络错误、429、部分 5xx),并且指数退避+抖动

  3. 并发与连接池:每个 host 的并发上限、全局并发上限、keep-alive

  4. 限流与排队:令牌桶/漏桶;避免瞬时把 429 打爆

  5. 流式处理背压:消费者慢时别把内存堆爆(逐块处理、超时与断线恢复)

  6. 观测与脱敏:记录耗时拆分、状态码分布、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/代理,并且能“手写并统一”重试、流式读取与观测;实现上就是围绕 TCPConnectorClientTimeoutTraceConfig、以及你自己的 retry/stream 逻辑来搭一个稳定的 client。

如果你告诉我你是 SSE 流式 还是 一次性 JSON、以及大概 并发/QPS,我可以把上面代码整理成一个可直接拷贝的 LLMHttpClient 类(带限流、重试、trace、stream)。

posted @ 2026-03-03 14:17  X1OO  阅读(0)  评论(0)    收藏  举报