跳过有状态的 OTel Collector:Elasticsearch 9.5 原生存储两种指标时间类型

作者:来自 Elastic Jonas Kunz

在同一个 metric 名称下摄入 cumulative 和 delta 两种 OpenTelemetry 指标,同时 ES|QL 和 PromQL 查询会自动按每个时间序列识别指标时间类型,无需新增语法或转换 pipeline。

Elasticsearch 9.5 原生支持存储 cumulative 和 delta 两种 OpenTelemetry(OTel)counter 和 histogram,即使同一个 metric 名称下混合使用这两种类型也没有问题。你可以通过 OpenTelemetry Protocol(OTLP)摄入指标,Elasticsearch 会自动保留时间类型(temporality)元数据。ES|QL TSPromQL 查询会按每个时间序列自动识别时间类型,并正确解释数据,无需新增语法、修改 OTel SDK 配置,也无需使用有状态的 OTel Collector 进行转换。现有查询和 downsample 后的数据也会继续按预期工作。

什么是 OpenTelemetry 中的指标时间类型?

指标存储通常接收客户端预聚合后的指标。例如,如果应用程序记录请求响应时间,它不会将每个请求的响应时间作为单独的数据点发送到指标后端。相反,应用程序(更准确地说,是 OTel SDK)会先将这些原始响应时间聚合成 counter 或 histogram。然后,这些预聚合后的值会按照固定的时间间隔导出,从而大幅减少数据点的数量。

**时间类型(temporality)**描述的就是这种预聚合值随时间如何表示。它有两种模型:**cumulative(累计)**和 delta(增量)

OTel 指标中的累计时间类型(Cumulative temporality)

采用 _cumulative(累计)时间类型_时,每个数据点表示从进程启动以来,指标值累计发生的总变化量。数值会单调递增,但可能偶尔重置为 0,例如进程重启时。

以一个用于跟踪 Java 虚拟机(JVM)消耗的总 CPU 时间的 counter 为例:

时间戳 含义
10:01 12.4s 从启动以来累计消耗的 CPU 时间为 12.4s
10:02 13.1s 从启动以来累计消耗的 CPU 时间为 13.1s
10:03 13.9s 从启动以来累计消耗的 CPU 时间为 13.9s

要计算 10:01 到 10:02 之间的变化率,我们可以做减法:13.1 - 12.4 = 0.7s,表示该时间段内消耗了 0.7s 的 CPU 时间。再除以该时间段的时长,就可以得到 rate

这是 Prometheus 和 OTel 中 counter 默认使用的时间类型。

OTel 指标中的增量时间类型(Delta temporality)

采用 _delta(增量)时间类型_时,每个数据点表示自上一次测量以来发生的变化量。各个数据点彼此独立。换句话说,每次导出之后,OTel SDK 都会将所有 series 的值重置。

上面 cumulative 示例中的相同原始观测数据,在使用 delta 时间类型时会如下所示。

时间戳 含义
10:01 0.5s 该时间段内消耗的 CPU 时间为 0.5s
10:02 0.7s 该时间段内消耗的 CPU 时间为 0.7s
10:03 0.8s 该时间段内消耗的 CPU 时间为 0.8s

要计算 rate 或 increase,可以直接使用该值,无需进行减法。

cumulative 和 delta OpenTelemetry 指标的权衡

两种时间类型都有各自的实际权衡:

  • **抗数据丢失能力:**Cumulative counter 是自描述的:即使某次导出丢失,下一个数据点仍然能够提供正确的累计总量。Delta 值是增量的,因此如果某个数据点丢失,相应的增量也会丢失。

  • **指标生产端的内存占用:**对于 cumulative 时间类型,OTel SDK 需要在内存中为每个 series 保存状态。对于 delta 时间类型,内存占用要低得多。此时,SDK 只需要跟踪自上次导出以来发生变化的 counter 或 histogram。如果存在大量 counter 或 histogram,而其中许多指标在每个周期内都没有增长,这种差异可能会非常明显。

  • **跨重启聚合:**Cumulative counter 需要进行 reset 检测,这在某些边缘情况下可能会失败。如果指标值降低,就会被检测为 reset,我们会认为应用程序发生了重启,并且 counter 又从 0 开始。但如果重启后首次上报的 counter 值高于重启前的值,就可能检测不到 reset。具体来说:

    • 服务消耗了 1 秒的 CPU 时间,然后重启。

    • 重启后,服务执行了一项 CPU 密集型任务,在指标再次导出之前消耗了 2 秒的 CPU 时间。

    • 指标后端看到的指标值只是先为 1,然后变成 2。由于它从未观察到数值下降,因此无法检测到 reset。

Delta 值不存在这个问题,因为每个值都是独立的。

如果你使用的是 histogram,这些权衡的影响会更加明显:

权衡项 Cumulative(累计) Delta(增量)
Histogram 大小 Bucket 会在多次导出之间持续累计,占用更多存储空间 Bucket 在每次导出时重置,因此生成更小的 histogram
最小值 / 最大值准确性 对于自定义时间范围,需要从 bucket 中进行近似计算(记录的值表示自进程启动以来的极值) 每次导出的最小值和最大值都是精确的
查询性能 更快:只需要时间范围内的第一个和最后一个值,以及 reset 信息 更慢:必须合并查询时间范围内的所有 histogram

OpenTelemetry 同时支持这两种模型,并允许你通过 OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE 环境变量,在每个 SDK 中选择时间类型。

为什么原生支持时间类型可以消除 OTel Collector 的变通方案

Prometheus 和大多数其他 metrics 后端只能选择一种时间类型:所有指标必须是 cumulative 或 delta。Elasticsearch 之前也采用这种模式,原生存储 cumulative counter 和 delta histogram,而其他情况则需要使用变通方案。Delta counter 会以 gauge 的形式存储,虽然可以正常工作,但在 rate 查询中不具备原生 counter 语义。而 cumulative histogram 则不受支持。

一种解决不支持的时间类型的变通方法,是配置指标生产端(例如 OTel SDK),让它们生成后端所支持时间类型的数据。在大规模部署环境中,这可能是一项非常具有挑战性的工作。而且有时甚至无法做到这一点,例如你从第三方服务接收 OTLP 指标时。

另一种变通方案是在摄入之前转换时间类型。在 OTel Collector 中,通常可以使用 cumulative-to-delta processor,但它明确标注了关于 **有状态(statefulness)**的警告。转换本身具有状态依赖,需要将同一个 metric series 按顺序发送到同一个 Collector,并且在 Collector 重启后保留状态。实际上这种方式可以正常工作,但在大规模环境中,会带来许多部署上的麻烦。

使用 Elasticsearch 9.5,你可以完全跳过转换 pipeline。Elasticsearch 原生支持使用这两种时间类型存储和查询指标数据。无需进行任何有状态转换,也无需对 OTel SDK 进行显式配置。

Demo:并行摄入 cumulative 和 delta OTel 指标

为了演示时间类型支持,我们将复用之前 OTel histogram metrics ES|QL 博客 中使用的 demo 环境:一个使用 OTel Java agent 进行 instrument 的 Java Renaissance benchmark。

这次的不同之处在于:我们运行两个 benchmark 实例,每个实例配置不同的时间类型:

  • renaissance-delta:使用 delta 时间类型导出指标。

  • renaissance-cumulative:使用 cumulative 时间类型导出指标。

两个实例都使用相同的 service name renaissance 上报相同的指标,但使用不同的 service.instance.id 值。下面是 docker-compose.yml 中的相关部分,该文件位于 配套代码 中:

yaml 复制代码
`

1.  renaissance-delta:
2.    environment:
3.      OTEL_SERVICE_NAME: renaissance
4.      OTEL_RESOURCE_ATTRIBUTES: "service.instance.id=delta-instance"
5.      OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: delta
6.      OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: BASE2_EXPONENTIAL_BUCKET_HISTOGRAM

8.  renaissance-cumulative:
9.    environment:
10.      OTEL_SERVICE_NAME: renaissance
11.      OTEL_RESOURCE_ATTRIBUTES: "service.instance.id=cumulative-instance"
12.      OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: cumulative
13.      OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: BASE2_EXPONENTIAL_BUCKET_HISTOGRAM

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

如果你想自己运行这个 demo,还需要填写 托管 OTLP endpoint URL 和对应的 API key:

arduino 复制代码
`

1.  OTEL_EXPORTER_OTLP_ENDPOINT: https://<cluster-endpoint>
2.  OTEL_EXPORTER_OTLP_HEADERS: "Authorization=ApiKey <base64 api key>"

`AI写代码

使用 docker compose up --build 启动 demo 后,两个实例都会开始向 Elasticsearch 上报指标。

使用 ES|QL 和 PromQL 查询 OTel counter 指标

让我们查询两个实例的 jvm.cpu.time 前几个原始数据点,看看不同时间类型的实际表现:

vbnet 复制代码
``

1.  After starting the demo with `docker compose up --build`, both instances will start reporting metrics to Elasticsearch.
2.  Querying OTel counter metrics with ES|QL and PromQL
3.  Let's query the first few raw data points of `jvm.cpu.time` for both instances to see the different temporalities in action:

``AI写代码

这样我们就可以看到每个 service instance 的前五个数据点:

这个 benchmark 以几乎恒定的速率消耗 CPU。通过 delta 时间类型的数据可以直接看出这一点:每次导出之间的值基本保持不变。相比之下,cumulative 时间类型的值会随着时间增长,因为它们表示 benchmark 实例累计消耗的 CPU 时间。

现在,让我们看看如何使用 PromQL 正确查询这个指标:

python 复制代码
`PROMQL sum by (service.instance.id) (rate(jvm.cpu.time))`AI写代码

截图显示,两个 benchmark 实例消耗的 CPU 核数都基本保持在 1 到 1.2 之间,但存在一定波动。这个查询之所以能够正常工作,是因为我们让 rate 的实现支持不同的时间类型:每个时间序列(在我们的示例中就是每个 service instance)都会将时间类型作为一个 metric 维度进行存储。rate 的实现会读取这个维度,并据此正确解释数据:对于 delta 时间类型,会对各个值进行求和;对于 cumulative 时间类型,则会计算差值。所有这些都会在后台自动完成,无需对现有查询做任何修改。

我们已经让 rateincreaseirate 都以这种方式工作。在 ES|QL TS 查询中使用这些函数时也是如此:

scss 复制代码
`

1.  TS metrics-*
2.  | STATS SUM(RATE(jvm.cpu.time)) BY TBUCKET(100), service.instance.id

`AI写代码

由于 Elasticsearch 会将时间类型作为一个维度进行跟踪,因此同一个 metric 可以包含多个具有不同时间类型的 series,就像 demo 中的使用场景一样。跨 series 进行聚合也可以按预期工作,因为在执行聚合时,rateincreaseirate 已经负责对数据进行标准化:

PromQL 查询:聚合两个实例的总 CPU 时间

跨时间类型查询 OTel histogram 指标

指标时间类型对 histogram 的作用方式与 counter 相同:histogram 的 bucket 本质上是一组 counter,每个 counter 都用于跟踪特定范围内的值。

与我们的 histogram demo 一样,我们使用 exponential histogram,其中 bucket 边界会自动调整,以尽可能降低相对误差。

由于两者具有这种相似性,histogram 同样可以使用 cumulative 或 delta 时间类型。区别在于:每个 bucket 的 counter 要么在每次指标导出后重置,要么累计计数会在多次导出之间持续累加。

让我们查询 benchmark 实例主要 garbage collection(GC)持续时间的中位数。这个指标是一个 histogram:

ini 复制代码
`PROMQL histogram_quantile(0.5,  sum by (service.instance.id) (increase(jvm.gc.duration{jvm.gc.action=~".*major.*"})))`AI写代码

或者对应的 ES|QL 查询:

markdown 复制代码
`

1.  TS metrics-*
2.  | WHERE jvm.gc.action LIKE "*major*"
3.  | STATS MEDIAN(jvm.gc.duration) BY TBUCKET(100), service.instance.id

`AI写代码

同样,这两个查询都会自动加载每个 series 的时间类型,并据此正确解释 histogram。在 PromQL 中,这是通过 increase 函数实现的。需要注意的是,在 ES|QL 中,你不需要对 histogram 显式调用 increase。当你使用 PERCENTILEMEDIANAVG 等聚合函数时,TS 命令会自动处理基于时间类型的 histogram 合并。

Elasticsearch 如何在 TSDB 中存储指标时间类型

Elasticsearch 的时间序列数据库(TSDB)会在每个文档中使用一个专用的维度字段来存储指标时间类型。index.time_series.temporality_field 索引设置允许你指定哪个字段携带时间类型信息。该字段必须是 keyword 类型,并设置 time_series_dimension: true,其允许的值为 "delta""cumulative"

一旦时间序列索引中存在这一设置,ES|QL 和 PromQL 在执行依赖时间类型的聚合时,就会读取对应字段。如果该字段不存在,或者文档中没有该字段的值,我们会根据对应指标的类型使用默认值:counter 默认为 cumulative,而 histogram 默认为 delta。这与历史行为保持一致,并确保现有查询和现有数据无需任何修改即可继续正常工作。

当你通过 OTLP endpoint 摄入指标时,Elasticsearch 会自动为每个文档添加 temporality 维度字段,并根据 OTLP AggregationTemporality 元数据填充该字段。对于自定义摄入方式(既不是 OTLP,也不是 Prometheus remote write),你需要手动设置 index.time_series.temporality_field,并填充时间类型维度。

在 downsampling 过程中也会保留时间类型:由于它是一个维度,因此会被自动保留,并用于计算聚合值。

在 Elasticsearch 中开始使用混合时间类型的 OTel 指标

使用 Elasticsearch 9.5 后,cumulative 和 delta 不再是你必须在一开始就做出正确选择的决定。你可以并行摄入这两种时间类型的数据,即使它们使用相同的 metric 名称,然后让 ES|QL 和 PromQL 处理剩下的工作。你可以在两种时间类型之间切换,而无需修改查询。

更多详情,请参阅 指标时间类型文档

原文:Ingesting cumulative and delta OTel metrics in Elasticsearch | Elasticsearch Labs

相关推荐
C++、Java和Python的菜鸟10 小时前
第2章 项目前置课-代码版本控制Git
大数据·elasticsearch·搜索引擎
Elasticsearch17 小时前
什么是混合搜索?
elasticsearch
Elasticsearch1 天前
从点击流中提升搜索相关性:使用 Learn To Rank 和 OpenTelemetry 行为信号
elasticsearch
倒流时光三十年1 天前
第六阶段 57 · ES|QL 与 SQL API(用 SQL 查 ES)
数据库·sql·elasticsearch
guwentian1 天前
Git Worktree 实战:用并行多 Agent 把开发提速 N 倍
大数据·git·elasticsearch·wroktree
Elastic 中国社区官方博客2 天前
Elasticsearch:使用 AI Agent 来创建 workflow
大数据·运维·人工智能·elasticsearch·搜索引擎·自动化·全文检索
阿里云大数据AI技术2 天前
AI Search+ES 9.4.X最佳实践:“更快、更准、更安全的企业级搜索引擎”"为AI Agent提供坚实底座”
人工智能·elasticsearch·agent
Elasticsearch2 天前
Elastic 社区通讯 — 2026 年 8 月
elasticsearch
lsh曙光2 天前
ES数据备份与恢复
大数据·elasticsearch·搜索引擎