接口一上线就发现:单机能跑,并发一上来就超时、排队、雪崩;日志里只看到「慢」,看不出慢在哪一步。这篇不讲协议怎么拼、客户端怎么配,只讲工程落地:一个请求从进到出要设哪些闸、哪几个数必须量出来、出问题按什么顺序查。适合正在把 AI 接口接进自己服务的后端开发者,判断标准给全,条件不成立的地方也一并说明。
很多后端开发者把 AI 接口接进自己服务后,头一周都会遇到同一个画面:本地单请求跑得挺顺,一上并发就开始超时、排队,最后连健康检查都挂。日志里翻半天只有一个「慢」字,看不出慢在连接、慢在上游首字节、还是慢在自己这一层。
这篇不讲协议怎么拼、客户端怎么配,只讲工程落地:一个请求从进到出要设哪些闸、哪几个数必须先量出来、出问题按什么顺序查。适合正在把模型接口包进自己服务的后端同学。下面每条判断标准都会说明什么情况下不成立,别照抄。
先把基线量出来,再谈架构
没有任何基线数据的架构讨论都是空谈。你至少要把三个数分开量:单请求端到端耗时中位数、上游首字节延迟、你自己这一层的处理耗时。这三个数混在一个平均值里,后面所有优化全是猜。
- 端到端耗时中位数:从请求进来到响应出去,反映用户真实感受;均值会被少数长尾拉偏,优先看中位数和长尾分位
- 上游首字节延迟:决定你等的是谁;这个数大,说明瓶颈不在你,加机器没用
- 本层处理耗时:串行化、拼 prompt、写日志、查缓存都算在内;这部分是你唯一能直接改的
- 错误形态分布:超时、连接失败、上游 5xx、限流分别占多少,指向完全不同的处理方式
拿压测把并发从低档往上抬,记下成功率、耗时拐点和错误形态变化,找到「还能撑住」的那一档。这个数比任何架构图都实在。外部链路可以先到 站内 /tools/speed-test 量一遍,再和你的服务内耗时对齐,否则很容易把上游的慢算到自己头上。
并发不是调大线程数,而是决定谁能进谁排队
把「并发上限」和「排队」当成两件事分开设计:上限决定同时处理多少,队列决定多出来的请求是等还是立刻拒绝。两个都不设,等于把压力原样透传给上游,高峰期大家一起挂。
- 先定池子按任务类型分池,轻量任务一个池、重任务一个池。别让一个长任务占满所有名额,重任务池满了就排队,轻任务池始终留出份额保证基本响应。
- 再定上限每个池设一个并发上限,这个值来自压测结果的八折左右,不是拍脑袋。改上限必须重新压一次。
- 最后定队列队列要有长度上限和等待超时。无上限队列只是把雪崩往后推,等到内存爆掉时更难查根因。
判断标准很简单:如果你的失败模式是「越来越慢直到全挂」,就是缺排队和上限;如果是「一部分请求立刻被拒」,说明上限卡得太死,该动的是队列长度,不是直接放开并发。
超时、重试与降级三件套怎么定档
| 层次 | 要设什么 | 判断依据 |
|---|---|---|
| 连接层 | 连接超时 | 网络抖动下能快速失败,避免占着名额干等 |
| 读取层 | 首字节/读取超时 | 比连接层大,但要有倍数关系,不能同级 |
| 整链路 | 外层总超时 | 比所有内层都短,防止内层没超外层先断 |
| 重试 | 只对幂等请求、最多一次 | 重试放大流量,高峰期能反过来压垮上游 |
| 降级 | 切通道/返回缓存/降规格/明确报错 | 降什么必须提前定,临时想等于没有方案 |
分层设超时这件事最容易被做成一个值。连接、读取、整链路各一个值,逐层放大并保持倍数关系。整链路超时设得比内层还长,内层早就卡住了,外层还在等,名额就被长期占用。
- 重试只对幂等且失败形态明确的请求做,且只重试一次
- 有副作用的写操作不要自动重试,上游明确禁止时也一样
- 降级方案提前写好并演练,别等告警响了才讨论
- 降级路径本身也要有超时,不能变成新的阻塞点
不适用的地方:如果你的上游明确不允许重试,或者重试会重复计费,那就只做超时和降级,重试这一层直接砍掉。降级后返回的结果要能让调用方分辨出来,静默降级会让上层逻辑误判。
日志记哪几个字段,出事才能复盘
每个请求至少记下这几项:请求 id、开始与结束时间、上游耗时、本层处理耗时、状态码、重试次数、最终走了哪条通道。缺任意一个,复盘都得靠猜,项目一大就没人愿意查。
- 请求 id
- 把同一次请求的所有日志串成一条链,排查时先按 id 拉全量
- 耗时分段
- 上游耗时和本层耗时分开记,一眼看出瓶颈在谁那边
- 分流字段
- 记录最终走的是主通道还是降级通道,否则事故时说不清影响面
- 重试标记
- 有几次重试、每次结果如何,判断问题是不是被重试掩盖了
一次请求打一行汇总日志,比打散成十几条更好用:排查先看汇总,判断慢在哪一段,再按请求 id 拉细节。另外要把「慢」和「错」分开统计,慢请求和错误请求的分布通常指向不同根因,混在一起会互相掩盖。可以对照 站内 /tools/quality-monitor 的监控思路搭自己的错误率与延迟看板,先看趋势再看单点。
上线前跑一遍的小评测清单
上线前准备一组固定用例,每次都跑同一套,结果才能横向比。用例不用多,覆盖住失败形态就行。
| 用例 | 观察什么 | 期望表现 |
|---|---|---|
| 正常请求 | 耗时与成功率 | 落在基线区间内 |
| 超大输入 | 本层处理耗时 | 不被上游超时误伤,有明确输入上限 |
| 上游超时 | 超时是否按设计触发 | 外层在预定时间内失败,名额被释放 |
| 上游返回错误 | 降级是否生效 | 走预设降级路径,且日志能分辨 |
| 并发打满 | 排队与拒绝行为 | 一部分排队、一部分明确拒绝,不整体雪崩 |
每次改动都记下这一轮的结果,和上一版对比。没有对比基线,改动的好坏只能靠感觉。评测要在接近真实的并发下做,单请求跑得再顺也证明不了高峰期可用。
下一步把你现在这套服务的耗时基线和上游账一起对一遍,再到站内核对当时的链路与价格数字,避免把上游成本当成自己的性能问题。→常见问题
轻量后端一定要上网关或队列中间件吗?
不一定。请求量不大、上游本身稳定的时候,进程内做并发上限加排队,再配合超时和降级,往往就够用了。中间件解决的是多实例协同、持久化和流量突增,如果你的服务只有一两个实例、日均调用量有限,先加组件只会增加运维面。等到出现多实例抢配额或者需要持久化重试,再补也不迟。
并发打上去就超时,最该先动哪个参数?
先别调参数,先量。把上游耗时和你自己的处理耗时分开统计,看超时是发生在哪一段。如果上游首字节就要很久,调你自己的并发只会让排队更长;如果是自己的处理慢,再去看是不是同步阻塞了不该阻塞的步骤。顺序反了,调完参数问题会换个形式出现。
降级之后用户看到的结果不对怎么办?
降级的结果要在响应里体现出来,比如标记这是缓存结果或者简化结果,让调用方能区分。最怕的是静默降级:用户以为拿到了完整结果,实际是残缺的,这类问题比直接失败更难发现。如果业务不能接受任何精度损失,那这个场景就不适合降级,应该在排队和容量上留余量。