首页 / 资讯中心 / 文章详情

Agent平台线上故障复盘:外部慢调用如何引发大面积超时

Agent平台线上故障复盘:外部慢调用如何引发大面积超时 ★ FEATURED ARTICLE
告警是从凌晨四点半开始响的。当时我正在睡觉手机在床头柜上震了第一下。我没醒。等到第二次震动的时候我翻身看了一眼——某智能客服项目的 Agent Platform生产环境报大面积任务超时。我瞬间清醒了。这个平台不是简单的定时任务系统它承担的是客服工单自动分类、语义识别、话术生成这一整条 AI Agent 链路的编排。外部大模型接口调用、内部知识库检索、业务系统回写全部串在一个工作流里。平时跑得好好的P99 虽然偶尔抖动但不会触碰超时阈值。偏偏这一天从凌晨四点半开始任务积压率直线往上走上游服务开始重试重试又把入口流量放大整个链路的负载被顶到历史高位。标题里说“踩了一次真实的线上超时故障”其实这个措辞很客气。真实情况是我们花了大半天才把根因链完全捋清楚中间还误判过一次方向甚至一度怀疑是数据库连接池的问题后来才发现问题的核心根本不在数据库。我一直认为分布式系统里最难排查的不是“报错类故障”而是“性能劣化类故障”。报错类故障有堆栈、有错误码、有明确的异常位置顺着链路追踪一路查下去总能找到那个抛异常的节点。性能劣化不一样所有节点看起来都是正常的没有报错没有异常日志但就是慢。慢在哪里为什么慢哪个环节最先开始劣化才是整个排查过程最磨人的部分。这篇文章就围绕这次线上故障把项目背景、故障触发的链路、排查思路、最终定位、修复合规方案以及事后的架构调整全部梳理一遍。重点不是什么高深理论而是一套可复用的排查方法论怎么看监控、怎么抓线程、怎么验证根因、怎么设计容错。对正在做 Agent 平台、任务编排系统、或者任何重度依赖外部接口的服务端开发者应该都有参考价值。1. 项目背景与 Agent Platform 的核心链路先说清楚这个平台是干什么的不然后面故障分析会看得一头雾水。我们做的 Agent Platform本质上是一个面向智能客服场景的 Agent 编排与执行平台。它的核心职责不是做某个单一的 AI 功能而是把多个能力编排成一条完整的处理链路。一个典型的工单处理流程长这样业务系统推送一条用户工单到平台的消息队列平台的任务调度模块从队列中拉取消息生成一个 Agent 执行实例Agent 执行实例启动后先调用一个意图识别服务内部服务判断这条工单属于什么类型——售后、投诉、咨询还是别的意图识别完成后Agent 根据意图类型组装 Prompt调用外部大模型接口生成处理建议大模型返回后Agent 再调用知识库检索服务把相关的知识条目拉出来附在处理建议后面最后通过回调接口把完整的处理结果返回给业务系统。整个流程看起来是串行的但每个环节之间都有异步化处理。消息队列负责削峰填谷Agent 执行器负责调度编排外部接口调用统一走一个 HTTP 网关层网关层做超时控制、重试策略和熔断降级。平台的整体架构设计在正常流量下是没问题的——单条任务耗时预估在 3 到 8 秒之间P99 控制在 10 秒以内这个指标在项目初期验证过稳定性还行。问题出在一个我们忽略了很久的薄弱环节外部大模型接口。这个外部接口是商用的生成式 AI 服务供应商那边有独立的限流策略和负载均衡机制。我们这边的 Agent 平台只是它的一个普通调用方没有协商过任何容量保障。也就是说如果供应商的服务出现波动或者我们这边的调用规模增长到一定程度接口的响应时间就会开始劣化从“偶尔慢”变成“持续慢”甚至出现部分请求直接挂起不返回。当时设计平台的时候大家把注意力放在了自己的服务治理上超时时间设得够不够、重试策略要不要加退避、队列长度怎么配置、线程池参数怎么调。这些自有服务层面的治理确实是必要的但外部依赖的不可控性才是这次超时故障里最核心的变量。这次故障就是从外部大模型接口的响应劣化开始的。2. 故障经过与前期表现故障不是突然爆发的有一个渐进的过程。复盘的时候我把监控数据拉出来看发现整个劣化进程大概持续了 40 分钟左右从凌晨四点出头开始到四点半左右触发大规模告警。前 20 分钟是温和劣化期。外部大模型接口的 P99 响应时间从平时的 5 秒一路爬升到 15 秒但这时候 Agent 平台的任务执行成功率还没有明显下降只是处理变慢积压的任务开始增多。平台的任务队列长度缓慢上涨但还没触碰告警阈值。中段 10 分钟是恶化加速期。大模型接口的 P99 突破 20 秒代理网关层开始出现大量等待的线程。由于网关层持有工作线程等待上游接口返回线程池开始逼近核心线程数的上限。新请求进入后网关的线程池已经拿不到空闲线程只能在队列里排队等待。后面就是爆发期。四点半左右好几路监控同时告警任务积压超过阈值、HTTP 网关线程池活跃线程数到达峰值、业务系统的回调请求开始大面积超时。我在排查时看到的第一个异常表象是任务执行成功率下降。直观感受上平台好像在“拒绝执行”新任务但其实不是拒绝而是线程池饱和后处理速度跟不上消费速度任务队列越堆越长。上游业务系统那边更难受。回调接口超时了上游会按自己的重试策略发起补偿。重试的请求又打到 Agent 平台的 HTTP 网关进一步加剧线程池的拥堵。这个循环一旦滚起来系统会进入一种“假死”状态——所有线程都在等待外部接口返回新请求排不上队系统吞吐趋近于零但 CPU 和内存使用率都不高完全不是传统意义上的资源耗尽型故障。这种故障最迷惑人的地方就是这里从 CPU、内存、磁盘这些基础指标看系统是健康的从业务指标看系统已经瘫痪了。3. 排查过程与关键证据整个排查过程大致可以分成三个阶段。每个阶段的思路、证据和判断都不一样我自己复盘的时候觉得这个分阶段的过程本身就是值得记录的干货。3.1 第一阶段从健康指标排查排除基础设施问题告警响后的第一时间我们首先看的是最基础的机器指标——CPU 使用率、内存占用率、磁盘 I/O、网络出入带宽。这套条件反射式的检查顺序还是对的因为基础设施层面的问题资源耗尽、磁盘打满、网络异常等会直接体现在这些指标上而且定位速度最快。结果确实让人意外所有机器的 CPU 使用率都不到 20%内存稳定磁盘 I/O 正常网络带宽也没有打满。一个已经大面积报任务超时的系统基础资源指标居然全部正常这说明问题不在资源层面而在链路协调层面。于是我们把视线转向中间件。消息队列的消费 lag 在持续拉大——这个指标间接反映了消费者处理能力下降。数据库的连接池使用率也在上升不过还在可接受范围内。缓存服务的命中率和延迟都正常。到了这一步基础设施层基本排除。问题大概率出在应用服务内部或者是某个外部依赖上。当时我们做了一个错误的初步判断因为数据库连接池使用率在上升队里有人怀疑是数据库慢查询导致连接被长时间占用。这个判断后来被证明是错的但在当时它确实是概率较高的一个方向。3.2 第二阶段调用链追踪定位瓶颈环节我们快速打开了链路追踪系统平台内置了全链路 Trace跑了一个正在积压的任务去查它的完整调用链。因为 Agent Platform 的任务执行链路很长消息队列 - Agent 执行器 - 内部意图识别 - HTTP 网关 - 外部大模型接口 - 知识库检索 - 业务系统回调。每一段都会产生 Trace 数据所以直接查 Trace 是最快的方式。查出来的结果比较有指向性绝大部分 Trace 的耗时大头都在“HTTP 网关调用外部大模型接口”这一段。内部服务的调用耗时都在正常范围只有这一个外部调用的耗时异常离谱——P99 已经到 20 秒以上甚至有一部分请求超过 30 秒还在等待响应。更关键的一个细节是Trace 里显示有一部分外部调用根本没有任何返回记录。这说明外部接口不仅有“慢”的问题还有“挂起”的问题——上游接了请求但是迟迟不吐响应或者响应在网络传输过程中丢失了。最终我们在这块找到了最核心的证据HTTP 网关层用的是默认的 HTTP 客户端配置——连接超时 5 秒读取超时 10 秒但连接池的 MaxConnections 只够支撑日常流量的 1.5 倍左右。外部接口一慢大量请求同时占住池中的连接不释放可用连接数迅速归零后进来的请求全部卡在获取连接的状态上。这就是经典的连接池饥饿问题不是没有线程而是拿不到连接不是网络不可达而是连接被慢请求全部占满。3.3 第三阶段线程快照验证锁定最终根因为了验证上面的推断我们对网关服务做了三次线程 Dump间隔 10 秒。拉出 Dump 分析后发现大量业务线程都卡在一个同样的调用栈上——httpclient.execute()-getConnection()-waiting for available connection。这说明线程不是在执行什么耗时的业务逻辑而是在获取连接池连接的阶段被阻塞了。到这里根因链算是完整闭环了外部大模型接口响应劣化P99 从 5 秒涨到 20 秒以上导致大量 HTTP 请求长期占用连接资源 - HTTP 客户端连接池被慢请求占满 - 新请求获取不到连接线程阻塞等待 - HTTP 网关线程池饱和 - Agent 执行器的任务消费速度下降 - 消息队列消费 lag 拉大 - 业务系统回调超时 - 上游重试放大流量 - 整个链路进入正反馈恶性循环。我们最初怀疑的数据库连接池实际上只是整个循环中的一个副作用。数据库连接池使用率上升是因为上游重试带来的额外流量打到了数据库上而不是数据库本身出现了性能问题。这个排查过程给我们的教训很直接现象可能出现在多个环节但根源往往只有一个。顺着调用链从源头到末端完整捋一遍比在某个碎片指标上反复怀疑有效得多。4. 修复方案与架构调整根因找到了修复方案做起来就顺理成章。这里有一个基本原则不要只针对表象做修补要把整条链路的脆弱点全部找出来逐个加固。4.1 第一层外部调用超时与限流策略调整把 HTTP 网关层的连接池配置和超时配置都做了全面调整。首先是连接池容量。之前 MaxConnections 只有支撑日常流量 1.5 倍的水平现在调整到 4 倍并且增加了每路由Route粒度的独立连接池配置避免某个接口的异常占用整个连接池。其次是超时策略。连接超时保持 3 秒不变连接建立阶段不应该等太久但读取超时从原来的 10 秒提升到 15 秒同时增加了整体请求超时从发起请求到读取完整响应的总耗时上限20 秒。这个策略调整的逻辑是给慢接口留一个合理的处理时间窗口但绝不允许无限制地等待。最后是限流策略。在网关层针对外部大模型接口增加了独立的信号量限流最高并发调用数做了硬性限制。超过并发上限的请求直接快速失败而不是排队等待。快速失败的请求会走平台内部的降级逻辑——返回一个兜底话术模板而不是无限期地等待大模型响应。// 伪代码示例针对外部大模型接口的独立限流 Semaphore limiter new Semaphore(50); CompletableFutureResponse future CompletableFuture.supplyAsync(() - { if (!limiter.tryAcquire(100, TimeUnit.MILLISECONDS)) { throw new FastFailException(并发超限); } try { return httpClient.execute(request); } finally { limiter.release(); } }); // 整体超时控制 future.orTimeout(20, TimeUnit.SECONDS) .exceptionally(ex - buildFallbackResponse());这段代码体现的关键思路是限流不是为了防止外部接口被我们打爆而是为了保护我们自己的处理链路不被外部故障拖死。快速失败降级是一种比无限等待更主动的容错方式。4.2 第二层重试机制的退避与熔断隔离原来的重试策略是失败后立即重试一次间隔 1 秒最多重试 2 次。这次故障暴露的问题非常明显当外部接口已经出现持续劣化时立即重试不仅没有意义反而会把请求更密集地打在同一个故障源上加剧连接池压力。调整后的策略是读取超时或连接池获取失败时不重试这类失败大概率是链路拥堵导致的重试只会加重拥堵只有在连接建立失败或网络层明确异常时才重试且使用指数退避策略1 秒、2 秒、4 秒并限制最大重试次数为 3 次。同时在网关层增加了一个简单的熔断器当外部接口的错误率达到 40%含超时时熔断器打开直接快速失败所有请求不再尝试真实调用。熔断后每 10 秒放行一小部分探测流量50 个请求确认接口恢复后再逐渐释放全部流量。这个熔断设计不是标准库实现的复杂版本就是一个基于滑动窗口的状态机但对于解决这类外部依赖故障已经足够有效了。4.3 第三层Agent 执行器的排队策略调整Agent 执行器作为整个平台的核心调度模块原本的设计是无界队列加固定线程池。故障期间任务直接在队列里堆积等前面的任务全都耗时结束后面的任务才能开始处理——这在感觉上就像是“系统卡住了”。调整后改成有界队列队列容量设为峰值流量的 2 倍。队列满时新到达的任务直接进入溢出拒绝策略先尝试将任务落盘保存持久化到本地文件或另一个备份队列同时立即返回一个应答给业务系统告知“任务已接收处理稍后继续”。这样既不会丢任务也不会让内存被无限增长的队列占满。同步场景下原有的串行调用方式改成了异步编排每一步执行返回一个 Future通过编排框架把它们串联起来整体的超时控制也更精细。这个改动对吞吐的提升虽然不是几何级数的但确实让平台在慢依赖环境下的表现稳健了很多。4.4 第四层入口流量控制与优先级隔离最后一个调整是在平台的入口处增加全局流量控制。不同业务系统的请求优先级不一样——实时用户会话相关的任务要求低延迟批量处理类的任务则可以容忍更高的延迟。通过把两者分流到不同的处理队列和线程池避免批量任务在故障时把实时任务的资源也抢占掉。这种隔离策略可以和“高速公路上的应急车道”做类比正常情况下大家都走一样的路但一旦发生拥堵应急车道必须保持畅通让关键车辆能够通过。没有隔离机制所有业务在故障下完全混跑很可能因为一个非核心任务的积压拖垮全平台的核心任务。5. 故障复盘与避坑清单故障处理完修复方案也上线了但真正有价值的工作才刚刚开始——复盘。我们复盘时整理出了一份清单每条都对应了一个差点踩进去的坑或者已经踩进去的坑。这里挑几条最有代表性的分享出来。5.1 连接池饥饿是慢依赖故障的放大镜这次故障最核心的放大器就是连接池饥饿。外部接口慢本身只是“慢”不会立刻导致系统崩溃但连接池一旦被慢请求占满系统就会从“处理慢”变成“完全阻塞”。避坑建议所有依赖外部 HTTP 接口的连接池配置都必须预留足够的冗余空间并且要按路由隔离连接池。同时工具层面定期检查线程 Dump看看是否存在大量线程阻塞在连接获取状态。这类检查应该成为每次大促前的固定检查项而不是故障发生时才想起来看。5.2 慢调用比报错更难发现必须用 Trace 定位报错类故障有明确的错误日志定位相对简单。慢调用不一样没有报错没有异常只有耗时的增长而且耗时增长是渐进的一开始可能完全感觉不到。避坑建议Agent 平台以及其他长链路系统必须接入全链路 Trace并且对每个子调用的耗时分布有可视化的监控看板。不能只看整体的耗时 P99要看每一跳的耗时变化。谁从“正常”到“变慢”谁从“变慢”到“不可用”这个演变过程是定位根因的关键线索。5.3 重试策略不是越多越好要设计退避与熔断这次故障中上游服务无脑重试的破坏力比我们想象中更大。外部接口一慢上游重试重试流量叠加在原本已经拥堵的链路上直接冲垮了后续环节。避坑建议重试必须设计退避机制同时对“快速失败”的请求连接池获取失败、限流拒绝等不应该重试。额外增加熔断器当错误率达到阈值时直接把故障源隔离掉让系统有机会恢复。熔断器的阈值要基于对接口正常错误率的实际观测来配置不要拍脑袋。5.4 健康检查要看业务指标不能只看资源指标这次故障中CPU、内存、磁盘全都没问题但系统已经处于半瘫痪状态。如果只看基础设施指标很容易得出“系统正常是外部问题”的错误结论。避坑建议监控体系必须同时覆盖三个层次——资源指标CPU、内存、磁盘、网络、中间件指标队列长度、连接池使用率、消费 lag、业务指标任务成功率、P99 耗时、积压数。不同层次的指标相互印证才能准确判断系统的真实状态。5.5 故障演练不能只练“宕机一键恢复”绝大多数团队做故障演练练的都是“机器宕机了怎么快速恢复”“数据库挂了怎么切换”。这类演练价值不小但覆盖不到性能劣化型故障。避坑建议主动在测试环境模拟外部依赖变慢的场景——把外部接口的响应时间人为增加到 20 秒以上观察全链路的反应。这种演练能提前暴露连接池参数、线程池配置、队列容量等设计上的薄弱点比等故障真实发生时再排查要划算得多。6. 经验总结与实际操作体会故障处理完了但有些体会是写进文档里的复盘也装不下的。第一个体会是外部依赖的控制力是有限的。我们的自有服务再怎么治理端口监听、日志采集、服务注册这些环节都可以做到精细化但像大模型接口这类外部商用服务我们既控制不了它的容量规划也控制不了它的发布节奏。这种情况下能做的就是把自己的容错边界设计得足够宽。之前我写代码的时候习惯给外部调用设置偏长的超时时间总觉得“多等一会儿没准就返回了”。这次故障彻底改变了我的习惯。长超时在单次请求上看起来无害但在大规模并发场景下它会成为资源耗尽的最大加速器。合适的超时时间不是“对方可能需要多久”而是“我们系统最多能容忍等多久”。这个逻辑完全不一样了。第二个体会是报警阈值要设置得比故障容忍度更敏感。这次故障的报警触发时间比我们实际希望的系统介入时间晚了 20 分钟左右。如果当初把任务积压的告警阈值设得更低一些比如积压数超过日常均值 30% 就开始告警我们至少能提前 15 分钟开始介入排查减少故障对业务的影响时间。第三个体会是故障出现后不要急着改代码。这次故障里最危险的一步是我们差点在第一步就动手改数据库连接池参数——如果当时真这么干了不仅解决不了问题还会掩盖根因层的证据。正确的顺序是先通过观测工具把根因链完整拼出来确定主因和次因再动手设计修复方案。消除根因永远优先于缓解症状。最后再分享一个小小的实操技巧排查线程饥饿的时候连续多次线程 Dump 比单次 Dump 更有价值。每次 Dump 间隔 10 秒左右如果多次 Dump 的调用栈信息高度重合大量线程卡在同一个位置那基本可以断定这是一个持续性的阻塞问题。如果每次 Dump 的栈都不一样更可能是 GC 停顿或者瞬时抖动类的问题。这次的故障就很典型——三次 Dump所有线程都卡在同一个获取连接的调用栈上完全一致的栈信息等于铁证。这次故障之后平台做了一次比较全面的架构加固。现在再回头看很多当初觉得“够用就行”的配置参数都重新按故障条件下的最坏情况做了一遍推演和调整。做 Agent Platform 这类任务编排型系统真正的考验不是它能跑多快而是当某个环节变慢甚至不可用的时候它能以多优雅的姿态继续保持可用。这个认知值得花一次线上故障的代价去换。
阅读完成 · 觉得有帮助?
咨询建站