OpenTelemetry 整合 Prometheus?目前尚不美好

如果主要目标是在 Prometheus 中使用指标,直接用 OpenTelemetry SDK 埋点并不总是最佳选择。本文从目标健康监控、指标命名转换、资源标签、OTLP 接收配置、SDK 复杂度和性能开销等方面,解释为什么 Prometheus 原生客户端库仍然更适合 Prometheus 指标场景。

作者 Julius Volz

尽管 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 的指标传输主要有以下几种方式:

  1. Prometheus Exporter:允许 Prometheus 服务器以 Prometheus 原生指标格式从 Collector 拉取指标。这种方式不适用于大规模场景,因为它会将所有指标整合为一次大型 Prometheus 拉取操作,可能引发扩展性和可靠性问题。
  2. Prometheus Remote Write Exporter:通过 Prometheus 用于服务器间指标传输的原生远程写入格式,将指标推送到 Prometheus 服务器。这种方式更适合大规模场景。
  3. OTLP Exporter(最常用):使用 OpenTelemetry 的 OTLP 协议将指标推送到 Prometheus。这是替代 Prometheus 远程写入格式的不错选择,也是实际中大多数人将 OTel 数据发送到 Prometheus 时采用的方式。

三、为什么不建议为了 Prometheus 指标优先选择 OTel

以下原因主要针对“我主要关注指标,并且要在 Prometheus 中使用指标”的场景。

原因一:丢失 Prometheus 的目标健康监控能力

Prometheus 是一套完整的监控系统,而监控系统不应只是被动接收随机传入指标的“容器”。要实现核心功能,监控系统必须知道三件事:系统应有的状态、系统当前的实际状态,以及两者之间的差异。

Prometheus 的原生监控模型通过两个关键概念解决这个问题:

  • 服务发现:Prometheus 集成多种服务发现机制,可自动识别当前应存在的监控目标。例如在 Kubernetes 集群中,Prometheus 服务器可通过 Kubernetes API 订阅 Pod、服务、入口资源等信息,且该列表会持续更新。
  • 带内置健康检查的拉取式指标采集:Prometheus 主动从每个已发现目标拉取指标,并自动记录抓取是否成功。

因此,Prometheus 在每次拉取目标指标时都会记录一个合成的 up 指标:根据拉取操作是否成功,该指标样本值为 01

这种机制让目标健康监控很直接,例如:

# 若“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/

延伸路径

继续看解决方案和产品对比

如果你正在做监控、可观测性或故障定位相关选型,建议从解决方案和产品对比继续往下看。

快猫星云 联系方式 快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云