1. 先别急着接监控系统:推理延迟到底在盯什么
做AI模型推理服务的同学,大概率都经历过这种半夜惊醒的时刻:手机报警短信疯狂震动,爬起来一看——延迟超标了,但等你打开Grafana,指标已经自己恢复了,只能悻悻躺回去。或者反过来,指标图一路狂飙,但业务方反馈“还能用”,你根本分不清是该紧急处理还是继续睡。
我做了几年的模型推理服务运维,最大的感受是:延迟监控和报警这件事,难点不在“怎么监控”,而在“监控什么”和“报警怎么定”。如果直接把业务侧常用的“平均响应时间超过200ms就报警”这套方法论搬过来,大概率会被折磨到怀疑人生。
先说第一个核心认知:大模型推理服务的延迟,和传统Web服务的RT(响应时间)有本质区别。传统Web一个请求就是一个完整的事务,响应时间能直接反映用户体验。但大模型推理是流式生成的,一个请求可能是几百个token连续吐出来,用户看到第一个字的时间、看到完整回答的时间、请求在队列里排队的时间,这三个数字完全不同,也分别代表不同环节的健康度。
我建议先做一次指标盘点,把延迟拆成五个维度再谈监控:
- 首Token延迟(TTFT,Time To First Token):从请求发出到模型吐出第一个token的时间。这是用户能感知到的“响应速度”,也是最关键的业务指标。一般超过3~5秒,用户感知就会很差。
- Token生成速度(TPOT,Time Per Output Token):生成后续每个token的平均耗时,决定用户看完回答的总时间。通常用“每秒生成多少个token”来描述更直观。
- 端到端总延迟:从请求发起到最后一个token返回的总时长,等于TTFT加上所有token的生成时间之和,再算上网络传输等开销。
- 排队时长(Scheduling/Waiting Time):请求到达服务后,等待调度器分配GPU资源的耗时。排队时间过长往往意味着服务容量不够,或者并发策略配置不合理。
- 冷启动/扩容延迟:在自动扩缩容场景下,新实例从创建到真正能承接流量的耗时。如果经常触发扩容,但扩容半天起不来,延迟照样崩。
这五个维度,每个都要有独立的统计和监控规则,绝对不能混在一个“平均延迟”里。混在一起的结果就是:某次请求因为排队等了10秒,平均延迟从200ms被拉到1800ms,报警倒是触发了,但你根本不知道是排队、显存不足还是模型推理本身变慢,排查效率极低。
另外还要强调一点:延迟监控必须按模型、按版本、按部署环境(如在线服务、离线批量任务)分开打标签。如果你的服务是多模型混部,或者同一个模型还挂着几个版本做灰度,标签没分开的话,报警一到你连是哪个模型出问题都不知道。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 指标采集的三个层次:从日志埋点到分位数统计
确定要监控哪些指标后,下一个问题是:数据从哪里来?我在实际落地中把采集方案分成三个层次,不同阶段选不同方案,性价比最高。
2.1 第一层:请求日志的精准埋点
最基础的采集方式,是每个推理请求在处理时,把上面五个维度的耗时数据记录到结构化日志里。具体来说,我通常会在推理服务的网关层或代理层做统一埋点,而不是让每个模型服务自己上报。
埋点的关键字段,我的建议是最少包含这些:
json复制{
"request_id": "a0f8c1d2-...",
"model_name": "chat-7b-v3",
"model_version": "20240112",
"timestamp": 1705123456789,
"ttft_ms": 850,
"tpot_ms": 46,
"total_tokens": 386,
"end_to_end_ms": 18624,
"queue_wait_ms": 120,
"inference_ms": 18200,
"status_code": 200
}
这里有个容易被忽略的点:第一次请求和后续请求的耗时差异巨大。如果模型文件还没加载到GPU显存,第一次请求可能要吃十几秒甚至几十秒的冷启动延迟。所以我一般在埋点里加一个is_warmup字段,用于标记是否首次请求。如果不把这个单独拎出来,首个请求会被误判为性能严重劣化,导致误报警。
日志埋点的缺点很明显:它不是实时的。日志写到磁盘、采集器解析、写入存储,再到查询出来,通常有几十秒到几分钟的延迟。用来做事后分析足够,但用来做实时报警就不太够。所以它通常作为“指标存储”的补充,不单独承担报警职责。
2.2 第二层:框架内置指标的主动暴露
到大模型推理这个阶段,大部分主流推理框架已经内置了监控指标接口。比如vLLM、Triton Inference Server、TensorRT-LLM,都会暴露Prometheus格式的metrics端点,里面常见的有:
vllm:request_success_total、vllm:request_failure_total:请求成功/失败计数vllm:request_latency_seconds:请求延迟直方图vllm:request_generation_tokens:生成token数量分布triton:inference_request_duration_us:推理请求耗时(微秒)- GPU相关:
nv_gpu_utilization、nv_gpu_memory_used_bytes
这个层次的采集属于“白嫖”性质,部署成本最低,搭好Prometheus后把target加上就行。但有两个坑必须提前知道。
坑一:框架内置指标的口径定义不透明。 比如vLLM的request_latency_seconds,它包含排队时间吗?包含网络传输吗?不同版本可能定义都不一样。我很早之前就因为没搞清楚口径,拿框架指标做报警基准,结果某次框架升级后指标口径变了,报警行为完全偏离预期。所以任何框架指标必须和日志埋点做一次对拍验证:同一批请求,两边统计的P99延迟误差不能超过5%。
坑二:有些指标只在请求结束后才更新。 如果存在长时间不结束的流式请求(比如持续生成上千token),这个请求在结束前都不会出现在直方图里。如果你用“最近5分钟的P99延迟”做报警,那么在持续高负载期间,直方图里可能全是短请求,长请求一直被当前窗口“遮住”,延迟指标反而显得健康。
2.3 第三层:端到端拨测与合成监控
第三层是我非常推荐但很多人会忽略的——探活和拨测。所谓拨测,就是定期用模拟请求打向真实推理服务,从外部视角测量可用性和延迟。这层监控解决的是“服务自己说没问题,但用户就是调不通”的疑难杂症。
在推理服务里,拨测不仅要测“能不能返回”,还建议测“生成质量是否正常”。比如说,可以构造一些预定义的prompt,请求完成后检查返回内容的长度是否在合理范围、有没有返回空结果或报错。有些模型的异常不是直接报错,而是会生成大量重复token或空白输出,这种异常在服务端日志里表现为“请求成功”,但业务上完全是失败的。
拨测的频率和请求量要控制好。我之前见过有人把拨测请求打到生产环境的主集群里,本身就把负载又拉高了一截。我的做法是单独部署一个轻量级探活服务,用一个很便宜的CPU实例跑,所有拨测请求发到独立的shadow副本或者用最低优先级调度,确保拨测本身不会影响线上推理服务的容量。
2.4 指标归一的存储选择
三个层次的采集数据,最终建议统一落到一个时序数据库里。Prometheus是事实标准,生态最全。但要注意:Prometheus本身不适合存高基数的日志明细。所以在我的架构里,日志明细走Elasticsearch或ClickHouse,聚合指标走Prometheus。
这里提一下Prometheus拉取模式的先天限制:如果你的推理服务实例是短生命周期(比如K8s里频繁重建的Pod),拉取可能跟不上实例的诞生和销毁速度。对于一次性的离线批量推理任务,如果任务跑完就退出,Prometheus根本来不及抓到完整数据。这种情况建议用Prometheus Pushgateway,或者直接在任务结束时把结果指标通过API写入到独立的指标服务里。
3. 阈值与报警规则设计:让报警学会分辨“真故障”和“假警报”
采集系统跑起来之后,真正折磨人的才刚刚开始——报警规则怎么定。这是我觉得全项目里最需要经验的地方,直接决定你晚上能不能睡好觉。
3.1 为什么固定阈值不可靠
绝大部分人上手第一版报警规则都会写成这样:
code复制延迟P99 > 2秒,持续5分钟,触发报警
这种固定阈值在流量稳定、模型版本不变、业务形态不变的情况下,确实能工作一阵子。但真实环境里,推理服务有两个天然属性会导致固定阈值失效:
第一个是负载波动。白天流量大,并发高,排队时间自然拉长;凌晨流量几乎为零,延迟轻松跑进500ms以内。用同一个阈值去卡,白天很容易误报,凌晨则可能漏掉真正的性能退化——因为低负载下怎么跑都不会超阈值。
第二个是模型版本迭代带来的基线漂移。模型升级到新版本后,延迟基线整体变化是非常正常的。比如同一个模型,从v1升级到v2,由于采用了更长的上下文压缩或更复杂的注意力机制,P99从1.2秒变成了1.8秒。固定阈值如果还卡在1.5秒,v2上线当天就会触发一轮报警。
所以我的核心建议是:不要用固定阈值作为主报警策略,只把它当作最后兜底。主策略应该是动态基线。
3.2 动态基线的两种落地方式
动态基线的思路很简单:用历史数据生成当前时刻的“正常区间”,当前指标超出区间才报警。
第一种方式是同环比基线。把当前5分钟的P99延迟,和过去7天同一个时间点的P99延迟对比,如果超出历史均值3倍标准差,就报警。这种方式实现最简单,天然具备周期性,能适配业务波峰波谷。
第二种方式是滑动窗口预测。用过去1小时的数据做指数加权移动平均(EWMA),算出期望值和波动范围,如果当前值超过期望值N个标准差就报警。这种方式对突变的响应更快,但对调参更敏感。
我在生产上用的是组合策略:同环比基线负责常规时段,滑动窗口负责突发检测。具体公式就不再赘述,PromQL里用stddev_over_time和abs配合就能实现,不复杂。
这里有一个特别关键的细节:不要拿“平均延迟”做动态基线,要拿分位数。平均值在长尾分布下几乎没有意义。我见过最典型的例子:99%的请求跑在500ms,1%的请求跑了20秒,平均延迟变成700ms,看起来还好,但实际P99已经爆了。
3.3 报警分级与时间窗口设计
报警规则定好阈值还不够,还要设计分级和防抖。
我把报警分成三级:
| 级别 | 触发条件 | 响应要求 |
|---|---|---|
| P0 | P99延迟超过基线的5倍,或服务可用性低于95%,持续3分钟 | 立即唤醒值班人,要求5分钟内确认 |
| P1 | P99延迟超过基线的2倍,持续10分钟 | 30分钟内响应 |
| P2 | 单机GPU利用率长期低于10%但延迟升高 | 工作时间处理,不拉休息的人 |
防抖是另一个关键环节。刚触发报警的1~2分钟内,不建议立即二次报警,但也不建议设置过长的时间窗口来“压制”报警。时间窗口太短容易抖动误报,太长则可能掩盖持续恶化。我的经验是:P0用3分钟窗口,P1用10分钟窗口,窗口期内持续超标才触发,一旦恢复就自动清除。
还有一个很容易踩的坑:报警后恢复通知的设计。很多监控系统只配置了触发通知,没配置恢复通知,导致值班人到了现场发现指标已经恢复正常,但监控系统一直挂着“未恢复”的状态,后续再触发新的报警时容易被混淆。建议一定要同时配置resolve恢复通知,让每条报警有始有终。
3.4 报警通知的人性化处理
报警通知本身也要花心思,不然值班人看到一条干巴巴的“P99延迟超限”,还得自己动手查半天才定位到是哪个模型、哪个版本、哪个集群。
我一般会把报警消息模板设计成下面这种结构:
text复制【P0】【生产环境】模型 chat-7b-v3 (v20240112) 延迟异常
- 集群:inference-prod-01
- 指标:P99延迟 3.2秒(基线 800ms,超限 4倍)
- 时间:2024-01-13 14:32:00 ~ 14:35:00(持续3分钟)
- 当前并发:48(历史同期 35)
- GPU平均利用率:92%
- 最近一次模型部署:2024-01-12 22:00(14小时前)
- 关联日志:跳转Kibana查询链接
- 最近代码变更:点击查看提交记录
这个模板里最有用的字段是“最近一次模型部署”和“最近代码变更”。很多延迟突发的根因就是发布引起,把这两个信息自动关联到报警消息里,能帮值班人省下一半的排查时间。
报警通知的渠道也要分级。P0走电话或短信,P1走IM群,P2走邮件或工单。不要把P2的邮件和P0的短信混在一起,不然等真正重要的报警出现时,人的注意力已经被噪音消耗完了。
4. 一次真实的长尾排查:从P99报警到根因定位的完整链路
光讲方法论不够,我分享一次真实的排查经历。那次排查之后,我把监控体系重构了一遍,也沉淀了不少经验。
那天下午14:32,报警群弹出P0消息:聊天模型chat-7b-v3的P99延迟从基线800ms飙到了3.2秒,持续3分钟。收到报警后,我按照定好的排查SOP一步步来。
4.1 第一步:确认影响面
首先不是冲去看模型状态,而是确认“影响范围有多大”。打开聚合面板,发现只有chat-7b-v3这个模型的延迟异常,其他模型如embedding、rerank全部正常。这说明问题大概率不是集群层面的(比如网络故障、存储故障),而是模型实例内部出了状况。
这一层判断非常重要。如果你看到所有模型延迟同时飙升,优先怀疑基础设施;如果只有单一模型异常,直接进模型实例排查,效率高得多。
4.2 第二步:看资源水位与排队情况
接着看GPU利用率和显存占用。指标显示:该模型的GPU利用率只有23%,远低于平时的90%左右;但请求的排队数量(queue_wait_ms)从原来的几十毫秒暴涨到1.5秒。
这个组合很有迷惑性——GPU利用率低但排队时间高,看着像是容量不够,但扩容又没必要(GPU根本没吃饱)。于是怀疑点转向了推理内部逻辑:是不是推理代码在生成过程中被什么东西阻塞了,导致GPU一直在空转等待。
4.3 第三步:抽样单请求日志
排队时间高但GPU利用率低,我初步判断是单条请求的生成耗时被拉长了。于是从日志系统里抽取了几个高延迟请求,发现它们的TTFT都正常(大约300ms),但TPOT异常——正常是每token约45ms,这些异常请求变成了每token约280ms。
然后进一步对比这些异常请求的共同特征,发现它们的输入prompt长度都超过了4000字符,而且返回内容中也频繁出现重复片段。看到这里,我心里基本锁定了方向:长序列输入导致推理退化。
4.4 第四步:确认根因与修复
查看代码变更记录后发现,当天上午有一次模型版本热更新,把上下文窗口从2048扩展到了4096。理论上窗口变大是好事,但问题恰恰出在某个自研算子没有针对超长序列做优化,在序列长度超过某个阈值后,注意力计算的cache命中率骤降,导致计算量飙升但GPU并行度发挥不出来。
修复方案分两层:短期内,在网关层加了输入长度截断策略,超过4000字符的请求先做摘要再进模型;长期来看,替换了那个自研算子并补了针对长序列的压测用例。
4.5 这次排查沉淀了什么
事后复盘,我总结出几条对监控体系有价值的改进:
- 报警消息里自动带上模型最近变更记录,这次如果一开始就有这个字段,能省掉至少20分钟。
- 增加输入长度分布的监控指标。只要发现长尾请求数量增多,就要警惕推理性能退化。
- 把“GPU利用率低 + 排队时间高”这个组合作为专门的一个报警规则,它代表“假繁忙”状态,根因往往在代码逻辑而非容量问题。
5. 架构选型与演进路线:从零搭建还是用现成方案
最后说一下架构选型和完整落地路径。这个东西没有标准答案,取决于团队人力、已有技术栈、业务规模,我按三个典型的阶段来讲。
5.1 阶段一:轻量自研(适合小团队或起步期)
如果团队只有两三个人,推理服务刚上线,我建议不要先铺一整套路Prometheus + Grafana + Alertmanager,太重了。可以用一个极简方案快速跑起来:
- 用Flask/FastAPI在推理服务里内嵌一个
/metrics接口,返回JSON格式的延迟统计。 - 写一个独立的Python脚本,每60秒轮询一次所有实例的这个接口,计算P99、平均延迟、成功率等。
- 把结果写入SQLite或直接存内存,并用
requests打到一个自建的状态页上。 - 报警逻辑同样在这个脚本里判断,阈值先写死,触发后通过飞书/钉钉/企业微信的webhook发通知。
这个方案的最大的好处是30分钟内能跑通,并且能让你快速积累“哪些指标是有用的”这一经验。缺点是没有历史数据存储,节奏分析很受限。所以建议从第一天起,就把指标同时写一份到CSV文件或简单数据库,哪怕后面迁移到正式方案,历史数据还能用来校准基线。
5.2 阶段二:标准Prometheus生态(适合生产环境)
当服务开始承载真实流量,我推荐立刻迁移到Prometheus + Grafana + Alertmanager这套组合。标准的部署架构是:
- 指标暴露端:推理服务通过Prometheus客户端库暴露
/metrics端点,或者直接接入框架内置端点。 - 采集端:Prometheus Server以K8s服务发现方式动态抓取所有实例。
- 存储端:指标保留时长建议至少30天。Prometheus默认的本地存储不适合长时间保留,建议配Thanos或VictoriaMetrics做远端长期存储。
- 可视化端:Grafana负责面板展示,把TTFT、TPOT、P99、GPU利用率、排队时间、请求量这些指标放在一个统一的面板里,横轴时间,按模型/版本分组。
- 报警端:Alertmanager负责接收Prometheus推送的告警,并在内部做分组、抑制、静默,再发到通知渠道。
这个阶段的关键任务,是把动态基线规则、报警分级、消息模板设计这些“软实力”补齐,而不是只搭个架子。
5.3 阶段三:引入AIOps和可观测性(适合规模化后)
当模型数量多了、服务拓扑复杂了,传统的“人工定阈值 + 动态基线”会开始不够用。这个阶段可以考虑引入更智能的异常检测模块来做辅助。
比如用一段滑动窗口的历史数据做时序预测,在模型上线新版本之前,先用shadow流量跑一段小样本,预测新版本延迟的P99区间,如果预测结果超过预期值就自动阻断发布。这套思路能显著减少“模型升级后延迟劣化”这类问题。
另外,把日志、链路追踪、指标打通是规模化后的必修课。推理延迟问题的排查,往往不是单看一个指标就能定位,而是需要把指标的“现象”和日志里的“线索”、链路里的“步骤耗时”串起来。比如追踪一条慢请求,从网关到推理引擎到tokenizer到模型前向计算,每段耗时都清晰可见,定位问题的速度会快非常多。
5.4 工具选型对比选型参考
我列一个当前主流的工具选型对比表,方便你根据团队情况快速决策:
| 环节 | 轻量方案 | 标准方案 | 规模方案 |
|---|---|---|---|
| 指标采集 | Python脚本轮询 / telegraf | Prometheus拉取 | Prometheus + 自定义Exporter + 日志采集器 |
| 指标存储 | SQLite / CSV | Prometheus本地 + 少量保留期 | 本地 + Thanos / VictoriaMetrics 长期存储 |
| 可视化 | 自建状态页 | Grafana | Grafana + 自定义大盘插件 |
| 报警 | 脚本内规则 + Webhook | Alertmanager | Alertmanager + 平台工单联动 |
| 异常检测 | 固定阈值 | 动态基线(同环比 + 滑动窗口) | 流量预测 + 智能异常检测 + 发布阻断 |
我个人建议:除非团队对PromQL和监控系统运维非常不熟,否则别长期停留在阶段一。Prometheus生态是行业事实标准,后面接任何组件都方便,过度定制和自研在运维侧纯属给自己找活干。
6. 落地过程中的高频踩坑清单与避坑建议
项目做到这个份上,我还想专门写一节“踩坑清单”,这些坑是我自己踩过、或者看身边同行踩过的,每一条都是真金白银换来的教训。
6.1 直方图桶设计不合理导致分位数失真
Prometheus直方图默认的bucket边界是0.1、0.2、0.5、1、2、5这样的指数级分布。对大模型推理这种延迟跨度极大的场景(有的请求100ms,有的要30秒),默认bucket会导致P99计算严重失真——因为P99落在了某个过宽的桶里,估算出来的延迟可能误差超过50%。
解决方法是自定义bucket,贴合你服务的延迟分布来设置。比如:
yaml复制buckets: [0.05, 0.1, 0.2, 0.5, 1, 2, 3, 5, 10, 20, 60]
另外,核心指标建议用Summary类型而不是Histogram,因为Summary在客户端直接计算分位数,不存在bucket失真的问题。缺点是不能跨实例聚合,所以要看服务的部署形态来决定用哪个。
6.2 时间窗口聚合带来的“掩盖效应”
很多人做延迟报警时,用histogram_quantile(0.99, sum(rate(...)[5m]))这样的PromQL。这个查询的结果是“过去5分钟内所有请求合并计算的P99”。如果5分钟内请求量非常大,那么极少数异常长请求会被海量正常请求稀释,P99看起来就没那么夸张,报警可能压根不会触发。
我建议做两类拆分:一是缩短聚合窗口到1~2分钟,提高对突变的敏感度;二是单独监控最高延迟,比如用max_over_time来捕捉极端长尾。P99管“大多数用户的体验”,max管“最坏情况”,两个指标都放到报警规则里。
6.3 并发状态与延迟指标割裂导致的误判
只监控延迟、不关联并发状态,会掉进一个经典的逻辑陷阱:并发升高导致延迟升高,延迟升高导致客户端超时重试,重试又进一步提高并发,形成恶性循环。这时候看到的延迟数据其实是“结果”,不是“原因”。
所以任何延迟报警触发时,都必须同时拉出当前并发数、排队长度、GC暂停时间、GPU利用率这些关联指标,才能判断是“容量不够”还是“单请求变慢”。我在Grafana里建了一个专门的“延迟根因分析”仪表盘,把这几类指标按时间对齐展示,排查效率提升非常明显。
6.4 报警风暴与依赖链崩溃
模型推理服务很少有单点故障,但报警系统很容易“从众”——某个上游服务挂掉,导致所有下游模型实例的请求全部超时,几千条报警同时轰炸值班群。为了避免这个问题,Alertmanager需要配置分组和抑制规则,建议按“服务-模型-实例”三个层级做分组,同时设置依赖关系:如果网关层已经报警,下游模型实例的报警自动抑制。
报警风暴本质上不是监控系统的问题,而是报警设计的问题。等到风暴发生再处理就晚了,一定要提前把分组、抑制、静默规则配好。
6.5 拨测拨到“缓存节点”导致失真
端到端拨测如果只是打到一个前置缓存或网关,而网关后面才是真正的模型推理服务,那拨测结果只能反映网关的健康度,不能反映模型本身。做拨测时,要么直接打到推理服务本身的接口,要么确保拨测请求不会被缓存等中间层拦截,否则你在监控“你以为的延迟”,不是“用户真实的延迟”。
7. 我的最终建议
整个项目做到最后,我最想分享的一条心得是:延迟监控和自动化报警,表面上是技术问题,本质上是“对系统行为建立预期”的问题。你对自己服务的延迟分布理解得越深,报警规则才能定得越准。一开始报警系统会频繁误报,不要觉得失败,那是你正在建立对系统的“感觉”。当报警系统逐渐稳定下来,误报率降低、每次报警都能精准定位根因的时候,你对整个服务的掌控力就真正上来了。
如果你正要开始搭建这套体系,从阶段一或者阶段二起步都可以,但记住一条原则:监控系统的首要目标不应该是“技术看起来先进”,而是“让一个人在凌晨3点收到报警后,能最快判断出问题在哪、要不要马上爬起来处理”。所有设计都围绕这个目标,就不会跑偏。
