
上午十点十六分手机像抽风一样连续震动。我打开告警平台一看AI服务线上响应异常红灯亮起P95延迟从平时800毫秒一路涨到了6秒错误率从0.5%飙到12%。更麻烦的是这个AI服务不只是一个问答接口直播间智能客服、文章摘要、代码助手、内部知识库检索全都挂在同一条链路上客服群里的用户反馈已经炸开了锅。这条线上故障并不罕见但处理过程值得完整复盘。AI服务和普通Web服务最大的区别在于它前面挂着一个你完全控制不了的外部依赖——大模型接口。你可以在自己的服务里做一百层防护但只要上游慢下来线程池、缓存、重试策略里任何一个环节稍微不克制就会把一次小抖动放大成雪崩。这篇内容就从这次真实故障出发把定位过程、根因、应急手段、长期修复和排查清单一次讲清楚适合正在运维AI应用、做后端开发或者负责线上稳定性的人参考。1. 故障现场AI服务突然“卡住”了1.1 第一个小时告警、看板、日志三连看收到告警之后我做的第一件事不是急着进服务器看日志而是先把监控面板看完整。告警平台给的信息太碎只有进入看板才能看到全局。第一屏看到的是三个关键指标——QPS、响应时间、错误率——的组合非常古怪QPS基本没有涨跟平时同时段差不多但P95响应时间从800ms一路爬到了6s错误率从0.5%涨到12%。这个组合意味着什么如果是流量大了导致扛不住QPS一定先涨但现在QPS没变服务却变慢、开始报错说明问题出在“处理能力”而不是“流量压力”。顺着这个思路我把CPU、内存、GC、活跃线程数这些饱和度指标翻出来CPU从平时30%升到了85%内存正常但线程池活跃线程数已经打满任务队列在疯狂积压。到这里基本可以先把“网络被攻击”这类方向排除掉把问题收敛到应用本身的处理链路上。接下来看日志。十点四十分我从日志系统拉了一个5分钟窗口的错误分类统计读取外部接口超时占47%线程池任务被拒绝占35%数据库连接获取超时占12%剩下的是些杂项异常。这三类错误放在一起其实已经能拼出一个粗略故事外部接口变慢请求占着线程不放线程池满了新的任务进不来连带着数据库连接也被拖垮。当然这只是推断要确认还得往下追。1.2 影响范围评估一个接口抖动半条业务线遭殃这次故障让我感触最深的一点是AI服务一旦挂掉波及面往往比普通业务接口大得多。普通接口挂了最多是某个页面功能不可用AI服务挂了意味着智能客服不回复、摘要生成不出来、知识库检索全部超时用户在页面上一遍遍地刷新每一遍刷新都在继续往里打请求给已经烫手的链路再浇一把油。一边排查一边做了个影响面清单线上智能问答、文档助手、代码辅助工具、客服机器人共计十多个入口依赖同一个AI服务其中客户直接可见的就有六个。这种局面下最优选择不是先追求“修好”而是先判断“要不要切降级、能不能挺五分钟”。当时我在监控上看了一眼核心问答的缓存命中率平时大概35%故障期间因为大量请求打不到模型层缓存命中率被推高到接近70%说明已经有一部分流量在靠缓存硬撑。这是一个非常重要的信号——只要缓存还能撑住一部分我就可以先把降级方案准备好而不是马上动刀切流量。影响面评估做完心里就有底了最坏情况下把所有问答入口切到兜底答案业务损失可接受风险可控后才放心进入第二阶段的深度排查。排障最忌讳的就是上来就放大招把还在正常工作的链路也一起切了那才是真正的灾难。2. 从入口到根因三层排查记录2.1 入口网关慢不是从网络来的既然是线上响应异常第一步肯定要看流量从哪进来、在哪个环节变慢的。我打开网关层的数据发现一个反直觉的现象网关没有报5xx入口请求量也没有明显波动但网关转发到应用服务的平均耗时从数十毫秒涨到数百毫秒而且这部分的耗时分布在慢慢拉长。网关本身不处理业务逻辑它的耗时变长只有两种解释要么是下游应用处理得慢要么是连接池等资源拿不到。再结合前面看到的线程池打满基本可以判断应用侧出了问题。这一步没有太多悬念但它的价值在于帮我排除了网络抖动、DNS解析、证书过期这些基础设施层面的干扰项——因为如果网关转发到应用的时间也变长而应用本身CPU不高那才要考虑跨机房网络或负载均衡配置问题。网关层的操作不多但有一件事值得做确认健康检查机制是否还正常。故障期间如果健康检查用的是比较宽松的接口即使负载已经很高网关依然会把流量继续打进来导致故障实例被反复压榨。我当时检查了一下健康检查接口反应还算正常所以没有触发摘除实例流量分布也没出现倾斜。如果你遇到同类故障这一条一定要看它决定了你后续是把所有实例“拖死”还是能靠网关摘除部分节点来降低压力。2.2 应用层线程池耗尽阻塞的真凶真正让人头皮发麻的是应用日志。我看到错误分布里线程池拒绝占35%于是立刻打开线程池的监控曲线活跃线程数在十分钟前已经顶到上限队列从几百条一路涨到几万条。这里需要一点理论背景——大家常说的Littles Law通俗讲就是系统里同时处理的请求数约等于QPS乘以单个请求耗时。平时这个AI服务QPS在300左右单请求平均耗时800ms同时占用的线程数差不多240个线程池配了300还有余量一旦外部接口的耗时从1秒涨到6秒同样QPS情况下占用的线程数就要接近1800个线程池不爆才怪。线程池耗尽带来的连锁反应不仅仅是延迟升高它会像一个坏掉的水龙头一样把后续所有请求都堵在池子外面。当时队列里积压了几万条任务新请求要么排队等要么直接触发拒绝策略抛异常。更隐蔽的是数据库连接池也受到了牵连——因为线程拿不到执行机会连接池里的连接长时间被占用不释放很快也出现获取连接超时。这就是前面说的“一个慢依赖拖垮整个服务”的真实写照线程池越满能处理的请求越少延迟越离谱错误率越高三个指标互相踩踏形成正反馈。看日志时还有一个细节值得注意错误类型不能只看顶层要看堆栈和异常来源。日志里大量的TimeoutException来自同一个HTTP客户端调用目标指向外部大模型接口。这一条直接把排查方向从“为什么系统满了”引向“为什么外部调用这么慢”为链路追踪提供了明确的下一站。2.3 链路追踪85%的时间花在外部模型接口上链路追踪在这类故障里是定海神针没有它只能靠猜。我打开SkyWalking按时间倒序挑了一个比较典型的慢请求展开它的Span列表整个请求的总耗时是6.3秒其中调用外部大模型接口的跨度占了5.4秒占比大概85%剩下的时间主要花在RAG检索的向量数据库查询上。看到这个分布心里基本已经确认了主路径的问题所在——瓶颈不在业务代码而在外部依赖。进一步看外部接口的调用记录发现一个很有意思的现象同样的外部接口平时P95耗时1.5秒故障期间P95涨到9秒而且开始出现一批HTTP 429响应。429这个状态码代表限流看到它反而松了口气因为这说明外部依赖没有完全宕掉只是节点状态变差加上限流策略被触发。为什么会被限流这就要回到我们自己的代码里找原因了因为429出现之前我们的重试已经把外部接口打出了比平时多出数倍的请求量。链路追踪的价值在于它能把“慢”定位到具体的一跳但它没法告诉你“为什么这一跳慢”。所以看完链路之后我做的下一件事是记录外部接口的供应商状态、告警面板和我们自己请求的分布然后带着“我们自己是不是过度重试”这个假设回到代码审查里。这一步才是根因确认的真正开始。3. 根因确认与处置动作3.1 根因链上游抖动、重试风暴、限流循环放大把所有线索拼在一起后这次故障的完整根因链就清晰了外部大模型供应商某个区域的节点出现抖动P99延迟升高部分请求开始变慢或超时我们的HTTP客户端读取超时设成了30秒请求一旦变慢线程就这么傻等30秒不释放超时后代码里的重试策略是“立刻重试最多重试3次”瞬时请求量翻了数倍上游收到大量重复请求触发限流返回429限流又让更多的请求失败失败的请求继续重试线程池被占满任务队列积压新请求等待时间指数上升队列积压触发拒绝策略错误率飙升健康检查开始不稳定部分节点被摘除剩余节点压力进一步加大。这条链路看完会发现最开始的诱因确实是外部环境不稳定但真正把故障放大到雪崩级别的是我们自己的重试策略、超时设置和线程池隔离方案。外部抖动我们控制不了但“抖动会不会变成雪崩”完全是我们能控制的。这个认知非常重要也是这次复盘里大家最后达成共识的结论。为什么很多团队会在这个场景里栽跟头因为单看日志时问题全都指向外部依赖“这是上游的问题”这种判断太合理了很容易让人止步于甩锅。但一个健康的依赖调用设计应该允许上游抖动自己扛住并优雅降级而不是跟着一起崩。后来重新审查代码时才确认这套调用逻辑在流量小时怎么跑都没问题但从来没人算过它在线程池满、队列积压的情况下会怎样缺少的是对“最坏情况”的设计。3.2 应急处理的正确顺序应急处理最忌讳“一上来就扩容”。故障发生后最容易想到的就是加机器但这次如果先扩容效果会很差原因很简单瓶颈不在CPU和内存而在线程被外部连接长期占用加机器只是增加更多被占死的线程还会把更多重试请求打向上游进一步触发限流。所以当时做的顺序是这样第一步限流。在接入层把总入口QPS限制到正常值的七成左右宁可让一部分请求快速返回失败也不能让所有请求都堵在系统里死等。这一步的目的是保护系统不再进一步恶化。第二步改超时。把外部接口的读取超时临时从30秒降到5秒同时给这个接口开熔断10秒窗口内失败率超过50%就快速失败不再等待。改超时是“釜底抽薪”每一个被占住的线程能更快释放出来线程池才能重新“转”起来。第三步降级。把智能问答切到热门问题缓存答案检索部分暂时用关键词匹配兜底。这一步是为了保证最核心的用户体验不受太大伤害哪怕答案质量打折也比一直转圈要好。第四步关掉激进重试。把失败重试从“立即重试3次”临时改成“不重试直接快速失败”。这一步当时争议比较大担心用户体验下降但实际做下来快速失败配合前端自动刷新客户感知反而比长时间转圈好。五分钟后延迟从6秒降回1.8秒十分钟后错误率从12%回到2%以内业务恢复正常。整个应急过程大概30分钟真正的处理动作就是“限流、降级、熔断、关重试”这四板斧没有一个是复杂的操作但顺序错了或者哪个都没做都可能导致故障继续放大。3.3 超时与重试参数事后重新设计了一遍故障恢复后长期修复才是重头戏。做的第一件事是重新设计外部依赖调用的超时和重试参数。这一步不能拍脑袋要对得起线上数据。超时方面用“连接超时”和“读取超时”分开配置。连接超时设3秒就够了TCP建连一般几十毫秒超过3秒说明网络大概率有问题读取超时不能太长根据平时P99的1.5秒留了余量设成10秒。原来的30秒读取超时等于没有超时一个慢请求可以占着线程半分钟线程池就是这么被耗死的。Java的HttpClient那次修改的核心配置如下HttpClient client HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .build(); // 读取超时通过请求级别控制 HttpRequest request HttpRequest.newBuilder() .uri(uri) .timeout(Duration.ofSeconds(10)) .POST(BodyPublishers.ofString(payload)) .build();重试方面原则是“只对连接失败重试对读取超时和限流不重试或退避重试”。连接失败说明请求根本没发出去重试是安全的读取超时说明上游已经收到了请求重试可能造成重复处理而且会叠加压力。限流更不用说429意味着上游明确告诉你“别打了”这时候还硬冲只会被限得更狠。如果业务确实需要重试必须用指数退避加抖动比如第一次等500ms、第二次等1s每次间隔在基础值上乘2再随机加一点偏移避免多个实例同时重试造成请求高峰。除了超时和重试还为外部大模型接口加了一层隔离。核心思路是不让外部依赖的慢请求把整个应用的线程池占满。实现上可以用独立线程池或者信号量池子满就快速失败把降级逻辑交给熔断器。当时用信号量隔离加Resilience4j的熔断器效果不错虽然引入了点代码复杂度但值回票价。4. 复盘与长期治理4.1 三个薄弱环节及其修复方案把这次故障沉淀下来归纳出三个典型的薄弱环节后面很多AI服务排障都用得上。第一个是外部依赖没有容错设计。调用大模型接口时只考虑了“正常返回”和“抛异常”两种情况没有考虑“变慢”“部分失败”“限流”这些中间态。修复方案是加超时、熔断、降级并按照依赖的P99而不是平均值来配置阈值。第二个是重试策略过于激进。立即重试、无上界重试、对所有异常都重试三重雷全踩了。修复方案是区分错误类型、设置重试上限、使用退避和抖动。第三个是线程池没有隔离。所有调用共享同一个线程池一个慢依赖就能拖垮整个服务。修复方案是用独立线程池或信号量做隔离确保最坏情况下其他功能还能响应。这三个问题点用一张表做了对照记录问题点代码表现修复方向外部依赖容错缺失无熔断、无降级判断引入熔断器按P99阈值触发重试策略激进立即重试3次无退避只重试连接类失败指数退避抖动线程池未隔离全局共用线程池独立线程池/信号量隔离这张表成了团队后续做外部依赖评审时的通用模板任何新需求接外部接口先过一遍这三项比等故障降临再补救便宜得多。4.2 30分钟快速排查清单另外一个很有价值的产出是一份可以复用的30分钟快速排查清单。排查线上响应异常最怕没有顺序地乱翻下面这条路径验证过很多次能覆盖绝大多数情况第一步看大盘。打开监控先看QPS、响应时间、错误率三个指标的组合关系。QPS没涨但错误率涨优先查处理能力QPS涨了才出问题优先查容量。第二步看饱和度。CPU、内存、GC、线程池活跃数、连接池活跃数找到哪个指标先被打满。GC耗时异常要看Full GC频率线程数打满就要沿着线程栈找到底在等什么。第三步看日志。按五分钟窗口拉错误分类统计重点看异常类型分布和堆栈定位是外部依赖、数据库还是业务代码。日志里频繁出现同一个外部调用的超时直接进下一步。第四步看链路追踪。挑一个典型慢请求看Span耗时分布找出占比最大的那一跳。这一跳要么是外部接口要么是慢SQL要么是网络转发九成问题都在这几个位置。第五步看外部依赖状态。查供应商的状态页面、限流情况和我们自身请求的重试次数。如果外部服务本身有波动优先做好自己的重试退避和降级而不是干等。第六步准备复现。如果线上不好动手就在测试环境用Mock制造一个“慢上游”用wrk或locust压一下验证假设。这个方法能确认修改是否有效也能提前暴露同类问题。这个清单贴在团队交流群里很多同事说排查时间从原来的几小时压缩到了半小时以内。排障的经验其实没有太高深的东西先把流程固定下来每个人都按同一条路走效率自然就上来了。4.3 AI服务排障与普通Web排障的三点差异平时大家排的Web故障大多是数据库慢查询、缓存穿透、GC卡顿这些但AI服务的排障逻辑不大一样总结了三处明显差异第一依赖链更长。AI服务往往串着大模型API、向量数据库、RAG检索、文本预处理、流式输出等多层依赖任何一层慢都会反馈到最终响应上。排障时不能只盯最后一跳要用链路追踪一层层剥洋葱。第二上游是黑盒。普通Web服务的内部依赖大多是自己团队维护的日志、指标随时可以查大模型API是第三方黑盒你只能看到请求和响应看不到它内部的负载情况而且它还没有针对你的响应时间承诺。所以设计上必须假设它会慢、会限流、会挂在这个前提下做兜底。第三语义降级复杂。普通服务挂了可以降级到静态页或缓存数据用户能理解AI服务输出的是“内容”降级成固定模板会显得很蠢继续调用又可能超时。这块需要产品和技术一起提前定义“什么算可接受的降级答案”而不是故障时临时想。这三处差异决定了AI服务的稳定性本质上是一个面向不确定性做工程设计的问题。代码上多写几个try-catch远远不够得把依赖治理、资源隔离、降级预案当成一等公民来对待。4.4 一些掏心窝的运维建议最后分享几条踩坑踩出来的建议每一条都是拿线上事故换的。第一条429一定要告警。很多人只对5xx和超时建告警忽视429限流。实际上429往往是上游抖动和自身重试问题叠加的信号比5xx出现得更早盯住它能提前处理很多隐性故障。第二条外部调用的日志要记录耗时分位值和错误码而不是只记“成功或失败”。不然出了故障连“是从什么时候开始变慢的”都说不清楚。后来把所有外部调用放到统一日志平台按分钟统计P50、P95、P99观测能力强了很多。第三条定期做故障演练。平时一切正常时不会去验证超时和重试策略真出故障就会发现各种问题。建议每个季度挑一个外部依赖在测试环境用故障注入工具把延迟拉高到三倍看自己的服务能不能扛住并优雅降级。这个习惯救过我们不止一次。第四条别在深夜一个人排障。这次故障如果只是改几个参数可能很快但“改完会不会引入新问题”需要团队一起判断。线上事故不可怕可怕的是处理事故的人手里没有一张完整的依赖图和预案表碰到问题全靠临场发挥。排完这次故障的第二天我做的第一件事就是把所有外部依赖的超时、重试和熔断参数整理成一张表拉上后端和产品一起过了一遍。说实话线上只要扛过一次雪崩你就会深刻理解AI服务的稳定性不是靠模型多智能而是靠每一个HTTP客户端够不够克制靠每一个开发者在写调用的时候有没有想过“这条依赖要是慢了怎么办”。如果这篇内容对你们解决类似的线上响应异常有帮助动手排查前建议先从重试策略和超时设置看起大概率能少走不少弯路。