
从推理服务上线第一天起延迟监控就是我最先补的一块短板。早期模型推理服务少、调用方单一出了问题靠调用方反馈也能撑一阵子。等服务多起来、流量开始波动才发现没有一套延迟监控体系基本等于蒙着眼开车——服务挂没挂、慢没慢、慢在哪一层全靠猜。这篇文章我把自己在AI模型推理延迟监控体系设计上踩过的坑、沉淀下来的方案、以及可以直接参考的落地做法完整梳理一遍覆盖从指标体系设计、技术选型、埋点实现到告警排查的完整链路给正在做AI工程化、模型部署和推理服务治理的同学一个可复用的参考。1. 先想清楚延迟监控到底要回答什么问题很多团队一上来就装监控组件、配面板结果面板上画了一堆曲线出了事还是不知道从哪查起。问题出在没想清楚监控的目标就动手把手段当成了目的。做推理延迟监控之前必须先搞清楚我们需要它回答哪几类问题。1.1 在线推理场景下延迟监控的核心矛盾在线推理服务的延迟本质上是一个端到端的用户体验问题。用户从发起请求到拿到结果中间经历的每一个环节都可能成为瓶颈。你单独看模型推理耗时是20毫秒但用户感知到的却是2秒为什么因为排队等了几百毫秒预处理和后处理又花了几百毫秒网络传输还要时间。这就是在线推理延迟监控的核心矛盾服务内部各阶段的原始耗时和用户真正感知到的端到端延迟往往是两回事。早期我给一个内部视觉模型服务做监控时只盯着模型推理耗时这一个指标。结果某次版本上线后模型推理耗时没变化用户却在反馈说变卡了。查下来发现是输入预处理阶段新增了一个图像增强逻辑单张图多花了300多毫秒因为推理服务是并发模型这个新增耗时直接把上游请求队列堵住了。所以延迟监控体系设计的第一步不是选工具而是把一次请求从进入到返回的全路径拆开明确每一段由谁负责、延迟预算分别是多少。1.2 监控体系设计之前必须明确的几个边界动手设计之前有几个边界一定要先理清否则后面会反复返工。第一监控粒度。你到底想监控一个服务的整体延迟还是想拆到每个模型的延迟、每个版本的延迟、甚至每条输入数据长度区间下的延迟粒度越细诊断越方便但埋点、存储和面板的复杂度也会成倍增加。我建议按服务级一请求级一阶段级三层先落地服务级用于告警请求级用于日常观察阶段级用于排查定位。第二监控口径。延迟是算从网关收到请求开始还是从推理服务进程收到请求开始算不算排队时间算不算网络往返不同团队对同一个指标的理解如果不一致后面做SLO评估、跨团队对比时会非常痛苦。建议在指标命名上就体现口径比如http_request_duration_seconds代表网络入口到出口的完整耗时inference_request_duration_seconds代表推理进程内部处理耗时两者严格区分。第三监控与告警的边界。监控负责记录和展示告警负责打扰人。不是所有指标都值得配置告警也不是所有异常都能通过告警发现。设计阶段就要把哪些指标只用于事后分析、哪些指标需要实时告警定下来避免告警疲劳之后真出了问题反而没人看。2. 指标体系设计不能只盯着一个p99延迟监控最忌讳的就是把全部注意力放在一个百分位上。p99虽然能反映长尾情况但它只是一个统计快照丢失了大量信息。一个真正可用的推理延迟指标体系应该围绕请求生命周期的不同阶段、不同统计口径、以及不同资源维度来设计。2.1 从请求生命周期拆分延迟阶段一次典型的在线推理请求在服务内部通常会经历这几个阶段接入排队、输入预处理、模型推理、输出后处理、响应返回。每个阶段都要有对应的延迟指标才能在故障时快速定位瓶颈。我自己常用的做法是给每个阶段定义一个独立的Histogram指标统一命名规范单位统一用秒。下面是我在一个视觉推理服务上实际落地过的阶段指标指标名含义建议聚合方式request_queue_duration_seconds请求在队列中的等待时间按服务、模型、优先级分桶preprocess_duration_seconds输入预处理耗时按服务、模型分桶inference_duration_seconds模型推理核心耗时按服务、模型、batch size分桶postprocess_duration_seconds输出后处理耗时按服务、模型分桶request_total_duration_seconds服务内部完整处理耗时按服务、模型、版本分桶upstream_request_duration_seconds从网关视角的端到端耗时按调用方、服务分桶这几个阶段指标加在一起基本覆盖了服务内部的全部耗时分布。出现了整体变慢但推理耗时正常的情况直接看队列和预处理指标就能缩小排查范围。2.2 那些比平均值更值得关注的统计量平均延迟在AI推理场景里基本没什么参考价值因为推理延迟的分布通常呈现显著的长尾特征。大部分请求很快但有少量请求因为排队、资源竞争、显存换页等原因会慢好几倍。平均值会被大多数快请求拉低掩盖长尾问题带来的真实用户体验损伤。所以我在设计指标体系时会同时保留p50、p95、p99、p999四个分位数。p50代表绝大多数用户体感p95和p99代表长尾质量p999则是用来捕捉极端抖动。四个分位数一起看才能还原延迟分布的真实形状。举个例子某个部署在GPU上的文本生成服务p50一直是40毫秒p99却从80毫秒漂移到400毫秒。单看p99会以为模型出问题了但结合GPU利用率看发现SM占用率并不高真正原因是并发请求增多后请求在推理引擎的连续批处理队列里等待时间被拉长了。这种问题只看平均值永远发现不了。2.3 算力侧指标与延迟的联动推理延迟不是模型自己决定的它和底层算力资源的使用状态强相关。设计延迟监控体系时一定要把GPU相关指标纳入联动分析否则延迟曲线出现异常时你很难判断是模型问题、代码问题还是资源问题。我至少会采集这样几类算力指标GPU利用率SM利用率反映计算单元繁忙程度显存占用和显存带宽很多大模型推理的瓶颈其实在显存带宽而不是算力温度与降频状态GPU过热降频会导致推理延迟明显增加推理引擎内部的动态批处理大小和排队长度直接决定请求在引擎内的等待时间。配合方式上我会把延迟指标和算力指标放在同一个Grafana面板里时间轴对齐。出现延迟抖动时先看同一时段GPU是否打满、显存是否吃紧、batch size是否有明显波动大部分问题在这一步就能定位。3. 监控体系的技术选型与整体架构聊完指标体系来说说落地时用到的技术组件和整体架构。我不倾向于一开始就上特别重度的APM全家桶AI推理服务的监控链路有自己的特殊性比如动态扩缩容带来的实例生命周期短、GPU指标采集需要专门适配等问题选型时要把这些因素考虑进去。3.1 选型原则别被全家桶绑架现在的可观测性产品很多有开源的Prometheus、Grafana、Loki、Tempo组合也有商业APM全家桶。我见过不少团队一开始就上了全家桶配置复杂先不说最尴尬的是很多采集项对AI推理场景并不适配反而给运维增加了很多噪音。我的选型原则很简单优先用经过大规模验证的开源组合按需补齐而不是一步到位。对于大部分中大型团队Prometheus加Grafana加Alertmanager的组合已经能覆盖90%的延迟监控需求。理由有三点第一Prometheus的指标模型天然适合延迟这类数值型监控Histogram和Summary类型都很成熟分位数计算可以直接在查询时完成。第二它的服务发现机制能很好适配推理服务的动态扩缩容不需要频繁改配置。第三社区生态非常丰富GPU节点采集有现成的DCGM Exporter应用侧埋点有各语言的Client Library不需要从零造轮子。3.2 整体架构一览与数据流向以Prometheus为核心的这套监控架构数据流向是这样的推理应用通过Client Library在代码里埋点暴露一个HTTP指标端点Prometheus Server定期从这个端点拉取指标数据。GPU节点的指标由DCGM Exporter采集同样暴露给Prometheus拉取。Prometheus将数据存入时序数据库Grafana从Prometheus查询数据并展示面板。Alertmanager负责接收Prometheus推送的告警规则触发结果再通过webhook、邮件等方式通知值班人员。这套架构里不引入消息队列也不引入额外的数据管道链路非常短故障面小。对于监控系统本身简单就是最大的可靠。3.3 埋点方式选哪一种SDK、Agent还是运行时Hook具体到代码层面的埋点有三种常见方式在业务代码里直接用SDK埋点、部署Agent做自动探针、在推理引擎层做运行时Hook。三者的成本和收益差别很大我逐个说下我的看法。SDK埋点是我最推荐的方式也最可控。在推理服务的关键路径上用Prometheus Client库显式埋点哪个阶段需要监控、粒度多细完全由自己决定。缺点是需要在代码里动手改对一些老服务来说改动成本略高。Agent自动探针适合那些不方便改代码的Java类服务通过字节码注入实现自动埋点。但对Python写的AI推理服务来说Agent方案并不成熟而且自动埋点拿到的往往只是框架层的HTTP耗时拿不到模型推理内部各阶段的细分耗时诊断价值有限。运行时Hook是指直接在推理引擎层面做采集比如在Triton、vLLM这类推理服务框架里通过插件机制拿到引擎内部指标。这种方式的优点是数据非常精准能够直接拿到prefill耗时、decode耗时、动态批处理排队时间这些关键指标缺点是依赖特定框架接口换引擎就要重新适配可移植性差。我实际采用的方式是SDK埋点为主、框架指标为辅。核心业务阶段用SDK精确控制引擎内部细节指标通过框架自带接口暴露给Prometheus两者结合既保证了可控性又拿到了深度的引擎侧数据。4. 上手落地一套可运行的埋点与告警参考实现前面讲了设计思路和架构这一节直接给出一套可以照着写的参考实现包括应用侧埋点、Prometheus抓取配置、Grafana面板设计、告警规则编写这几个核心部分。我以一个典型的FastAPI推理服务为例代码用Python实现。4.1 建立基础观测指标代码埋点示例先安装依赖pip install prometheus-client fastapi uvicorn一个标准做法是单独建一个metrics模块统一管理和创建指标对象避免到处new Histogram导致指标重复注册。# metrics.py from prometheus_client import Histogram, Counter, Gauge, generate_latest, CONTENT_TYPE_LATEST from prometheus_client import start_http_server # 请求队列等待耗时 QUEUE_DURATION Histogram( request_queue_duration_seconds, Queue wait time for inference requests, labelnames[service, model, priority], buckets(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0), ) # 预处理耗时 PREPROCESS_DURATION Histogram( preprocess_duration_seconds, Preprocess duration, labelnames[service, model], buckets(0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0), ) # 模型推理耗时 INFERENCE_DURATION Histogram( inference_duration_seconds, Core inference duration, labelnames[service, model, engine], buckets(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0), ) # 后处理耗时 POSTPROCESS_DURATION Histogram( postprocess_duration_seconds, Postprocess duration, labelnames[service, model], buckets(0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0), ) # 服务内部端到端耗时 TOTAL_DURATION Histogram( request_total_duration_seconds, Total service processing duration, labelnames[service, model, version], buckets(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0), ) # 推理请求总数和错误数 REQUEST_COUNT Counter( inference_requests_total, Total inference requests, labelnames[service, model, version, status], ) # 当前排队请求数 QUEUE_SIZE Gauge( request_queue_size, Current queue size, labelnames[service, model], )需要注意的点是Histogram的buckets要结合业务实际情况来定。bucket设得太稀疏分位数计算结果会粗糙太密集存储开销又大。上面这套bucket范围覆盖了5毫秒到60秒适合大多数在线推理服务你可以根据自己的延迟分布动态调整。在FastAPI应用里通过中间件和依赖注入的方式埋点我比较推荐这样做# main.py import time from fastapi import FastAPI, Request from metrics import ( QUEUE_DURATION, PREPROCESS_DURATION, INFERENCE_DURATION, POSTPROCESS_DURATION, TOTAL_DURATION, REQUEST_COUNT, QUEUE_SIZE ) app FastAPI() app.middleware(http) async def metrics_middleware(request: Request, call_next): start time.perf_counter() try: response await call_next(request) status response.status_code except Exception: status 500 raise finally: duration time.perf_counter() - start TOTAL_DURATION.labels( serviceinference-svc, modelrequest.path_params.get(model, unknown), versionv1 ).observe(duration) REQUEST_COUNT.labels( serviceinference-svc, modelrequest.path_params.get(model, unknown), versionv1, statusstatus ).inc() return response app.post(/v1/models/{model}/infer) async def infer(model: str, request: Request): # 模拟入队 queue_start time.perf_counter() # 这里替换成你的真实队列逻辑比如asyncio.Queue或者消息队列 await simulate_queue_wait() QUEUE_DURATION.labels(serviceinference-svc, modelmodel, prioritynormal).observe(time.perf_counter() - queue_start) QUEUE_SIZE.labels(serviceinference-svc, modelmodel).dec() # 预处理 pre_start time.perf_counter() input_data await request.json() preprocessed preprocess(input_data) PREPROCESS_DURATION.labels(serviceinference-svc, modelmodel).observe(time.perf_counter() - pre_start) # 推理 infer_start time.perf_counter() result run_inference(preprocessed, model) INFERENCE_DURATION.labels(serviceinference-svc, modelmodel, enginemy-engine).observe(time.perf_counter() - infer_start) # 后处理 post_start time.perf_counter() output postprocess(result) POSTPROCESS_DURATION.labels(serviceinference-svc, modelmodel).observe(time.perf_counter() - post_start) return output这里QUEUE_SIZE的inc逻辑我简化了真实场景中入队时inc、出队时dec要确保成对出现避免队列长度指标漂移。启动Prometheus指标端口if __name__ __main__: import uvicorn start_http_server(8000) # 在8000端口暴露指标 uvicorn.run(app, host0.0.0.0, port8080)也可以不在应用内起HTTP服务而是让Prometheus通过exporter模式周期性抓取/metrics二选一即可我习惯单独起一个8000端口和应用主端口隔离避免监控请求影响业务。4.2 配置Prometheus抓取与Grafana面板Prometheus的抓取配置核心是服务发现和抓取频率。推理服务如果是部署在Kubernetes里直接用PodMonitor或者annotation做自动发现如果是传统虚拟机部署就用static_configs写死目标地址。下面是一个基于静态配置的简化示例scrape_configs: - job_name: inference-service scrape_interval: 15s metrics_path: /metrics static_configs: - targets: [10.0.0.11:8000, 10.0.0.12:8000] labels: service: inference-svc - job_name: gpu-node scrape_interval: 15s metrics_path: /metrics static_configs: - targets: [10.0.0.11:9400, 10.0.0.12:9400] labels: service: gpu-exporter抓取频率我建议15秒起步不要设成1秒。推理服务的延迟指标是聚合型数据15秒的采样窗口足够捕捉到分钟级的异常趋势。抓取频率过高Prometheus和应用的负载都会显著上升对监控本身也是一种压力。Grafana面板布局我有一个多次验证过比较顺手的方式上半部分放服务级和阶段级延迟曲线下半部分放算力指标。上半部分包括request_total_duration_seconds的p50、p95、p99多分位数曲线各阶段耗时的堆叠图直观看到耗时占比变化请求QPS和错误率曲线。下半部分包括GPU利用率、显存占用、显存带宽、动态batch大小。这样一块面板基本能把服务慢和资源瓶颈两个层面的问题同时呈现。查询语句示例# p99 总延迟 histogram_quantile(0.99, sum(rate(request_total_duration_seconds_bucket[5m])) by (le, model)) # 各阶段平均耗时对比 sum(rate(preprocess_duration_seconds_sum[5m])) by (model) / sum(rate(preprocess_duration_seconds_count[5m])) by (model)4.3 告警规则怎么定才不吵人告警规则设计是延迟监控体系里最容易翻车的环节。p99延迟直接配阈值告警非常容易产生抖动误报搞得值班同学每天被叫起来好几次最后看到告警也无感了。我的经验是告警阈值围绕趋势和错误率来设计而不是围绕瞬时值。具体的告警规则分两类第一类是快速失败类告警比如错误率突增、可用性下降。这类告警要灵敏groups: - name: inference-service-alerts rules: - alert: InferenceHighErrorRate expr: | sum(rate(inference_requests_total{status~5..}[5m])) by (service) / sum(rate(inference_requests_total[5m])) by (service) 0.05 for: 5m labels: severity: critical annotations: summary: 推理服务错误率超过5%第二类是延迟质量类告警用分位数连续上升的趋势来判断而不是单点阈值。比如p99延迟在15分钟内持续高于基线的1.5倍并且持续了10分钟以上才触发- alert: InferenceHighP99Latency expr: | histogram_quantile(0.99, sum(rate(request_total_duration_seconds_bucket[10m])) by (le, service)) / histogram_quantile(0.99, sum(rate(request_total_duration_seconds_bucket[1h]) offset 1h) by (le, service)) 1.5 for: 10m labels: severity: warning annotations: summary: 推理服务p99延迟相对1小时前上升超过50%这个相对上升比例的告警表达式比单纯写死p99 500ms要实用得多。它能自动适应不同服务、不同时段的延迟基准不需要频繁调整阈值。4.4 从指标到诊断日志、链路与在线分析三板斧光有延迟指标还不够指标告诉你哪里慢了但没告诉你为什么慢。排查具体原因时我会把延迟指标和日志、链路追踪、以及在线分析工具组合起来用。最简单的联动方式是当p99告警触发时通过请求ID或trace ID去日志系统里拉出同一批慢请求的日志看异常栈、入参大小、模型版本等信息。比如很多时候慢请求是因为输入文本特别长token数远超平均水平导致推理阶段耗时暴增。这个信息在指标上看不出来但日志里一搜就能看到。更进一步可以给推理服务接入OpenTelemetry把请求在各个阶段的span信息上报到Tempo或Jaeger。这样一段耗时异常能直接下钻到具体是预处理慢、还是模型推理慢、还是后处理慢和前面拆分的阶段指标互相印证。这也是前面埋点时要给每个阶段打不同Metric的原因指标定位到阶段链路定位到函数日志定位到上下文三层证据链合在一起定位问题的效率会非常高。5. 真实场景中的踩坑记录与排查实录延迟监控体系落地过程中我踩过的坑不比解决的问题少。分享几个典型的案例这些场景在文档里通常不会写但对实际运维非常有参考价值。5.1 一次被假p99骗了的排查经历有一段时间某个推理服务的p99延迟监控曲线突然从200毫秒飙升到3秒告警频繁触发整个团队严阵以待。按常规思路先查GPU、再查模型版本、再查网络全部正常。折腾了两个小时最后发现是埋点代码引入的一个低级bug新上线的版本把耗时单位从秒传成了毫秒导致观测值被放大了1000倍。这个教训让我做了一次埋点规范强制检查所有延迟类指标的单位必须用_seconds或_milliseconds后缀明确标注并在代码评审时作为必查项。监控体系本身的正确性比监控体系的丰富性更重要一个错误的指标比没有指标更有害因为你可能被错误数据带偏方向。建议大家在指标上线前用一个已知耗时的小脚本或压测工具跑一遍比对埋点数据是否和真实耗时基本一致再做上线。5.2 GPU显存和带宽瓶颈导致的隐性长尾另一个印象很深的案例是一个基于自研Transformer的生成模型。流量低峰期一切正常一旦并发上来p50延迟变化不大p99却出现明显的阶梯式上升。一开始怀疑是GPU算力不足但查看SM利用率后发现利用率并不高甚至还有闲置。后来加入了显存带宽指标监控才定位到问题。这个模型的权重非常大推理时需要反复读取参数矩阵显存带宽成了真正的瓶颈。并发升高后多个请求同时进行矩阵计算对显存带宽的争抢急剧增加导致部分请求的计算时间被拉长形成长尾。用生活化的类比来说这就像一条马路车辆不多时每辆都能跑起来一旦车流增大路很宽但收费站出口太少车就全堵在出口了。SM利用率相当于路面铺得宽不宽显存带宽才是真正的出口吞吐能力。解决方式也比较直接调整推理引擎的batch size策略避免请求同时发起导致的峰值争抢同时把部分计算算子改为显存访问更友好的实现把p99降了下来。5.3 避免监控链路拖垮业务接口监控埋点本身是有成本的这个成本在推理服务上会被放大因为推理服务普遍对延迟敏感。我见过一个团队用Python的logging模块把每个阶段的耗时都写到日志文件再由Filebeat采集结果日志量大到把磁盘IO打满反而拖慢了推理接口。这个问题在指标侧也常见。Prometheus Client在默认情况下每次调用histogram.observe()都会做一次全局锁操作高并发场景下这本身就是不小的开销。我自己的优化经验有三条第一采样上报。在极高并发的推理服务里不需要每个请求都记录指标可以按比例采样比如每10个请求记录一次也能得到足够统计意义的数据同时大幅降低埋点开销。第二异步聚合。不要在主推理路径上执行耗时统计和metric的序列化操作用一个后台协程批量聚合和上报。第三控制指标基数。label的取值集合不能无限增长。比如把用户ID、请求ID加进label里会导致指标基数爆炸直接把Prometheus存储拖垮。高基数信息应该放在日志或者链路追踪里而不是指标里。6. 几个可能不是最优解、但很实用的经验建议监控体系建设到后期技术层面的问题反而退居其次真正影响效果的反而是使用习惯和运维机制。这里写几条我基于实际工作沉淀下来的经验不一定是最优解但都经受过真实场景检验。6.1 监控面板不是越花哨越好Grafana最大的陷阱就是可以无限堆叠图表导致面板越来越复杂最后没人看得懂。我的原则是把核心指标压缩到一块值班面板上每次打开不超过10个图20秒内能判断出服务整体是否健康。这块值班面板上只放四个东西总延迟分位数趋势、各阶段耗时占比、请求量和错误率、GPU关键指标。其他更细节的分析面板按需点击进入而不是一上来全铺开。6.2 告警值班轮换时必须配一份排查手册告警配了值班轮换了最怕的就是告警触发后值班同学不知道从哪查起。我会给每个核心服务维护一份排查手册内容包括常见告警的可能原因、对应的查询语句、典型的trace ID查找方法、以及上一次类似问题的处理记录。这份手册的价值在故障时刻才会体现出来。没有手册时值班同学遇到p99告警可能要从头开始摸索排查思路有手册时第一步查什么、第二步查什么、大概率是什么问题都有现成的路径MTRR时间可以明显缩短。6.3 延迟监控只是一半剩下的一半是容量预案延迟指标不只是事后诊断用的它更重要的价值在于容量规划和扩容决策。根据延迟-并发曲线的走势可以推算出服务在什么QPS下开始出现明显延迟恶化这一个拐点就是扩容的触发阈值。我会定期做压测记录不同并发下的p50、p99和GPU利用率绘制出一条延迟拐点曲线。之后结合线上实时QPS和延迟趋势在延迟开始出现爬升苗头时就提前扩容而不是等到告警触发了再紧急处理。这样做的好处是用户基本感知不到服务质量的波动扩容总是发生在问题产生之前。最后说一点个人体会。延迟监控体系设计这件事技术选型、指标方案、埋点实现固然重要但真正让体系发挥价值的还是持续运维的习惯和意识。我不追求一套面面俱到的完美方案而是先搭起一个能回答现在慢没慢、慢在哪、为什么慢的最小闭环再在一次次真实故障和复盘里把它打磨完善。每一个新坑填进去这套体系就更可靠一分。如果你正准备给自己团队的推理服务做延迟监控我建议先从这一节的小闭环开始不要一开始就铺得很大。体系是一步步长出来的不是一次设计出来的。