日期:2026-08-19
我们推理服务刚上线那阵,偶发一个慢请求把整批拖死,前端等了三十秒才超时,用户早跑了。更糟的是链路长了:网关到模型,模型到向量库,每层都在等,却没有统一超时,故障像滚雪球。排查时网关说没超时,模型说没超时,谁都觉得自己清白,最后发现是向量库那一跳卡了。我们事后复盘,那次事故从第一个慢请求到全链路恢复,花了六分钟,期间新请求全在排队,损失比想象中大。还有一次,一个用户上传的超长文档把向量库查询拖住,连带把同在排队的普通问答全卡了,客服那边一下收到几十条超时反馈。
我们当时就一个判断:推理链路没有超时预算,就像电路没装保险丝,一层慢全链路陪葬,而且越长的链路越容易出这种事,因为每一跳都在等下一跳,谁都不先断。后来我们把带预算写进接入规范,新链路默认带它,不带预算的接入申请我们直接打回,从源头堵住漏洞。
我们搭了五件事。一是请求级超时预算:每个请求进入就带一个总预算,比如八秒。二是跨层超时传播:预算从网关传到模型、再传到向量库,每层扣掉自己耗时。三是慢请求快速失败:某层超时立刻返回,不占用推理队列。四是超时指标埋点:记录每层实际耗时和超时发生在哪跳。五是级联熔断:下游持续超时,上游主动降级,不再无脑转发,保护住还能正常服务的那部分流量。
最磨人的是超时预算怎么分。入口给八秒,网关处理占五百毫秒,模型推理可能占六秒,向量库占一秒,留一点余量。我们用 context 的 deadline 透传,每层拿到剩余预算,超了就停,不猜,也不让某一层自己偷偷加时。
另一个坎是跨层传播容易断。早期只设了网关超时,模型层自己没接 deadline,结果网关断了模型还在跑,算力白烧。后来强制每层必须在 deadline 内检查,超了直接 cancel,哪怕返回结果也不要了,因为等下去只会拖累更多请求。
还有慢请求快速失败和级联放大的平衡。太快失败会误杀正常长请求,太慢又挡不住雪崩。我们按 P99 加两倍标准差设预算,并且下游超时率超阈值才触发上游熔断,避免抖动误伤,正常波动不会触发降级。
还有一处值得单独说,是超时预算和用户体验的权衡。太短的预算虽然保护了系统,但会把本来能答出来的长请求也砍掉,用户看到的是一半的答案。我们给长文档类请求单独开了更长的预算档,并且做成异步模式,先返回任务已接收,结果好了再通知,而不是让用户干等。这个区分很关键,否则为了保护系统把正常长请求也误杀了,业务方照样投诉。我们也因此把超时从单纯的系统保护,变成了带业务语义的调度策略,不同请求类型有不同的耐心,这部分我们还在按业务反馈持续调。压测时我们还专门造过慢请求洪流,验证预算传播在极端下的表现,果然第一次跑就暴露了某层没接 deadline 的遗漏,趁早补上了。现在新模型接入默认带预算,不用再各自实现一遍超时逻辑。
案例片段(已脱敏): 超时预算与传播(伪配置):
timeout_budget: 8000ms propagation: gateway: 500 model: 6000 vector: 1000 margin: 500 cancel_on_exceed: true cascade_breaker: downstream_timeout_rate > 0.15某次向量库索引重建,查询延迟从 80ms 飙到 3s,超时传播让模型层在预算内提前 cancel,网关返回降级结果,整链路没被拖垮,而此前同款故障曾导致全站超时约六分钟,那次我们查了半小时才定位。案例片段(已脱敏): 某业务方上线一个长文档摘要功能,单请求平均耗时要七秒,超过我们八秒预算里的模型档位。第一版没有分层预算,这批请求把向量库查询队列占满,普通问答跟着超时。接入预算传播后,长文档请求到档位就提前返回"稍后取结果"的异步提示,普通问答的 P99 重新回到正常区间,两类请求互不拖累。
整改后 P99 延迟从 31 秒降到 8.4 秒量级,慢请求(超预算)占比从最高 12% 压到 0.3% 以下。级联雪崩事故从每月一到两次归零。超时定位从原来的平均半小时缩短到看一眼埋点面板就知道卡在哪一跳。文中数据为项目复盘口径,已做脱敏。
超时预算这件事,必须从入口一路传到底,任何一层不接 deadline 都是漏洞,我们就是栽在模型层没接那一次,白烧了一晚上算力。预算值别拍脑袋,拿 P99 加波动去定,太紧误杀、太松挡不住雪崩。级联熔断要设触发阈值,靠抖动误伤比雪崩更烦人。下一步我们打算把这个 budget 透传做成 SDK 的默认能力,新接的模型不用各自再写一遍超时逻辑,少一处手写就少一处漏接。这套预算传播上线后,我们顺手把超时埋点接进了统一监控,哪一层慢一眼能看到,后来排查类似问题从半小时缩到几分钟,运维也说省心。