不要让指标白白浪费:使用 ES|QL TS command 来查询它们
作者来自 Elastic Felix Barnsteiner重新校准你对 time series 查询的心智模型了解为什么 FROM 会在 metrics 上产生不准确结果TS 如何修复这一点以及何时使用各个 command。开始动手实践 Elasticsearch深入查看 Elasticsearch Labs 仓库中的示例 notebooks启动免费云试用或立即在本地机器上尝试 Elastic。如果你使用 ES|QL 来处理日志和 traces那么FROM可能已经非常熟悉但在 metrics 上它可能会返回数值错误的结果。类似FROM metrics-* | STATS SUM(request_count)这样的查询会把所有主机上每个 sample 的累计计数值相加。结果会不断增长但它既不是 rate也不是 count更不是任何有意义的指标。TS通过先将数据点按时间序列分组来解决这个问题然后提供RATE、AVG_OVER_TIME和LAST_OVER_TIME等函数在每条时间序列内部进行计算。想了解 metrics analytics 在 ES|QL 和 Discover 中的整体视角可以参考 Elastic Observability 中的《轻松探索和分析指标》。本文重点讲机制层面。下面是五点心智模型FROM将每条文档视为独立行。这对事件数据是正确的但 metrics 聚合通常需要知道每一行属于哪条时间序列。TS增加了时间序列上下文它在任何聚合发生之前先按 time series 对数据点进行分组和聚合并支持RATE、AVG_OVER_TIME和LAST_OVER_TIME等函数。一个TS | STATS查询通常包含两层聚合。内层会在每条 time series 内对样本进行归约外层再对这些每序列的结果进行分组和合并。默认的内层聚合是LAST_OVER_TIME因此TS | STATS AVG(cpu_usage)和FROM metrics | STATS AVG(cpu_usage)可能会返回不同结果。使用TS来查询 time series data stream (TSDS)使用FROM来查询事件数据和原始文档检索。什么是 time series时间序列时间序列是一个由时间戳值组成的序列这些数据点由指标名称以及一组唯一的维度值共同标识。例如来自数据中心 dc1 中主机 h1 每 30 秒上报一次的 request_count就是一条时间序列。同一个指标在 dc1 中的主机 h2 上则是另一条不同的时间序列。在 time series data stream 中每条 metric 文档都会携带一个内部的_tsid字段用于唯一标识一条时间序列。共享同一个_tsid的样本属于同一条时间序列并且会按时间戳排序、顺序存储。这种存储布局使得按时间序列进行高效聚合成为可能。这也解释了为什么TS只能用于 time series data streams。其他索引模式没有“时间序列”的概念因此 TS 所依赖的 per-series 操作无法关联到对应的标识。FROM不支持这些操作这也是下一节要解释的内容。为什么FROM会在 metrics 上 “留下分析空间leaves metrics on the table”考虑一个名为request_count的计数器指标它每 30 秒从三个主机采集一次。计数器是一种累计型指标每个样本表示自进程开始以来的累计总数。对于request_count来说数值 1,000 的含义是 “这个 time series 到目前为止已经观测到 1,000 次请求”而不是 “自上一个样本以来发生了 1,000 次请求”。当进程重启时计数器会重置为 0因此在 1,004 之后突然出现 4 的样本是一次新的计数起点而不是负流量。ES|QL 的RATE函数会在 time series 内计算每秒变化率并且能平滑处理重置情况。现在你想计算所有主机的总请求率并按 5 分钟分桶。如果你习惯在 event 数据上写 ES|QL你可能会这样开始查询FROM metrics-* | WHERE TRANGE(1h) | STATS SUM(request_count) BY BUCKET(timestamp, 5m)最开始生成的图看起来是合理的一条随时间上升的曲线。但 y 轴上的数值其实是把每个时间桶里所有累计计数器值加总后的结果。每个主机都会在每个采样点贡献自己的运行总数并在每个 bucket 中重复计入。由于查询对这些累计值使用了SUM结果既不是 rate也不是某个时间桶内的请求数即使应用已经停止接收请求它仍然会持续增长。request_count是单调递增计数器因此它的原始值表示 “这个主机历史上累计发生了多少请求”而不是 “这个时间桶内发生了多少请求”。正确的计算方式是“先在每个主机的 time series 内计算每秒增长量然后再跨主机求和”。但FROM无法直接表达这种操作。它可以按字段分组行但没有“同一条时间序列随时间变化”的概念也无法在每条 time series 内计算 counter 的变化。它也不能使用类似RATE(request_count, 5m)这样的滑动时间窗口函数我们稍后会再讨论这一点。TS正是为了解决这个问题而引入的它提供了一种更简洁的语法来表达时间序列聚合TS metrics-* | WHERE TRANGE(1h) | STATS SUM(RATE(request_count)) BY TBUCKET(5m)RATE(request_count)在每条 time series 内运行生成一个按秒计算的增长率并且能正确处理 counter reset重置。然后SUM再把各个主机的 rate 加总起来。两阶段聚合inner 和 outer每一个TS | STATS查询都包含两个明确的聚合阶段。我们用一个更具体的查询来说明计算每个 data center 的请求速率。TS metrics-* | WHERE TRANGE(1h) | STATS SUM(RATE(request_count)) BY datacenter, TBUCKET(5m)下方的图展示了 TS 如何执行这个查询。它首先在每条 time series 内部对样本进行归约然后再按 data center 和 time bucket 对这些每条 time series 的结果进行分组与合并最终生成每个时间桶、每个数据中心的结果。这些阶段分别是内部阶段time series 内部在每一条 time series 内分别执行。它会在每个时间桶内把多个timestampvalue数据点压缩成每条 time series 在该 bucket 的单一值方法是应用内部聚合函数例如上面例子中的RATE。相关函数包括RATE、AVG_OVER_TIME、MAX_OVER_TIME、LAST_OVER_TIME、STDDEV_OVER_TIME等等。完整列表可以参考 time series aggregation functions 页面。外部阶段跨 time series 的 “分组” 阶段把每条 time series 的结果再聚合成每个分组、每个时间桶的单一值。使用的是标准 ES|QL 聚合函数例如SUM、AVG、MAX、MIN、percentiles 等。在SUM(RATE(request_count)) BY datacenter, TBUCKET(5m)中RATE(request_count)是内部聚合它在每条 time series 上运行。SUM(...)是外部聚合它在同一个 datacenter 和时间桶内合并多条 time series。TBUCKET(5m)定义时间桶边界等价于BUCKET(timestamp, 5m)。外部聚合是可选的。如果你只需要每条 time series 的结果可以直接使用 time series 聚合函数TS metrics-* | WHERE TRANGE(1h) | STATS request_rate RATE(request_count) BY TBUCKET(5m)这个查询会保留每条 time series 在每个 bucket 内的 per-series rate而不是再用SUM、AVG或其他聚合把不同 time series 混合在一起。默认的内部聚合LAST_OVER_TIMETS必须先在每条 time series 内部对原始样本进行归约然后才能执行外部聚合。这意味着在TS | STATS聚合中每一个 metric 字段都需要一个内部聚合即使查询里没有显式写出来。考虑一个名为cpu_usage的指标它是一个 gauge用于记录某个时间点的值可以上下波动。比如一个样本值 0.42 表示 “该主机在这个时刻 CPU 使用率为 42%”。对于 gauge 来说一个时间桶内 “最合理的值” 就是最新的样本。这正是 ES|QL 自动帮你补上的部分。如果你写TS metrics | STATS AVG(cpu_usage) BY host.name, TBUCKET(5m)那么隐式的内部聚合就是LAST_OVER_TIME(cpu_usage)整个查询等价于TS metrics | WHERE TRANGE(1h) | STATS AVG(LAST_OVER_TIME(cpu_usage)) BY host.name, TBUCKET(5m)对于每一条 time seriesLAST_OVER_TIME会在每个 bucket 中选取最新的一条样本。然后AVG再在不同 time series 之间做平均。这也是为什么看起来相同的查询在FROM和TS上可能会返回不同结果。FROM会对每一条 document 直接做平均而TS是先在每条 time series 内把数据归约成一个 bucket 值再进行聚合。如果你的主机上报频率不同这种差异会被放大。例如在一个 5 分钟的 bucket 中一个主机每秒上报一次300 条 document而另一个主机每两分钟上报一次23 条 document。使用FROM | STATS AVG(cpu_usage)时上报频率高的主机会对平均值产生更大影响而在TS中每条 time series 先被压缩成一个 bucket 值因此外层平均时每个主机只贡献一个值。如果你想要的是 “bucket 内的平均值”而不是 “最新值”可以显式指定内部聚合TS metrics-* | WHERE TRANGE(1h) | STATS AVG(AVG_OVER_TIME(cpu_usage)) BY host.name, TBUCKET(5m)AVG_OVER_TIME会在每条 time series 内对所有 CPU 使用率样本求平均。然后外层的AVG再对这些每条 time series 的结果在相同 host 之间求平均。这样得到的结果是先在 time series 内做 “按采样点加权” 的平均再在 time series 之间做“等权”的平均。适用于你关心 bucket 内整体行为而不仅仅是最终状态的情况。峰值和谷值也是同样的规则。对于 CPU 峰值图表应使用MAX(MAX_OVER_TIME(cpu_usage))而不是简单的MAX(cpu_usage)。内层MAX_OVER_TIME先在每条 time series 内找到峰值外层MAX再在所有匹配的 time series 中选出最大值。计数器的情况则相反。它的样本值本身是累计总量因此单独看最新值通常没有意义。对于 counter几乎总是应该使用RATE计算每秒增长率或INCREASE计算 bucket 内总增量作为内部聚合。而退回默认的LAST_OVER_TIME就会得到最新的累计值这正是前面FROM查询踩到的陷阱。关键是内部函数必须有意识地选择外部函数反而是更简单的一层。什么时候用 TS什么时候用 FROM一个实用的经验法则当你在 time series data stream 上做指标聚合时使用TS。它是为这种数据设计的查询入口会默认应用 per-series 语义。当你处理事件数据logs、traces、audit records、transactions时使用FROM。每一行都是独立事件不存在 time series 上下文。FROM在 TSDS 索引上仍然可以工作有时也有用例如你只想查看原始 metric 文档而不做 time series 聚合。但对于 dashboards、alerting 以及任何图表分析场景TS才是正确的默认选择。如果你需要先发现有哪些 metrics 或 time series 存在可以在TS之后、STATS之前使用METRICS_INFO或TS_INFO。更多内容可参考《ES|QL METRICS_INFO 和 TS_INFO对 time series 数据进行目录化分析》的深入讲解。使用 ES|QL 对 TS 结果进行后处理第一个STATS命令是 time series 处理与普通 ES|QL 处理之间的分界点。在第一个STATS之前TS 需要保持数据按_tsid分组因此不允许使用会改变行顺序或数据形状的命令。在第一个STATS之后输出就变成了标准的 ES|QL 表格。你可以对其进行排序、限制结果、关联 lookup 数据、做 enrich或者计算派生列。例如下面这个查询会先计算每个主机在每个 bucket 的平均 CPU然后找出每个主机在所有 bucket 中的最大平均值并返回比例TS metrics-* | WHERE TRANGE(1h) | STATS avg_cpu AVG(AVG_OVER_TIME(cpu_usage)) BY host.name, time_bucket TBUCKET(5m) | INLINE STATS max_avg_cpu MAX(avg_cpu) BY host.name | EVAL cpu_ratio avg_cpu / max_avg_cpu | KEEP host.name, time_bucket, cpu_ratio | SORT host.name, time_bucket DESC内层聚合的滑动窗口时间序列聚合函数支持第二个参数用于内层阶段的窗口大小。TS metrics-* | WHERE TRANGE(1h) | STATS AVG(RATE(app.requests, 5m)) BY TBUCKET(1m)这会在一个 5 分钟滑动窗口上计算 rate但每 1 分钟输出一个值。当你希望在较细的 bucket 粒度下得到更平滑的图表时这非常有用。这个 window 是 ES|QL 对 PromQL range vector selector 的对应物RATE(app.requests, 5m)的作用等价于rate(app_requests[5m])。需要注意的几个坑TS 中有一些行为可能看起来出乎意料尤其是从基于事件的FROM心智模型切换过来时。这些都不是 bug而是 per-series 模型的直接结果。下面是需要注意的点。COUNT(*)会被拒绝比如你想统计每个 service 在每个 bucket 收集了多少样本。在FROM的思维里会写COUNT(*)但 TS 会直接拒绝在按 time series 分组之后已经不存在“普通行”的概念因此 row count 没有明确语义。你需要明确你想统计的是什么每个 service 的样本数量STATS samples SUM(COUNT_OVER_TIME(cpu_usage)) BY service.name, TBUCKET(5m)内层COUNT_OVER_TIME统计每条 time series 的样本数外层SUM再跨 time series 相加。每个 service 报告的不同主机数STATS hosts COUNT_DISTINCT(host.name) BY service.name, TBUCKET(5m)这是跨 time series 的唯一值计数。在STATS之前不能 sort、limit、lookup join 或 enrichTS metrics | SORT timestamp | STATS ...会失败。因为必须先按_tsid完成分组否则数据语义不成立。如果需要缩小范围应使用WHERE。在第一个STATS之后结果变回标准 ES|QL就可以继续做任何 pipeline 操作如前文所述。Gauge 和 counter 的映射问题time series 函数对字段 mapping 中的 metric 类型是敏感的。RATE只适用于 counter*_OVER_TIME用于 gauge。如果手动构建 TSDS mapping需要特别注意这一点。这一点对 Prometheus 用户尤其容易造成困扰。Prometheus 的 metric type 元数据在进入 Elasticsearch 时可能不可用因此经常需要依赖命名规则如_total表示 counter进行推断。这些 heuristic 并不完美如果 metric 被错误分类就会被函数拒绝。更底层的机制包括 Prometheus Remote Write 如何映射到 TSDS可以参考《Prometheus Remote Write 在 Elasticsearch 中的摄取工作原理》。更灵活的转换函数gauge↔counter已经在规划中用于在查询时修复这类问题。Kibana 图表在缩小时变空在 Kibana 中TBUCKET会跟随时间选择器变化。缩小时间范围会减小 bucket size。当 bucket 小于采集间隔时一些 bucket 会没有数据点导致RATE等函数返回 null图表就会 “空白”。Elastic 正在考虑改进比如bucket 太小时提示警告、设置最小 bucket、或自动扩大 window/bucket 来避免空图。总结对于指标查询优先使用TS除非你明确需要处理原始文档。然后根据 “每条 time series 内部应该表达什么含义” 来选择内部聚合函数计数器用RATE当前值型 gauge 用LAST_OVER_TIME峰值、均值、最小值或分布则用显式的*_OVER_TIME函数。一旦每条 time series 的值定义正确外层聚合就是更熟悉的一步把这些 time series 再按你需要的方式分组、汇总生成图表、告警或表格。完整参考可以查看 TS command 文档以及 time series aggregation functions 列表。常见问题为什么 ES|QL 的FROM在 counter 指标上会返回错误结果FROM会把每个 metric 文档当作独立行处理因此SUM(request_count)会把跨主机、跨时间的累计值直接相加结果会无限增长并不是 rate 或请求数。应使用TS配合RATE在每条 time series 内计算每秒变化率再跨主机求和。TS 和 FROM 在 ES|QL 中有什么区别TS是 time series data streamsTSDS的专用入口会在聚合前按 time series 分组从而支持RATE、AVG_OVER_TIME、LAST_OVER_TIME等函数。FROM则是按文档读取没有 per-series 语义。指标数据用 TS事件和原始文档分析用 FROM。为什么TS metrics | STATS AVG(cpu_usage)和FROM metrics | STATS AVG(cpu_usage)得到的平均值不同因为 TS 有隐式的内部聚合默认LAST_OVER_TIME每条 time series 在每个 bucket 只贡献一个值而 FROM 是对所有 document 做平均上报频率高的主机会权重更大。TS 是“按 time series 等权”FROM 是“按文档等权”。如何在 ES|QL 中计算 counter 的每秒速率使用 TS 和RATETS metrics-* | STATS SUM(RATE(request_count)) BY TBUCKET(5m), host.nameRATE在每条 time series 内计算增长率并处理 reset外层SUM再跨主机汇总。什么时候需要在 ES|QL metrics 查询里加时间过滤在 Kibana 中Dashboard、Discover 等时间范围由全局时间选择器控制无需手写过滤。在 Kibana 之外或 Dev Tools 中应显式加WHERE timestamp ...或使用TRANGE(1h)来限制扫描范围。TS | STATS 的两个聚合阶段是什么内部阶段在每条 time series 内用RATE、AVG_OVER_TIME等函数把多个样本压缩为一个值外部阶段再用SUM、AVG等标准 ES|QL 聚合跨 time series 汇总。正确性取决于内部函数外部只是组合。为什么 Kibana 指标图在缩放后会变空当 bucket 小于采集间隔时会出现空桶RATE返回 null图表就会消失。需要扩大时间范围或增大 inner window例如RATE(request_count, 5m)。TS 查询里可以在 STATS 前用 SORT、LIMIT 或 LOOKUP JOIN 吗不可以。TS 在第一个STATS之前必须保持_tsid分组状态因此任何改变行结构或顺序的操作都会被拒绝。应先WHERE过滤再STATS之后才能做排序、join 或 enrich。原文https://www.elastic.co/search-labs/blog/esql-ts-command-querying-metrics