2.1 指标命名与标签设计:地基里的地基


文档摘要

2.1 指标命名与标签设计:监控体系的“地基里的地基” 很多人把监控体系的成败归结于“Grafana 面板好不好看”“告警灵不灵”,但真正决定这套体系能活多久、扩多大的,是更早的一件事:你给指标起的名字、打的标签,到底合不合理。名字起错,后面所有查询、面板、告警都要跟着错;标签打错,轻则查询变慢,重则 Prometheus 内存被打爆、整个监控直接雪崩。 这一节不教你“怎么画面板”,而是把指标体系最底层、也最容易被糊弄过去的两个决策——命名规范与标签设计——彻底讲透。它们是大模型推理监控能不能“从能跑”走向“扛得住”的分水岭。读完这一节,你应该能拿着一份命名与标签约定,直接交给同事去写 exporter,而不会出现“三个人写出四种命名风格、谁也看不懂谁的图”的场面。

2.1 指标命名与标签设计:监控体系的“地基里的地基”

很多人把监控体系的成败归结于“Grafana 面板好不好看”“告警灵不灵”,但真正决定这套体系能活多久、扩多大的,是更早的一件事:你给指标起的名字、打的标签,到底合不合理。名字起错,后面所有查询、面板、告警都要跟着错;标签打错,轻则查询变慢,重则 Prometheus 内存被打爆、整个监控直接雪崩。

这一节不教你“怎么画面板”,而是把指标体系最底层、也最容易被糊弄过去的两个决策——命名规范标签设计——彻底讲透。它们是大模型推理监控能不能“从能跑”走向“扛得住”的分水岭。读完这一节,你应该能拿着一份命名与标签约定,直接交给同事去写 exporter,而不会出现“三个人写出四种命名风格、谁也看不懂谁的图”的场面。

一、Prometheus 命名规范:不是风格,是硬约束

Prometheus 的指标名不是随便起的自由文本,它有一套被广泛遵循、且被官方文档明确推荐的约定。不遵守的后果不是“不好看”,而是你的指标在 PromQL 里难以聚合、在告警里难以复用、在别人接手时难以理解。

核心规则可以浓缩成一句话:指标名采用三段式到四段式,单位必须用基本单位,类型要用后缀显式表达。

```mermaid graph TD N[指标名结构] --> A[namespace 命名空间] N --> B[subsystem 子系统] N --> C[name 具体含义] N --> D[unit_suffix 单位后缀] A -->|例 llm| A1[业务/系统域] B -->|例 inference| B1[模块:推理引擎] C -->|例 request_duration| C1[被测对象+动作] D -->|例 _seconds _bytes _total| D1[基本单位+类型] A1 --> F[llm_inference_request_duration_seconds] F --> G[可被 PromQL 自由聚合的稳定契约] ```

逐条拆解这套约定,并对照大模型推理场景:

  • 用下划线分隔的 snake_case:不要用驼峰,不要用点。Prometheus 本身也把 . 当成 label 分隔符,混用会出乱子。
  • 带命名空间前缀:用 llm_vllm_tgi_ 之类的前缀标明这是哪一类系统的指标,避免和主机、中间件的指标撞名。当你把推理指标和节点 node_exporter 指标放同一套 Prometheus 时,前缀是你区分它们的唯一抓手。
  • 单位必须是基本单位:延迟用 _seconds(不是 _milliseconds),容量用 _bytes(不是 _kb),比率用 _ratio(取值 0~1)。这是反直觉但极重要的规矩——很多团队图省事写 ttft_ms,结果 downstream 的 Grafana 面板和 SLO 定义都按秒算,时间一久没人记得这个指标的单位,alert 阈值写错、图表单位标错,事故就是从这种“小随便”里长出来的。
  • 用后缀表达类型
    • 计数器累计值加 _totalllm_requests_total);
    • 直方图自动产生 _bucket_sum_count 三个序列,命名时只写基名如 llm_request_duration_seconds
    • 瞬时态用 gauge,无需特殊后缀,但常量的“元信息”指标用 _info 后缀(如 llm_model_info),它的值通常是 1,靠 label 携带信息(model="qwen2-72b", version="...")。

一个真实对照:某团队把“完成 token 数”命名为 completion_tokens,没有单位后缀、也没有前缀。后来另一个服务也暴露了同名指标,Prometheus 直接把两套序列合并成一张混乱的图;更糟的是,这个指标其实是从不同模型聚合来的,却没有 model 这个标签。改名成 llm_tokens_total{role="completion", model="..."} 之后,查询从“看个一团糊”变成“按模型自由拆分”,差距就是命名规范带来的。

二、指标类型选型:Histogram 才是延迟的正确容器

命名之外,第二个底层决策是给每个指标选对类型。Prometheus 四类核心类型里,大模型推理场景最常踩的坑,是把“延迟”用错了类型。

  • Counter(计数器):只增不减的累计值,适合“请求总数”“错误总数”“生成 token 总数”。要算速率用 rate(),要算增量用 increase()。注意它不能表示“当前值”,只能表示“从启动到现在累计了多少”。
  • Gauge(仪表盘):可增可减的瞬时值,适合“KV Cache 占用率”“在途请求数”“显存已用字节”。它直接反映当下状态,告警直接 > 阈值 即可。
  • Histogram(直方图):把观察值分到预先定好的桶里,自动产出 _bucket_sum_count这是延迟唯一正确的类型。
  • Summary(摘要):客户端算好分位数再上报,服务器端无法跨实例聚合。推理场景多为多副本,summary 几乎不可用,一律用 histogram。

为什么延迟必须用 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 会把每条序列都当成独立对象存,内存和磁盘都会被拖垮。

```mermaid graph TD L[标签设计失衡] --> C[高基数标签] C --> C1[user_id: 上万个不同值] C --> C2[prompt_hash: 每次请求都不同] C --> C3[request_id: 每条请求唯一] C1 --> X[时间序列爆炸] C2 --> X C3 --> X X --> M[Prometheus 内存打满 OOM] M --> K[抓取失败 / 写入丢弃] K --> E[监控整体雪崩, 故障期恰巧失明] L -.正确做法.-> S[低基数受控标签] S --> S1[model: 有限几个] S --> S2[route: 有限路由] S --> S3[finish_reason: 有限枚举] ```

这是生产里真实发生过的荒唐事:某团队为了“能查每个用户”,把 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_idtenant_id(若租户极多)——除非租户数固定且很少,否则必须换成聚合维度如 tenant_tier(免费/付费);
    • request_idtrace_id——每条请求唯一,等于没有基数上限;
    • promptprompt_hash——每次请求内容不同;
    • session_id——同 request_id 性质;
    • 时间戳、随机串——任何“每次都不同”的东西。

那如果业务真的想按用户下钻怎么办?两个正道:一是用 Exemplar 机制,在 histogram 的桶里附上少量高基数样本的 trace 链接,既保留下钻能力又不增加序列数;二是把高基数维度落到 日志和链路追踪(trace) 里,监控负责“宏观趋势”,追踪负责“微观定位”,各司其职。记住一句话:监控看趋势,追踪看个案,别让 Prometheus 去干追踪的活。

四、大模型推理的推荐命名与标签方案(可直接复用)

把前面三节收敛成一套可直接落地的约定。建议你把它写进团队的 exporter 开发规范,作为“接收入门门槛”。

命名空间约定(前缀)

  • llm_ 作为推理业务指标的统一前缀;
  • 若想区分不同推理引擎,可在 subsystem 段区分:llm_vllm_*llm_tgi_*
  • 主机/容器/GPU 等基础设施指标仍用 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_totalllm_cache_hit_ratio。命名体系就是把需求清单翻译成稳定契约的过程。

```mermaid graph TD R[第一章需求清单 四层] --> RES[资源层] R --> GEN[生成层] R --> SVC[请求/服务层] R --> COST[业务/成本层] RES --> M1[llm_kv_cache_usage_ratio] RES --> M2[llm_queue_wait_seconds] RES --> M3[llm_gpu_utilization_ratio] GEN --> M4[llm_ttft_seconds] GEN --> M5[llm_tpot_seconds] GEN --> M6[llm_tokens_total] SVC --> M7[llm_requests_total] COST --> M8[llm_cache_hit_ratio] M1 --> L[统一标签: model/route/stage 低基数] M2 --> L M3 --> L M4 --> L M5 --> L M6 --> L M7 --> L M8 --> L L --> C[稳定契约: 命名+标签一次定好, 全队复用] ```

五、最常见的命名与标签反模式

讲完正道,再点几个我在生产里反复见到的反模式,照着避雷:

  1. 单位乱写:把延迟写成 _ms_millis,导致和按秒设计的 SLO、面板、rate 计算全部错位。统一 _seconds_bytes_ratio
  2. 把高基数维度当标签:上面讲过的 user_idrequest_id 灾难。任何“可能上万个不同值”的维度,一律出局。
  3. 用 label 做本该用 metric 的事:比如为了看“每个模型的版本”,有人用 llm_model_version="v1" 当 label,版本一多又爆炸。模型版本这类相对静态、有限的信息,更适合用 _info 类 gauge(值为 1,label 携带信息),而不是塞进高频指标。
  4. 直方图桶设成“拍脑袋”:要么只有两三档粗桶(算不出 P99),要么设了上百档(存储爆炸)。桶要覆盖你告警阈值附近的敏感区,业务量级各留一两档。
  5. 命名不统一、各写各的:同一含义出现 ttftfirst_token_latencyllm_ttft_seconds 三种写法,查询时谁都对不上。约定先于编码,这节给你的清单就是约定。
  6. label 值带空格或大小写混用route="/v1/Chat"route="/v1/chat" 在 Prometheus 里是两条不同序列,拼写不一致会悄悄制造重复序列。约定好枚举值,必要时在 exporter 里做归一化。

小结

这一节把指标体系最底层的两个决策讲透了:命名要守三段式+基本单位+类型后缀,标签要只打低基数可枚举维度、坚决把高基数维度赶出 label(交给 Exemplar 和 tracing)。它们看似琐碎,却决定了你的监控体系是“越用越顺”还是“上线即埋雷”。

记住一句话总结:好的指标名是一份稳定契约,好的标签是受控的维度——命名错了全员返工,标签炸了监控雪崩。 带着这套约定,下一节(2.2)我们进入“这些指标到底从哪来”:如何为 vLLM / TGI 等推理框架对接 exporter,把推理专有指标暴露成 Prometheus 能抓的格式。


发布者: 作者: 分词分到自闭的小龙虾 转发
评论区 (0)
U