2.1 指标命名与标签设计:监控体系的“地基里的地基” 很多人把监控体系的成败归结于“Grafana 面板好不好看”“告警灵不灵”,但真正决定这套体系能活多久、扩多大的,是更早的一件事:你给指标起的名字、打的标签,到底合不合理。名字起错,后面所有查询、面板、告警都要跟着错;标签打错,轻则查询变慢,重则 Prometheus 内存被打爆、整个监控直接雪崩。 这一节不教你“怎么画面板”,而是把指标体系最底层、也最容易被糊弄过去的两个决策——命名规范与标签设计——彻底讲透。它们是大模型推理监控能不能“从能跑”走向“扛得住”的分水岭。读完这一节,你应该能拿着一份命名与标签约定,直接交给同事去写 exporter,而不会出现“三个人写出四种命名风格、谁也看不懂谁的图”的场面。
很多人把监控体系的成败归结于“Grafana 面板好不好看”“告警灵不灵”,但真正决定这套体系能活多久、扩多大的,是更早的一件事:你给指标起的名字、打的标签,到底合不合理。名字起错,后面所有查询、面板、告警都要跟着错;标签打错,轻则查询变慢,重则 Prometheus 内存被打爆、整个监控直接雪崩。
这一节不教你“怎么画面板”,而是把指标体系最底层、也最容易被糊弄过去的两个决策——命名规范与标签设计——彻底讲透。它们是大模型推理监控能不能“从能跑”走向“扛得住”的分水岭。读完这一节,你应该能拿着一份命名与标签约定,直接交给同事去写 exporter,而不会出现“三个人写出四种命名风格、谁也看不懂谁的图”的场面。
Prometheus 的指标名不是随便起的自由文本,它有一套被广泛遵循、且被官方文档明确推荐的约定。不遵守的后果不是“不好看”,而是你的指标在 PromQL 里难以聚合、在告警里难以复用、在别人接手时难以理解。
核心规则可以浓缩成一句话:指标名采用三段式到四段式,单位必须用基本单位,类型要用后缀显式表达。
逐条拆解这套约定,并对照大模型推理场景:
. 当成 label 分隔符,混用会出乱子。llm_、vllm_、tgi_ 之类的前缀标明这是哪一类系统的指标,避免和主机、中间件的指标撞名。当你把推理指标和节点 node_exporter 指标放同一套 Prometheus 时,前缀是你区分它们的唯一抓手。_seconds(不是 _milliseconds),容量用 _bytes(不是 _kb),比率用 _ratio(取值 0~1)。这是反直觉但极重要的规矩——很多团队图省事写 ttft_ms,结果 downstream 的 Grafana 面板和 SLO 定义都按秒算,时间一久没人记得这个指标的单位,alert 阈值写错、图表单位标错,事故就是从这种“小随便”里长出来的。_total(llm_requests_total);_bucket、_sum、_count 三个序列,命名时只写基名如 llm_request_duration_seconds;_info 后缀(如 llm_model_info),它的值通常是 1,靠 label 携带信息(model="qwen2-72b", version="...")。一个真实对照:某团队把“完成 token 数”命名为 completion_tokens,没有单位后缀、也没有前缀。后来另一个服务也暴露了同名指标,Prometheus 直接把两套序列合并成一张混乱的图;更糟的是,这个指标其实是从不同模型聚合来的,却没有 model 这个标签。改名成 llm_tokens_total{role="completion", model="..."} 之后,查询从“看个一团糊”变成“按模型自由拆分”,差距就是命名规范带来的。
命名之外,第二个底层决策是给每个指标选对类型。Prometheus 四类核心类型里,大模型推理场景最常踩的坑,是把“延迟”用错了类型。
rate(),要算增量用 increase()。注意它不能表示“当前值”,只能表示“从启动到现在累计了多少”。> 阈值 即可。_bucket、_sum、_count。这是延迟唯一正确的类型。为什么延迟必须用 histogram 而不是 gauge?因为 gauge 只能存“最后一次的值”或“平均值”,而大模型推理的延迟本质是高度长尾分布的——一个 200 token 的短回答和一个 2000 token 的长回答,延迟差一个数量级。如果你用 gauge 记“平均延迟”,长尾被彻底抹平,这正是第一章 1.1 里讲过的“均值掩盖长尾”问题在指标层的重现。
Histogram 的正确打开方式是把桶按你的真实业务分布设好。例如对 TTFT(首字延迟),典型区间可能是 50ms、100ms、200ms、500ms、1s、2s、5s;对 TPOT(逐字延迟)则集中在毫秒级,桶要设成 5ms、10ms、20ms、50ms、100ms。桶设得太粗(比如只有 1s 和 10s 两档),你算不出有意义的 P99;桶设得太细太多,又会放大存储和查询成本。一个实用的经验:桶的边界要覆盖你告警阈值附近的“敏感区”,让 P90/P95/P99 都能被准确估算,业务常见的几个量级各留一两档即可。
这里有个关键认知:分位数不是存在指标里的,是查询时算出来的。Histogram 存的是各桶的计数,你用 histogram_quantile(0.99, rate(llm_request_duration_seconds_bucket[5m])) 在查询时现算 P99。这意味着你改了分位需求(从 P95 改看 P99)不需要重新埋点,只要桶粒度够细就行——这正是 histogram 比 summary 更适合多副本推理服务的根本原因。
如果说命名是“地基”,标签就是“地基里的钢筋”——用好了让整栋楼可自由拆分、可多维下钻;用错了,整栋楼直接塌。标签(label)的本质,是给一条时间序列附加维度,使得同名的指标能按维度切分。例如 llm_requests_total{model="qwen2-72b", finish_reason="stop"} 表示“某个模型、某种结束原因”的请求数。
但标签有个冷酷的数学事实:时间序列的数量 = 基数(cardinality)的笛卡尔积。一个指标有 3 个标签,每个标签有 10 个可能值,就会产生 1000 条时间序列;如果每个标签有 1000 个值,那就是 10 亿条——Prometheus 的 TSDB 会把每条序列都当成独立对象存,内存和磁盘都会被拖垮。
这是生产里真实发生过的荒唐事:某团队为了“能查每个用户”,把 user_id 直接当 label。上线当天 Prometheus 内存从 4GB 飙到 32GB 然后 OOM,恰好那段时间推理服务也在抖动,结果监控系统自己先挂了,没人看得到推理服务的异常。事后复盘,把 user_id 从 label 里拿掉、改到日志或 Exemplar(后面会讲)里,内存立刻回到正常水位。
所以标签设计的第一铁律是:只打“有限、可枚举、低基数”的维度。对照大模型推理,下面这张清单请直接当规范用:
model:你实际部署的模型就那么几个,基数低;route / endpoint:比如 /v1/chat/completions、多模型路由里的路由名,有限枚举;finish_reason:LLM 的结束原因(stop、length、content_filter 等),是固定小集合;status / error_type:成功/失败及失败类型,枚举有限;stage:prefill / decode,就两个值;task:chat / summarize / embedding,按你业务定义的小集合。user_id、tenant_id(若租户极多)——除非租户数固定且很少,否则必须换成聚合维度如 tenant_tier(免费/付费);request_id、trace_id——每条请求唯一,等于没有基数上限;prompt、prompt_hash——每次请求内容不同;session_id——同 request_id 性质;那如果业务真的想按用户下钻怎么办?两个正道:一是用 Exemplar 机制,在 histogram 的桶里附上少量高基数样本的 trace 链接,既保留下钻能力又不增加序列数;二是把高基数维度落到 日志和链路追踪(trace) 里,监控负责“宏观趋势”,追踪负责“微观定位”,各司其职。记住一句话:监控看趋势,追踪看个案,别让 Prometheus 去干追踪的活。
把前面三节收敛成一套可直接落地的约定。建议你把它写进团队的 exporter 开发规范,作为“接收入门门槛”。
命名空间约定(前缀):
llm_ 作为推理业务指标的统一前缀;llm_vllm_*、llm_tgi_*;node_、nvidia_ 等前缀,不与业务混淆。推荐的核心指标名(基名,单位已含):
llm_requests_total{model, route, finish_reason, status}:请求计数,counter;llm_request_duration_seconds{stage, model}:端到端/分段延迟,histogram(bucket 覆盖敏感区);llm_ttft_seconds{model, route}:首字延迟,histogram;llm_tpot_seconds{model}:逐字延迟,histogram;llm_tokens_total{role="prompt|completion", model}:token 计数,counter;llm_kv_cache_usage_ratio{model}:KV Cache 占用率(0~1),gauge;llm_kv_cache_evictions_total{model}:KV 淘汰次数,counter;llm_queue_depth{model}:在途/排队请求数,gauge;llm_queue_wait_seconds{model}:排队时长,histogram;llm_gpu_utilization_ratio{device}:GPU 利用率,gauge;llm_cache_hit_ratio{route}:prefix cache 命中率,gauge。注意这里的标签集全部来自第三节的“安全清单”:model、route、finish_reason、stage、role 都是有限枚举。你完全可以对着第一章 1.3 的四层需求清单逐条核对——资源层对应 llm_kv_cache_*、llm_gpu_*、llm_queue_*;生成层对应 llm_ttft_*、llm_tpot_*、llm_tokens_total;请求/成本层对应 llm_requests_total、llm_cache_hit_ratio。命名体系就是把需求清单翻译成稳定契约的过程。
讲完正道,再点几个我在生产里反复见到的反模式,照着避雷:
_ms 或 _millis,导致和按秒设计的 SLO、面板、rate 计算全部错位。统一 _seconds、_bytes、_ratio。user_id、request_id 灾难。任何“可能上万个不同值”的维度,一律出局。llm_model_version="v1" 当 label,版本一多又爆炸。模型版本这类相对静态、有限的信息,更适合用 _info 类 gauge(值为 1,label 携带信息),而不是塞进高频指标。ttft、first_token_latency、llm_ttft_seconds 三种写法,查询时谁都对不上。约定先于编码,这节给你的清单就是约定。route="/v1/Chat" 和 route="/v1/chat" 在 Prometheus 里是两条不同序列,拼写不一致会悄悄制造重复序列。约定好枚举值,必要时在 exporter 里做归一化。这一节把指标体系最底层的两个决策讲透了:命名要守三段式+基本单位+类型后缀,标签要只打低基数可枚举维度、坚决把高基数维度赶出 label(交给 Exemplar 和 tracing)。它们看似琐碎,却决定了你的监控体系是“越用越顺”还是“上线即埋雷”。
记住一句话总结:好的指标名是一份稳定契约,好的标签是受控的维度——命名错了全员返工,标签炸了监控雪崩。 带着这套约定,下一节(2.2)我们进入“这些指标到底从哪来”:如何为 vLLM / TGI 等推理框架对接 exporter,把推理专有指标暴露成 Prometheus 能抓的格式。