OpenTelemetry 整合 Prometheus?目前尚不美好

Julius Volz 2025-11-13 12:02:34

尽管 OpenTelemetry(简称 OTel)风头正劲,你可能会倾向于使用 OpenTelemetry 及其 SDK 来满足所有应用埋点需求。但如果是为了生成可在 Prometheus 中使用的指标,在完全采用 OTel 之前,你至少应该三思。因为这样做不仅可能会让你错失 Prometheus 作为监控系统所特有的部分核心功能,还会面临指标转换不畅、转义问题,以及其他效率低下和复杂棘手的情况。因此,若你希望获得最佳的 Prometheus 监控体验,我仍然建议使用 Prometheus 自身的原生埋点客户端库,而非 OTel SDK。接下来,让我们看看具体原因。

一、OpenTelemetry 与 Prometheus 的适用范围对比

首先,如果你对 OpenTelemetry 和 Prometheus 不是特别熟悉,这里先简要对比一下两个系统的适用范围:

简而言之:

  • OpenTelemetry 可处理三种信号类型(日志、指标和追踪数据),但它仅关注信号的生成(即埋点)以及将生成的信号传输到第三方后端系统。这种传输通常通过 OpenTelemetry 协议(OTLP)实现。
  • Prometheus 仅处理指标(不涉及日志或追踪数据),但它的功能不止于指标生成。作为一套完整的监控系统,Prometheus 还提供了主动采集和存储数据的解决方案,并支持通过查询实现仪表盘展示、告警触发等功能——查询操作通过 PromQL 查询语言完成。

两个系统的适用范围在指标生成和传输环节存在重叠,但在这一重叠部分,它们各自拥有独立的客户端库、传输协议、数据模型,且设计理念也有所不同。

二、将 OpenTelemetry 指标接入 Prometheus 的基本方式

虽然这两个系统是独立发展起来的,但如今已有多种方法可将 OpenTelemetry 指标发送到 Prometheus。你可以直接从应用程序向 Prometheus 服务器发送指标,但在 OTel 生态中,更常见的做法是先将所有数据发送到 OpenTelemetry Collector(一个独立服务,用于接收和聚合多个应用程序的可观测性数据),再由 Collector 将处理后的数据发送到 Prometheus 等后端系统:

OTel 到 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 时采用的方式。

三、不建议将 OpenTelemetry 与 Prometheus 搭配使用的原因

了解上述背景后,以下是我推荐使用原生 Prometheus 埋点而非 OpenTelemetry 的原因——尤其是当你主要关注指标且需在 Prometheus 中使用指标时。

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

Prometheus 是一套完整的监控系统,而监控系统不应只是被动接收随机传入指标的“容器”。要实现其核心功能,监控系统必须清楚以下三点:系统“应有的状态”、系统“当前的实际状态”,以及两者之间的差异。要做到这一点,它需要知晓基础设施中哪些应用进程、机器及其他监控目标当前应处于存在且健康的状态。

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

  • 服务发现:Prometheus 集成了多种服务发现机制,可自动识别当前应存在的监控目标。例如,在 Kubernetes 集群中,Prometheus 服务器可通过 Kubernetes API 订阅特定类型的所有 Pod、服务、入口资源等信息,且该列表会持续更新。若 Prometheus 不支持你所需的服务发现类型,你甚至可以自行构建。
  • 带内置健康检查的拉取式指标采集:通过主动从每个已发现的目标拉取指标并为其添加标签,Prometheus 能自动检测目标是否宕机或响应异常。

因此,Prometheus 始终能实时掌握应存在的目标列表,并且在每次拉取目标指标时,都会记录一个合成的 up 指标:根据拉取操作是否成功,该指标的样本值会设为 01

这种机制让为所有目标构建基础健康监控变得轻而易举——你可以轻松发现并针对宕机或不可达的目标触发告警,例如:

# 若“demo”任务中的任何目标宕机超过5分钟,则触发告警
alert: TargetDown
expr: up{job="demo"} == 0
for: 5m

与之相反,OTLP 是一种推送式协议,且未与 Prometheus 的服务发现功能集成。因此,Prometheus 无法判断“预期的指标源是否未上报数据”(或反之)。你将无法检测以下故障场景:

  • 本应运行的目标虽处于启动状态,但未发送指标;
  • 本应运行的目标已宕机/不存在,因此也未发送指标;
  • 网络故障或分区导致一个或多个目标无法发送指标;
  • 本不应由该 Prometheus 服务器监控的目标,却在向其发送指标(拉取模型可自动避免此问题)。

即便某个目标先上报了数据,之后又停止上报,你也无法判断它是被故意关闭,还是出现崩溃或指标上报功能故障。如此一来,你可能根本不知道自己有数百个服务进程已出现故障。

若想通过 OTLP 实现类似级别的目标健康监控,你需要从“能告知每个监控目标应有状态的可信源”生成并采集另一组指标,然后手动将这组指标与传入的 OTLP 数据关联(通过兼容的标识标签进行匹配)。这需要大量额外工作,因此许多人(无论是否知情)会完全忽略这个问题,甚至从未意识到自己缺乏完善的目标健康监控能力。

原因二:指标名称被修改或 PromQL 选择器变得复杂难用

在 Prometheus 中使用 OpenTelemetry 指标时,你会遇到字符集兼容性问题、不同的指标命名规范以及转义难题:

1. 字符集差异

与 OpenTelemetry 不同,Prometheus 不仅关注指标的生成和传输,还重视指标采集后的使用。Prometheus 通过 PromQL 查询语言实现指标使用——该语言可用于编写表达式,为仪表盘、告警规则、临时调试等场景提供支持。在设计查询语言时,自然需要避免标识符与运算符等其他语言结构发生语法冲突,理想状态是无需对标识符进行转义处理。这也是 Prometheus 历来对指标和标签名称的字符集限制较为严格的原因。

这与大多数编程语言的逻辑类似:编程语言通常不允许在标识符中使用 .-/ 等字符,因为它们可能与语言中的运算符冲突。因此,在 Prometheus 3.0 版本之前,标签名称仅允许包含字母、数字和下划线(正则表达式为 [a-zA-Z_][a-zA-Z0-9_]*),指标名称则额外允许包含冒号(正则表达式为 [a-zA-Z_:][a-zA-Z0-9_:]*)。

而 OpenTelemetry 允许在指标和属性(即 Prometheus 中的“标签”)名称中使用点号、短横线及其他类似运算符的字符——这让我怀疑,在设计 OpenTelemetry 时,是否曾将“在查询语言中使用其指标”作为核心考量因素。要知道,在 OpenTelemetry 标准化过程中,PromQL 早已是广泛应用的指标查询事实标准。

在实际使用中,Prometheus 3.0 版本之前,来自 OpenTelemetry 的指标和属性名称必须转换为 Prometheus 兼容格式:将不支持的字符替换为下划线。例如,my.metric.name 会变为 my_metric_name

2. 单位和类型后缀

同时,Prometheus 的指标命名规范要求指标名称以表示“指标单位”的后缀结尾;对于计数器类型指标(Counter),还需添加 _total 后缀(以区分表示当前计数的仪表盘类型指标 Gauge)。这两种规范都能提升指标的可读性,尤其在复杂的 PromQL 表达式场景中(例如表达式嵌入在已纳入代码管理的 YAML 文件中时)。

相反,OpenTelemetry 的命名规范仅将指标的单位和类型视为独立的元数据字段,不要求将其包含在指标名称中。若你在无法直接获取指标元数据的环境中使用 PromQL 表达式,这种设计会带来极大不便。

为解决此问题并确保指标名称的可读性,OTLP 到 Prometheus 的转换层默认会为 OpenTelemetry 指标名称添加 Prometheus 风格的单位和类型后缀。例如,OTel 指标 k8s.pod.cpu.time 转换后会变为 k8s_pod_cpu_time_seconds_total:既将点号替换为下划线,又明确了指标的单位(seconds,秒)和类型(total,计数器)。但遗憾的是,这种名称修改也可能让用户感到困惑。

3. Prometheus 3.0 中的 UTF-8 支持

从 3.0 版本开始,Prometheus 对指标和标签名称提供了完整的 UTF-8 支持,理论上可在 Prometheus 中存储未经修改的 OTel 指标名称。但这在编写 PromQL 选择器时会带来一些弊端:若名称使用了扩展字符集,你必须对其添加引号,并且要将这些带引号的指标名称放入时间序列选择器的花括号(即标签匹配器列表)内。否则,PromQL 无法区分指标名称和普通字符串字面量表达式。

例如,传统的选择器格式如下:

my_metric{my_label="value"}

若要支持扩展字符集(如下例中用点号替代下划线),则需改写为:

{"my.metric", "my.label"="value"}

这种选择器语法可读性差、编写繁琐,因此在决定突破 Prometheus 原有的标识符字符集限制前,务必考虑这一点。

从 Prometheus 3 版本的 UTF-8 支持开始,你可通过配置文件中的 otlp.translation_strategy 选项,设置 OTel 指标的转换策略:

  • UnderscoreEscapingWithSuffixes:将名称转换为传统字符集(使用下划线),与之前的默认行为一致;
  • NoUTF8EscapingWithSuffixes:保留所有原始字符,但仍添加计数器后缀和单位后缀以提升可读性;
  • NoTranslation:保留所有原始字符且不添加任何后缀(目前处于实验阶段,可能会被移除)。

是否保留原始字符集并接受上述复杂的选择器语法,取决于你的实际需求。但我强烈建议不要使用 NoTranslation 选项——它甚至会省略单位和类型后缀,这会让你后续使用指标时更难理解其含义。

然而,若你使用 Prometheus 原生的埋点客户端库并遵循其命名规范,就无需考虑这些字符集或命名差异问题,且指标和标签名称在“埋点端”和“使用端”会保持完全一致。

原因三:资源标签与目标标签——看似相似,实则不同?

Prometheus 和 OpenTelemetry 都倾向于为指标附加一组标签(OTel 中称为“属性”),用于描述指标来源信息(例如生成指标的应用或服务)。在 OpenTelemetry 中,这类标签称为“资源属性”(resource attributes);在 Prometheus 中,则称为“目标标签”(target labels)。

但两个系统对这些标签的使用逻辑存在差异:

  • Prometheus 目标标签:由 Prometheus 服务器根据目标的元数据(通常来自动态服务发现机制)附加到拉取的指标上。目标标签的数量通常较少,且主要用于“标识”(即仅包含识别目标所必需的信息,而非额外补充信息)。
  • OpenTelemetry 资源属性:由生成指标的应用程序自行定义,通常数量更多、内容更详细。它们常包含指标来源的额外上下文信息,例如 OTel SDK 的编程语言及版本,以及其他非关键元数据。

由于 Prometheus 目标标签数量少且以标识为核心,Prometheus 会默认将所有目标标签附加到从该目标拉取的每个指标上。这种设计很便捷,但在处理 OpenTelemetry 资源属性时会失效——若将大量描述性属性附加到每个摄入的时间序列上,会导致生成的指标存储成本升高,且更难使用。

因此,当通过 OTLP 接收指标时,Prometheus 默认仅将 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 服务器管理员,你也可以配置“从来源中选择哪些资源属性提升为每个摄入时间序列的标签”。但无论如何,你都需要思考这些问题:如何在查询中关联额外标签,或如何配置 Prometheus 服务器以选择合适的资源属性集提升为目标标签。

相反,若你使用 Prometheus 原生的埋点客户端库和拉取模型,通常只需由 Prometheus 管理员考虑通用的目标标签配置(或直接使用默认配置)即可,无需额外操作。

原因四:需额外配置 Prometheus 才能摄入指标

诚然,这不算严重缺陷,但仍需提醒:要让 Prometheus 能通过 OTLP 接收指标,你需要在 Prometheus 服务器上额外配置两项设置:

  1. 启用 OTLP 接收器:需设置命令行参数 --web.enable-otlp-receiver,让 Prometheus 服务器在 /api/v1/otlp/v1/metrics 路径上启动 OTLP 接收器。这是因为 Prometheus 本质上是一个拉取式监控系统,允许外部客户端向其推送指标可能存在安全风险,因此需要显式启用该功能。此外,你还需保护该端点,防止未授权来源的推送请求——而在 Prometheus 主动拉取指标的场景中,无需考虑此类问题。

  2. 配置时序数据库(TSDB)支持乱序数据:OTLP 不保证数据的有序传输(这与 Prometheus 自身“拉取数据并添加时间戳”的模型不同),因此你需要配置 Prometheus TSDB,允许在一段时间内(例如 30 分钟)接收乱序数据:

storage:
  tsdb:
    out_of_order_time_window: 30m

使用 OTLP 与 Prometheus 搭配时,可能还需要配置其他设置,具体可参考 Prometheus 官方文档。

原因五:OTel SDK 复杂度高且性能可能极差

另一个需要关注的问题是:与使用 Prometheus 原生客户端库相比,使用 OpenTelemetry SDK 会增加系统整体复杂度,且其实现效率可能较低。

OTel 是一个庞大复杂的系统,需处理多种任务(包括日志、追踪数据,以及在埋点库中实现指标的视图、聚合等复杂功能)。这意味着其 SDK 也体积庞大、逻辑复杂,你需要理解更多概念才能使用。相反,Prometheus 原生埋点库体积小巧,仅专注于高效生成 Prometheus 指标。

免责声明:以下示例主要基于 Go 语言的 Prometheus 和 OpenTelemetry SDK。我无法确定其他所有语言的情况,但有两点需要说明:

  1. Go 语言是云原生领域的热门语言,常用于对性能要求较高的重型服务器应用,因此具有代表性;
  2. 在“事件(如计数器递增)到最终 OTLP 输出”的流程中,OTel 本身存在更高的概念复杂度,因此其他语言的情况很可能与 Go 语言类似(至少趋势一致)。

我还针对 Java 语言的 Prometheus 和 OTel SDK 设计了简化版基准测试(尽管我不精通 Java),结果也类似:在多线程性能测试中,Prometheus SDK 的速度最高可达 OTel SDK 的 30 倍以上。

此处暂不讨论 OTel SDK 中更复杂的初始化和设置代码——这类操作属于一次性成本,在大型代码库中很快会被“摊薄”。但我想重点对比 OpenTelemetry 和 Prometheus Go SDK 中“计数器递增操作”的性能:在高并发多核服务器应用中,计数器递增可能每秒发生数百万次,是一种非常常见的操作。此时,你不会希望埋点框架占用过多 CPU 资源,因此 Prometheus Go SDK 针对此类操作进行了高度优化——对于一般的指标值更新操作,情况也是如此。

计数器递增性能对比

你可在 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 代码复杂度的主观感受

另一个主观对比点是:在 SDK 代码中查找“实际执行值递增的代码行”时,我在 VS Code 中从应用代码的 Inc() 调用开始,仅用了约 5 秒就找到了 Prometheus Go SDK 中的对应代码;但在 OpenTelemetry Go SDK 中,我花了约 15 分钟浏览各种间接调用和抽象逻辑后,最终放弃了查找。或许我最终能找到(例如通过分析运行中的代码或换个思路),但这一过程也足以体现两个 SDK 在复杂度上的巨大差异。

原因六:若追求开放标准,Prometheus 同样开放且成熟

最后,许多人采用 OpenTelemetry 的原因是希望为监控和可观测性需求选择一套开放标准。但实际上,Prometheus 同样是开源项目,且采用开放治理模式。它是一套成熟、经过实战检验且应用广泛的系统,拥有庞大的贡献者和用户社区。

许多云厂商已将 PromQL、Prometheus 远程写入格式及其他 Prometheus 接口作为“指标摄入和查询”的事实标准。此外,Prometheus 用于“从目标暴露指标”的文本格式非常简洁,极大降低了其使用门槛——在某些场景下,你甚至无需依赖库,只需几行 Shell 脚本就能实现该格式的基础功能。例如,以下脚本可启动一个简单的 Prometheus /metrics 端点,并包含一个指标:

#!/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 指标。

四、结论

若你使用 Prometheus 作为监控系统,我仍然强烈建议使用 Prometheus 原生的埋点客户端库和“拉取式监控模型”来监控你的服务。这样做能让你获得更可靠、更完整、更高效的监控体验,同时避免指标名称转换、转义问题以及“在查询中关联额外标签”等繁琐操作。

当然,你可能仍有其他理由选择 OpenTelemetry,但希望本文能让你清楚了解这种选择可能带来的弊端。

原文链接:https://promlabs.com/blog/2025/07/17/why-i-recommend-native-prometheus-instrumentation-over-opentelemetry/

快猫星云 联系方式 快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云
OpenSource
开源版
Flashcat
Flashcat
Flashduty
Flashduty