想象市第一体检中心的一天:门口导诊台把印着统一格子的体检单发给每一位客人,项目名、数值、参考范围一一对齐;各科室护士用血压计、体温计测量,把结果填进同一张单子;总检医生按固定时间收齐报告,统一解读。没有人问“这单子怎么填”,因为大家遵循同一套填写规范。 监控系统也一样。应用就像体检者,Exporter 就像血压计和体温计,Prometheus 则是定期收单的总检医生。而 OpenMetrics 就是那张全国统一的体检单——它规定了指标“长什么样”(数据模型)、“怎么填”(Exposition 格式),让任何应用产出的指标都能被任何采集系统读懂。本文就带你先看清这张“体检单”,再亲手做一台合格的“体检仪器”,最后让它与总检医生顺畅配合。
掌握 OpenMetrics 与 Exporter 开发,是搭建标准化可观测体系的核心能力,也是 SRE 与可观测性工程师面试必考、日常排障必备的硬技能。
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
三大支柱
体检中心门口挂着三块牌子:“基础体检”、“问诊记录”、“检查单流转”。基础体检测体温、量血压,输出一个个数值;问诊记录写下病人描述和医生观察,是一段段文字;检查单流转则追踪“这张单子到过哪个科室、下一步该去哪”。三块牌子服务同一个目标:全面了解病人状况。
软件系统的可观测性(Observability)也由三根支柱撑起:Metrics 指标、Logs 日志、Traces 链路追踪。Metrics 是数值型快照,适合聚合与告警;Logs 是离散事件文本,适合排查细节;Traces 是请求跨服务的传播路径,适合定位瓶颈。三者各有所长,互为补充。
本文的主角 OpenMetrics,正是为 Metrics 这根支柱定制的“体检单”标准。它不关心日志怎么写,也不管链路用什么协议,只专注解决一个问题:让任何应用吐出的指标都长成一个样,任何采集系统都能直接读懂。理解这一点,你就知道 OpenMetrics 在可观测性版图里的位置了。
回到体检中心,三块牌子缺一不可;而 OpenMetrics 要做的,就是先把“基础体检”这一块的报告单格式彻底统一。统一之后,下一节我们仔细端详这张体检单上究竟印了哪些格子。
- 可观测性(Observability)系统内在状态可被外部测量的能力,就像体检中心对病人健康状况的全面掌握。
- 并集(\cup)表示三者之和,缺一不可。没有数值只有日志,无法快速告警;没有日志只有数值,难以定位根因。
- 三支柱集合$Metrics \cup Logs \cup Traces$ 共同构成完整可观测性,OpenMetrics 站在 Metrics 这一支柱上。
数据模型
拿起体检中心的统一体检单,你看到的格子由四部分拼成:项目名称(比如“血压”)、被检人信息(姓名、年龄)、测得数值(120/80)、测量时刻(08:30)。缺了任何一项,总检医生都无法准确解读。OpenMetrics 的数据模型与之惊人相似。
OpenMetrics 把一次“指标测量”称为一个样本(Sample)。样本由四要素构成:指标名(Metric Name)、标签集合(Labels)、值(Value)和时间戳(Timestamp)。指标名告诉你“测的是什么”,标签告诉你“测的是谁”,值和时间戳告诉你“结果如何、何时测得”。
指标名遵循命名规范:通常为 namespace_subsystem_unit_suffix,例如 http_requests_total 由 namespace(http)+ 单位(requests)+ 类型后缀(total)组成。标签是 key-value 对,同样的指标名配上不同标签,就构成不同的时间序列。类型上,Counter 只增不减,Gauge 可增可减,Histogram 记录分布,Summary 在客户端计算分位数,Info 描述静态信息,StateSet 表示状态枚举。
这样一张格子分明的体检单,让总检医生一眼就知道每个数字的含义。但要注意:格子设计得再好,填写规范不统一,各科室还是各写各的——这正是下一节“填单规范”要解决的问题。
- 样本(Sample)一次测量结果的最小单位,就像体检单上一个“项目名 + 数值 + 时间”填满的格子。
- 名称与标签集$(Name, \{label_1: v_1, \cdots, label_n: v_n\})$,指标名像项目名,标签集合像被检人信息,共同定位“谁在什么维度上的指标”。
- 值与时间戳$Value$ 是检测数值,$Timestamp$ 是测量时刻;同一指标同标签下,不同时间戳构成一条时间序列。
格式规范
统一模板印刷好了,但护士的字迹五花八门:有人写“120/80“,有人写”血压正常“,还有人画了个箭头。总检医生再专业也吃不消。所以体检中心规定:每个格子只能填规定格式的数字,且必须附带说明这一行是什么。OpenMetrics 的 Exposition 格式就是这份”书写规范”。
Exposition(呈现)格式规定了指标在 HTTP 响应体里如何以纯文本呈现。最核心的是三类行:HELP 行(# HELP 指标名 帮助文本)、TYPE 行(# TYPE 指标名 类型),以及样本行(指标名{标签} 值 [时间戳])。文本以 # EOF 结束,表示“报告单到此为止”。
一个经典输出长这样:# HELP http_requests_total 累计请求数、# TYPE http_requests_total counter、http_requests_total{code="200"} 1024。标签值中的反斜杠、双引号、换行符都需要转义;Content-Type 协商让 Prometheus 知道收到的是 0.0.4 兼容文本还是 OpenMetrics 1.0 文本。任何语言实现的 HTTP 服务,只要输出符合这个文本协议,就能被标准采集器读取。
填单规范一统一,护士们不再各写各的,总检医生手里的每份报告都是同一种笔迹。有了统一格式,接下来我们该造一台“自动填单的体检仪器”——自定义 Exporter。
- 指标名与标签$name\{label_1="v_1", label_2="v_2", \cdots\}$ 精确定位一条时间序列,标签值必须用双引号包裹。
- 值(value)紧跟在标签后的数值,对应体检格子里的测量结果。
- 时间戳([timestamp])方括号表示可省略;省略时采集系统以抓取时刻作为该样本的时间。
Exporter 开发
体检中心里,测血压靠血压计,测体温靠体温计。每台仪器只做一件事:从人体采集一种生理信号,再按统一格式把它印到体检单上。在监控世界里,Exporter 就是这样的体检仪器。
Exporter 的职责可以拆成三段:采集(Collect)、标准化(Normalize)、暴露(Expose)。采集是从目标应用中读取状态;标准化是整理成 OpenMetrics 规定的样本结构;暴露是以 HTTP 端点的形式把文本输出出去。三段加起来,就是一个合格的 Exporter。
开发自定义 Exporter 时,多数语言都有官方客户端库,Python 的 prometheus_client 是最常用的一种。核心流程:定义指标对象(如 Counter('http_requests_total', '计数帮助'))、在采集回调中更新值、注册到默认注册表、启动 HTTP 服务在 /metrics 暴露。类型选择有讲究:只增不减用 Counter,可增可减用 Gauge,要分析位数分布用 Histogram 或 Summary。
血压计、体温计各有分工,但共同点是都按同一张体检单填数。自定义 Exporter 只要遵循采集-标准化-暴露三段式,无论监控的是进程、数据库还是 AI 模型服务,Prometheus 都能一视同仁地收走数据。仪器造好了,下一节该让总检医生与它约定工作节奏。
- 采集(Collect)从目标应用读取原始状态,比如读取健康指标、数据库连接数或模型推理耗时。
- 标准化(Normalize)把采集结果整理成 OpenMetrics 规定的样本结构和文本格式。
- 暴露(Expose)通过 HTTP 服务在指定路径(默认 /metrics)对外输出文本,供采集端读取。
集成实践
总检医生不会 24 小时守在每台仪器旁边,而是定好时间表:每天早晨 8 点整来体检中心收报告,每份报告必须在 5 分钟内交出来,交不出的算超时。这个节奏,就是 Prometheus 的抓取周期。
Prometheus 通过配置文件 prometheus.yml 中的 scrape_configs 来定义抓取任务。每个 job 可以配置:job_name(任务名)、scrape_interval(抓取间隔,如 15s)、scrape_timeout(抓取超时,如 10s)、metrics_path(指标路径,默认 /metrics)、static_configs(静态目标列表)或服务发现配置。
抓取生命周期是这样的:Prometheus 根据目标列表,周期性发起 HTTP GET 请求到 <target>:<port>/metrics,读取响应体文本,按 OpenMetrics 格式解析后写入时序数据库。超时配置有一条黄金法则:抓取超时必须小于抓取间隔,否则上一次抓取还没结束,下一次就开始了,形成积压雪崩。
时间表约定好后,总检医生每天准时收单,仪器们各就各位。但体检高峰季来了:成百上千台仪器同时出报告,总检医生和仪器都开始吃力。怎么让整套流程既快又不互相拖累?这就是性能优化要解决的问题。
- 抓取超时(scrape_timeout)单次抓取允许的最大等待时间,像收单的“截止时间”,超时则本次作废。
- 抓取周期(scrape_interval)相邻两次抓取的间隔,像医生的查房频率。
- 严格小于(<)截止时间必须早于下一次查房开始,否则上一次还没收完、下一次又来,报告会越积越多。
性能优化
体检高峰季,上百人被安排在同一天。如果每台仪器都磨磨蹭蹭,下一位体检者就会被无限延后;如果几十台仪器同时测量,又可能把体检者折腾得受不了。聪明的体检中心会做三件事:给每台仪器设定单人的时间上限、错峰并行测量、把常测结果缓存下来复用。
Exporter 的性能优化同样围绕三个方向。一是超时控制:用 Context 给每次采集设置超时,目标应用无响应时快速放弃,避免线程堆积。二是并发采集:内部用信号量(Semaphore)限制并发数,既能并行提升速度,又不会打爆目标应用。三是指标缓存:对代价高的采集结果做短时间缓存,避免每次抓取都重复计算。
还有一个容易被忽略的优化点——降低基数(Cardinality)。指标基数等于各标签取值数的乘积。如果一个标签是 user_id,有 100 万用户,那么这个指标瞬间膨胀成 100 万条时间序列,存储和查询都会雪崩。经验法则是:标签只放取值有限的维度(如 method、status_code),不放无限维度(如 user_id、request_ip)。
经过提速增效,体检中心在高峰季依然秩序井然:超时上限防住慢仪器,并发策略平衡负载,缓存减少重复劳动,而标签设计避免了报告单无限膨胀。仪器状态越来越好,但别忘了:新仪器上岗前,还得先通过校准。
- 基数(Cardinality)一个指标在所有标签组合下展开出的时间序列条数。
- 标签集合(Labels)$l \in Labels$ 遍历指标上的全部标签维度。
- 乘积(\prod)$|D_l|$ 表示每个标签的取值个数,全部相乘就是总序列数。三个取值 10 的标签相乘即 1000 条序列。
测试调试
新买的血压计不能直接上岗。工程师先用标准水银柱对比读数,再让护士连续测三次确认稳定,最后在真实体检中抽查几份报告,确认打印格式无误。这套“校准”流程,对应到 Exporter 就是测试与调试。
测试 Exporter 通常分四步走。第一步单元测试:用客户端库的测试工具直接断言指标值是否符合预期。第二步 curl 验证:手动请求 http://localhost:9100/metrics,肉眼检查输出行的格式。第三步格式检查:用 promtool check metrics 自动校验指标命名、类型、格式是否合规。第四步日志排查:开启 Exporter 的调试日志,观察采集耗时和错误信息。
用 prometheus_client 写单元测试时,可以用 generate_latest() 拿到最新文本,再断言其中是否包含期望的指标行。curl 是快速验证的利器:curl -i http://localhost:9100/metrics 能看到状态码和 Content-Type,curl -s ... | head 能查看前几行。promtool 则是格式规范的最终裁判,它会告诉你哪一行标签转义不对、哪个指标名不合规。
校准通过的仪器才能放心交到护士手里。至此,你手里已经有了一台符合 OpenMetrics 标准、经过测试校准的体检仪器,也知道总检医生如何与它协同。从一张体检单开始,到一整套体检流程跑通——这就是 OpenMetrics 标准化与 Exporter 开发实战的全部脉络。
- 合格(valid)Exporter 输出可以被 Prometheus 正确解析并可靠使用的状态。
- 格式与类型(format\_ok \land type\_ok)文本语法正确,且 TYPE 声明的类型与数值行为一致。
- 数值合理(value\_ok)数值在业务上合理,比如 Counter 不减、Gauge 范围正确。
AI 应用场景
电商大促业务自定义指标监控
阶段:数据采集与预处理
解决的问题:业务团队需要下单量、支付耗时等自定义指标,传统埋点散乱、格式各异,告警无统一入口。用 OpenMetrics 标准 + 自定义 Exporter 统一采集,让业务指标与系统指标进入同一套监控体系。
具体案例:电商后端用 Python 实现一个自定义 Exporter,暴露 order_created_total 和 payment_duration_seconds 两个指标,挂在:9100 端口。Prometheus 每 15 秒抓取一次,Grafana 大屏实时展示大促进度,SRE 按 _total 后缀自动识别 Counter 并配置增量告警。
数据库与中间件指标快速接入
阶段:特征工程与监控数据整合
解决的问题:MySQL、Redis 自带状态命令但无统一输出格式,手工解析成本高且易错。社区成熟的 Exporter(mysqld_exporter、redis_exporter)已经按 OpenMetrics 规范输出,开箱即用,节省大量开发时间。
具体案例:SRE 在 Kubernetes 集群中为每个 MySQL Pod 部署边车模式(Sidecar)的 mysqld_exporter,通过服务发现自动注册到 Prometheus,无需手动维护目标列表。新扩容的数据库实例上线即被自动纳入监控。
AI 模型服务在线监控
阶段:模型服务部署与在线监控
解决的问题:推理服务的 QPS、响应延迟、GPU 利用率等指标需要统一可视化和告警。用自定义 Exporter 按 OpenMetrics 格式暴露推理指标,可无缝接入 Prometheus 生态。
具体案例:MLOps 团队在推理服务旁挂一个自定义 Exporter,暴露 inference_requests_total(Counter)和 inference_latency_seconds(Histogram),并按模型版本打标签。当 p99 延迟超过阈值时,Prometheus 告警自动触发模型回滚流程。
Python 演示
"""模拟一个标准库实现的自定义 Exporter 演示运行后访问 http://127.0.0.1:9100/metrics 查看指标"""import randomimport timefrom http.server import BaseHTTPRequestHandler, HTTPServerfrom threading import Thread METRICS = { "http_requests_total": {"type": "counter", "help": "累计处理的 HTTP 请求数", "value": 0}, "http_request_duration_seconds": {"type": "gauge", "help": "最近一次请求耗时", "value": 0.0},} def render_metrics(): lines = [] for name, info in METRICS.items(): lines.append("# HELP {} {}".format(name, info["help"])) lines.append("# TYPE {} {}".format(name, info["type"])) lines.append("{} {}".format(name, info["value"])) lines.append("# EOF") return "\n".join(lines) class MetricsHandler(BaseHTTPRequestHandler): def do_GET(self): if self.path == "/metrics": body = render_metrics().encode() self.send_response(200) self.send_header("Content-Type", "text/plain; version=0.0.4") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) else: self.send_response(404) self.end_headers() def log_message(self, fmt, *args): pass def simulate_traffic(): while True: METRICS["http_requests_total"]["value"] += 1 METRICS["http_request_duration_seconds"]["value"] = round(random.uniform(0.01, 0.5), 3) time.sleep(0.5) if __name__ == "__main__": Thread(target=simulate_traffic, daemon=True).start() server = HTTPServer(("127.0.0.1", 9100), MetricsHandler) print("exporter is running at http://127.0.0.1:9100/metrics") server.serve_forever()
METRICS = {...}用字典作为指标仓库,记录每个指标的类型、帮助文本和当前值,这是“体检单”的数据来源。lines.append("# HELP {} {}".format(...))输出 HELP 行,向采集端说明这个指标的含义。lines.append("# TYPE {} {}".format(...))输出 TYPE 行,声明指标类型(counter/gauge),这是格式规范的核心。if self.path == "/metrics":只有访问 /metrics 路径才返回指标文本,其余路径返回 404。self.send_header("Content-Type", "text/plain; version=0.0.4")声明响应体为 0.0.4 兼容格式,Prometheus 据此解析。Thread(target=simulate_traffic, daemon=True).start()启动后台线程模拟业务流量,让指标值持续变化,便于观察。
常见误区
误区只要给指标加上 `_total` 后缀,它就是一个 Counter。比如把当前在线人数命名为 `online_users_total`,然后在 TYPE 行声明为 gauge。
正解_total 后缀是 Counter 的命名约定,但决定类型的是 # TYPE 行与数值行为。在线人数可增可减,应命名为 online_users 并声明为 gauge。就像体检单上的“体温”格子不能因为想省事就填成“身高”的写法。
为何常见:初学者刚接触命名规范时,容易把“后缀规则”误解为“加后缀即类型”,忽略了类型声明与数值语义的一致性。
误区不用 prometheus_client 或 client_golang 写出来的就不是正规 Exporter,Prometheus 不会认。
正解OpenMetrics 的核心是文本协议。任何 HTTP 服务,只要输出符合规范的文本(HELP/TYPE/样本行/EOF),Prometheus 都能采集。官方客户端库只是帮你省事,不是唯一通道——就像体检单只要能填对格式,用钢笔还是圆珠笔并不重要。
为何常见:教程大多基于官方库演示,导致初学者把“开发工具”当成“准入资格”,忽略了标准本身。
误区抓取越慢越容易超时,那把 scrape_timeout 设置成比 scrape_interval 大,不就安全了吗?
正解超时大于间隔会导致抓取任务重叠:上一次还没结束,下一次已经启动,最终造成并发堆积和资源耗尽。正确做法是超时严格小于间隔,例如间隔 15s 配超时 10s。这就像收单截止时间不能晚于下一次查房开始时间。
为何常见:直觉上“更大更宽松”看似安全,但抓取系统是周期性任务模型,重叠才是灾难。
误区为了监控更细,把 user_id、request_ip、session_id 这些都加进标签,这样每个用户都能单独看指标,多好。
正解标签值是无限增长的高基数维度时,时间序列数量会按乘积爆炸,直接拖垮存储和查询。合理做法是只保留取值有限的维度(method、status_code、instance),真正需要按用户分析时走日志系统。
为何常见:从“业务粒度”出发而非从“系统容量”出发思考标签设计,是监控新人最容易踩的坑。
误区`curl` 能返回指标文本、状态码也是 200,说明 Exporter 已经写好了,不用再做别的检查。
正解curl 只能证明“有响应”,不能证明格式完全合规。标签转义错误、TYPE 行缺失或类型声明错误,都可能导致 Prometheus 解析失败或告警误判。必须再用 promtool check metrics 做自动校验,就像仪器校准不能只靠看一眼刻度。
为何常见:能返回数据给了人“已经成功”的错觉,而格式合规性的验证需要额外工具和耐心,容易被跳过。
面试题
Q1. 下列哪项是 OpenMetrics 中 Counter 指标的核心特征?
- A.只能增加不能减少← 正确答案
- B.可以增加也可以减少
- C.只能表示瞬时状态
- D.只能记录字符串值
答案:A。Counter 用于累计事件次数,数值只增不减,进程重启后可从 0 重新累计,适合记录请求总数这类指标。Gauge 才可增可减。就像体检中心的“累计接待人数”计数器,只会往上加。
Q2. 指标名 http_requests_total 中 _total 后缀的作用是什么?
- A.表示该指标有总量上限
- B.暗示该指标是 Counter 类型← 正确答案
- C.表示该指标包含时间戳
- D.表示该指标支持多标签
答案:B。命名规范约定 Counter 类型指标使用 _total 后缀,方便 PromQL 中自动识别并可配合 rate() 计算速率。但实际类型仍以 TYPE 行为准。就像体检单上的项目名含“累计”字样,帮助人快速理解含义。
Q3. 从应用状态转换到可被 Prometheus 抓取的文本格式,由哪个组件完成?
- A.Prometheus 服务器
- B.目标应用自身
- C.Exporter 程序← 正确答案
- D.Grafana 面板
答案:C。Exporter 负责采集应用状态、标准化为 OpenMetrics 文本并通过 HTTP 暴露。Prometheus 只负责按周期抓取与存储。就像血压计负责测量并把数值印在单子上,总检医生只管收单。
Q4. OpenMetrics 文本格式中 TYPE 行的作用是什么?
- A.声明指标值的类型
- B.声明指标名与指标类型← 正确答案
- C.声明标签键值集合
- D.声明时间戳的格式
答案:B。# TYPE 指标名 类型 声明该指标属于 counter、gauge、histogram 等哪一类,采集端据此决定数值的解释方式。A 是混淆项,TYPE 声明的是指标类型而非数据类型的数值格式。
Q5. 在 prometheus.yml 中用哪个配置块定义抓取任务?
- A.告警规则配置
- B.抓取配置区← 正确答案
- C.存储保留配置
- D.查询规则配置
答案:B。scrape_configs 是 Prometheus 中定义抓取任务的核心配置块,包含 job_name、scrape_interval、scrape_timeout、metrics_path 以及目标列表。告警规则在 rules 中定义,存储保留在 storage 中配置。
Q6. 关于抓取超时与抓取间隔的关系,正确的是?
- A.超时必须大于间隔
- B.超时必须小于间隔← 正确答案
- C.两者必须完全相等
- D.两者没有任何关系
答案:B。若 scrape_timeout 大于 scrape_interval,前一次抓取尚未结束、下一次抓取就启动,形成任务重叠与资源堆积。正确配置是超时小于间隔,例如间隔 15s 配超时 10s。
Q7. 指标基数(Cardinality)过高的主要风险是什么?
- A.应用 CPU 使用率下降
- B.存储与查询性能降级← 正确答案
- C.指标无法暴露为文本
- D.网络带宽持续提升
答案:B。基数过高意味着时间序列数量爆炸,会拖垮 Prometheus 的存储、查询和告警计算。应避免 user_id、ip 等高基数标签,只保留取值范围有限的维度。
Q8. 下列哪些属于 OpenMetrics 标准的指标类型?
- A.计数器型← 正确答案
- B.摘要型← 正确答案
- C.桶结构型
- D.信息型← 正确答案
答案:ABD。OpenMetrics 标准类型包括 Counter(计数器)、Gauge、Histogram、Summary(摘要)、Info(信息)、StateSet。Bucket(桶)是 Histogram 内部的概念,不是独立指标类型。
Q9. 开发自定义 Exporter 时,合理的性能优化手段有哪些?
- A.用 Context 设置采集超时← 正确答案
- B.高频指标并发抓取← 正确答案
- C.无条件全量采集所有指标
- D.对采集结果使用缓存复用← 正确答案
答案:ABD。Context 超时防止卡死,并发采集提升吞吐但需限流,缓存减少重复计算。C 恰恰相反,无条件全量采集会导致性能恶化,应结合业务需求精简采集项。
Q10. 测试与调试 Exporter 时,常用的手段包括哪些?
- A.curl 验证暴露输出← 正确答案
- B.promtool 检查格式← 正确答案
- C.日志打印采集耗时← 正确答案
- D.修改 Prometheus 源码
答案:ABC。curl 验证响应内容、promtool 校验格式合规、日志排查采集耗时是三类标准手段。D 没有解决 Exporter 自身问题,而且修改监控系统源码成本极高、与实际排查目标无关。