☰
Agent平台线上超时故障全记录:从线程池打穿到超时预算体系重构
2026/10/26 4:41:24 网站建设 项目流程

做Agent Platform那阵子,我满脑子想的都是Agent的规划能力、工具调用、上下文管理这些偏“算法”的东西,代码怎么写、Prompt怎么调、模型怎么选,几乎占满了整个迭代周期。结果真正让我半夜爬起来开电脑的,不是Agent理解错了用户意图,而是上线后第四周的一次线上超时故障。这个平台说白了,就是把一堆外部AI模型能力和内部工具编排起来,对外提供一个统一的Agent调用入口,内部要管任务分发、并发调度和结果聚合。这种架构天然和“慢”绑定在一起,因为一次Agent任务可能要串行调用多个模型接口,单次响应轻则几秒,重则几十秒。也正是这种“慢”,让一次本不起眼的偶发超时,最终演变成全站不可用的级联故障。那次故障之后我才徹底想明白一件事:超时不是一个配置项,而是一整套需要提前设计的系统工程。

这篇文章不打算讲Agent的算法细节,就讲这次真实踩坑的来龙去脉,包括平台的架构背景、故障现场、排查路径、根因分析,以及最后我们做了哪些修复。如果你也在做类似的AI平台、异步任务系统,或者正在维护任何一条重度依赖外部接口的链路,这篇文章应该能帮你躲过几个坑。

1. Agent Platform的架构背景与核心链路设计

1.1 平台定位与整体模块划分

我们做的这个Agent Platform,本质上是把用户请求转成Agent任务,再交给一组执行器去调度模型、调用工具、收集结果,最后统一返回。整体上分成接入层、调度层、执行层、依赖层四块。

层级核心模块主要职责
接入层网关服务接收HTTP请求、鉴权、限流、参数校验
调度层任务队列 + 调度中心拆分任务、维护状态机、分发到执行器
执行层Worker执行器集群驱动Agent循环、调用模型、触发工具、聚合结果
依赖层模型API、内部RPC、工具服务提供实际能力,是整个链路的下游

我印象最深的一版核心链路是这样:用户请求先到网关,网关把请求转成一条任务消息丢进消息队列,调度中心消费后根据任务类型匹配执行器,执行器开始跑Agent循环。Agent循环里会做意图识别、上下文组装、模型调用、工具结果合并,甚至可能递归二次规划。一次完整的任务,往往要经历“主模型调用→工具调用→结果整理→主模型汇总”这样好几轮。

这个设计本身没有大问题,问题出在我对“慢”的预估严重不足。做传统后端的时候,一个RPC超过3秒就觉得不对劲了,但到了Agent场景,一次模型调用等30秒都不稀奇。外部模型接口的响应时间呈现典型的长尾分布,P95可能只有5秒,P99却可能飙到30秒以上。这种慢接口一旦出现在同步调用链路上,线程池一定会被打穿。

1.2 技术选型里埋下的隐患

技术选型上,我们当时用的是Java生态,网关和调度层是Spring Boot,任务队列用了RabbitMQ,执行器是自研的Worker进程,内部通信走gRPC。模型调用这块,因为要接多个厂商的模型API,所以封装了一层统一的Model Client,底层是OKHttp + 重试拦截器。

这里有个很坑的细节。我们在封装Model Client时,给连接超时设了10秒,读超时设了45秒,重试次数设了3次。表面看每个参数都“有讲究”,但后来计算极端情况才发现,一次模型调用理论最差耗时是:连接超时10秒 + 读超时45秒,然后重试3次,也就是(10 + 45)乘以3,等于165秒。而当时Agent链路整体的超时阈值是60秒,调度层超时是30秒。这几组数字互相打架,整个系统根本没有一个统一的超时预算体系。


2. 线上超时故障现场与排查实录

2.1 故障时间线还原

故障发生在某个工作日下午,大概两点四十分左右。当时运营同学正在做一个面向外部用户的活动,流量比平时翻了差不多三倍。我们的监控系统先是在两点四十一分发出一条告警,说网关服务P99延迟超过了8秒,紧接着调度中心的活跃线程数告警也响了。我打开监控面板的时候,Executors的活跃线程显示已经到了上限,RabbitMQ的消息堆积量从几百条一路涨到了十几万条。

从用户体验来看,表现就是请求一直转圈,超过一分钟才返回一个超时错误,部分请求甚至直接连接被重置。更麻烦的是,因为消息队列积压越来越多,调度中心还在持续往Worker里投递新任务,Worker的线程池开始拒绝执行,抛出一堆RejectedExecutionException。我后来看日志,整个故障期大概持续了四十分钟,期间线上成功率跌到35%以下,几乎所有涉及Agent的任务都在报错。

2.2 排查路径:从网关到执行器的逐步定位

那天的排查路径,现在回想起来还是挺典型的,值得记一笔。

第一步是看网关。网关的日志显示,大量请求在等待下游返回,超时集中在调用调度中心同步接口的那一段。也就是说,不是网关本身被压垮,而是它等在等待下游。这就很说明问题了——查了下游。

第二步是看RabbitMQ和调度中心。队列长度暴涨,调度中心消费速度明显跟不上生产速度,但奇怪的是,数据库的负载并不高,Redis也正常。CPU、内存这些基础指标都没到瓶颈,但线程池却全部卡在等待状态。

第三步是看Worker执行器日志。这时候真相已经浮现了——大量Worker线程Block在Model Client的调用上,也就是在等待外部模型API返回。我翻了某个执行器的线程栈,几十个线程全部停在OKHttp的读取响应阶段,没有超时,没有报错,就这么干等。

顺着这个线索去查上游模型API的提供方,才确认是他们的服务出现了一段时间的响应变慢,部分请求甚至要120秒才能返回。我们的Model Client因为有45秒读超时和3次重试的存在,等于把所有慢请求又放大了好几倍。每个任务进入Agent循环后需要调用模型,模型响应慢,任务就卡住,任务卡住,调度层线程池就被占满,消息队列开始积压,网关也开始连锁超时。

2.3 这次故障的关键监控指标

故障期间有几个数据值得记录,我到现在都还在用这几个指标来评判系统健康度:

指标正常时段故障时段
网关P99延迟300ms ~ 800ms超过20s
Worker线程池活跃度30% ~ 50%100%持续打满
RabbitMQ消息堆积量0 ~ 10012万+
外部模型API P952s35s
成功率99.9%以上35%以下

我当时盯着这几条数据,心里只有一个想法:外部依赖一旦变慢,我们的整个容错机制几乎等于零。所有线程都在等待,所有重试都在放大,所有队列都在积压,最终把一个小问题滚成了一个大雪崩。


3. 根因分析:为什么一次慢响应就能放倒整个平台

3.1 超时参数互相打架的放大效应

我先不扯理论,直接把这笔账算给你看。假设上游模型API响应变慢,单次响应需要120秒才能返回,而我们的Model Client配置是:

连接超时10秒,读超时45秒,最大重试3次。

一次模型调用的最差耗时就会这样计算:第一次调用,连接成功,但读取等了45秒触发读超时;然后重试第二次,同样等45秒;再重试第三次,还是45秒。这三轮加在一起,耗时就是45乘以3等于135秒,这还没算连接和重试之间的间隔。一个Agent任务如果串行调用两三次模型,那总耗时就是135乘以2或者乘以3,完全超过任何链路超时。

这里面最致命的设定是重试。重试在传统接口场景下确实是保障可靠性的常用手段,因为它假设“重试一次可能就成功了”。但在一个已经超时的外部依赖面前,重试完全是反向操作——你不知道对方是暂时抖动还是已经濒临崩溃,盲目重试只会把雪球越滚越大。我们当时的代码里甚至没有区分“连接失败重试”和“读取超时重试”,只要抛异常就重试,效果就是自己的线程池活活被拖死。

3.2 线程池排队导致的假死状态

很多人以为线程池打满了系统就会立刻报错,其实不是。线程池的拒绝策略我们当时用的是CallerRunsPolicy,意思是线程池满了,任务不会被丢弃,而是回到调用方线程执行。这个策略的本意是缓解压力,但在那种场景下它起到了反向作用:调度中心把任务丢给Worker时发现Worker线程池满了,任务又回到调度中心自己的线程里跑,调度中心的线程也被拖住,然后整个调度链路就变成了一个互相等待的网格。

从外部看,数据库没瓶颈,内存没瓶颈,CPU也不高,但服务就是“假死”了。系统不是没有资源,而是资源全部被阻塞在等待外部响应的状态上,这种状态比资源耗尽更难排查,因为基础监控看起来一切正常。

3.3 链路超时缺失引发的连锁反应

还要提一个链路层面的问题。Agent任务的执行链路比较长,我们当时只在网关做了60秒的总体超时,但内部每个环节都没有设置独立的超时预算。消息队列消费环节没设,调度环节没设,Worker执行环节没设。这就导致一个现象:网关虽然60秒就断开了连接,但任务在后台其实还在执行,Worker线程还在傻等模型响应,队列还在不断投递消息。

断开连接不意味着逻辑停止,这可能是整次故障里最容易被忽略的一环。用户那边已经放弃等待,但我们的系统资源被这些“幽灵任务”继续占用,又拖慢了正常任务的处理。一个超时配置的缺失,直接浪费了后面所有环节的线程和队列资源。


4. 修复方案与落地实施细节

4.1 重新设计统一的超时预算体系

那次故障后,我们做的第一件事,不是改某一行代码,而是重新梳理整条链路的超时预算。核心原则很简单:从用户可接受的最长等待时间出发,从前往后设置每一层超时,且后一层的超时时间不能大于前一层的剩余时间。

具体策略上,我们定了三层预算:

环节原配置修复后说明
网关总超时60s25s用户侧等待是硬指标
调度中心30s15s必须小于网关总超时
Worker执行无10s每轮Agent执行严格卡时间
单次模型调用连接10s + 读45s连接3s + 读8s重试次数从3降到1

模型调用的读超时从45秒直接砍到8秒,可能有人会觉得太激进,担心正常请求会被误杀。这里要说一下,经我们压测确认,模型服务的P95响应时间在5秒左右,8秒已经足够覆盖绝大多数请求。我们要防的是长尾请求,而不是正常请求。超时设置不是取平均值,也不是取最大值,而是取“你能容忍的最慢响应”,一旦超过就直接失败,快速放出线程。

4.2 重试策略的收敛与熔断降级

重试策略我们也做了大改。原来的3次重试直接砍到1次,而且只允许在连接失败时重试,读取超时一律不重试。这背后的逻辑是:连接失败可能是个别网络节点抖动,重试成功率较高;但读取超时说明服务端已经在处理请求且响应慢,重试只会加重对方压力。

另外,我们引入了一个简易的熔断器。基于本地滑动窗口统计最近100次模型调用的错误率,如果错误率超过50%,熔断器直接打开,后续请求快速失败,不再调用外部模型,而是返回一个降级提示。熔断器每10秒尝试放行一个试探请求,如果试探成功就逐渐恢复。这个机制在后续一次上游抖动中起了大作用,系统P99只涨到了2秒左右,没有再次雪崩。

4.3 线程池隔离与容量计算

调度中心原来只有一个公共线程池处理所有任务,模型调用慢的时候,普通管理操作也被拖累。修复后我们按任务类型拆分了线程池,一个池子只跑Agent任务,一个池子跑轻量管理任务,两者互不干扰。同时把CallerRunsPolicy改成了丢弃策略加告警,避免任务回流拖死调用方。

线程池容量我们也重新算了。参考模型接口的QPS上限和P99延迟,用公式:线程数 = QPS × 响应时间(秒)来估算。如果压测得出模型接口能支撑的并发上限是20,那Worker线程池核心线程数就设置在10,最大线程20,队列长度卡在200,超过就直接拒绝并丢弃。这样做的代价是峰值流量下会丢弃部分任务,但换来的是整个系统不崩溃,任务可以被重新投递或人工介入处理。

4.4 快速失败与结果缓存

还有一个小优化,效果很明显:在调度层加了同参数请求的结果缓存。Agent平台里很多请求其实是相似的,比如同一个工具调用场景,只是入参有一些微调。我们在Redis里给模型返回结果做了一层短期缓存,TTL设了几十秒。高峰期间,同样参数的调用直接命中缓存,跳过模型调用,大大降低了对下游的压力。

那块代码现在看起来很简单:

public ModelResponse callWithCache(Request request, Duration cacheTtl) { String key = buildCacheKey(request); ModelResponse cached = redis.get(key); if (cached != null) { return cached; } ModelResponse response = modelClient.invoke(request); redis.set(key, response, cacheTtl); return response; }

这个改动不复杂,但对同类流量高的场景非常有效。活动期间,缓存命中率一度达到40%左右,相当于给模型接口减了快一半的流量压力。


5. 故障复盘清单与实战避坑建议

5.1 超时设计自查清单

这次故障沉淀出的一份自查清单,我后来在团队里反复推广。每次有人要接新的外部依赖,我都会让他按这个清单过一遍:

检查项具体内容是否满足
每跳超时所有RPC、HTTP调用都设置了明确的连接和读超时每一项都必须覆盖
重试上限重试次数是否有明确上限,是否区分了连接失败和读超时读超时尽量不重试
剩余预算内层超时时间是否小于外层剩余时间遵循超时预算体系
资源隔离慢依赖是否使用了独立线程池至少隔离核心链路
熔断降级是否有快速失败机制,失败后是否返回降级结果必须有兜底方案
幽灵任务用户断连后任务是否还在后台继续执行需要可取消机制

5.2 依赖拓扑与慢依赖治理

另一点体会是要把外部依赖当成“有生命的东西”来对待,而不是一个稳定的黑盒。我们在日常开发中很习惯把外部API当成一个可靠的组件来调用,但现实是,所有网络调用都可能失败,所有外部服务的响应时间都可能出现长尾。每次上游服务发布新版本或者做流量调度,我们都应该复盘其对下游的影响。

这次故障其实就是上游模型服务做了一次内部变更导致的响应变慢,他们自己可能都没意识到影响到了下游。所以在Agent平台这种重度依赖外部模型的系统中,我建议把外部依赖的SLA监控做得更细一点,不只是看错误率,还要看P95和P99响应时间的变化趋势。一旦发现P99连续多分钟超过我们设置的读超时阈值,就要立刻考虑降级或切流。

5.3 压测要覆盖慢依赖场景

最后一定建议大家做“慢依赖压测”。常规压测往往只测正常情况下的吞吐和延迟,但真正能检验系统鲁棒性的,是模拟某个外部依赖突然变慢甚至不可用的场景。我们后来专门做了一个故障注入工具,可以动态给指定外部接口增加延迟或者返回错误,然后观察系统整体表现。

这个压测做下来,你会发现很多平时注意不到的坑:某个融断参数太灵敏导致正常流量也被断掉,或者某个线程池的拒绝策略不合理导致雪上加霜。慢依赖压测不是锦上添花,而是Agent平台这类系统的标配测试项目。


那次故障之后,我在团队里立了一个规矩:任何新接入的外部依赖,上线前必须先过一遍超时预算评审、熔断降级评审和慢依赖压测。看起来繁琐,但对比一次线上事故造成的业务损失,这点流程成本真的不算什么。做Agent平台,算法和模型固然重要,但真正决定平台稳定性天花板的,往往是你怎么设计那些看起来不起眼的超时参数。我现在看代码,第一眼一定是找超时和重试配置,这两个东西写得好不好,基本能判断一个系统的稳定性底线。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询