作者:来自 Elastic Dmitry Leontyev
两个新的 ES|QL 命令将时间序列发现变成一行查询: METRICS_INFO 和 TS_INFO 告诉你在你的数据中实际存在的指标和时间序列,而不仅仅是 mapping 所声明的内容。
通过 Elasticsearch 进行实践操作:深入了解我们在 Elasticsearch Labs 仓库中的示例 notebooks,开始免费的云试用,或者现在就在你的本地机器上试用 Elastic。
ES|QL 可以对你的时间序列数据进行聚合。它过去无法告诉你的 —— 直到现在 —— 是对于你关心的数据切片,哪些指标和时间序列实际上有数据。字段映射会显示曾经声明过的所有字段,但不会显示在特定环境、特定时间窗口或特定过滤条件下实际存在的指标。这个差距使得构建仪表板、接入不熟悉的数据流、验证查询以及进行数据质量调查变得更加困难。两个新的 ES|QL 处理命令, METRICS_INFO 和 TS_INFO,通过一行目录查询填补了这个差距。
为什么时间序列发现很重要
Elasticsearch 使用时间序列数据流( time series data streams - TSDS )来高效存储指标数据。基于完全列式存储,在 Elasticsearch 9.4 中存储在 TSDS 中的指标相比使用标准索引最多可减少 17 倍的存储空间。从 Elasticsearch 9.2 开始,我们还在 Elasticsearch 查询语言( ES|QL )中增加了时间序列支持,使其在查询存储在 TSDS 中的数据时成为一个完全支持的能力。
如果你在 Elasticsearch 中使用 TSDS,你已经熟悉这种模式:维度(dimensions)用于标识一个时间序列,指标(metrics)承载诸如 gauge 或 counter 之类的类型化数值,而 ES|QL 中的 TS 源命令支持诸如 RATE 和 AVG_OVER_TIME 这样的时间序列聚合函数。
这个处理流程无法告诉你的(但你同样经常需要知道的)是:对于你关心的数据切片,当前实际存在哪些指标和时间序列。字段映射会列出曾经声明过的所有字段;它们不会显示在特定集群、环境或时间窗口中当前正在被写入的数据。这种差距会在多种不同的工作流程中体现出来:
- 仪表板构建。指标和维度选择器应该反映当前集群在用户过滤条件下实际包含的内容,而不是所有曾经映射过的字段。否则,下拉菜单会被过时选项填满,面板会渲染为空。
- 接入不熟悉的 TSDS。新的集群、新的集成、客户的数据。快速列出正在被写入的指标及其类型、单位和适用维度,可以替代花费数小时查看映射和进行临时探测查询。
- 数据质量调查。映射漂移(同一指标在一个 backing index 中声明为 gauge,在另一个中声明为 counter)以及维度基数爆炸都可以在目录输出中立即显现。
- 查询验证。在运行昂贵的 TS ... | STATS 聚合之前,先确认你将使用的指标和维度在你的时间窗口中确实有数据。
Kibana 已经在内部使用这一能力。可观测性体验中的动态指标目录会将 METRICS_INFO 附加到用户当前的 TS 查询中,从而使 UI 只提供在当前过滤条件下真实存在的指标,而不是映射中的所有字段。
问题:映射是字段的清单,而不是时间序列
运维团队经常需要回答一些仅靠 mapping API 无法回答的问题:
- 在这个环境中、这个集群里、这个时间范围内,哪些指标实际上有数据?
- 这些指标的类型是什么,在构建或验证查询时适用哪些维度?
- 每个指标存在多少个不同的时间序列?
在此之前,回答这些问题意味着需要拼凑 mapping API、临时查询以及猜测。 METRICS_INFO 和 TS_INFO 将这些问题转变
`
1. TS k8s
2. | WHERE cluster == "prod"
3. | METRICS_INFO
4. | SORT metric_name
`AI写代码
| metric_name | data_stream | unit | metric_type | field_type | dimension_fields |
|---|---|---|---|---|---|
| network.eth0.rx | k8s | packets | gauge | integer | [cluster, pod, region] |
| network.eth0.tx | k8s | packets | gauge | integer | [cluster, pod, region] |
| network.total_bytes_in | k8s | bytes | counter | long | [cluster, pod, region] |
| network.total_cost | k8s | usd | counter | double | [cluster, pod, region] |
这些命令如何与 ES|QL 管道查询集成
这两个命令都是处理命令。一旦运行其中一个,表就会被替换:下游命令,例如 KEEP、 WHERE 或 STATS,作用于元数据行,而不是原始的时间序列文档。
需要记住的一些规则:
- 它们只在 TS 源之后生效。在 FROM 之后使用,或在没有前置 TS 源的情况下使用,会产生错误。
- 它们必须出现在针对 TS 返回的时间序列行运行的 STATS、 SORT 或 LIMIT 之前。例如, TS ... | STATS ... | METRICS_INFO 是无效的; TS ... | METRICS_INFO | STATS ... 是有效的,因为此时 STATS 是在元数据表上运行。
- 在 METRICS_INFO 或 TS_INFO 之后,你可以使用常规处理命令对元数据列进行过滤和聚合。
- 你可以在它们之前添加过滤条件,例如按 @timestamp 或维度进行收窄,从而使生成的元数据反映与你的查询上下文匹配的时间序列,而不是整个索引。
从概念上讲,这个管道看起来是这样的:
`TS + filters → METRICS_INFO or TS_INFO → KEEP / WHERE on metadata → STATS / SORT / LIMIT`AI写代码
这种设计意味着你可以将目录精确限定到你关心的数据切片,然后根据需要使用更多 ES|QL 命令对结果进行后处理。
如何在实践中使用 METRICS_INFO 和 TS_INFO
METRICS_INFO 检索你时间序列数据流中可用指标的信息,以及适用的维度和其他元数据,所有内容都限定在当前 TS 查询范围内。 TS_INFO 对单个时间序列执行相同操作。每一行表示一个指标以及标识一个时间序列的维度值。
每个命令为时间序列元数据提供不同的视图: METRICS_INFO 将结果折叠为每个不同指标签名一行:指标名称以及其声明方式(类型、单位、字段类型、适用的维度字段),这些信息是基于各个 backing index 观察得到的。 TS_INFO 则为每个指标和每个时间序列增加一行,其中包含一个 dimensions 列,用于保存每个时间序列的具体标签集合,格式为 JSON 对象(例如, {"job":"elasticsearch","instance":"instance_1"} )。
如果相同的逻辑指标名称在不同位置以不兼容的元数据出现,你会看到多行或多值单元格。这在你排查 mapping 漂移时是一个有用的信号。
这两个命令暴露相同的核心列;只有 TS_INFO 会额外包含 dimensions。
| Column | Meaning |
|---|---|
| metric_name | 指标名称。 |
| data_stream | 包含该指标的数据流;当跨多个数据流时为多值。 |
| unit | 在 mapping 中声明的单位(例如 bytes);当各个 backing index 定义不一致时为多值;也可能为 null。 |
| metric_type | 指标类型,例如 gauge 或 counter;当各个 backing index 定义不一致时为多值。 |
| field_type | Elasticsearch 字段类型(long、double 等);当各个 backing index 定义不一致时为多值。 |
| dimension_fields | 该指标的维度字段名称(多值):该指标所有时间序列中的维度键的并集。 |
| dimensions | 仅 TS_INFO 提供。以 JSON 编码的维度键/值对,用于标识一个时间序列。 |
从名称和类型的目录开始。最小可用的查询是 TS source、 METRICS_INFO 和一个 sort,使表格更易浏览:
`
1. TS k8s
2. | METRICS_INFO
3. | SORT metric_name
`AI写代码
| metric_name | data_stream | unit | metric_type | field_type | dimension_fields |
|---|---|---|---|---|---|
| network.eth0.rx | k8s | packets | gauge | integer | [cluster, pod, region] |
| network.eth0.tx | k8s | packets | gauge | integer | [cluster, pod, region] |
| network.total_bytes_in | k8s | bytes | counter | long | [cluster, pod, region] |
| network.total_cost | k8s | usd | counter | double | [cluster, pod, region] |
你可以像往常一样在 ES|QL 中对结果进行后处理。例如,你可以在聚合之前对元数据进行列裁剪或过滤:
`
1. TS k8s
2. | WHERE cluster == "prod" AND TRANGE(1d)
3. | METRICS_INFO
4. | KEEP metric_name, metric_type
5. | SORT metric_name
`AI写代码
| metric_name | metric_type |
|---|---|
| network.eth0.rx | gauge |
| network.eth0.tx | gauge |
| network.total_bytes_in | counter |
| network.total_cost | counter |
要查找有多少不同的指标名称匹配某个模式(而不是具体哪些时间序列),可以将 METRICS_INFO 与 STATS 结合使用:
`
1. TS k8s
2. | METRICS_INFO
3. | WHERE metric_name LIKE "network.total*"
4. | STATS matching_metrics = COUNT_DISTINCT(metric_name)
`AI写代码
| matching_metrics |
|---|
| 2 |
文档谓词(document predicates)在 catalog 命令之前可以先对时间序列进行收窄,使处理范围仅限于在当前时间窗口中真实存在的数据样本。最终列出的指标只包括那些在匹配数据中出现的指标,而不是所有曾经在 mapping 中声明过的字段:
`
1. TS k8s
2. | WHERE cluster == "prod" AND TRANGE(1d)
3. | METRICS_INFO
4. | SORT metric_name
`AI写代码
| metric_name | data_stream | unit | metric_type | field_type | dimension_fields |
|---|---|---|---|---|---|
| network.eth0.rx | k8s | packets | gauge | integer | [cluster, pod, region] |
| network.eth0.tx | k8s | packets | gauge | integer | [cluster, pod, region] |
| network.total_bytes_in | k8s | bytes | counter | long | [cluster, pod, region] |
| network.total_cost | k8s | usd | counter | double | [cluster, pod, region] |
运行同一个带范围限制的 pipeline,但把中间命令替换为 TS_INFO,这时问题从 “哪些指标匹配” 变成 “哪些时间序列标识匹配”。每一行对应一个指标以及一组维度值的组合;按 metric_name 和 dimensions 排序,使相关的时间序列聚合在一起:
`
1. TS k8s
2. | WHERE cluster == "prod" AND TRANGE(1d)
3. | TS_INFO
4. | SORT metric_name, dimensions
`AI写代码
| metric_name | data_stream | unit | metric_type | field_type | dimension_fields | dimensions |
|---|---|---|---|---|---|---|
| network.eth0.rx | k8s | packets | gauge | integer | [cluster, pod, region] | {"cluster":"prod","pod":"one","region":"[eu, us]"} |
| network.eth0.rx | k8s | packets | gauge | integer | [cluster, pod, region] | {"cluster":"prod","pod":"three","region":"[eu, us]"} |
| network.eth0.rx | k8s | packets | gauge | integer | [cluster, pod, region] | {"cluster":"prod","pod":"two","region":"[eu, us]"} |
| network.eth0.tx | k8s | packets | gauge | integer | [cluster, pod, region] | {"cluster":"prod","pod":"one","region":"[eu, us]"} |
| network.eth0.tx | k8s | packets | gauge | integer | [cluster, pod, region] | {"cluster":"prod","pod":"three","region":"[eu, us]"} |
| network.eth0.tx | k8s | packets | gauge | integer | [cluster, pod, region] | {"cluster":"prod","pod":"two","region":"[eu, us]"} |
| network.total_bytes_in | k8s | bytes | counter | long | [cluster, pod, region] | {"cluster":"prod","pod":"one","region":"[eu, us]"} |
| network.total_bytes_in | k8s | bytes | counter | long | [cluster, pod, region] | {"cluster":"prod","pod":"three","region":"[eu, us]"} |
| network.total_bytes_in | k8s | bytes | counter | long | [cluster, pod, region] | {"cluster":"prod","pod":"two","region":"[eu, us]"} |
| network.total_cost | k8s | usd | counter | double | [cluster, pod, region] | {"cluster":"prod","pod":"one","region":"[eu, us]"} |
| network.total_cost | k8s | usd | counter | double | [cluster, pod, region] | {"cluster":"prod","pod":"three","region":"[eu, us]"} |
| network.total_cost | k8s | usd | counter | double | [cluster, pod, region] | {"cluster":"prod","pod":"two","region":"[eu, us]"} |
这个额外的列可以用来推导指标的基数(cardinality)。由于 TS_INFO 的每一行对应一个给定指标的一个时间序列,因此可以通过 STATS 分组来统计每个指标下有多少个不同的时间序列:
`
1. TS k8s
2. | TS_INFO
3. | STATS series_count = COUNT(*) BY metric_name
4. | SORT metric_name
`AI写代码
| series_count | metric_name |
|---|---|
| 9 | network.eth0.rx |
| 9 | network.eth0.tx |
| 9 | network.total_bytes_in |
| 9 | network.total_cost |
选择它们的方式:当你只需要在过滤后的 TS 上下文中获取一个紧凑的指标名称与类型清单时,使用 METRICS_INFO。当你需要标签组合、以及每个指标的时间序列数量时,使用 TS_INFO。在实际使用中,可以先用 METRICS_INFO 快速浏览,然后在答案依赖“维度是什么”而不仅是“有哪些指标”时切换到 TS_INFO。
底层机制:这些命令如何执行
METRICS_INFO 和 TS_INFO 都运行在驱动任何 TS 查询的同一套分布式 ES|QL 执行体系中。除了标准能力(如分片级并行、Lucene 过滤下推、以及协调节点合并)之外,这些实现还特别优化,使成本随着匹配的时间序列数量而扩展,而不是随着文档数量扩展。下面是每一行输出是如何生成的:
-
TS 命令定义作用范围
TS 会将你的 data stream pattern 解析为 TSDS backing indices,并把在 catalog 命令之前的过滤条件(例如 @timestamp 时间范围或 WHERE 中的维度谓词)转换为 Lucene 查询,在所有可能匹配的分片上执行。时间窗口之外的分片会在前置阶段被剪枝,不会被访问。 -
每个分片逐个时间序列处理文档
TSDS 索引在物理上按 _tsid 排序,其次按 @timestamp(降序)。这个排序非常关键:同一时间序列的所有文档在磁盘上是连续存放的,因此分片在扫描时,只需要为每个新的 _tsid 保留它看到的第一条文档,其余可以跳过。这样可以为每个匹配过滤条件的时间序列生成一条代表性文档。 -
mapping 决定字段语义
backing index mapping 是字段元数据的真实来源:- 声明为 time_series_metric 的字段是 metric,mapping 中包含 metric_type、field_type,以及(如果存在)meta.unit。
-
synthetic source 填充真实维度与指标存在性
对于每个时间序列的代表性文档,分片只重建 mapping 所声明的 dimension 与 metric 路径的 _source 子集。TSDS 使用 synthetic _source,因此这些数据主要从 doc values 重建,而不需要存储完整 _source。由此分片可以得到两件事:- 该时间序列的维度键值对(用于 TS_INFO 的 dimensions,以及用于两个命令的 dimension_fields)
- 该时间序列在该 backing index 中实际存在数据的 metric 字段
-
TS 命令定义作用范围
分片不会把所有原始行发送到上游,而是先进行局部聚合,这也是 catalog 查询保持高性能的关键之一。 -
协调节点合并结果
每个 data node 会先合并本地分片结果,然后将结果发送到 coordinator,再进行一次全局 merge。 -
后续 pipeline 正常执行
catalog 命令之后的所有 ES|QL 操作(KEEP、WHERE、STATS、SORT、LIMIT)都会在 coordinator 上对这个“元数据表”继续执行,行为与其他 ES|QL 阶段一致。
整体效果是:catalog 查询只做足够的工作来找到每个时间序列的代表性文档,读取一个很小的重建 _source 片段,根据 mapping 对字段进行分类,并将结果折叠为少量元数据行。由于输出基数被限制在“匹配的时间序列数量(TS_INFO)”或“不同指标签名数量(METRICS_INFO)”,而不是文档数量,这些命令即使在长时间范围和高吞吐数据流上也能保持低延迟。
在一个完整 TSDB benchmark 数据集上(未加时间范围过滤,1.84B 文档 / 140 万时间序列 / 2.77TB 未压缩),在单节点 Elasticsearch(AWS c8gd.8xlarge,24 cores,24 GiB heap,NVMe SSD,3 primary shards,force-merged)上运行 METRICS_INFO 大约需要 4 秒。
超越临时查询
这些命令同样支持 Kibana 内部的产品化工作流。UI 会在用户的 TS 查询中追加 METRICS_INFO(当查询本身没有包含 STATS 时),以构建一个与用户过滤条件一致的指标目录,而不是仅仅依赖 mapping。
这些新命令也是我们正在为 Elasticsearch 增加的 Prometheus 兼容元数据 API 的基础,Prometheus 生态工具将可以直接使用它们。后续我们会发布专门的博客深入说明这一部分。
数据质量
在 METRICS_INFO 输出中出现 multi-valued 的 unit、 metric_type 或 field_type,意味着不同 backing indices 对同一指标的定义并不一致,这是一个简洁的告警信号。TS_INFO 则更容易看出基数(cardinality)的爆炸来源:是少数几个指标导致的,还是维度基数导致的,从而帮助你在告警和聚合中进行合理处理。例如,通过按 series count 对指标排序,可以一眼发现异常值:
`
1. TS k8s
2. | TS_INFO
3. | STATS series_count = COUNT(*) BY metric_name
4. | SORT series_count DESC
`AI写代码
| series_count | metric_name |
|---|---|
| 12000 | network.eth0.rx |
| 9 | network.eth0.tx |
| 9 | network.total_bytes_in |
| 9 | network.total_cost |
当单个指标远远高于其他指标时,例如上面的 network.eth0.rx,说明基数增长集中在少数几个指标上,这时检查该指标的 dimensions 可以定位到底是哪个标签在增长。相反,如果各个指标的计数都比较接近,则通常意味着是共享维度基数在增加,例如新引入的 pod 或 instance 值传播到了所有时间序列中。
可用性
METRICS_INFO 和 TS_INFO 已在 Elastic Cloud Serverless 中正式发布,并且从 Elasticsearch 9.4.0 版本开始在 basic 版本中提供支持。
关于命令页面(语法、限制和示例),请参考 METRICS_INFO 和 TS_INFO。
关于 TSDS 以及 TS 命令本身的背景信息,可以从官方文档中的 time series data streams 和 TS source command 开始阅读。
这篇内容对你有帮助吗?