Chat背景:为什么“Operation Timed Out”总在凌晨爆发
凌晨两点,监控群里突然告警:批量调用 ChatGPT 的链路超时率飙到 18 %。
日志里清一色requests.exceptions.ReadTimeout与502 Bad Gateway。
根因往往逃不出下面三类:
网络抖动:跨境链路 RT 从 180 ms 涨到 1.2 s,TLS 握手阶段就把 5 s 超时吃光。
请求膨胀:为了“让模型一次答完”,开发者把 8 k token 的上下文全塞进去,结果首包时间(TTFB)线性增长,触发云端 idle timeout。
并发配额:组织级账号默认 3 k RPM / 350 k TPM,一旦流量突增,边缘节点直接丢包,客户端侧只能看到“timeout”,而实际收到的是 429 或 503,被网关吞掉响应体。
技术方案对比:短轮询、长轮询还是指数退避?
| 策略 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 短轮询(固定间隔重试) | 低峰期、小并发 | 实现简单 | 易放大服务器压力,重试风暴 |
| 长轮询(阻塞到有响应) | 需要实时结果、长连接 | 减少空转 | 客户端连接池易被挂起 |
| 指数退避 + 全抖动(Full Jitter) | 高并发、跨地域 | 打散重试峰,对服务端友好 | 增加尾延迟 |
生产经验:
- 对 ChatGPT 这种“全局限速”服务,优先选“指数退避 + 全抖动”,退避上限 64 s,重试 6 次即可覆盖 99.5 % 偶发抖动。
- 若业务对尾延迟极度敏感(如客服坐席),可改用“断路器 + 长轮询”双模:正常走长连接,失败率超阈值 5 % 时自动降级到短轮询,30 s 后探测恢复。
核心实现:Python 重试装饰器(含 JWT 鉴权)
以下代码基于tenacity==8.2.0,同时兼容 Azure OpenAI 的 JWT 换取,关键处中英双语注释。
import os, time, jwt, requests, logging from datetime import datetime, timedelta from tenacity import retry, stop_after_attempt, wait_exponential_jitter # 生成 AAD JWT,用于 Azure OpenAI def _get_aad_token(audience: str) -> str: # 使用托管身份或 Service Principal 换取 token resp = requests.post( f"https://login.microsoftonline.com/{os.getenv('TENANT_ID')}/oauth2/v2.0/token", data={ "grant_type": "client_credentials", "client_id": os.getenv("CLIENT_ID"), "client_secret": os.getenv("CLIENT_SECRET"), "scope": f"{audience}/.default" }, timeout=5 ) resp.raise_for_status() return resp.json()["access_token"] # 统一超时参数,方便压测时调节 DEFAULT_TIMEOUT = (3.5, 10) # (connect, read) @retry( reraise=True, stop=stop_after_attempt(6), wait=wait_exponential_jitter(initial=1, max=64, jitter=True) ) def chat_completion(payload: dict) -> dict: """ 调用 OpenAI / ChatGPT completions 接口 支持 OpenAI 官方与 Azure 两种 endpoint """ is_azure = bool(os.getenv("AZURE_OPENAI_ENDPOINT")) if is_azure: token = _get_aad_token("https://cognitiveservices.azure.com") url = f"{os.getenv('AZURE_OPENAI_ENDPOINT')}/openai/deployments/{os.getenv('DEPLOYMENT')}/chat/completions?api-version=2023-05-15" headers = {"Authorization": f"Bearer {token}"} else: url = "https://api.openai.com/v1/chat/completions" headers = {"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}"} # 记录首包时间,方便排查慢查询 start = time.perf_counter() try: resp = requests.post(url, json=payload, headers=headers, timeout=DEFAULT_TIMEOUT) # 429/503 也抛异常,交给重试器处理 if resp.status_code in {429, 503, 502}: logging.warning("Hit rate limit or gateway error, will retry") resp.raise_for_status() resp.raise_for_status() return resp.json() finally: logging.info(f"TTFB={time.perf_counter()-start:.3f}s status={resp.status_code}")请求分块(Chunking)与负载测试伪代码
当输入 token 超过 4 k 时,即使模型支持 8 k+,也建议按“段落”切分,再并发拼接,降低单请求 hang 死概率。
def chunk_text(text: str, max_tokens: int = 1500) -> list[str]: """简易按双换行分段,可换成 tiktoken 精确计算""" paragraphs = text.split("\n\n") buf, chunks = [], [] for p in paragraphs: buf.append(p) if len(" ".join(buf)) > max_tokens: chunks.append(" ".join(buf[:-1])) buf = [p] if buf: chunks.append(" ".join(buf)) return chunks # 并发调用示例(伪代码) async def async_map_chat(chunks): tasks = [asyncio.create_task(chat_completion(chunk)) for chunk in chunks] return await asyncio.gather(*tasks, return_exceptions=True)负载测试:
使用locust -f locustfile.py --u 100 -r 10 -t 5m观察 P99 延迟,若 > 8 s 占比 > 2 %,则调低并发或继续细化 chunk。
性能考量:QPS、冷启动与连接池
QPS 与 TPM 双层限速
官方返回的x-ratelimit-limit-requests与x-ratelimit-limit-tokens需缓存到本地内存,令牌桶算法按 100 ms 粒度填充,否则极易“突刺”后超时。冷启动延迟
当部署在 Azure 且选择“按量付费”时,若 5 min 无调用,实例会被回收,首请求 RT 可能陡增 4–7 s。解法:- 使用“预置吞吐量”(PTU) 保底;
- 在连接池里加 30 s 一次的空转探活(keep-alive),携带
max_tokens=1的 dummy 请求。
连接池优化
requests默认池大小 10,高并发下立即耗尽。
推荐requests.adapters.HTTPAdapter(pool_maxsize=100, pool_connections=20),并打开HTTP/2(hyper) 减少 TLS 重复握手。
避坑指南:生产环境三大血泪教训
忽略 429 状态码
很多 SDK 只把 429 当“稍后再试”,却没回读Retry-After头,导致退避失效。务必在重试器里解析该字段并动态设置wait=retry_after。日志缺失 request_id
OpenAI 返回的x-request-id是官方排障唯一凭证。未落盘导致后续工单无法定位,被退回“请复现”。未配置连接读超时差异
把timeout=30一把梭,结果内网代理 5 s 就返回 504,客户端空等到 30 s 才抛异常,线程池被占满。正确姿势:connect / read 分离,connect 3.5 s,read 10 s,既给网络抖动留余地,也避免挂死。
互动思考
- 在分布式微服务架构中,如何设计一套基于 Redis+Lua 的限流方案,既支持滑动窗口又避免单点热点 key?
- 当指数退避遇上消息类长连接(WebSocket),如何权衡“重试尾延迟”与“消息顺序”冲突,保证业务幂等?
动手拓展:把“稳定调用”升级为“实时对话”
当你已经能把超时率压到 < 0.3 %,不妨再往前一步:让模型“开口说话”。
从0打造个人豆包实时通话AI 这个动手实验,用火山引擎豆包·语音系列模型,把 ASR→LLM→TTS 整条链路串成低延迟 Web 通话。
我本地跑通只花了 45 min,官方模板已帮你搞定回声消除、流式语音合成等脏活,小白也能顺利体验。
把上面沉淀的“超时治理”套进去,就能得到一个既稳又能“聊”的 AI 伙伴,或许下一个深夜告警的就是“用户聊得太嗨,RPM 又打满了”。