同一个延迟指标,A 团队叫 latency(单位毫秒),B 团队叫 request_duration_ms(单位毫秒),C 团队叫 http_request_duration_seconds(单位秒);同一个计数器,有人叫 requests,有人叫 requests_total。当这些指标汇入同一个 Prometheus 时,跨服务的聚合查询、告警复用、大盘拼接全部失效——不是技术做不到,而是数据本身没有统一的语义。
指标规范治理要解决的就是这件事。而 OpenMetrics 是这个问题的标准化答案:它规定了暴露格式、命名与单位约定、元数据、Exemplar 语法,让「指标长什么样」不再是各家的自由发挥。本文从格式规范讲到治理落地,包括用 linter 和 CI 把规范变成可执行的约束。
1. 指标治理要解决的四个问题
问题一:命名混乱
latency / latency_ms / request_latency_seconds 三套并存
无法跨服务写一条聚合查询
问题二:单位不明
duration 是秒还是毫秒?只看名字猜不出
跨服务求和时单位不一致 → 结果荒谬
问题三:类型错用
把单调递增的计数写成 gauge,或把瞬时值写成 counter
导致 rate() / increase() 结果错误
问题四:标签爆炸
用标签承载高基数维度(用户 ID、请求 ID)
存储与查询双双崩溃
前三个问题都可以靠规范 + 自动检查解决;第四个问题属于基数治理,需要专门的优化手段。规范的价值在于:把"约定"从文档里搬到 CI 里,让不合规的指标在合并前就被拦下。
2. OpenMetrics 暴露格式
2.1 文本格式全貌
OpenMetrics 是 Prometheus 文本格式的标准化演进,核心结构如下:
# HELP http_requests_total 已处理的 HTTP 请求总数
# TYPE http_requests_total counter
# UNIT http_requests_total requests
http_requests_total{method="get",code="200"} 1027 1696233600.123
http_requests_total{method="post",code="500"} 3 1696233600.123
# EOF
要素说明:
# HELP 人类可读描述,同一指标只出现一次,必须在使用前
# TYPE 指标类型:counter / gauge / histogram / gaugehistogram / summary / info / stateset
# UNIT 单位(OpenMetrics 新增),如 seconds / bytes / requests
# EOF 流结束标记(OpenMetrics 新增),告诉抓取端"本次抓取完整"
样本行 metric_name{label="v"} value [timestamp]
时间戳可选,单位毫秒(OpenMetrics 中为秒,见 2.4)
2.2 # EOF 的意义
Prometheus 文本格式没有结束标记,抓取端只能靠连接关闭或超时判断「抓完了」。这带来一个隐患:传输中途截断时,抓取端无法区分"就这些"与"还没传完",可能写入不完整的数据。
OpenMetrics 的做法:
正常结束 → 发送 "# EOF\n",抓取端确认完整
连接断开且未见 # EOF → 抓取端标记为不完整,可重试或丢弃
实践价值:
在高频抓取(如 1s 间隔)或大指标量(数万序列)场景下,
截断检测能显著减少"看似正常实则残缺"的数据
2.3 内容协商(Content Negotiation)
抓取端通过 HTTP Accept 头声明自己支持的格式,服务端据此决定返回哪种格式:
请求:
Accept: application/openmetrics-text; version=1.0.0; charset=utf-8
, text/plain; version=0.0.4
含义:
首选 OpenMetrics 1.0.0
若服务端不支持,退回 Prometheus 文本 0.0.4
服务端响应:
Content-Type: application/openmetrics-text; version=1.0.0; charset=utf-8
或 Content-Type: text/plain; version=0.0.4; charset=utf-8
# 手工验证服务端是否支持 OpenMetrics
curl -s -H 'Accept: application/openmetrics-text' http://localhost:9100/metrics | tail -3
# 若最后一行是 "# EOF" 则支持;否则退回的是 Prometheus 文本格式
# 查看响应头确认协商结果
curl -sI -H 'Accept: application/openmetrics-text' http://localhost:9100/metrics \
| grep -i content-type
ℹ️ 核心:兼容性靠的是「协商 + 回退」,而不是「强制升级」。服务端应同时支持两种格式,抓取端用
Accept表达偏好。这样新旧客户端可以共存,迁移不需要大爆炸式切换。
2.4 时间戳单位的差异
这是迁移时最容易踩的坑:
Prometheus 文本格式:时间戳单位是【毫秒】
OpenMetrics: 时间戳单位是【秒】(含小数部分)
# Prometheus 文本
metric 42 1696233600123
# OpenMetrics
metric 42 1696233600.123
风险:
客户端库若在切换格式时未同步调整单位,时间戳会相差 1000 倍
表现为数据被丢弃(时间戳过于久远/未来)或严重错位
自查:迁移后对比同一指标在两种格式下的写入时间
3. 命名与单位约定
3.1 三条硬规则
规则一:使用基本单位(base unit),不加前缀
✅ request_duration_seconds ❌ request_duration_ms
✅ response_size_bytes ❌ response_size_kilobytes
理由:单位可换算,但指标名不可;统一基本单位后
跨服务聚合才有意义,展示层的换算交给 Grafana
规则二:单位作为后缀,且是复数
seconds / bytes / meters / volts / amperes / joules / ratio
例外:无单位的比例用 ratio,百分比用 ratio(0~1)而非 0~100
规则三:类型决定后缀
counter → 以 _total 结尾(如 http_requests_total)
gauge → 无固定后缀,描述瞬时值
histogram → _bucket / _sum / _count
summary → 分位数 / _sum / _count
info → 以 _info 结尾,值恒为 1
stateset → 以 _state 结尾(OpenMetrics 特有)
3.2 反例清单
| 反例 | 问题 | 正确写法 |
|---|---|---|
latency_ms | 用了非基本单位 | latency_seconds |
requests(counter) | 缺 _total 后缀 | requests_total |
http_requests_total(gauge) | 类型与后缀矛盾 | 改名或改为 counter |
memory_usage | 单位不明 | memory_usage_bytes |
cpu_percent(0~100) | 用了百分比而非 ratio | cpu_usage_ratio(0~1) |
user_id 作为标签 | 高基数 | 移出标签,改用日志/链路 |
ServiceName | 大写与驼峰 | service_name(小写蛇形) |
http_status(值 200) | 数值型状态码做标签值尚可,但做指标名不行 | 标签 code="200" |
3.3 标签命名约定
合法字符:[a-zA-Z_][a-zA-Z0-9_]*
推荐风格:小写蛇形(snake_case)
语义对齐:优先复用 OpenTelemetry 语义约定,便于与链路/日志对齐
service.name → service_name
http.request.method → method(或 http_method)
http.response.status_code → status_code(或 code)
保留标签:__name__(指标名)、__address__ 等由 Prometheus 内部使用,
业务标签不得以 __ 开头
指标名与标签的统一,是让指标、链路、日志三者在语义层面对齐的前提。日志字段的同类约定可参考 结构化日志与语义约定 ,两者共用同一套命名哲学。
4. Exemplar 与元数据
4.1 Exemplar 语法
Exemplar 是 OpenMetrics 相对 Prometheus 文本格式的关键增量,它允许在样本行尾附加「示例引用」(通常是 trace_id):
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.5"} 24054 # {trace_id="a1b2c3"} 0.42 1696233600.123
http_request_duration_seconds_bucket{le="1.0"} 24110 # {trace_id="d4e5f6"} 0.91 1696233601.456
http_request_duration_seconds_bucket{le="+Inf"} 24110
http_request_duration_seconds_sum 53423.0
http_request_duration_seconds_count 24110
# EOF
语法要点:
# {label="value"} value timestamp
trace_id 是约定俗成的 key,也可用 span_id / request_id
只有 OpenMetrics 格式支持,Prometheus 文本格式不支持
仅 counter 与 histogram 有语义(gauge 的 exemplar 意义弱)
从指标跳到具体 trace 的完整工程链路(存储、保留窗口、Grafana 配置)在 Exemplar 与指标-链路关联 中有详细展开。
4.2 _created 与 info/stateset
OpenMetrics 还引入了几个 Prometheus 文本格式没有的约定:
_created 时间戳序列:
metric_name_created 表示该指标(或该序列)的创建时间
用途:区分"新序列刚创建"与"老序列值为 0"
避免 rate() 在序列刚出现时算出异常峰值
info 类型:
build_info{version="1.2.3",commit="abc"} 1
值恒为 1,信息全在标签里;改标签值即表示"信息变了"
用途:把版本、构建信息等非数值元数据表达为指标
stateset 类型:
feature_enabled{feature="x"} 0/1,且用 # UNIT 声明
是 OpenMetrics 1.0 新增,用于表达"一组互斥或共存的状态"
兼容性:许多后端尚不支持,落地前需确认
5. 兼容性与迁移
5.1 三方矩阵
| 组件 | OpenMetrics 支持情况 | 注意事项 |
|---|---|---|
| Prometheus 抓取端 | 2.x 起支持协商,需 --enable-feature=openmetrics 类开关(版本而异) | 建议同时支持两种格式 |
| 客户端库 | 多数 Go/Java/Python 库可输出两种 | 注意时间戳单位差异 |
| 推送网关 / remote_write | 走 Protobuf,与文本格式无关 | Exemplar 需显式开启 |
| 后端(Mimir/Thanos) | 支持 OpenMetrics 语义 | _created 与 stateset 支持度不一 |
5.2 迁移步骤
第一步:并行暴露
服务端同时支持两种格式,用 Accept 协商决定返回哪种
此时抓取端仍以 Prometheus 文本为主,验证 OpenMetrics 输出正确
第二步:验证一致性
对同一指标,分别抓两种格式,比对样本值、标签、时间戳
重点核对时间戳单位(毫秒 vs 秒)
第三步:开启 Exemplar(若需要)
确认后端与存储链路支持,且保留窗口满足排障需求
第四步:切换抓取端偏好
把 Accept 头改为优先 OpenMetrics
观察写入错误率与数据缺口
第五步:清理旧格式
确认无客户端依赖后,评估是否关闭 Prometheus 文本输出
5.3 迁移中的常见故障
故障一:时间戳错位 1000 倍
现象:数据全部被拒或落到错误的时间点
原因:客户端库切换格式时未改时间戳单位
对策:先小流量验证,比对时间戳
故障二:`# EOF` 未发送
现象:抓取端报 "unexpected end of stream" 或标记不完整
原因:服务端拼接响应时忘了加结束标记
对策:用 curl 检查最后一行
故障三:Exemplar 丢失
现象:Grafana 直方图上没有菱形点
原因:抓取端未开启 exemplar 存储或远端未开 send_exemplars
对策:检查 --enable-feature=exemplar-storage 与远端配置
6. 把规范变成可执行的约束
6.1 用 linter 检查
规范写在文档里没人看,写进 CI 才会被遵守。Prometheus 生态的 promlint 与 pint 可以做基础检查:
# promtool 自带 lint:检查命名、类型、HELP 一致性
promtool check metrics < /metrics.txt
# pint(Prometheus 规则与指标 linter)可做更细的规则检查
pint lint --checks all metrics.prom
# 常见输出示例
# metrics.txt:12: http_request_duration_ms should use base unit 'seconds'
# metrics.txt:30: counter 'requests' should have '_total' suffix
6.2 CI 中的指标规范门禁
# .github/workflows/metrics-lint.yml
name: metrics-lint
on: [pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 抓取被测服务的 /metrics
run: |
docker compose up -d app
sleep 5
curl -s http://localhost:8080/metrics > metrics.txt
- name: 运行 promtool lint
run: |
promtool check metrics < metrics.txt
- name: 自定义命名规则检查
run: |
python3 scripts/check_metric_names.py metrics.txt
# scripts/check_metric_names.py —— 自定义命名规则(节选)
import re, sys
ALLOWED_UNITS = ("seconds", "bytes", "meters", "volts", "amperes",
"joules", "ratio", "requests", "total", "info", "state")
def check(name: str) -> list[str]:
errs = []
if re.search(r"[A-Z]", name):
errs.append(f"{name}: 指标名必须小写")
if re.search(r"_(ms|us|ns|kb|mb|gb|kilobytes|megabytes)$", name):
errs.append(f"{name}: 使用了非基本单位,改用 seconds/bytes")
if name.endswith("_total") is False and is_counter(name):
errs.append(f"{name}: counter 必须以 _total 结尾")
return errs
if __name__ == "__main__":
for line in open(sys.argv[1]):
m = re.match(r"^([a-zA-Z_:][a-zA-Z0-9_:]*)", line)
if m:
for e in check(m.group(1)):
print(e)
6.3 治理清单
□ 全站统一基本单位:seconds / bytes / ratio(0~1)
□ counter 一律 _total 结尾,histogram 用 _bucket/_sum/_count
□ 指标名小写蛇形,标签名复用 OTel 语义约定
□ 每个指标必须有 HELP 与 TYPE,可选 UNIT
□ 禁止以 __ 开头命名业务标签
□ 高基数维度(用户 ID、请求 ID)不得进标签
□ CI 中跑 promtool / pint,不合规即阻断合并
□ 迁移 OpenMetrics 前核对时间戳单位与 # EOF
□ 新指标上线前登记到指标目录(metric catalog)
7. 与更广的可观测性体系的关系
指标规范不是孤立的「洁癖」,它直接影响下游三件事:
影响一:查询与告警复用
统一命名后,一条 rate(http_requests_total[5m]) 可跨服务复用
告警规则可以模板化,而不是每个服务写一套
影响二:跨信号关联
指标名/标签与链路属性、日志字段对齐后,
指标 → 链路 → 日志的跳转才能真正自动化
影响三:长期存储效率
规范的类型与单位让降采样、聚合、压缩算法更有效
命名混乱则会让存储层无法做"同族指标"的优化
想理解指标从暴露到存储的完整链路(抓取、TSDB、远端写),可参考 Prometheus 深入解析 ;端到端的 SDK 埋点与导出器配置可参考 OpenTelemetry 完整指南 。规范是这些环节共同的地基。
小结
OpenMetrics 把指标从「各家自定义的文本」推进到「有规范、可协商、可校验的协议」。落地时抓住三件事:格式层用 Accept 协商 + 回退,服务端同时支持 OpenMetrics 与 Prometheus 文本,迁移时重点核对时间戳单位(秒 vs 毫秒)与 # EOF;语义层坚持基本单位、_total 后缀、小写蛇形与 OTel 语义约定,让跨服务聚合与跨信号关联成为可能;治理层把规范写进 CI,用 promtool/pint 与自定义脚本做门禁,让不合规指标在合并前被拦下。规范看似琐碎,但它决定了你的可观测性数据到底是「一堆数字」还是「一套可复用的语言」。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。