资讯动态

Agent平台线上超时故障全记录:从线程池打穿到超时预算体系重构

发布时间:2026/10/26 4:41:36 来源:尧图企业网站定制
做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超过20sWorker线程池活跃度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平台算法和模型固然重要但真正决定平台稳定性天花板的往往是你怎么设计那些看起来不起眼的超时参数。我现在看代码第一眼一定是找超时和重试配置这两个东西写得好不好基本能判断一个系统的稳定性底线。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑