尽管 OpenTelemetry(简称 OTel)风头正劲,你可能会倾向于使用 OpenTelemetry 及其 SDK 来满足所有应用埋点需求。但如果目标是生成可在 Prometheus 中使用的指标,在完全采用 OTel 之前,至少应该三思。
这样做不仅可能让你错失 Prometheus 作为监控系统所特有的部分核心功能,还会面临指标转换不顺畅、转义问题、标签关联复杂,以及其他效率低下和维护复杂的问题。因此,若你希望获得最佳的 Prometheus 监控体验,我仍然建议使用 Prometheus 自身的原生埋点客户端库,而非 OTel SDK。
核心结论
如果你的主要后端和查询语言是 Prometheus/PromQL,优先使用 Prometheus 原生客户端库通常更简单、更高效,也更符合 Prometheus 的拉取式监控模型。
OpenTelemetry 当然有价值,尤其适合统一日志、指标、Trace 的采集和传输。但在“应用指标 -> Prometheus -> PromQL 告警/大盘”这条链路上,OTel 会带来以下代价:
| 问题 | 影响 |
|---|---|
| 目标健康监控缺失 | Prometheus 无法天然知道哪些目标应该存在、是否抓取成功 |
| 指标名称转换 | OTel 指标名和属性名可能被替换、加后缀或需要复杂转义 |
| 资源属性处理复杂 | 额外资源属性不一定会直接成为每条时间序列的标签 |
| Prometheus 需要额外配置 | 需要启用 OTLP receiver,并考虑乱序数据 |
| SDK 更复杂 | 概念、初始化、聚合和性能开销都更重 |
| 开放标准并非 OTel 独有 | Prometheus 本身也是开放、成熟、广泛采用的生态 |
一、OpenTelemetry 与 Prometheus 的适用范围对比
首先,如果你对 OpenTelemetry 和 Prometheus 不是特别熟悉,这里先简要对比一下两个系统的适用范围:

简而言之:
- OpenTelemetry 可处理三种信号类型:日志、指标和追踪数据。它关注信号的生成,也就是埋点,以及将生成的信号传输到第三方后端系统。这种传输通常通过 OpenTelemetry 协议(OTLP)实现。
- Prometheus 只处理指标,不涉及日志或追踪数据。但它的功能不止于指标生成。作为一套完整的监控系统,Prometheus 还提供主动采集、存储、PromQL 查询、仪表盘展示和告警触发等能力。
两个系统的适用范围在指标生成和传输环节存在重叠,但在这一重叠部分,它们各自拥有独立的客户端库、传输协议、数据模型和设计理念。
二、将 OpenTelemetry 指标接入 Prometheus 的基本方式
虽然这两个系统是独立发展起来的,但如今已有多种方法可将 OpenTelemetry 指标发送到 Prometheus。你可以直接从应用程序向 Prometheus 服务器发送指标,但在 OTel 生态中,更常见的做法是先将所有数据发送到 OpenTelemetry Collector,再由 Collector 将处理后的数据发送到 Prometheus 等后端系统:

从 Collector 到 Prometheus 的指标传输主要有以下几种方式:
- Prometheus Exporter:允许 Prometheus 服务器以 Prometheus 原生指标格式从 Collector 拉取指标。这种方式不适用于大规模场景,因为它会将所有指标整合为一次大型 Prometheus 拉取操作,可能引发扩展性和可靠性问题。
- Prometheus Remote Write Exporter:通过 Prometheus 用于服务器间指标传输的原生远程写入格式,将指标推送到 Prometheus 服务器。这种方式更适合大规模场景。
- OTLP Exporter(最常用):使用 OpenTelemetry 的 OTLP 协议将指标推送到 Prometheus。这是替代 Prometheus 远程写入格式的不错选择,也是实际中大多数人将 OTel 数据发送到 Prometheus 时采用的方式。
三、为什么不建议为了 Prometheus 指标优先选择 OTel
以下原因主要针对“我主要关注指标,并且要在 Prometheus 中使用指标”的场景。
原因一:丢失 Prometheus 的目标健康监控能力
Prometheus 是一套完整的监控系统,而监控系统不应只是被动接收随机传入指标的“容器”。要实现核心功能,监控系统必须知道三件事:系统应有的状态、系统当前的实际状态,以及两者之间的差异。
Prometheus 的原生监控模型通过两个关键概念解决这个问题:
- 服务发现:Prometheus 集成多种服务发现机制,可自动识别当前应存在的监控目标。例如在 Kubernetes 集群中,Prometheus 服务器可通过 Kubernetes API 订阅 Pod、服务、入口资源等信息,且该列表会持续更新。
- 带内置健康检查的拉取式指标采集:Prometheus 主动从每个已发现目标拉取指标,并自动记录抓取是否成功。
因此,Prometheus 在每次拉取目标指标时都会记录一个合成的 up 指标:根据拉取操作是否成功,该指标样本值为 0 或 1。

这种机制让目标健康监控很直接,例如:
# 若“demo”任务中的任何目标宕机超过5分钟,则触发告警
alert: TargetDown
expr: up{job="demo"} == 0
for: 5m
与之相反,OTLP 是推送式协议,且未与 Prometheus 的服务发现功能集成。因此,Prometheus 无法天然判断“预期的指标源是否未上报数据”。你将难以检测以下故障:
- 本应运行的目标处于启动状态,但未发送指标。
- 本应运行的目标已经宕机或不存在,因此也未发送指标。
- 网络故障或分区导致一个或多个目标无法发送指标。
- 本不应由该 Prometheus 服务器监控的目标,却在向它发送指标。
若想通过 OTLP 实现类似级别的目标健康监控,需要从可信源生成另一组“目标应有状态”指标,然后手动把这组指标与传入的 OTLP 数据关联。这需要额外工程量,也很容易被忽略。
原因二:指标名称会被修改,PromQL 选择器更复杂
在 Prometheus 中使用 OpenTelemetry 指标时,会遇到字符集兼容、指标命名规范和转义问题。
Prometheus 在 PromQL 查询语言中使用指标和标签名称,因此长期对字符集有较严格的限制。Prometheus 3.0 版本之前,标签名称仅允许包含字母、数字和下划线,指标名称额外允许冒号。
OpenTelemetry 允许在指标和属性名称中使用点号、短横线以及其他类似运算符的字符。因此,在 Prometheus 3.0 之前,来自 OpenTelemetry 的指标和属性名称必须转换为 Prometheus 兼容格式,例如把 my.metric.name 转成 my_metric_name。
同时,Prometheus 的指标命名规范要求指标名称带有单位后缀;计数器类型指标还要添加 _total 后缀。OpenTelemetry 则把单位和类型视为独立元数据字段,不要求包含在指标名称里。于是,OTLP 到 Prometheus 的转换层默认会为 OTel 指标名称添加 Prometheus 风格的单位和类型后缀。例如 OTel 指标 k8s.pod.cpu.time 转换后会变为 k8s_pod_cpu_time_seconds_total。
Prometheus 3.0 开始支持完整 UTF-8 指标和标签名称,理论上可以保留未经修改的 OTel 指标名称。但如果名称使用扩展字符集,PromQL 选择器要写得更复杂。
传统选择器:
my_metric{my_label="value"}
扩展字符集选择器:
{"my.metric", "my.label"="value"}
从 Prometheus 3 版本的 UTF-8 支持开始,你可以通过配置文件中的 otlp.translation_strategy 设置 OTel 指标转换策略:
UnderscoreEscapingWithSuffixes:将名称转换为传统字符集,使用下划线,并添加单位和类型后缀。NoUTF8EscapingWithSuffixes:保留所有原始字符,但仍添加计数器后缀和单位后缀。NoTranslation:保留所有原始字符且不添加任何后缀,目前处于实验阶段,可能会被移除。
是否保留原始字符集取决于你的实际需求。但作者强烈建议不要使用 NoTranslation,因为它会省略单位和类型后缀,让后续指标理解更困难。
原因三:资源标签与目标标签看似相似,实则不同
Prometheus 和 OpenTelemetry 都会为指标附加标签,用来描述指标来源。在 OpenTelemetry 中,这类标签叫“资源属性”;在 Prometheus 中,则更接近“目标标签”。
两者差异在于:
- Prometheus 目标标签:由 Prometheus 服务器根据目标元数据附加到拉取的指标上。目标标签数量通常较少,且主要用于标识目标。
- OpenTelemetry 资源属性:由生成指标的应用程序自行定义,通常数量更多,也更偏描述性上下文。
Prometheus 默认会把目标标签附加到从该目标拉取的每个指标上。但如果把大量 OTel 资源属性都附加到每条时间序列上,会导致存储成本升高,也会让查询更难维护。
因此,当 Prometheus 通过 OTLP 接收指标时,默认仅将 OTel 资源属性中的一小部分附加到所有指标上:
service.name:对应 Prometheus 中的job标签。service.instance.id:对应 Prometheus 中的instance标签。
其他资源属性会附加到名为 target_info 的指标上。如果查询时确实需要某个额外属性,就必须通过 PromQL 做关联查询。
原本简单的查询:
rate(http_server_request_duration_seconds_count[5m])
如果要同时包含 target_info 指标中的 k8s_cluster_name 资源属性,就需要改写为:
rate(http_server_request_duration_seconds_count[5m])
* on(job, instance) group_left(k8s_cluster_name)
target_info
Prometheus 3.0 新增了 info() 函数,旨在简化此类操作,但该函数目前仍处于实验阶段,功能尚未完善。
原因四:Prometheus 需要额外配置才能摄入 OTLP 指标
要让 Prometheus 能通过 OTLP 接收指标,需要在 Prometheus 服务器上额外配置至少两项设置。
第一,启用 OTLP 接收器:
--web.enable-otlp-receiver
这会让 Prometheus 在 /api/v1/otlp/v1/metrics 路径上启动 OTLP 接收器。由于 Prometheus 本质上是拉取式监控系统,允许外部客户端向其推送指标可能存在安全风险,因此需要显式启用该功能,并保护该端点,防止未授权来源推送。
第二,配置 TSDB 支持乱序数据。OTLP 不保证数据有序传输,这与 Prometheus 自身“拉取数据并添加时间戳”的模型不同,因此需要配置 Prometheus TSDB,允许在一段时间内接收乱序数据:
storage:
tsdb:
out_of_order_time_window: 30m
使用 OTLP 与 Prometheus 搭配时,可能还需要配置其他设置,具体可参考 Prometheus 官方文档。
原因五:OTel SDK 复杂度高且性能可能较差
与 Prometheus 原生客户端库相比,OpenTelemetry SDK 会增加系统整体复杂度,并且实现效率可能较低。
OTel 是一个庞大系统,需要处理日志、追踪、指标,以及指标视图、聚合等复杂功能。它的 SDK 体积和概念也更重。相反,Prometheus 原生埋点库更小,只专注于高效生成 Prometheus 指标。
原文中的性能讨论主要基于 Go 语言的 Prometheus 和 OpenTelemetry SDK,同时作者也提到对 Java 语言做了简化版基准测试,结果趋势类似:在多线程性能测试中,Prometheus SDK 的速度最高可达 OTel SDK 的 30 倍以上。
在高并发多核服务器应用中,计数器递增可能每秒发生数百万次。这种场景下,埋点框架的 CPU 和内存开销会直接影响服务本身。
计数器递增性能对比
作者在 GitHub 上提供了简易基准测试代码,并在 Gist 中提供原始结果。测试会在不同条件下创建计数器指标,然后频繁递增计数器。在测试环境(Intel i7-12700KF CPU、20 核、Arch Linux 系统)中,Prometheus Go SDK 的速度都明显快于 OpenTelemetry SDK:



| 测试场景 | 性能对比结论 |
|---|---|
| 无标签的计数器递增 | 在最差场景(无标签、并行度 16)下,Prometheus SDK 速度约为 OTel SDK 的 26 倍 |
| 未缓存标签的计数器递增 | 在最佳场景(未缓存标签、并行度 2)下,Prometheus SDK 速度仍约为 OTel SDK 的 4.4 倍 |
| 已缓存标签的计数器递增 | Prometheus Go SDK 允许缓存特定标签子指标的引用,性能差距最高达 53 倍;OTel SDK 不支持此类优化 |
从原始基准测试结果还可发现:每当为计数器递增操作设置属性时,OTel SDK 都会产生内存分配;而在所有测试场景中,Prometheus Go SDK 均不会产生新的内存分配。
作者还提到一个主观感受:在 SDK 代码中查找“实际执行值递增的代码行”时,从应用代码的 Inc() 调用开始,他很快找到了 Prometheus Go SDK 中的对应代码;但在 OpenTelemetry Go SDK 中,浏览各种间接调用和抽象逻辑后仍然很难定位。这体现了两个 SDK 在复杂度上的差异。
原因六:Prometheus 同样开放且成熟
许多人采用 OpenTelemetry 的原因是希望选择一套开放标准。但 Prometheus 同样是开源项目,并采用开放治理模式。它是一套成熟、经过实战检验且应用广泛的系统,拥有庞大的贡献者和用户社区。
许多云厂商已将 PromQL、Prometheus 远程写入格式及其他 Prometheus 接口作为指标摄入和查询的事实标准。Prometheus 用于从目标暴露指标的文本格式也非常简洁,在某些场景下甚至不需要依赖库,只需几行 Shell 脚本就能实现基础功能:
#!/bin/bash
mkdir -p /tmp/metrics
echo 'my_metric{label="value"} 42' > /tmp/metrics/metrics
npx serve -p 8080 -d /tmp/metrics
之后,只需将 Prometheus 服务器指向 http://localhost:8080/metrics,即可从该端点拉取指标。
相比之下,OTLP 是一种基于 Protocol Buffer 的深度嵌套传输格式,若没有完善的 SDK,几乎无法实现其功能。
此外,如果后续确实需要,仍可将 Prometheus 指标端点桥接到 OpenTelemetry/OTLP 中。例如在 Go 语言中,可以使用 Prometheus Bridge,通过 OTLP 暴露原生 Prometheus 指标。
什么时候仍然可以选择 OpenTelemetry
本文并不是说 OTel 不应该用于指标。更准确的边界是:
- 如果团队需要统一日志、指标、Trace 的采集和传输,OTel Collector 与 OTLP 仍然有价值。
- 如果指标主要进入非 Prometheus 后端,OTel SDK 可能更符合统一标准化诉求。
- 如果已经有成熟的 OTel 采集链路,且能够接受目标健康、标签、命名和性能方面的额外治理成本,也可以继续使用。
但如果目标很明确:应用只需要产生 Prometheus 指标,并在 Prometheus 中用 PromQL 做告警和大盘,那么原生 Prometheus 埋点仍然是更直接的选择。
FAQ
Q1:OpenTelemetry 指标不能进 Prometheus 吗? 可以。可以通过 Prometheus Exporter、Remote Write Exporter 或 OTLP Exporter 等方式接入。本文讨论的是“是否应该优先这么做”,不是“能不能做”。
Q2:为什么 Prometheus 的拉取模型对健康监控重要?
因为 Prometheus 知道哪些目标应该存在,并且每次抓取都会产生 up 指标。推送式 OTLP 默认无法提供同等的目标存在性和抓取成功性判断。
Q3:如果公司已经全面使用 OTel,是否还要改回 Prometheus SDK? 不一定。需要看目标后端、性能要求、PromQL 使用强度、标签治理能力和迁移成本。本文的建议主要适用于以 Prometheus 为核心指标系统的场景。
结论
若你使用 Prometheus 作为监控系统,作者仍然强烈建议使用 Prometheus 原生的埋点客户端库和拉取式监控模型来监控服务。这样做能让你获得更可靠、更完整、更高效的监控体验,同时避免指标名称转换、转义问题以及在查询中关联额外标签等繁琐操作。
当然,你可能仍有其他理由选择 OpenTelemetry,但希望本文能让你清楚了解这种选择可能带来的弊端。
原文链接:https://promlabs.com/blog/2025/07/17/why-i-recommend-native-prometheus-instrumentation-over-opentelemetry/