先交代一个背景:我们团队负责的推荐模型服务,线上流量在凌晨四点出现了明显延迟上升,P99 从 40ms 飙到 280ms,但诡异的是服务没有报错,CPU 也没跑满。如果不是当天值班同事多看了一眼监控面板,这个隐患可能要到早高峰流量进来才会爆发,到时候就是事故级别的问题。这事之后,我花了两周时间把模型推理这条链路从头到尾捋了一遍,补上了之前一直缺位的推理性能监控和预警机制。这篇文章就把这段实践完整拆开讲,从指标设计、工具选型到告警阈值设定,再到最后踩过的几个坑,一次说清楚。
1. 为什么模型推理监控不能照搬 Web 服务那套
很多人刚接触 AI 模型部署时,下意识会把推理服务当成普通 Web 服务来监控——看 CPU、看内存、看请求 QPS,最多再加一个磁盘 IO。这套东西在传统后端没问题,但放到模型推理场景里,远远不够。
模型推理服务与传统 Web 服务最大的区别在于:推理过程存在明确的“资源强依赖”和“调度不确定性”。GPU 显存分配、CUDA 上下文切换、模型权重从显存到寄存器再进算子的搬运路径,这些环节里任何一个出现抖动,都会直接反应到单次推理延迟上。更麻烦的是,GPU 的利用率指标和推理延迟之间并不是简单的线性关系,可能 GPU 利用率看起来只有 40%,但延迟已经翻倍了——因为部分算子根本没跑到 GPU 上,在 CPU 上做数据预处理和搬运时已经卡住了。
我之前踩过一次比较刻骨铭心的坑:服务刚上线时只监控了容器 CPU 和内存,GPU 相关指标完全没接。某次模型版本升级后,新模型在 FP16 权重下显存占用比旧模型多了 2GB,刚好摸到 GPU 显存上限,训练卡开始做显存 swap,性能断崖式下跌。但监控面板上 CPU 和内存都正常,服务也没有报错,直到用户反馈"转圈好久才出结果"才定位到问题。这就是监控维度过粗的代价。
所以做推理监控的第一原则:必须建立推理链路专属的指标集,覆盖请求入口(网关)、推理框架内部(框架层)、底层资源(GPU/CPU/内存)、模型行为(版本/输入特征分布)四个层面。传统监控可以告诉你机器是否健康,只有推理专属监控才能告诉你模型服务是否还“够快、够稳、够准”。
1.1 四个必须盯住的监控层次
我梳理了一套适用于绝大多数推理服务的最小监控分层,从底层到上层依次是:
- 资源层:GPU 利用率、GPU 显存占用、显存带宽、CPU 使用率、内存占用、PCIe 读写速率、GPU 温度/功耗。这一层回答“硬件资源够不够”。
- 框架层:模型加载耗时、推理 batch size 分布、算子执行耗时、CUDA 错误计数、JIT 编译缓存命中率、显存动态分配次数。这一层回答“推理引擎本身健不健康”。
- 服务层:推理延迟(P50/P95/P99)、吞吐量(QPS)、请求队列长度、超时率、并发数、错误码分布。这一层回答“线上服务质量怎么样”。
- 业务层:推理结果置信度、输入/输出数据长度分布、特征缺失率、模型版本、回退逻辑触发次数。这一层回答“模型效果是否发生漂移”。
这个分层最大的价值在于:当线上出现“变慢”或“不稳”时,你能通过从上层往下层逐层下钻,快速定位是业务量突增、模型退化、框架配置不合理还是物理资源不够。而不是像之前那样在 CPU 内存里瞎猜。
1.2 延迟指标里最容易骗人的三个数值
延迟指标是大家最关心的,但也是最容易看走眼的。我有三个具体经验:
第一,只看平均延迟(AVG)几乎没用。一次 2 秒的超时就能把平均数拉高 30%,但 90% 的请求其实都在正常范围内。我习惯把 P50、P95、P99 三个分位数一起看:P50 反映普遍体验,P95 反映一般压力下的表现,P99 反映极端尾部延迟。如果 P99 和 P50 差距超过 5 倍,说明存在明显的抖动源,需要排查是否有关键路径上的资源竞争。
第二,推理延迟必须拆成排队时间和计算时间分别统计。排队时间表示请求在网关或推理框架的队列里等了多少毫秒;计算时间表示真正进模型算子到出结果花了多少毫秒。这两个值放在一起才是用户感知到的总延迟。如果计算时间稳定但排队时间暴涨,说明入口并发过高或者 batch 策略配置不当;如果排队时间稳定但计算时间变长,大概率是 GPU 资源被抢占或模型权重发生了换入换出。
第三,预热期和稳定期的延迟要分开看。模型刚重新加载时,CUDA kernel 需要编译,显存需要分配,第一批请求往往非常慢。这个阶段如果你把延迟直接打进告警,那每次发版都会引发一堆误报。我的做法是服务启动后预留 1 到 2 分钟“预热窗口”,预热期间的延迟只记录不计入告警。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从零搭建一套推理监控的最小可用方案
接下来是实操部分。我不会直接给出一套需要购买商业产品的方案,而是基于开源工具链,搭建一套可以跑在 1 到 4 台机器规模上的最小可用监控系统。这套方案足够覆盖大部分中型团队的推理服务监控需求,后期再按需扩展。
我选的组合是:Prometheus + Grafana + prometheus-client + 自定义 Exporter + Alertmanager。这套体系在云原生生态里是事实标准,资料多、坑少、后续迁移到 Kubernetes 也顺滑。整个方案的架构是:
- 推理服务(比如基于 Triton、vLLM、TorchServe 或自研 Python Flask/FastAPI 服务)启动时,同时启动一个独立的 Python 监控 Exporter 进程。
- Exporter 定期从推理服务暴露的状态接口或日志中采集四层指标(资源层、框架层、服务层、业务层),格式化为 Prometheus 指标,通过 HTTP 接口暴露。
- Prometheus 按 15 秒间隔来拉取 Exporter 的指标,存储到本地 TSDB,同时通过 rules 配置做阈值判断。
- 触发预警后 Prometheus 将报警推给 Alertmanager,由 Alertmanager 做分组、去重、抑制,然后往钉钉/企微/邮件发通知。
- Grafana 负责从 Prometheus 读取数据并可视化展示。
选择这个组合而不是直接上 SkyWalking 或者 Pinpoint 这类 APM 工具,原因有两个:一是推理服务不像微服务那样有大量 RPC 调用链可以追踪,核心瓶颈通常在单一节点的 GPU 计算和显存管理上,APM 的分布式链路追踪能力在这里有点杀鸡用牛刀;二是 Prometheus 生态对 GPU 指标的采集支持非常成熟,nvidia-dcgm-exporter 直接就能把显存利用率、温度、功耗、带宽这些指标拉出来,省去自己造轮子的时间。
2.1 打点代码怎么写才不容易崩
打点是监控系统的灵魂。我在项目里定义了一个轻量级的 MetricsCollector 类,负责采集各类指标。先说服务层指标,这是最直接的用户体感:
python复制from prometheus_client import Counter, Histogram, Gauge, start_http_server
import time
class InferenceMetrics:
def __init__(self):
self.request_total = Counter(
'inference_requests_total',
'Total inference requests',
['model_name', 'model_version']
)
self.latency = Histogram(
'inference_latency_seconds',
'Inference latency seconds',
['model_name', 'model_version'],
buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0)
)
self.queue_size = Gauge(
'inference_queue_size',
'Current request queue size',
['model_name']
)
self.gpu_util = Gauge(
'inference_gpu_util_percent',
'GPU utilization percent',
['gpu_id']
)
metrics = InferenceMetrics()
# 在推理入口处
def predict(request):
metrics.request_total.labels(model_name='bert_sentiment', model_version='v1').inc()
metrics.queue_size.labels(model_name='bert_sentiment').inc()
start = time.time()
try:
result = model_infer(request)
return result
finally:
metrics.queue_size.labels(model_name='bert_sentiment').dec()
metrics.latency.labels(
model_name='bert_sentiment', model_version='v1'
).observe(time.time() - start)
这段代码有两个细节值得注意。第一,Histogram 的 buckets 我特意按照推理场景的延迟分布来设置,从 5ms 到 5s 一共 11 个桶。Prometheus 的 Histogram 默认 buckets 是从毫秒到秒级别的大跨度,直接拿默认配置会导致 P99 计算不准确,因为桶太粗了。第二,request_total 这个 Counter 和 latency 这个 Histogram 都挂上了 model_name 和 model_version 标签,这样后面做模型版本对比时就不需要重新埋点,直接在 PromQL 里按标签过滤就行。
框架层指标我推荐从推理框架自带的 metrics 端点抓取,而不是自己写代码打点,因为像 vLLM、Triton 这些框架已经把关键指标暴露得很全面了。以 vLLM 为例,它内置了 /metrics 端点,能直接拿到 cache hit rate、running sequences、waiting sequences 这些非常有价值的框架层指标。cache hit rate 尤其关键,它表示前缀缓存命中的比例,命中率高说明可以用更少的显存空间换更高的吞吐量,如果这个值下跌,通常意味着输入序列的公共前缀发生了变化,需要检查数据处理链路。
2.2 一张我实际在用的 Grafana 看板
可视化方面,我整理了一套自己的看板布局,供参考。看板一共七行,从上到下分别是:
- 第一行(状态总览):当前在线 GPU 数量、总显存/已占用显存、当前队列深度、最近 5 分钟错误率。
- 第二行(延迟梯队):P50/P95/P99 延迟曲线,多模型版本用不同颜色区分。
- 第三行(吞吐):QPS 总览 + 分模型 QPS。
- 第四行(GPU 资源):每张 GPU 的利用率、显存占用、温度、功耗,用时间序列折线图,按 GPU ID 分组。
- 第五行(框架内部):cache hit rate、running sequences、waiting sequences、JIT 编译缓存命中率。
- 第六行(业务健康):输入长度分布(直方图)、推理置信度均值、回退逻辑触发次数。
- 第七行(资源饱和度):CPU 利用率、内存利用率、磁盘 IO 等待、网络吞吐。
这套布局的思路是让任何人打开看板,第一眼先看整条链路的健康度,然后按需往下钻。如果你在值班时接到告警,先看第一行有没有资源打满,再看第二行延迟有没有突变,基本就能判断问题的方向。
2.3 Prometheus 配置里最容易忽略的抓取频率
Prometheus 的 scrape_interval 默认是 60 秒,对这个监控场景来说太慢了。模型推理的延迟抖动往往在十几秒内就会出现,如果 60 秒才拉一次数据,等你看到指标变化时,问题可能已经缓解或者转移了。我把抓取间隔调成了 15 秒,同时把 Alertmanager 的 group_wait 和 repeat_interval 也调短,保证告警能尽快触达。
一个常见的误操作是:为了节省存储空间,把 Prometheus 的 storage.tsdb.retention.time 设置得很短,比如 1 周。我建议至少保留 30 天,原因很实际——排查“昨天这个时间点是不是也出现过同样问题”这个场景时,如果数据只留一周,你根本没法做周期性对比。只要小心控制指标基数(不要给高基数标签加 UUID 这种值),30 天存储的磁盘开销并不夸张。
3. 预警阈值设计的血泪教训与实用公式
阈值设计是整个预警机制里最容易“拍脑袋”的部分,但也是决定这套系统到底是“救命稻草”还是“狼来了”的关键。
先说一个普遍现象:很多人第一次配阈值时,延迟超过 200ms 就告警,结果每分钟都在告警,值班同事拉黑告警群;然后把阈值调到 1000ms,结果线上真的出问题了,告警却没触发。阈值不是拍脑袋定出来的,而是基于一段稳定周期内的基线数据计算出来的。我整理了一套自己用着比较顺手的做法。
3.1 动态基线比固定阈值靠谱
对于延迟这类高度依赖流量和负载的指标,固定阈值在低峰期误报、高峰期漏报。我采用的方法是“分位数 + 宽限期 + 环比”的组合:
第一,取最近 7 天同一时段的 P95 延迟作为基线。比如现在是上午 10 点,就取过去 7 天每天上午 9:50 到 10:10 这个时间窗口的 P95 延迟,算出平均值和标准差。第二,当实时的 P95 延迟超过基线的 1.5 倍,并且持续 3 分钟以上,才触发告警。第三,如果 P95 延迟超过基线的 3 倍,不需要等待 3 分钟,立即触发紧急告警。这个“分位数 + 倍数 + 持续时长”的组合,能大幅减少误报,同时保证对突发问题的检测灵敏度。
对于显存这类资源型指标,我选择用固定阈值加趋势预测。显存占用超过总容量的 85% 预警,超过 92% 紧急告警。85% 这个数字听上去保守,但实测下来很合理——显存占用一旦接近上限,模型推理时如果出现动态 batch 增大(比如用户流量瞬间翻倍),触发显存 reallocation 的概率会急剧上升,性能会出现锯齿状抖动,比显存不足直接 OOM 更难排查。
3.2 告警分级的完整规则表
我把告警分级分成 P0/P1/P2 三级,并把触发条件、自动处置动作都固化在配置里。
| 级别 | 触发条件 | 自动处置动作 |
|---|---|---|
| P0(紧急) | P95 延迟超过基线 3 倍;显存占用超过 92%;GPU 利用率 10 分钟内持续为 0 但 QPS > 0;连续 5% 请求返回超时 | 立即通知值班人员,自动扩容(可用时),停止新版模型流量切换,回滚到上一稳定版本 |
| P1(警告) | P95 延迟超过基线 1.5 倍且持续 3 分钟;显存占用超过 85%;模型置信度均值环比下降 10% | 通知模型/运维负责人,记录现场数据,自动抓取线程栈和 CUDA 事件回调 |
| P2(提示) | 单次延迟超过 500ms 但总占比低于 1%;GPU 温度超过 80 度;队列深度超过设定值 30 秒 | 记录日志,不主动通知,留待每日巡检 |
这里有一个设计重点:每个级别的通知渠道要不一样。P0 走电话加钉钉紧急群双重触达,P1 走钉钉普通群,P2 只记录不进群。这样才能保证真正紧急的问题不会被大量低优先级告警淹没。
3.3 延迟突增时的 15 分钟排查路径
预警机制的价值不仅在于“报警”,更在于“报警之后怎么办”。我给自己定了一个 15 分钟快速排查路径,到点没定位到问题就立即升级:
- 0-2 分钟:打开 Grafana 看板,确认延迟突变发生在哪个模型的哪个版本,QPS 是否有同步变化。
- 2-5 分钟:检查 GPU 利用率、显存占用曲线,看是否出现显存 swap 或温度过高。
- 5-8 分钟:检查框架层指标(cache hit rate、queue depth、running sequences),确认是不是 batch 策略因流量变化触发异常。
- 8-12 分钟:拉取最近 5 分钟的模型日志,检查是否有输入长度分布突变(比如某个用户上传了超长文本),导致计算量和显存占用剧增。
- 12-15 分钟:如果以上都没定位到,抓取 CUDA 事件样本,检查是否存在 ECC 错误、GPU 降频等硬件问题。
这套路径执行下来,绝大多数问题能在 15 分钟内有方向。我最常遇到的情况其实是第二种——显存 swap,这跟第一部分提到的案例完全一致。
4. 我在落地一周后遭遇的告警风暴
方案上线第一周,我经历了三次印象深刻的告警风暴,这几次教训直接改变了整个监控系统的设计。
第一次是模型预热期的误报。当时告警规则里已经给延迟指标加了 1.5 倍基线的判断,但模型热加载完成后需要跑一批请求来触发 CUDA kernel 编译,这个阶段的 P95 延迟能达到稳定期的 8 倍。告警规则触发了 P0,值班同事半夜爬起来一看,啥事没有。解决办法是给每个模型实例增加 model_loading_status 指标,在预热完成前将实例标记为 not_ready,Prometheus 的告警规则里过滤掉这个标签。
第二次是 GPU 利用率持续为 0 的误报。规则当时设计的是“GPU 利用率 10 分钟内持续为 0 且 QPS > 0 触发紧急告警”,目的是检测 GPU 被休眠或上下文丢失。结果发版时测试流量打过来,QPS 有值,但模型还没完成加载,GPU 利用率确实为 0。这个规则在上线第三天凌晨触发了一次误报。我把规则的判断条件改成“GPU 利用率持续为 0 且 QPS 超过正常基线的 50% 且模型状态为 ready”,过滤了预热期的误报。
第三次是跨可用区网络抖动导致的延迟突变。某天下午 P95 延迟超过基线 2 倍,告警触发。排查一圈发现 GPU 正常、框架正常、输入分布正常,最后才定位到请求经过的跨节点网络出现了 200ms 的抖动,根因是交换机固件升级引发的动态路由重算。这次之后,我在监控里加了网关层的上游响应耗时指标,把“服务自身计算耗时”和“网络传输耗时”彻底分开,才真正把延迟问题定位精确到段。这一点对多节点部署的团队尤其重要,否则每次延迟突增都会陷入“先排除网络还是先排除模型”的两难中。
4.1 从告警风暴里沉淀出的三大配置原则
经历过这几次折腾,我总结出三条原则:
第一,告警规则必须先“静默观察一周”再上线。把规则配成只记录不通知的观察模式,跑一周,统计触发次数和误报率,然后根据实际情况调阈值。直接上线通知模式,大概率会变成惊弓之鸟。
第二,告警抑制规则一定要配好。当一个节点已经触发 P0 时,同一节点上的 P1/P2 告警应自动抑制,避免告警轰炸。Alertmanager 里通过 inhibit_rules 实现,按严重级别抑制,把链条从下到上串起来。
第三,所有告警必须附带当前上下文信息。告警通知里不要只有“P95 延迟超限”这一句话,要带上模型名、版本号、GPU ID、当前 QPS、最近 10 分钟的延迟曲线截图链接。人都是有认知惯性的,一句话告警和带上下文的告警,处理时间差距是数量级的。
4.2 预警机制如何反哺到模型版本发布流程
监控和预警不应该是孤立的运维工具,它完全可以融入到模型发布的流程里,形成闭环。
我的做法是:每个模型新版本上线前,先在灰度环境中运行 2 小时,用这套监控系统自动采集延迟、显存、置信度三个关键维度的数据,并和线上旧版本的基线做自动对比。如果延迟劣化超过 10% 或显存增加超过 5%,系统直接阻止自动放量,要求模型工程师确认是否继续上线。这套机制上线后,团队因为模型问题引起线上事故的次数从平均一个月一次降到了半年一次。预警机制的价值也从一个被动的“事后报警器”,变成了主动的“质量闸门”。
当然,这套系统的初始搭建成本也不低。我前后花了大约两周时间,中间踩坑无数,但整体看下来,这笔投入非常值。至少下次凌晨四点再出现延迟飙升,我可以在十分钟内给出定位方向,而不是像上次那样慌慌张张地在 CPU 内存日志里来回翻找,等到用户投诉才意识到问题。监控系统的价值不是让你永远不出事,而是让出事时你有章法、有节奏、有依据地处理。
