☰
AI服务线上响应异常排查:从P99延迟飙升到根因定位的完整指南
2026/11/2 3:44:09 网站建设 项目流程

上周二晚上十点,告警群连着弹了三条消息:AI服务接口P99延迟从平时的450毫秒直接飙到4.8秒,错误率超过12%,核心客户在群里反馈“对话一直转圈”。我第一反应是“模型又挂了”,但登录机器一看,GPU利用率只有30%出头,模型进程也健在。这个场景相信不少做AI应用的朋友都不陌生——线上响应异常这件事,表象看起来都是“慢了、超时了、报错了”,但根因往往藏在完全不同的层面,排查起来特别容易绕弯。

这篇内容我打算把自己处理AI服务线上响应异常故障的经验完整捋一遍,包括快筛思路、排查链路、典型事故复盘、止血动作和长期治理手段。如果你正在维护一个对外提供AI能力的服务,不管是自研大模型应用、对话式AI还是推理平台,这篇文章应该能帮你少熬几个半夜。

1. 先别急着重启:5分钟定位异常范围

故障一出现,最大的诱惑是“先重启再说”。重启确实能解决一部分内存泄漏、连接池耗尽之类的问题,但也经常把现场破坏掉,后面想查根因就难了。我现在的习惯是,收到告警先做一件事:在群里按住自己,花5分钟判断这个异常的边界和类型,再决定动不动手。

1.1 从一次真实告警说起

那次事故我印象很深,最开始只有一条告警:“chat_api.p99 > 3000ms 持续5分钟”。随后客户侧、监控侧、日志侧的信息陆续汇总上来,现象描述五花八门:有人说“首页对话加载很慢”,有人说“发送消息一直没反应”,还有人反馈“偶尔报500,重试就好了”。

这些碎片信息如果不梳理,很容易被带偏。我当时的做法是拉起一个共享文档,把观察到的现象按时间线列出来,标注每条现象来自哪个模块、哪个接口、哪些用户受影响。半小时后再看,规律就出来了:受影响的都是需要调用外部知识库接口的会话,纯内部模型生成的请求其实延迟没有明显恶化。

实操心得:故障一开始,信息是混乱的。花几分钟把现象按模块、接口、用户群、时间线四个维度归档,比急着查日志有效得多。磨刀不误砍柴工。

1.2 异常现象分类是排查的地图

处理多了之后我发现,AI服务线上响应异常大致能分成四类,每类对应的排查方向完全不同。

现象类型典型表现优先排查方向
延迟升高P99变大,平均延迟尚可推理性能、资源争抢、外部依赖
错误率上升5xx变多,异常堆栈刷屏代码Bug、上游限流、配置变更
超时假死请求挂起不返回,连接被占满线程池、连接池、死锁、阻塞调用
偶发抖动周期性或随机性短时变慢GC、定时任务、弹性扩容触发、宿主机争抢

比如延迟升高这类,如果只是P99变大而平均延迟正常,大概率是某条分支路径变慢了,比如走了长上下文、触发了向量检索、调用了慢的外部接口;如果平均延迟也变大,那往往是整体负载上去了或者推理资源不够了。

而错误率上升但延迟稳定,一般先怀疑代码变更和配置变更,再往下看依赖服务的限流、熔断策略是否被触发。最诡异的是超时假死,这类问题通常不在模型本身,而是某个同步点把线程全部占住,服务看似活着,实际上已经无法接受新请求。

1.3 五分钟快筛:先判断“单点”还是“全局”

快筛的核心问题只有一个:这个异常是影响所有流量,还是某类特定流量?

具体操作很简单。拉出网关入口的QPS和错误率曲线,按来源、用户、接口维度各切一刀。只要是某一类来源或某一个接口异常,基本可以排除整个服务宕掉的可能,把注意力放到该链路独有的依赖上。如果所有流量全面异常,则优先查服务本身的资源水位和最近变更。

我常跟团队说的一个比喻是:这就像去医院分诊,先量体温、看是发烧还是外伤,再决定挂内科还是外科。异常分类做好了,排查方向就收窄了大半。很多新人上来就翻模型推理日志,翻半天发现连日志都没打,问题的核心浪费在开局了。

2. 锁定根因的排查链路

快筛结束后,进入真正的根因排查阶段。AI服务排查链路比普通Web服务多出一层模型推理,但多出来的这一层也经常成为“背锅侠”。我见过太多同事一口咬定是模型推理慢了,结果查到最后是数据库连接池满了,或者是缓存穿透把下游打挂了。

2.1 模型推理环节为什么最容易出事

模型推理确实是最大的嫌疑对象,尤其是自研的Transformer类模型。它的耗时受几个因素影响:输入序列长度、输出序列长度、batch大小、GPU利用率、显存状态、是否触发recompute等。

我处理过一个案例:P99延迟在下午某个时段突然从700ms变成2.3秒,但GPU利用率一直不高。最后定位到问题出在动态batch的聚合逻辑上——某个大客户的请求带了一大批候选文本,把一个batch撑到了极限长度,其余请求在后面排队。表面看是“推理慢”,实际是“排队慢”。

所以排查推理环节,不要只看单个请求的耗时曲线,还要看batch分布、排队等待时间、GPU内核态时间。我通常会在推理服务里埋三类指标:请求排队耗时、模型前向耗时、token生成速率。排队耗时占比高,问题在调度或流量;前向耗时长,问题在模型或数据;生成速率下降,再看显存和算力争抢。

注意:很多AI框架自带的profiler工具(如PyTorch的torch.profiler、NVIDIA的Nsight Systems)可以在测试环境复现性能问题,但线上压测时开启profiler会显著影响性能,要谨慎操作。线上问题优先用日志和指标定位,profiler留到事后复现。

2.2 中间件与上下游链路:不止看服务自己

AI服务几乎不可能孤立运行。模型要读特征数据,要查向量库,要调知识库接口,要把结果写回缓存和数据库。任何一个下游变慢,都会表现为AI服务的响应异常。

这部分排查我有一套固定的顺序:

  • 先看依赖的耗时曲线:调用外部接口的P99、P95是否同步抬升。如果下游P99涨了,大概率问题在别处,AI服务只是受害者。
  • 再看连接池和线程池状态:连接数是否打满,线程等待时间是否增加。连接池满了之后,新请求会排队等待获取连接,延迟直线上升,而且几乎不会在依赖本身的监控里体现。
  • 三看重试逻辑是否合理:AI服务为了稳定性普遍会加重试,但重试在故障期就是“放大器”。下游已经慢了,重试再把流量翻倍,可能会把下游彻底打挂。

我经手的一次严重事故就是这么来的:下游向量库接口偶尔超时,AI服务里的Feign客户端配置了3次重试,超时阈值是2秒。下游故障时,一个请求最多要等6秒才返回错误,线程池被这些慢请求占满,后续所有正常请求全部排队,服务整体雪崩。事后把重试降为1次,超时阈值改成800毫秒,情况立刻好转。

2.3 资源水位与容量视角:边查根因边算账

排查AI服务响应异常,不能光盯服务进程,还要看资源账单:CPU、内存、GPU、磁盘IO、网络带宽。尤其容器化部署的AI服务,经常出现“进程活着但资源被邻居吃光”的情况。

关注点有两个。一个是CPU和内存是否设置了合理的request和limit。K8s集群里如果limit设置过小,容器会被内核杀掉再重启,表现为周期性错误率上升。另一个是GPU显存:显存碎片化是长期运行后的隐形杀手,进程没崩,但可用显存越来越少,申请显存的耗时越来越长,推理延迟悄悄爬升。

容量计算这块,我一般会做一个简单的估算:单实例单日最大QPS、平均处理耗时、并发线程数三者相乘,就是实例承载上限。公式不复杂,但很多人忽略了对峰值的估算。比如一个推理服务日常QPS为200,平均耗时500毫秒,那么平均并发就是100。如果实例线程池配置只有80,高峰期一到就会开始排队,P99直接恶化。

实操心得:故障期间最忌讳“猜”。每个判断都要有数据支撑。拿着监控曲线说“感觉是网络问题”,不如直接做一次跨服务ping和curl测试。拿到数据再下结论,排查效率至少翻一倍。

3. 三个真实案例复盘:每次故障都是一次教学

光讲方法论不够,我挑三个自己处理过的典型故障,把从现象到根因再到解决的完整过程拆开讲一讲。这三个案例分别对应了算力层资源问题、上游依赖配置问题和缓存层流量问题,基本能覆盖AI服务线上响应异常的大部分套路。

3.1 案例一:GPU显存碎片化让P99延迟悄悄爬升

现象是持续一周多的P99缓慢上升,从600毫秒涨到1.5秒,平均延迟变化不大,业务流量没有明显增长。错误率稳定,服务也没有重启过。一开始我们都怀疑是模型版本退化,回滚过一版也没有改善。

后来把nvidia-smi的显存占用曲线拉出来,发现进程3500毫秒的“可用显存余量”在持续下降,显存碎片率达到28%。PyTorch的缓存分配器会缓存已释放的显存块以便复用,但长时间处理长短不一的输入后,显存块被切得越来越碎,申请大块显存的耗时大幅增加,个别请求在显存分配上就卡了几百毫秒。

解决办法分两步:一是调整PyTorch的显存分配策略,设置PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True,让显存分配器在需要时扩展段而不是反复整理碎片;二是加了一个低峰期的定时任务,错峰重启模型实例,打散显存碎片。改动之后P99逐步回到500毫秒以下。

3.2 案例二:上游限流参数写错引发的连锁超时

这个事故比较隐蔽。某个AI应用依赖一个第三方内容安全审核接口,平时响应20毫秒。突然某天开始,接口每隔几分钟就会出现一小段300-500毫秒的延迟,AI服务本身的P99跟着跳到3秒。

最开始怀疑网络波动,但ping和mtr都很稳定。后来去查上游审核服务的调用日志,发现他们在给客户配置限流时,把我们的每秒限额从3000误写成300,而我们的峰值流量确实会短暂超过300QPS。触发限流后,上游返回429,我们这边的重试逻辑启动了,于是上游又被多出来的重试流量继续打限流,形成恶性循环。

处理方式是在网关层把针对该接口的重试次数降为0,同时跟上游确认把配额恢复为3000QPS。那次之后我定了一个规矩:所有外部依赖调用都必须在上线前做配额确认,并且把429响应单独统计成指标,一旦触发就告警。如果不是这个case,我可能永远想不到“上游限流配置错误”这种低级故障会造成这么大影响。

3.3 案例三:热key缓存击穿打爆数据库

又一次故障是线上的一个AI辅助写作功能突然大面积超时,错误率在几分钟内从0.2%涨到35%。我们第一反应是模型服务崩了,但赶到控制台发现模型服务的CPU和GPU都很平稳。

查日志发现,超时请求都卡在读取“创作模板配置”这一步。原来这个功能上线时做了一个“模板配置缓存在Redis、过期时间5分钟”的设计,平时没问题。但当天运营同学批量上线了一批热门模板,某个大V的推广视频带火了一个模板ID,这个Key的访问量瞬间涨到平时的几十倍,且刚好碰上缓存过期,一瞬间所有请求都穿透到数据库,数据库连接被打满,连带把其他模块也拖住了。

这就是典型的缓存击穿。我的解决方案是两层:一是为热点Key增加逻辑过期时间,即缓存永不失效,但保存一个逻辑过期时间戳,线程异步刷新;二是加分布式锁,当缓存失效时只允许一个线程去数据库加载,其他线程短暂自旋等待。改动上线后,同样的流量冲击下P99稳定在800毫秒以内。

注意:类似的热点问题还会出现在向量库的某个热门分片上。如果某个知识库段落被大量并发请求检索,向量库单分片同样会被打爆。排查AI服务线上响应异常时,不要只盯着模型服务,凡是有共享依赖的环节都值得拉出来看看。

4. 故障处置动作与恢复流程

排查到根因之后,下一步是处置。很多人喜欢“跳步”,根因还没完全确认就直接上生产改代码、改配置,结果把问题搞得更复杂。我建议严格按照“止血、确认、复盘”三个步骤走。

4.1 止血三板斧:限流、扩容、重启的先后顺序

先说限流。限流的本质是“牺牲一部分流量,保住核心体验”。如果服务已经处于过载状态,继续接受所有请求只会让整体延迟继续恶化。我通常在网关层配置一个比平时略低的QPS阈值,一旦超过就快速返回“系统繁忙”提示,而不是让请求一直排队。

然后是扩容。扩容不能盲目扩,否则可能只是把压力从一个瓶颈转移到另一个瓶颈。比如模型推理慢,扩容实例确实有效;但如果瓶颈在下游数据库连接池,加再多的AI服务实例只会让数据库更惨,正确操作是先把数据库连接池放大,或者把慢查询问题解决。

最后才是重启。重启是万不得已的手段,比如确认内存泄漏、线程池状态完全混乱、进程进入死循环。重启前一定要保留现场证据,至少把线程dump、堆dump、metrics快照留下来,不然事后没法复盘。有一次我们怀疑goroutine泄漏,重启前先执行了kill -3拿到线程栈,重启后分析才发现是某个外部调用的连接没有设置超时,一直在等待响应。

4.2 恢复的确认标准:别急着说“已恢复”

我在项目里吃过一次亏:某个服务错误率降下来了,群里刚喊“已恢复”,五分钟后又被客户投诉“还是慢”。后来才发现,错误率下降只是因为限制了一部分流量,延迟并没有恢复到正常水平。所以现在我对“恢复”的定义有三个硬指标:

  • 错误率回到基线以下,且持续观察至少15分钟
  • P99延迟恢复到故障前的浮动范围,不是仅仅比故障时低了
  • 业务侧确认关键链路(比如对话发送、结果返回)无异常反馈

指标恢复之后,我还会额外观察30分钟左右,尤其是关注是否存在“延迟滞后”或“连接池缓慢回填”的情况。很多时候故障表面平息了,但比如连接池里的连接正在逐步重建、缓存在慢慢回填,这段时间里如果遇到流量脉冲,问题还会二次爆发。

4.3 复盘与改进:不只修Bug,还要补机制

每次故障处理完,我都会在24小时内组织一次复盘。复盘会采用简单直接的形式:时间线、根因分析、直接触发原因、深层原因、改进项。重点不是追责,而是回答两个问题:为什么监控没有提前发现?为什么自动化预案没有生效?

针对这两点,我总结过一类高频改进点:

  • 监控盲区:很多AI服务只监控了模型推理耗时,没有监控“排队耗时”和“依赖调用耗时”。结果是故障发生时,指标上根本看不出来卡在哪个环节。
  • 预案缺失:事前没有定义降级策略,比如外部依赖不可用时是直接报错还是返回兜底结果。没有预案的团队,故障发生时只能现场开会拍脑袋。

5. 排查工具箱与长期治理建议

故障处理完,如果不沉淀工具和机制,下次还会踩同样的坑。这块分享一下我这边长期在用的排查工具链和一些容易忽略的坑。

5.1 日志、指标、链路三方配合

现在做排查,最忌讳的是只盯某一个数据源。日志告诉你发生了什么,指标告诉你定量影响,链路追踪告诉你请求经过了哪些环节、每步耗时多少。三者配合才能拼出全貌。

数据源回答的问题典型工具
日志报了什么错、堆栈是什么、参数是什么ELK、Loki、Graylog
指标延迟、错误率、资源水位的变化趋势Prometheus、Grafana、Datadog
链路追踪请求在哪个服务/环节耗时最多Jaeger、Zipkin、SkyWalking

AI服务尤其要在埋点上下功夫。我建议每个推理请求都带上独特的traceId,从网关到模型服务再到下游依赖全程透传,这样出现延迟升高时,可以一键定位到链路中的具体环节。日志和traceId必须打通,否则排查时还得在海量日志里找关联,效率很低。

5.2 压测与容量规划的具体做法

线上出故障后再扩容,永远是最差的选择。提前做压测和容量规划,才能把“故障”变成“波动”。

我这边每个AI推理服务上线前都会做一轮完整的压测,输出三个关键数据:单实例最大QPS、单实例最佳并发数、延迟随QPS的变化曲线。有了这些数据,再根据预估峰值流量做容量规划:实例数 = 预估峰值QPS / 单实例最大QPS × 安全系数(通常1.5到2倍)。比如预估峰值QPS是3000,单实例最大QPS是500,安全系数取2,那么至少需要12个实例。

压测工具方面,普通HTTP接口用 wrk 或 vegeta 就可以,但AI推理服务普遍是流式输出、长连接,我建议用支持自定义脚本的压测工具(比如JMeter或Locust),并且要模拟真实的输入序列长度分布——只压短文本效果再好也没意义,因为长文本的耗时可能是短文本的几倍甚至几十倍。

5.3 几个容易踩的坑

最后整理一份我自己踩过或观察别人踩过的坑清单,供参考。

  • 盲目设置超时时间。超时太长会让慢请求拖垮线程池,太短又会频繁触发重试。我的经验是:先基于压测数据取P99的1.5倍作为超时基线,再根据业务容忍度微调。
  • 只设置数据库连接池上限,没有设置等待队列长度。连接池满后请求会在获取连接时无限等待,表面上看是服务卡死,实际是连接池等待队列填满。
  • 忽略冷启动问题。模型加载、缓存预热都需要时间,K8s的滚动更新把旧实例摘掉但新实例还没Ready时,流量会瞬间全部打到剩余实例上,造成短时延迟升高。
  • 重试没有考虑幂等性。AI生成类接口尤其危险,一次请求重试两次,可能产生两份内容,业务侧如果没做幂等处理,就会出数据问题。

最后再分享一个小技巧

每次排查完线上响应异常,我都会把当次故障的关键指标截图、时间线、根因、处置动作整理成一份独立文档,命名规范是“年月日-现象简述-根因”。这个习惯坚持了两年多,现在团队里新同学遇到类似问题,先翻知识库能直接命中一半以上的情况。

还有一个小细节:告警阈值不要常年一成不变。AI服务的流量和模型版本在变,基线也在变,固定阈值的告警要么静默失效,要么天天误报。我这边每季度会根据近一个月的数据重新校准一次P99、错误率和资源利用率的告警阈值,确保告警只在真正需要喊人的时候才响。

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

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

立即咨询