☰
Agent 平台超时故障全解析:从线程池耗尽到异步化改造
2026/10/23 6:44:06 网站建设 项目流程

1. Agent Platform 整体设计与核心模块

1.1 为什么需要自己搭一个 Agent Platform

先交代一下背景。团队在做一个把大模型能力接入业务系统的中间层,最初只是几个单点接口,谁要调用模型就直接 HTTP 请求。但很快发现,业务方要的不只是“问一句答一句”,而是“让模型替用户把一件事完整跑完”:查库存、对比价格、填单、回执确认,这种多步骤操作如果全部写在业务代码里,每个调用方都要自己维护一套复杂的状态流转和上下文拼接,谁都扛不住。

于是我们决定做一个统一的 Agent Platform,把模型的调用、工具的执行、上下文的维护、任务的编排统一收口。这个平台对内暴露两类入口:一类给实时对话场景用,一类给任务型场景用。前者的核心诉求是响应快,后者的核心诉求是稳。

架构上我们没有一上来就堆微服务,而是按照职责拆成四个核心模块:

  • 接入网关层:负责鉴权、限流、协议转换,把外部请求统一转成平台内部的任务结构。
  • 编排调度层:负责拆解用户意图,规划执行步骤,决定先调哪个工具、再问哪一轮模型。
  • 执行器层:真正执行工具调用、模型调用、代码片段运行等逻辑。
  • 状态与记忆层:负责保存任务快照、对话历史、工具返回结果,按任务 ID 维度做隔离。

每个模块之间通过内部消息通道传递上下文,而不是直接同步调用。这个设计思路后来越来越被证明是对的,但一开始我们并没有把“异步化”贯彻到入口层,这也为后面那次超时故障埋下了雷。

1.2 核心模块与数据流

看数据流会更清楚一些。用户在前端发起一个请求,比如“帮我把上个月的销售数据整理成报告并发送给相关同事”,这个请求先到网关,网关做完身份校验后立刻生成一个任务 ID,然后把任务投递到编排队列。

编排调度层拿到任务后开始工作:它会先把这条自然语言请求转成一组结构化步骤,比如“查询销售数据”、“整理成表格”、“生成分析总结”、“调用消息接口发送”。每执行一步,系统都会把中间结果存进状态存储,并更新任务状态机。

这里我标注一下关键点:每一步的执行都不是瞬时完成的。模型生成一段分析可能需要 2 到 5 秒,工具查询可能又要几百毫秒,整条链路算下来,一个完整 Agent 任务的执行时间通常在 10 到 30 秒之间,甚至更长。

这个“长任务”特性,是我们整个故障故事的最核心背景。如果我们做的只是一个普通 CRUD 接口,每个请求 50 毫秒返回,那后面所有的事情都不会发生。

1.3 技术选型逻辑

在做技术选型时,我们当时处于“快速验证业务可行性”的阶段,没有选择引入大量的新基础设施,而是尽量复用团队已有的技术栈。

选型对比大概是这样的:

模块方案选项我们最终的选择理由
主语言Java / Go / PythonJava团队主力栈,运维体系成熟,排查问题经验丰富
任务调度自研线程池 / 消息队列消息队列 + Worker天然支持削峰、重试、故障恢复
状态存储MySQL / RedisRedis + MySQL 组合Redis 存热状态,MySQL 做持久化归档
模型调用直连模型 API / 统一网关自研模型网关统一处理鉴权、限流、超时和模型降级
对外接口REST 同步 / REST 异步 / WebSocketREST 同步(第一阶段)快速上线,客户端接入成本低

现在回头看,第一阶段的“REST 同步”选型,属于典型的业务倒逼技术决策。当时业务方希望 API 调用方式越简单越好,最好发一个请求就能拿到最终结果,不想在客户端做任何异步逻辑。我们为了让平台尽快跑起来,接受了这个妥协。

但从这次故障之后,我们彻底转向了“任务提交 + 轮询/推送”的异步模型。这个代价是客户端接入稍微复杂了一点,但换来的却是平台整体的稳定性。凡是可能耗时超过 3 秒的接口,在设计阶段就必须按异步来考虑——这句话现在已经是我们的内部设计红线了。

2. 超时故障发生的背景与第一现场

2.1 现象描述与告警

那是一个普通的周二下午,监控群突然连续弹出告警。告警内容并不陌生:网关接口“任务执行接口”的 P99 延迟从平时的 1.2 秒飙到了 8.5 秒,超时率从前一天的 0.1% 上升到了 23%。紧接着,上游业务方开始在群里反馈,说平台返回了大量 504 和 502。

这里要说一下我们平台的超时配置。入口网关给“任务执行接口”配置的读超时是 5 秒,写超时是 5 秒。而 Agent 任务动辄需要十几秒才能执行完成。按理说这两者天然是冲突的——同步接口根本不可能在 5 秒内返回一个完整执行结果。

那为什么还能跑起来?

因为我们做了一层“伪同步”:收到请求后,同步执行编排逻辑,但编排内部每一步之间其实是异步流转的,如果整条链路能在 5 秒内跑完就正常返回;如果跑不完……鬼知道会怎样。实际业务中,很多轻量任务确实能在 5 秒内完成,比如“查一下某个客户最近三笔订单的状态”这种简单工具调用,模型一轮 + 工具一轮就完了。所以我们一直在这个临界状态下运行,直到那天流量上来,把所有问题一次性暴露了。

2.2 初步排查与假设

接到告警后,我们第一反应是查下游模型服务的状态。

这也是个很自然的排查方向。Agent 平台里最耗时、最不稳定的就是模型调用,如果模型服务出现拥堵或限流,我们的接口延迟会被直接拉高。当时我们看到模型网关的调用耗时确实有上升:之前平均 800 毫秒到 1.5 秒的模型响应,在故障期间变成了 3 到 6 秒。这更加坚定了我们“模型服务背锅”的判断。

我们还去查了数据库慢查询。平台里的状态存储用了一部分 MySQL,Agent 任务每流转一步都会更新状态,如果某些任务上下文很长、子步骤很多,那更新 SQL 可能会扫描较大的数据块。不过当时慢查询日志里并没有出现特别离谱的大查询,数据库侧相对安静。

资源监控也在看。每台物理机的 CPU、内存看起来都还算健康,没有明显的突刺和内存溢出。因为问题不在算力层面,而在“等待”层面——大量线程在等待外部 IO 返回,CPU 根本跑不满。

2.3 真正定位:线程池被长任务占满

排查持续了大概二十分钟,真正让我们发现问题的是一个不太起眼的指标:应用服务器的活动线程数。

我们用的容器默认线程池是 200 个线程。平时这个池子的活跃线程通常在 30 到 60 之间,故障期间直接打满到了 200,之后新请求再也拿不到线程,只能在队列里等待。而等待的请求又会占用客户端连接,客户端等不到响应就开始超时。超时之后,上游服务会做一次重试,把同样的请求再打进来,继续排队。这个循环一直转下去,系统进入了典型的“线程饥饿”状态。

其实这种问题在传统 Java 后端里非常经典,理解起来不难。但为什么我们当时没有第一时间想到?

因为我们的服务不是普通后端,它内部有不同的线程池在管理模型调用和工具调用。我们有专门给模型调用用的 HttpClient 线程池,有给工具执行用的线程池,还有给状态写入用的连接池。在主线程池打满之前,我们已经观察到这些子池也在排队,但当时的判断是“子池排队是结果,不是原因”。

真正的原因链条是这样的:

  • 主线程池里有大量线程在处理 Agent 执行任务。
  • 每个线程会阻塞等待模型 API 返回,这段等待时间少则 2 秒、多则 10 秒。
  • 因为所有上游请求都同步等待最终结果,所以主线程池被这些“看起来活着、实际在等”的线程全部占住。
  • 新请求进不来,入口超时率飙升。
  • 上游重试进来,线程池彻底堵死。

这里我还想强调一点:线程池打满不等于资源不够,而可能是线程被“卡”在外部依赖上。这是排查超时问题最容易误导人的地方。CPU 没跑满、内存没报警、数据库也不慢,并不代表系统是健康的。对于大量依赖外部 IO 的服务,POOL 的排队长度和 Web 容器活跃线程数反而更能说明问题。

3. 根因深挖:为什么“每个任务都很慢”只是表象

3.1 Agent 任务的结构性耗时

很多人在设计 Agent 平台时,会对“慢”这件事估计不足。一个完整 Agent 任务的耗时构成大概是这样的:

  • 第一轮模型调用:把用户意图解析为执行计划,耗时 1 到 3 秒。
  • 工具调用:比如 SQL 查询、HTTP 请求,耗时 0.2 到 2 秒。
  • 第二轮模型调用:根据工具结果生成下一批动作或最终回复,耗时 1 到 3 秒。
  • 多轮循环:如果任务复杂,可能循环 3 到 5 次。
  • 上下文整理与持久化:0.1 到 0.5 秒。

也就是说,一个中等复杂度的 Agent 任务,总耗时普遍在 8 到 20 秒之间。这还算是顺利的情况,如果中间某次模型调用触发限流重试,或者工具调用超时重试,那 30 秒以上是很常见的。

这个数据放到业务视角里去理解:我们的平台是给内部业务系统做智能助手的,用户点了按钮之后,等 10 秒出结果,体验上是“慢”,但还在可接受范围。但服务端传统同步模型的容忍度只有几百毫秒。这两者之间的差距,就是我们故障发生的结构性原因。

3.2 共享资源竞争产生的雪崩效应

除了任务本身慢,还有一个更致命的东西:所有任务都在抢同一批线程资源。

Agent 平台承载的请求不只来自外部业务方,还来自平台内部的定时任务、健康检查、配置刷新、缓存预热的线程。这些看起来无关紧要的小流量,在正常情况下占比很低,可一旦主线程池被长任务堵满,它们的失败会引发一系列连带反应。

举个例子,负载均衡器会定期发送健康检查请求到每个应用实例。如果实例的健康检查接口因为线程池耗尽而迟迟不返回,负载均衡器会把这个实例标记为不健康,然后把流量切到其他实例。其他实例承受了更多流量,也陷入一样的线程饥饿状态,最终所有实例都被踢出流量调度。这就变成了一个极端的恶性循环。

而且,我们平台作为中间层,它本身也在调用其他内部服务。线程池满后,内部服务之间的调用也会排队;下游服务看到上游长时间不返回,就可能提前断开连接。一旦连接断开,上游线程还要做异常处理、记录日志、决定是否重试,这些动作同样消耗线程和 CPU。整个系统的吞吐量会断崖式下跌。

3.3 重试风暴是放大器

这次故障中,重试机制扮演了放大器角色。

我们的上游采用的是非常标准的重试策略:默认重试一次,超时时间为 3 秒。也就是说,当平台入口线程池开始排队时,上游发来的第一次请求会在客户端超时 3 秒后返回错误,然后客户端立刻用同样的参数重发一次。

这相当于每 1 个用户的真实操作,在最坏情况下会给平台带来 2 到 3 倍的请求量。而 Agent 平台本身的业务特性决定了,一次真实操作在内部可能已经创建了一个执行任务。如果这个任务在后台仍然在跑,重试请求又会创建一个新的重复任务。两个任务同时跑同一段逻辑,可能还会对业务数据造成重复处理。

我们后来在平台的任务记录里确实发现了一批重复执行的任务,它们的任务 ID 不同,但业务参数一致,排查了很久才确认是重试导致的重复调用。

所以那次故障给我的第二个深刻教训是:在引入超时和重试机制时,必须同步考虑幂等性。平台侧要能在入口识别出“这是同一类请求的重试”,并把多次提交合并成一次执行,或者在任务执行前做好去重检查。

4. 修复方案与架构改造落地

4.1 第一时间的止血措施

先把时间拨回故障发生当天。我们的核心目标是让服务尽快恢复,不做激进的重构。

当天我们做了三件事,顺序很重要:

第一,在入口网关把对模型调用和工具调用的全局超时改成一个更小的值,避免单个任务无限期等待。这个动作能释放一部分线程,但效果有限,因为我们不能把任务超时压得太低,否则业务根本做不成事。

第二,我们把上游服务的重试次数临时调整为 0,并且联系上层调用方同步修改其超时配置。这是止血措施中最关键的一步。切断重试风暴之后,线程池的排队情况立刻开始缓解,大约十分钟后,新请求的进入速度恢复正常。

第三,我们将入口网关对“任务执行类接口”的调用方式,从同步等待改为异步提交。这一步是应急操作,但方案是临时的:客户端提交任务后立即返回任务 ID,然后客户端每 1 秒轮询一次任务状态接口。这样至少让网关线程不被长任务卡住,主线程池的压力迅速降下来了。

不过我必须强调,这一步当时属于“拆东墙补西墙”的临时办法。因为客户端切换轮询模式后,并没有真正解决“后台任务并发执行数量不受控制”的问题。如果没有后续的队列改造,等到流量再上来,后台还是会崩溃,只是从“线程池崩溃”变成了“执行器任务堆积”。

4.2 根治方案:任务队列化与异步化改造

故障恢复后,我们用两周时间把整个架构做了一个关键改造,核心思路其实一句话:把“同步等待结果”变成“异步流转状态”。

改造后的流程是这样的:

  • 客户端提交任务到网关,网关生成唯一任务 ID,将请求写入任务队列并立即返回。
  • 任务队列使用持久化消息存储,确保任务在服务重启后不丢失。
  • 调度器从队列里拉取任务,更新任务状态为“执行中”,然后把任务派发给空闲的 Worker 执行。
  • Worker 按内部编排逻辑执行模型调用、工具调用。每完成一步,就把结果写回任务存储,并更新任务状态。
  • 客户端通过轮询任务状态接口来感知任务进度,等任务状态变为“成功”或“失败”后,再拉取最终结果。

为了不把客户端的接入成本提得太高,我们还提供了另一种模式:客户端在提交任务时可带上一个回调地址,任务完成后,平台主动推送结果。这样客户端连轮询都不用做,只用一个 HTTP 接口收结果即可。

改造后的任务状态机相对清晰:

状态含义流转方式
PENDING已入队,等待调度调度器拉取后变为 RUNNING
RUNNING正在执行编排逻辑执行完成后变为 SUCCESS 或 FAILED
FAILED整体执行失败可手动或自动触发重试,回到 PENDING
SUCCESS全部步骤执行完毕结果数据保留一定时间供客户端拉取

这里有个值得说的细节:我们并没有把整个 Agent 执行过程做成原子性的事务。因为一次 Agent 执行涉及模型调用、外部工具调用、持久化等多个环节,任何一步失败都可能导致全局回滚,这在业务上是不现实的。我们采取的是“步骤快照”策略:每个执行步骤的结果都先存储,如果某一步失败,系统可以从最近的快照点恢复重试,而不需要从头再跑。

这个设计思路也可以理解为“断点续跑”。它让“重试”从重来一次变成从失败点继续。实际效果是,重试任务的耗时开销比原来降低了 60% 左右。

4.3 监控与可观测性补强

这次故障也暴露了我们在可观测性上的明显短板。当时我们有基础设施的监控,但缺少“任务级”的视角。于是我们补了这么几类指标:

  • 任务维度:任务排队时间、执行时长、状态分布、步骤级耗时。
  • 线程池维度:活跃线程数、队列深度、拒绝次数。
  • 依赖维度:模型调用耗时分布、工具调用耗时分布、错误码占比。

我们还在执行链路里埋了 Trace,这样任意一个任务失败,都能通过 Trace 看到具体哪一次模型调用超时、哪一个工具访问异常,上下级调用关系一目了然。

关于监控这件事,我的实际经验是:监控要建在链路的关键节点上,而不是只盯着服务器资源。Agent 平台最核心的链路是“入口 → 队列 → 编排 → 工具/模型 → 持久化”,每一跳都要有延迟指标和失败指标。有了这些,下次再出问题,定位时间能从小时级降到分钟级。

4.4 稳定性治理的后续动作

除了架构改造,我们还做了几项稳定性治理,这些动作当时看起来不起眼,但实际作用非常大。

限流方面,我们在接入网关增加了基于用户和任务类型的双重限流。不同任务类型的成本差异其实是很大的:简单问答任务只需要一次模型调用,而复杂数据分析任务要做多轮调用和数据读取。如果两者混在一起共用一套限流策略,简单任务会被复杂任务挤掉,复杂任务又容易把系统拖垮。分开限流之后,资源分配问题基本解决。

熔断方面,我们对模型调用和工具调用增加了熔断机制。以模型调用为例,如果某一路模型服务的错误率连续 10 秒超过 50%,平台上对应的调用会被快速降级,返回给编排层一个可识别的错误码,由编排层决定是转用备用模型还是终止任务。这能有效防止一个下游服务的故障拖垮整个平台。

队列监控方面,我们给任务队列增加了堆积告警。当队列积压超过阈值时,系统会自动扩容 Worker 数量;如果扩容还跟不上,就触发入口限流,拒绝新任务进入,保护已有任务不饿死。

5. 排查 Agent 平台超时故障的实战清单

5.1 排查顺序建议

如果你也维护一个 Agent 平台或者类似的异步长任务系统,遇到线上接口超时问题时,建议按下面的顺序排查,可以少走弯路:

第一,确认入口网关的超时配置和客户端的超时配置是否匹配。很多超时问题源于网关设了 5 秒超时,而上游服务或业务方设的是 3 秒。这种配置冲突会导致客户端先超时,重试请求又打进来,系统在没有任何真实故障的情况下被压垮。我见过的线上故障里,至少有三分之一属于这一类。

第二,看核心线程池和连接池的使用情况。不要只盯着 CPU 和内存。要看业务线程池的活跃线程数、等待队列长度、线程拒绝次数。如果有大量线程处于“等待”状态,大概率是外部依赖返回太慢。

第三,逐个排查外部依赖的耗时分布。Agent 平台里最典型的外部依赖是模型 API、工具 API、数据库和缓存。要分别看它们的 P99 耗时和错误率。如果发现某个依赖从某个时间点开始 P99 明显升高,那基本就是瓶颈所在。

第四,打开任务状态表,看是否有大量任务卡在“执行中”状态,持续超过正常时长。这种情况说明 Worker 可能已经崩溃或挂起,或者某个执行步骤进入了死循环。

第五,回看最近是否有发布或配置变更。Agent 这种多依赖系统,一次配置修改(比如模型超时时间从 3 秒改成 8 秒)都可能导致下游堆积。

5.2 几个常见坑位

有些坑我们踩过之后,发现只要在设计阶段提前避免,后面就能省下很大的精力。

第一个坑是任务没有超时上限。模型调用有超时,工具调用有超时,但是整个任务没有超时上限,导致一个卡死的任务永远占用着一个 Worker 资源。后来我们给所有任务都加了一个最大执行时长,超时后自动标记为失败并释放资源。

第二个坑是重试没有考虑幂等。很多平台在接 Agent 能力时,都只设了一个简单的重试次数,完全没有考虑任务重复提交的问题。这个在 Agent 场景尤其致命,因为一次任务可能在重试请求前已经执行了一部分外部操作,重复执行等于重复操作一套业务流程。最安全的做法是在任务提交接口强校验唯一键,同键任务在后端幂等处理。

第三个坑是异步化改造不彻底。只把入口改成异步提交,但内部仍然用同步方式等待编排结果,那问题只是被后移了,并没有真正解决。异步改造必须覆盖全链路——入口异步、执行异步、状态持久化异步、通知异步。

第四个坑是限流粒度太粗。按照总请求量来限流,并不能区分“简单任务”和“复杂任务”的成本差异。更合理的做法是按资源消耗来估算,经过计算的配额分配给不同任务类型。

5.3 给 Agent 平台设计者的几点建议

经历过这次故障,我对 Agent 平台的建设有了一些非常个人的理解。

Agent 平台本质上是一个“长任务编排系统”,而不是一个“高性能接口系统”。它的设计目标不应该是在多少毫秒内响应请求,而应该是在多少秒内可靠地完成一件事。所有围绕“可靠性”的设计——队列、重试、超时、幂等、监控、限流——都比吞吐量更优先。

如果你想把这个平台做扎实,我建议在最早期就引入任务列表和状态机,而不是到用户量上来以后再补。任务列表不只是给用户看进度用的,它还是我们排查问题、重置卡死任务、统计业务量的基础数据资产。

举一个具体的设计细节:每个任务都要记录“上次活跃时间”。在排查卡死任务时,这个字段可以直接过滤出那些执行了半小时还没有进展的异常任务,根本不需要去翻日志。

最后一点建议是,所有依赖外部 IO 的地方都要有备选方案。模型服务可能在高峰期不稳定,工具服务可能眠了。平台层面可以做降级策略:某个模型挂了,自动切到备用模型;某个工具挂了,任务可以带着部分结果返回,而不是整体失败。Agent 的容错能力本质上决定了平台的上限。

6. 写到最后想分享的体会

这次超时故障解决之后,我有很长一段时间都在复盘一个问题:为什么一个概念上这么简单的线程池问题,我们花了近一个小时才定位?

我觉得最根本的原因是,我们对 Agent 平台的认识还停留在“模型调用服务”这个层面,而实际上它已经是复杂的任务编排系统了。用一套处理短事务的思路去设计长任务平台,必然会在某一天被现实教育。

另外,技术上的修复其实没有那么难,难的是在故障发生时做出正确的取舍。当时我们其实还可以选择继续加超时、加线程数来扛流量,但那只是在延长系统的生命力,并不会改变它必然崩溃的结局。真正让这个平台变得稳定的,是那一次果断的异步化改造。

如果你的团队也正在做一个 Agent 平台,或者准备把现有服务改造成支持多步骤 Agent 任务的形态,我真心建议提前看看自己的架构里有没有这些隐患:同步等待模型调用,没有任务队列,没有幂等设计,没有任务级别的监控。这些看不见的问题,往往比看得见的代码缺陷更容易在线上给你致命一击。希望我这次的踩坑过程,能让你少走几步弯路。

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

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

立即咨询