
1. 故障表象与初步定位从“响应超时”到“问题收敛”先说结论这次AI服务线上响应异常故障从用户可感知的“AI问答一直转圈、偶尔报错”到我们最终定位根因并恢复前后花了大约40分钟。如果把后面复盘和加固的时间也算上其实折腾了一整个下午。但真正让系统恢复稳定的核心动作其实只有几个。这40分钟里踩过的坑、排过的错我觉得比故障本身更有分享价值。那天下午14:00刚过值班群里的告警就开始刷屏。监控显示AI问答服务的错误率从平时的0.1%左右飙升到23%P95延迟从800ms直接拉到12秒。用户侧反馈也很直接页面上的AI助手要么长时间“正在思考”要么直接弹“服务繁忙请稍后重试”甚至有一部分请求返回了空响应。我们这套服务是典型的AIGC应用——上层是AI网关服务中间接了大模型推理集群另外还挂了Redis做会话级缓存和热点问答缓存。当时我第一反应是模型推理服务出问题了但直觉不能当证据排查还得一层层来。我先把问题时间线拉出来14:02第一条告警14:05错误率曲线开始陡增14:07就有用户侧投诉进来。同时我确认了一个关键信息——13:50左右算法团队刚发布了一个新的模型版本并且在14:00完成了推理服务的滚动更新。时间点高度重合基本可以把嫌疑范围锁定在“模型服务发布”和“AI网关到模型服务的链路”这两个方向上。这里有个经验遇到线上故障第一件事不是急着看日志而是先拉时间线、确认变更窗口。绝大多数响应类故障都和最近的发布、配置变更、容量调整强相关。把变更和时间点对上排查范围能缩小一大半。反过来如果你上来就翻代码、看日志很容易被表面现象带偏。确认变更后我先在网关层做了一次快速验证直接用curl带同样的参数请求AI网关接口连续发20个请求发现大约有5个请求在5秒左右超时3个请求返回了空响应体。空响应这个现象很有意思——HTTP状态码是200但body里没有内容。这说明网关本身没有拒绝请求而是它后面的服务返回了“空”的东西或者在写响应过程中连接被中断了。于是我把排查重心移到网关上开始翻AI网关的access log和upstream日志。当时我意识到这个故障的定位不能只靠猜必须形成一条完整的证据链从客户端到接入层再到应用层、依赖层每一层都要有数据支撑。这也是后面能够快速定位根因的关键。2. 链路逐层排查从接入层到模型推理服务的完整证据链2.1 接入层先确认“是网关的问题还是上游的问题”接入层我们用的是Nginx做统一入口后面挂AI网关服务。我先看了Nginx的access log重点观察upstream_response_time这个字段。正常时候这个值基本在800ms到1.5s之间但故障期间出现了大量超过5s的记录部分甚至到了10s以上。同时我注意到Nginx日志里有不少“upstream timed out”的记录但HTTP状态码依然是200。这个细节很关键——说明Nginx已经把请求转发给了AI网关但是等不到网关的完整响应。换句话说问题出在网关这一层或者更后面而不是Nginx本身。我又顺手确认了Nginx到AI网关的连接数。故障期间Nginx与网关之间的TCP连接数从平时的200左右涨到了800多而网关所在Pod的CPU并不高只有30%上下。这就排除了“网关机器负载过高导致响应慢”的可能。CPU不高但连接堆积典型的信号是网关线程被卡住了不是在计算而是在等待什么。2.2 应用层线程池耗尽与“假死”现象接着我登录到AI网关服务所在的容器抓了一次线程dump。不看不知道一看吓一跳——大约40%的线程都阻塞在同一个调用链路上HttpClient.execute - getResponse - socketRead目标地址是内网模型推理服务的地址。这里要解释一下AI网关服务对模型推理服务的调用是同步阻塞式的每个请求占用一个Tomcat工作线程直到模型服务返回或者超时。我们配置的HTTP连接超时是3秒、读取超时是5秒。在正常情况下这个配置没问题但一旦模型服务变慢所有线程都会排队等着读响应。线程dump里还发现了一个更隐蔽的问题健康检查接口也超时了。我们的健康检查接口内部会做两件事——检查Redis连接是否正常以及向模型服务发送一个极短的探测请求。模型服务一慢健康检查的探测请求也跟着排队导致健康检查响应变慢。这直接引发了K8s的探针误判部分网关Pod被判定为不健康被自动摘除进一步加剧了剩余Pod的压力。这里有个重要的排查心法线程dump是最诚实的证据。它不会说谎也不会猜测。当你看到大量线程阻塞在同一个外部调用上基本可以断定瓶颈在下游而不是当前服务本身。后面我们做的所有动作都是围绕“保护网关线程、隔离下游故障”来展开的。2.3 模型推理服务GPU利用率不高但延迟翻了好几倍我绕到模型推理服务的监控面板上看指标。首先是GPU利用率只有40%左右看起来并不算高。但是再看另外两个指标排队请求数和平均解码耗时问题就暴露了。排队请求数从平时几乎为0一路涨到了几百。平均每token的解码时间从正常的30ms/token涨到了120ms/token翻了4倍。这就解释了为什么模型服务GPU利用率不高但延迟很高——大量请求堆在队列里GPU其实处于“稀疏计算”状态每个batch里有效计算的比例下降了所以才出现利用率低、延迟高的矛盾组合。进一步翻模型服务的日志发现了一个关键错误连续几次推理请求都报出“CUDA out of memory”的警告随后服务内部的显存分配器触发了自动碎片整理。新版模型在推理时申请的显存临时缓冲区比旧版大了大约30%在并发请求多的时候显存碎片化导致新请求无法申请到连续的显存块只能等待碎片整理完成这期间新请求全部进入排队状态。这也是一个经典坑大模型服务发布新版本不能只看GPU利用率还得看显存分配行为、batch策略、token吞吐这些细粒度指标。表面上看是“服务慢了”实际上是显存碎片化导致的有效吞吐下降。2.4 Redis缓存命中率跳水回源风暴雪上加霜AI网关对模型结果的缓存策略是这样的同一个问题、同一组参数在Redis里缓存2小时。正常情况下缓存命中率在95%左右但故障期间我打开缓存监控命中率掉到了60%以下。原因是两方面的第一热门问答缓存key的过期时间设置的是同一个固定值导致大量key在同一时间窗口集中过期形成缓存雪崩第二模型服务变慢之后原本能命中缓存的请求也因为上游超时没能写入新的缓存导致后续请求只能反复穿透到模型服务。这就形成了一个恶性循环模型服务慢 → 请求超时 → 缓存没有及时回填 → 更多请求穿透到模型服务 → 模型服务更慢。到这一步问题的全貌已经比较清晰了模型服务发布引发的性能劣化是导火索但真正把故障放大的是网关的超时配置、线程模型、重试策略、缓存过期策略这些看似“一直没问题”的细节。3. 根因确认超时窗口、重试风暴和缓存雪崩的三重叠加3.1 为什么“扩容模型服务”救不了当时的场面在排查过程中有同事提议直接给模型服务扩容多上几个GPU副本。我拦了一下理由是当时GPU利用率并不高说明瓶颈不在算力上而在显存碎片化和排队机制上。无脑扩容只会增加成本并不能解决“新请求申请不到连续显存”的问题。真正该做的第一步是止血——先在网关上做快速失败和流量隔离让故障不要继续向上游扩散。扩容这种事应该放在根因确认之后再决策。事实上后面我们只临时加了2个副本就足够支撑恢复了如果当时盲目扩容至少要多开4个副本浪费资源不说恢复时间也不会更快。3.2 根因一HTTP超时时间配置过短触发重试风暴我们把三个根因按影响面排了个序。第一个要说的就是AI网关的超时配置。我们当时的配置是连接超时3秒、读取超时5秒。对普通HTTP接口来说这个配置算合理。但AI问答是一个典型的长时间流式场景一次完整的模型推理往往需要10秒甚至更久。5秒的读取超时意味着只要模型服务稍有波动大量请求就会在“已经等了4秒、还差1秒就能拿到结果”的时候被强制掐断。更麻烦的是客户端SDK的重试机制。SDK配置了失败自动重试2次每次重试都会重新发起请求。也就是说一个原本只需要等8秒就能成功的请求因为5秒超时提前失败客户端立刻重试重试的请求又排队又超时再重试。原本1个请求的流量变成了最多3个请求的流量直接放大了对模型服务的压力。从数据上看故障期间AI网关在单位时间内对模型服务的请求量比正常时期高了约2.5倍。这多出来的部分基本都来自重试。这就是典型的“超时配置过短 自动重试”组合拳把自己的系统打垮了。3.3 根因二同步阻塞线程模型让故障沿着调用链向上传染第二个根因是架构层面的。AI网关服务对模型推理的调用是同步阻塞式的这意味着线程的生命周期和请求的生命周期完全绑定。哪怕模型服务只是慢一点比如从2秒变成8秒每个请求占用的线程时间就翻了4倍。线程池是有限的一旦线程耗尽新请求连“排队等线程”的机会都没有直接失败。更关键的是这种线程模型没有故障隔离的概念。模型服务的性能劣化会像个病毒一样沿着调用链向上传染模型服务慢 → 网关线程被占满 → 网关接口变慢 → Nginx连接堆积 → 客户端超时重试 → 流量放大。这就是为什么我们看到的错误率曲线不是缓慢上升而是断崖式飙升。我们用的线程池核心配置是200最大400配合一个无界队列。这个配置平时看着没问题但在线程被外部调用阻塞的时候无界队列会导致请求无限堆积内存压力和响应延迟同步恶化。这个教训后来直接写进了我们的架构规范里对下游依赖的调用绝不允许用无界队列必须用有界队列配合拒绝策略。3.4 根因三缓存过期时间一刀切雪崩时甚至来不及回填第三个根因是缓存策略的粗糙。当时Redis里热点问答缓存设置的TTL统一是7200秒所有key在同一时刻写入就会在同一时刻过期。当某个热点问题的缓存key在故障期间过期后第一波穿透请求到达模型服务时恰好模型服务正在排队这些请求全部超时缓存没有成功回填。于是第二波、第三波请求继续穿透形成持续的回源压力。这个问题的本质不是“缓存过期时间太短”而是“过期时间没有加随机扰动导致过期行为高度同步”。高同步意味着高并发穿透高并发穿透叠加下游故障就会把一个小问题放大成雪崩。修复其实很简单——TTL加一个随机偏移把过期时间分散到5分钟到2小时之间就能有效避免集中过期。3.5 三重因素如何叠加用一组数据还原故障放大过程我用一组模拟数据来还原当时的放大过程。假设正常情况下模型服务能支撑100 QPS单个请求延迟2秒超时率为0。故障发生后模型服务因显存碎片化导致有效处理能力降到40 QPS单个请求延迟变成8秒。此时网关的5秒读取超时启动约60%的请求超时失败。客户端SDK自动重试2次网关侧请求量变成原来的3倍即300 QPS。而模型服务实际只能处理40 QPS排队长度以每秒260个的速度增长。线程池400个线程很快被全部占满健康检查探针也超时Pod被摘除剩余Pod的负载进一步升高。整个过程从“模型服务性能劣化”到“整个AI问答入口不可用”只用了大约3分钟。如果没有监控告警这个故障的扩散速度还会更快。这就是我强调“三重叠加”的原因——单看任何一个因素都不会导致全站不可用但它们凑在一起就像是三个小bug互相踩了油门。4. 修复方案落地与线上验证从止血到根除4.1 快速止血限流、熔断、摘除异常节点止损要分两步走。第一步在AI网关配置里临时打开了对模型服务调用的快速失败策略当模型服务的错误率超过30%时直接熔断10秒不再发起新的请求而是快速返回一个降级提示。同时把网关的全局限流阈值从2000 QPS降到500 QPS优先保障已建立连接的请求能顺利完成。第二步暂停了模型服务的滚动更新保留旧版本副本同时临时扩容了2个GPU副本。这里有一个细节扩容新副本时我们特意等了5分钟让显存预热和KVCache初始化完成才把流量切过去避免新副本一上线就被流量打爆。这个“预热窗口”是推理服务扩缩容最容易忽略的点。止血操作上线后大约5分钟错误率从23%降到了5%左右P95延迟从12秒回落到4秒。虽然还谈不上健康但系统已经稳住了至少不会再继续恶化。这时才有空间去做真正的根因修复。4.2 配置调整超时、重试、线程池一个都不能少等流量平稳后我立刻开始修改AI网关的配置。核心改动有三块第一块是超时配置。原来的“连接超时3秒、读取超时5秒”改成了“连接超时3秒、响应超时15秒、总超时20秒”。注意这里我把HTTP的socket读取超时和业务上的响应超时拆开了。HTTP层面只负责连接和socket读业务层面用Future.get(timeout)来控制总的响应时间。这样就不会出现“连接建立很快但数据读不完被强行掐断”的尴尬情况。第二块是重试策略。客户端SDK的自动重试从2次改为0次只在网络层连接失败比如connection refused时允许重试1次而业务层的超时一律不重试。理由很简单当上游服务已经过载时任何重试都是在往火上浇油。宁可让少部分请求失败也不能让失败请求无限放大流量。第三块是线程池。把AI网关对模型服务的调用改成独立线程池核心线程数16、最大32、队列长度200拒绝策略为CallerRunsPolicy。同时给健康检查接口单独开了一个线程池并给它设置了独立的超时时间和熔断阈值确保健康检查永远有资源可用不会被业务线程拖死。提示CallerRunsPolicy这种拒绝策略其实是一把双刃剑。它能让被拒绝的请求在调用方线程里执行起到天然限流的作用但如果调用方线程本身是Tomcat工作线程那么这个线程也会被占用。所以它只适合在“被调用方已经过载调用方愿意牺牲自身吞吐来保护下游”的场景下使用。我们是在确认了模型服务恢复后才把它的优先级调低的。4.3 代码层面的根除异步化改造与缓存防击穿配置调整只能缓解症状真正要根除问题还得改代码。我们做了两个方向的改造。方向一是把AI网关对模型服务的调用从同步阻塞改为异步非阻塞。具体来说用WebClient替换了RestTemplate配合Project Reactor的Mono实现异步调用。这样同一个Tomcat线程在等待模型服务返回时可以去处理其他请求线程利用率大幅提升。配合前面说的独立线程池相当于给每个下游依赖建了一条“独立车道”一条车道堵车不会影响其他车道。方向二是缓存防击穿。我们给Redis加了两层保护第一层是热点key的分布式锁Redis SETNX当缓存过期且请求未命中时只允许一个请求去回源其他请求短暂等待后重新读取缓存第二层是TTL随机化把固定7200秒改成“基础值 随机0到30分钟”。返回结果也做了异步回填不阻塞主请求。注意分布式锁实现防击穿时一定要设置锁的过期时间。我们用的锁过期时间是10秒并且每次回源前都会判断当前锁是否过期防止持有锁的请求异常退出后锁永久不释放。4.4 验证过程与效果对比代码改动回归测试通过后我们在灰度环境做了三轮压测验证。压测数据对比如下指标故障前基线故障期间修复后压测错误率0.1%23%0.3%P95延迟800ms12s1.1s模型服务排队数0-23003-5缓存命中率95%60%96%网关线程池活跃度25%100%45%单请求平均占用线程时间800ms8s950ms从数据上可以明显看到修复后的表现不仅恢复了基线在延迟和排队数上甚至优于故障前。这是因为异步化改造后网关对线程的利用率提高了模型服务的请求也变得更加平滑不再出现突发流量毛刺。压测通过后我们按“1% → 10% → 50% → 100%”的比例逐步放量每个阶段观察5分钟确认错误率没有反弹后才继续。全程大约花了25分钟线上服务完全恢复正常。这次故障的处置记录后来被整理成了标准的应急响应SOP再遇到类似场景团队可以直接照章执行。5. 复盘与长效机制把一次故障变成一套防御体系5.1 监控盲区为什么我们没有更早发现复盘会上大家问得最多的一个问题是为什么模型服务都快“假死”了我们的监控还在显示GPU利用率40%答案很简单——监控指标选错了。我们当时的模型服务监控只有GPU利用率、显存占用、QPS、错误率这几个传统指标。但这些指标在大模型推理场景下都有明显的滞后性和欺骗性。GPU利用率高不代表在干正事它可能在等I/OGPU利用率低也不代表空闲它可能在被显存碎片化卡住。真正能反映推理服务健康状况的是这几个指标排队请求数所有进入了推理引擎但还没开始处理的请求数量。这个值持续上涨说明处理能力跟不上输入速度。平均解码耗时ms/token每个token生成的耗时。这个值上涨说明模型服务内部出现了瓶颈。token吞吐tokens/s整个服务每秒能产出的token总数。它比QPS更能反映推理服务的真实处理能力。显存碎片率或连续显存块分布这是后来我们专门加的用来提前发现类似“CUBE OOM”的前兆。这些指标现在被我们做成了“模型服务黄金四指标”任何一条触发阈值都会直接告警到值班群。经过这次故障我深切体会到在大模型推理场景里延迟和排队长度比资源利用率重要得多。5.2 故障预案把“应急反应”变成“肌肉记忆”我们以前也写过故障预案但大多是纸面文章没人演练过。这次故障暴露了一个问题当多个告警同时响起、线程dump一片飘红的时候现场很容易乱。有人想扩容有人想重启还有人想改配置最后可能谁说的都对但没人知道按什么顺序做。所以这次复盘后我们落地了两件事。第一件事是制定了“模型服务慢响应”的专项预案里面明确了处理顺序先熔断隔离再定位根因再恢复流量。每一步都有具体的执行命令、预期效果和判断标准。第二件事是安排了季度故障演练专门模拟这次故障的场景让团队在没有真实故障的情况下反复练习确保每个人都知道自己的角色。预案里有一个很关键的细节所有命令都要写成脚本不能靠人肉敲。比如熔断开关、限流阈值调整、Pod摘除这些操作都做成了自动化平台的一键按钮。人肉敲命令在压力下容易出错而且慢5分钟的误操作可能就决定一次故障是在10分钟内恢复还是40分钟恢复。5.3 技术债清单哪些坑必须先填最后列一下这次故障留下的技术债清单。按优先级排序我们给后面排了三项必须完成的工作第一项全链路超时时间一致性梳理。这次故障最痛的教训是客户端SDK的5秒超时、网关的5秒读取超时、模型服务的8秒正常延迟这三者之间完全不自洽。现在我们要求所有依赖链路上的超时时间都必须做成配置中心统一管理并且要在发布前做一次超时时间自检确保“上一层级的超时必须至少是下一层级正常P99延迟的1.5倍”。第二项网关线程池隔离改造。我们已经对模型服务调用做了独立线程池但不能止步于此。所有外部依赖——Redis、向量数据库、模型推理——都应该有自己的独立线程池和独立的熔断配置这样任何单点故障都不会把整个网关拖垮。目前Redis调用已经完成了隔离向量数据库还在改造中。第三项缓存过期时间随机化全覆盖。这次我们给热点问答缓存加了随机TTL但还有一些会话级缓存和配置类缓存没有做同样的处理。我们需要对整个Redis缓存体系做一次TTL策略审计确保不存在“一批key同时过期”的隐患。我个人在实际操作中的体会是这次故障里最值钱的部分不是我们修好了线而是我们建起了一套能提前发现类似问题的监控体系以及一张永远不会过时的技术债清单。做线上服务就是不断和不确定性打交道你没法拦住每一次故障但可以保证每一次故障都让系统变得更结实。哪怕只是把超时参数调对了也是在给未来的自己少埋一颗雷。