3. 监控与可观测性:日志、指标、追踪三大支柱
在诊断生产环境AI推理服务故障时,仅依赖“服务是否存活”这类粗粒度判断远远不够。推理服务通常涉及客户端接入层、负载均衡、推理引擎(如Triton、vLLM)、模型加载器、GPU资源池、特征存储等多个环节,故障可能发生在任意一跳。可观测性(Observability)正是为了回答三个核心问题:哪里出了问题(日志)、问题有多严重(指标)、一次请求到底经历了什么(追踪)。本章围绕日志、指标、追踪三大支柱,讲解如何用Prometheus、Grafana、Jaeger构建面向AI推理服务的可观测体系。
3.1 三大支柱的分工与关联
三大支柱并非彼此孤立,而是从不同维度描述同一系统的运行状态。日志记录离散事件,指标提供聚合视图,追踪还原请求链路。对于AI推理服务,三者缺一不可:日志告诉你模型报错的具体堆栈,指标告诉你GPU利用率是否打满,追踪告诉你慢请求卡在了预处理还是模型推理阶段。
3.2 日志:从ELK到Loki的实践
AI推理服务的日志具有高并发、大流量、多实例的特点。一次批量推理可能产生数百条日志,而GPU节点上的日志采集不能影响推理性能。生产环境推荐采用Filebeat + Kafka + Logstash + Elasticsearch的异步采集链路,或者使用更轻量的Promtail + Loki方案,后者与Grafana集成更紧密,且对象存储成本更低。
日志采集的关键在于结构化。原始文本日志难以检索,必须统一为JSON格式,至少包含以下字段:
{
"timestamp": "2025-06-18T14:23:45.123Z",
"level": "ERROR",
"service": "inference-server",
"trace_id": "8f3a2b9c1d4e5f6a",
"model_name": "resnet50",
"gpu_id": 2,
"error_code": "CUDA_OOM",
"message": "CUDA out of memory. Tried to allocate 256 MiB",
"request_id": "req_20250618_142345"
}
在排查GPU显存溢出(OOM)时,通过error_code=CUDA_OOM过滤日志,再按gpu_id聚合,可以快速定位是哪张卡、哪个模型实例触发的异常。如果日志中缺少trace_id,则无法关联到具体请求,诊断效率会大幅下降。
3.3 指标:Prometheus监控体系搭建
Prometheus采用拉取(Pull)模型,通过HTTP端点周期性采集指标。对于AI推理服务,除了常规的CPU、内存、网络指标外,还必须关注以下AI专属指标:
| 指标类别 | 指标名称 | 说明 | 告警阈值建议 |
|---|---|---|---|
| GPU利用率 | gpu_utilization_percent | GPU计算核心利用率 | > 95% 持续5分钟 |
| 显存占用 | gpu_memory_used_bytes | 当前显存使用量 | > 90% 总显存 |
| 推理延迟 | inference_latency_seconds | P50/P95/P99延迟 | P99 > 2s |
| 吞吐量 | inference_requests_total | 每秒处理的请求数 | 低于基线50% |
| 批处理大小 | batch_size_current | 当前动态批处理大小 | 持续为1(未生效) |
| 排队长度 | inference_queue_length | 等待处理的请求数 | > 100 |
在Prometheus中配置采集任务时,需要为每个推理服务实例添加独立的job,并通过标签区分模型版本和GPU节点:
scrape_configs:
- job_name: 'inference-gpu'
static_configs:
- targets: ['10.0.1.10:9100', '10.0.1.11:9100']
labels:
gpu_node: 'node-a'
model: 'resnet50-v2'
- job_name: 'inference-metrics'
metrics_path: '/metrics'
static_configs:
- targets: ['10.0.1.10:8080', '10.0.1.11:8080']
Grafana负责将Prometheus中的指标可视化。针对AI推理服务,建议创建三个独立Dashboard:GPU资源总览(显存、利用率、温度)、推理服务质量(延迟分布、吞吐、错误率)、队列与批处理(排队长度、批大小、超时请求数)。当P99延迟飙升时,首先查看GPU利用率是否打满,再检查队列长度是否堆积,这两个指标往往能直接指出瓶颈所在。
3.4 追踪:Jaeger还原请求全链路
日志和指标能告诉你“发生了什么”,但无法回答“这个慢请求经过了哪些环节”。Jaeger通过Trace ID将一次推理请求的完整调用链串联起来,每个环节称为一个Span。对于AI推理服务,典型的Span链路如下:
Trace: 8f3a2b9c1d4e5f6a
├── [0ms] HTTP入口 (gateway)
│ ├── [2ms] 鉴权/限流 (auth)
│ ├── [5ms] 请求预处理 (preprocess)
│ │ ├── [3ms] 文本Token化 (tokenizer)
│ │ └── [2ms] 特征拼接 (feature_merge)
│ ├── [150ms] 模型推理 (inference)
│ │ ├── [120ms] GPU执行 (cuda_kernel)
│ │ └── [30ms] 采样/解码 (sampler)
│ └── [8ms] 后处理 (postprocess)
└── [165ms] 总耗时
在Jaeger UI中,可以按service=inference-server过滤,再按model_name=resnet50分组,查看不同模型的耗时分布。如果发现GPU执行Span耗时异常,但GPU利用率并不高,说明可能存在数据加载瓶颈或CPU与GPU同步等待,此时需要进一步查看cudaMemcpy的耗时。
生产环境接入Jaeger时,推荐使用OpenTelemetry SDK进行埋点,它同时支持日志、指标、追踪三种信号的统一上报,避免为每个工具单独接入SDK。在Python推理服务中,只需在关键函数上添加装饰器即可完成埋点:
from opentelemetry import trace
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
tracer = trace.get_tracer("inference.tracer")
@tracer.start_as_current_span("model_inference")
def run_inference(input_tensor):
# 模型推理逻辑
with tracer.start_as_current_span("gpu_execute"):
output = model(input_tensor)
return output
3.5 案例:一次P99延迟突刺的联合诊断
某生产环境AI翻译服务在晚高峰出现P99延迟从800ms飙升至3s的故障。监控团队按以下流程完成根因定位:
- Grafana查看指标:发现
inference_queue_length从20飙升至500,gpu_utilization_percent维持在99%,但inference_requests_total并未明显增长,排除流量突增。 - Jaeger查看追踪:随机抽取慢请求Trace,发现
gpu_executeSpan耗时正常(约120ms),但preprocessSpan耗时从5ms增至800ms,定位到预处理环节。 - 日志定位细节:按
trace_id过滤日志,发现大量tokenizer_timeout错误,提示分词器线程池耗尽。 - 根因确认:分词器使用同步阻塞调用,在并发请求增加时线程池被打满,导致请求排队。
修复方案是将分词器改为异步调用,并增加线程池大小。修复后P99延迟回落至850ms,队列长度恢复正常。
3.6 常见误区与复盘
另一个常见问题是三大支柱数据割裂。日志、指标、追踪必须通过trace_id和request_id关联起来。在日志中打印trace_id,在指标中增加trace_id标签(或至少保留request_id),在追踪中记录日志索引,才能在诊断时从Grafana跳转到Jaeger,再从Jaeger跳转到Loki日志,形成完整的诊断闭环。
最后需要强调的是,可观测性建设不是一次性工作。每次故障复盘后,应检查现有监控项是否覆盖了本次故障的盲区,并持续补充告警规则和Dashboard面板。只有将三大支柱真正融入日常运维流程,才能在AI推理服务出现异常时快速完成根因诊断。