AI服务线上响应异常排查实战:从监控指标到根因定位
1. AI服务线上响应异常故障的排查思路与实战复盘
做AI服务运维和开发的同学大概都有过这种体验:凌晨两点,监控群突然炸了,接口成功率从99.9%直接掉到60%以下,P99延迟从200ms飙到十几秒,用户侧开始大面积报“请求超时”“服务不可用”。你打开电脑,一边揉眼睛一边翻日志,心里想的只有一件事——到底是模型的问题、网络的问题,还是流量的问题?
AI服务跟传统的Web服务有一个本质区别:它天然是计算密集型+IO密集型+外部依赖密集型的混合体。一次推理请求背后可能牵扯到GPU资源调度、模型权重加载、向量数据库检索、第三方API调用、消息队列削峰等一连串环节。任何一个环节抖动,都会以“响应异常”的形式暴露在用户面前。所以排查AI服务线上故障,不能只盯着应用层看,得有一套从外到内、从粗到细的系统性方法。
这篇内容适合三类人看:一是刚接手AI服务运维的工程师,二是正在做AI应用开发、需要自己兜底线上稳定性的后端同学,三是技术负责人,需要建立一套故障响应机制。我会把整个排查链路拆开讲,包括怎么快速定位故障域、怎么区分是模型问题还是基础设施问题、怎么用最小成本恢复服务,以及那些只有踩过坑才知道的细节。
2. 故障现象分类与第一响应原则
2.1 先搞清楚“响应异常”到底长什么样
“响应异常”这个词太笼统了,不同表现对应的根因完全不同。我在实际工作中会先把异常分成几类:
- 全量超时:所有请求都超时,成功率直接归零。这种通常是服务进程挂了、端口不通、或者上游依赖整体不可用。
- 部分超时:成功率下降但没归零,P99延迟明显上升。常见于资源竞争、连接池耗尽、某个下游节点故障。
- 间歇性失败:请求时好时坏,没有明显规律。可能是负载均衡问题、DNS解析抖动、或者某个副本的GPU出现ECC错误。
- 响应内容异常:HTTP状态码200但返回内容为空、乱码、或者模型输出明显退化。这类问题最隐蔽,监控指标可能完全正常。
我一般会先看监控大盘上的成功率曲线形状。如果是断崖式下跌,基本可以锁定是某个硬依赖挂了;如果是缓慢下滑,多半是资源逐渐耗尽;如果是锯齿状波动,那就要怀疑是周期性任务或者流量突刺导致的。
2.2 第一响应:先止损,再定位
很多工程师的本能反应是“先找到根因再处理”,但在线上故障场景下,这个顺序是错的。用户正在受影响,每一分钟都是损失。我的原则是:5分钟内必须做出止损动作,哪怕这个动作只是重启一个副本。
止损手段按优先级排:
- 扩容:如果是资源不足导致的,直接加副本或加GPU节点。K8s环境下改一下replicas就行,最快。
- 限流:如果是流量突刺,在网关层把QPS限制到服务能承受的水平,牺牲部分用户保住整体。
- 降级:关掉非核心功能,比如把向量检索降级为关键词匹配,把大模型推理降级为缓存命中。
- 回滚:如果故障时间点和最近一次发布高度吻合,别犹豫,直接回滚。
- 重启:万金油手段,但要注意重启可能触发冷启动风暴,得配合就绪探针慢慢放量。
注意:止损动作一定要在群里同步,让所有人都知道你在做什么。我见过太多因为两个人同时操作导致故障扩大的案例。
3. 从监控指标快速锁定故障域
3.1 必看的四层监控指标
AI服务的监控体系我一般分四层来看,从外到内依次是:
| 层级 | 关键指标 | 异常特征 | 常用工具 |
|---|---|---|---|
| 接入层 | QPS、成功率、P99延迟、HTTP状态码分布 | 成功率下跌、5xx增多 | Nginx日志、网关监控 |
| 应用层 | 线程池活跃数、连接池使用率、GC时间、队列深度 | 线程阻塞、队列堆积 | JVM/Go pprof、Prometheus |
| 模型层 | 推理耗时、GPU利用率、显存占用、batch size | GPU打满、显存OOM | nvidia-smi、DCGM |
| 依赖层 | 数据库连接数、Redis命中率、向量库查询延迟 | 连接超时、慢查询 | 各中间件自带监控 |
排查时我会从接入层往下逐层看,找到第一个出现异常的层级,那基本就是故障域。比如接入层成功率跌了,但应用层线程池正常,模型层GPU也正常,那问题大概率在依赖层——可能是数据库连接池被打满了。
3.2 一个真实的排查案例
之前遇到过一次故障:AI问答接口成功率从99.9%掉到75%,P99从300ms涨到8s。按上面的方法逐层看:
- 接入层:5xx占比升高,主要是504超时
- 应用层:线程池活跃数打满,大量线程处于WAITING状态
- 模型层:GPU利用率只有30%,明显不是算力瓶颈
- 依赖层:向量数据库查询延迟从10ms涨到3s
问题定位到向量数据库。进一步查发现,向量库的某个分片节点磁盘IO打满了,原因是后台在做全量索引重建,抢占了查询的IO资源。解决办法很简单:把索引重建任务挪到低峰期,并且限制重建任务的IO带宽。
这个案例说明一个道理:AI服务的瓶颈往往不在模型本身,而在周边依赖。很多人一遇到AI服务慢就怀疑GPU不够,实际上大部分时候GPU都是闲着的。
4. 常见故障根因与排查手册
4.1 资源类故障
资源类故障是最常见的,表现也很典型。我整理了一个速查表:
| 现象 | 可能原因 | 排查命令 | 解决方式 |
|---|---|---|---|
| GPU利用率100%,请求排队 | 算力不足或batch配置不合理 | nvidia-smi、dcgm-exporter | 扩容、调大batch、启用量化 |
| 显存OOM,进程被杀 | 模型太大或并发过高 | nvidia-smi看显存、dmesg看OOM日志 | 限制并发、用更小模型、开启显存复用 |
| CPU打满,推理变慢 | 预处理/后处理是CPU瓶颈 | top、pidstat | 优化预处理、加CPU节点 |
| 内存持续增长直至OOM | 内存泄漏 | pprof、valgrind | 定位泄漏点、加内存限制 |
GPU这块我要多说一句。很多人看到GPU利用率高就以为要扩容,其实不一定。如果batch size设得太小,GPU虽然忙但吞吐很低,这时候调大batch比加卡更有效。反过来,如果batch已经很大了利用率还是上不去,那可能是数据预处理拖了后腿,GPU在等数据。
4.2 网络与连接类故障
AI服务经常要调用外部依赖,网络问题特别多。常见的几种:
- 连接池耗尽:表现为请求排队等待连接,日志里大量“connection timeout”。解决方式是调大连接池、缩短连接超时、加熔断。
- DNS解析失败:间歇性失败,日志里能看到“no such host”。解决方式是本地缓存DNS、配置多个DNS服务器。
- 跨可用区延迟:如果服务和依赖不在同一个可用区,网络延迟可能翻好几倍。解决方式是尽量同区部署。
- TCP重传率高:表现为延迟抖动大。用
ss -s看重传统计,解决方式是检查网络链路质量。
实操心得:连接池的大小不是越大越好。我见过有人把连接池调到1000,结果数据库那边直接被打挂。连接池大小应该根据下游能承受的并发来定,一般建议是下游最大连接数的70%左右。
4.3 模型与推理类故障
这类故障最隐蔽,因为监控指标可能完全正常,但用户就是觉得“回答变蠢了”。常见原因:
- 模型权重加载不完整:某个副本加载了旧版本或者损坏的权重,导致输出异常。排查方式是比对各个副本的模型文件hash。
- 量化精度损失:为了省显存用了INT8量化,结果某些输入下输出质量明显下降。解决方式是关键场景用FP16,非关键场景才用量化。
- KV Cache配置不当:长对话场景下KV Cache爆了,导致请求被截断。解决方式是限制最大上下文长度、开启KV Cache分页。
- 采样参数漂移:配置中心推送了错误的temperature/top_p,导致输出风格突变。排查方式是检查配置变更记录。
4.4 集群与调度类故障
K8s环境下跑AI服务,调度问题也不少:
- Pod频繁重启:可能是OOM、健康检查配置不当、或者节点资源不足。用
kubectl describe pod看事件。 - 节点NotReady:节点挂了或者kubelet异常。用
kubectl get nodes看状态,journalctl -u kubelet看日志。 - 调度失败:Pod一直Pending,通常是资源请求太大没有节点能满足。用
kubectl describe pod看调度事件。 - 服务发现异常:Service的Endpoints为空,导致请求打不到Pod。检查Readiness Probe是否通过。
5. 完整排查流程与实操步骤
5.1 第一步:确认故障范围
接到告警后,第一件事不是打开日志,而是确认范围。我会问三个问题:
- 影响面多大:是所有用户还是部分用户?是所有接口还是某个接口?
- 什么时候开始的:精确到分钟,然后对照最近的变更记录。
- 有没有相关变更:发布、配置修改、扩缩容、依赖升级,任何变更都可能是诱因。
这三个问题的答案基本能决定后续排查方向。如果影响面是全量的,那大概率是基础设施问题;如果是部分用户,可能是地域或网络问题;如果时间点和某次发布吻合,直接回滚最快。
5.2 第二步:抓取现场信息
在止损之前,一定要先抓现场。因为一旦重启或回滚,现场就没了。需要抓的信息包括:
# 抓取Pod日志(最后1000行) kubectl logs <pod-name> --tail=1000 > pod.log # 抓取Pod详情 kubectl describe pod <pod-name> > pod-desc.log # 抓取线程栈(Java) kubectl exec <pod-name> -- jstack <pid> > thread-dump.log # 抓取GPU状态 nvidia-smi > gpu-status.log nvidia-smi -q -d ECC > gpu-ecc.log # 抓取网络连接状态 ss -s > net-stat.log ss -tnp > net-conn.log这些信息抓下来之后,再去做止损动作。我吃过亏:有一次直接重启了Pod,结果问题复现不了,根因一直没找到,后来同样的问题又发生了一次。
5.3 第三步:逐层排查
按第3节说的四层监控逐层看,找到第一个异常层级。这里补充几个具体的排查命令:
# 查看Pod资源使用 kubectl top pod <pod-name> # 查看节点资源使用 kubectl top node # 查看GPU使用(需要DCGM) dcgmi dmon -e 1001,1002,1003,1004,1005 # 查看慢查询(MySQL) pt-query-digest slow.log # 查看Redis延迟 redis-cli --latency # 查看向量库查询延迟 curl http://vector-db:port/metrics | grep query_latency5.4 第四步:定位根因并修复
找到异常层级后,进一步缩小范围。比如定位到向量库慢,就要看是查询慢还是写入慢,是某个分片慢还是全部慢,是磁盘IO问题还是CPU问题。这一步需要结合具体的中间件知识,没有通用方法。
修复的时候要注意:先恢复服务,再优化根因。比如向量库慢是因为索引重建,那就先停掉重建任务恢复服务,然后再去优化重建任务的调度策略。
6. 高频问题速查与避坑指南
6.1 故障排查速查表
| 故障现象 | 优先检查项 | 快速验证方法 |
|---|---|---|
| 全量504 | 服务进程、端口、上游依赖 | curl localhost:port/health |
| 部分超时 | 连接池、线程池、下游节点 | 看连接池监控、ss -tnp |
| 间歇失败 | DNS、负载均衡、副本健康 | dig、看LB日志、kubectl get pods |
| 输出异常 | 模型版本、配置、量化 | 比对模型hash、看配置变更 |
| GPU OOM | 并发数、batch size、显存碎片 | nvidia-smi、看OOM日志 |
| Pod重启 | OOM、健康检查、节点资源 | kubectl describe pod |
6.2 避坑经验
坑一:只看平均值不看分位数。平均延迟200ms看起来很正常,但P99可能是10s。AI服务的延迟分布往往是长尾的,一定要看P95、P99。
坑二:忽略冷启动。AI服务重启后模型加载可能要几分钟,这期间请求全部失败。解决办法是配置就绪探针,加载完成后再接入流量。
坑三:健康检查太激进。健康检查超时设得太短,GPU繁忙时响应慢一点就被判定为不健康,导致Pod被反复重启。建议健康检查超时至少设5秒。
坑四:日志级别太高。生产环境日志级别设成DEBUG,磁盘IO被日志写满,反而拖慢服务。建议生产环境用INFO,排查时临时开DEBUG。
坑五:没有熔断降级。下游依赖挂了,请求全部堆积,把自身也拖垮。一定要配熔断器,下游失败率达到阈值就快速失败。
坑六:扩容不看冷启动时间。流量突刺时扩容,新Pod还没加载完模型,流量又打过来了,结果新Pod也被打挂。扩容要配合预热和慢启动。
6.3 建立故障响应机制
最后说点体系化的东西。单次故障排查靠个人能力,但长期稳定靠机制。我建议至少建立这几样:
- 告警分级:P0是服务不可用,P1是部分用户受影响,P2是性能下降。不同级别对应不同的响应速度。
- 值班制度:保证任何时候都有人能第一时间响应。
- 故障复盘:每次故障后写复盘文档,记录时间线、根因、改进措施。复盘不是追责,是为了不再犯。
- 混沌演练:定期模拟故障,比如随机杀Pod、注入网络延迟,验证系统的容错能力。
我在实际运维中的体会是,AI服务的稳定性问题,80%都不是AI本身的问题,而是周边基础设施和工程化的问题。把连接池、熔断、限流、健康检查这些基础功做扎实,比调模型参数更能提升稳定性。踩过几次坑之后,我现在接手任何AI服务,第一件事就是看它的依赖治理和容错设计,而不是看模型效果。