Prometheus Remote Write 在 Elasticsearch 中的摄取原理
作者来自 Elastic Felix Barnsteiner深入了解 Elasticsearch 对 Prometheus Remote Write 的实现protobuf 解析、指标类型推断、TSDS 映射以及数据流路由。Elasticsearch 最近新增了对 Prometheus Remote Write 协议的原生支持。你可以将 Prometheus或 Grafana Alloy直接指向一个 Elasticsearch 端点在无需任何中间适配器的情况下发送指标。本文将介绍当一个 Remote Write 请求到达时Elasticsearch 内部发生了什么。如果你想理解其实现方式、评估 Elasticsearch 与其他 Prometheus 兼容后端的对比或参与贡献这篇文章适合你。配套文章《使用 Remote Write 将 Prometheus 指标发送到 Elasticsearch》则介绍了设置和配置相关内容。请求生命周期从 HTTP 到索引文档Request lifecycle: from HTTP to indexed documents在深入之前先简单说明 Prometheus 的数据模型Prometheus 将所有指标值存储为 64 位浮点数并将指标名称视为一个普通的 label__name__。存储引擎本身并不区分一个值是 counter 还是 gauge。在理解 Elasticsearch 如何映射这些概念时请记住这一点。下面是一个 Remote Write 请求在 Elasticsearch 中的完整路径HTTP 层—— 端点接收压缩的 protobuf 负载检查索引压力使用 Snappy 解压并解析 protobuf WriteRequest。文档构建—— 每个时间序列中的每个 sample 都会被转换为一条 Elasticsearch 文档包含 timestamp、labels.* 和 metrics.* 字段。批量索引—— 单个请求中的所有文档通过一次 bulk 调用写入目标数据流。下面的章节将详细介绍每个阶段。HTTP 层HTTP layer该端点接受 application/x-protobuf 的 POST 请求。传入的请求体会按照与 bulk 索引 API 相同的索引压力限制indexing pressure limits进行跟踪。如果集群已经处于较高的索引负载下请求会在解析之前直接以 429 被拒绝。Prometheus 使用 Snappy 对 Remote Write 负载进行压缩。Elasticsearch 以流式方式对请求体进行解压而不会将其物化为一个连续的大块内存并且会根据可配置的最大值校验声明的解压后大小以防止解压炸弹攻击。解压后的数据体随后会被反序列化为 protobuf 的 WriteRequest。每个 WriteRequest 包含一个 TimeSeries 列表而每个 TimeSeries 包含一组 labels键值对以及一个 samples 列表时间戳 float64 值。文档构建对于每个时间序列中的每个 sampleElasticsearch 都会构建一个索引请求。下面是单条文档的示例结构{ timestamp: 2026-04-01T12:00:00.000Z, data_stream: { type: metrics, dataset: generic.prometheus, namespace: default }, labels: { __name__: http_requests_total, job: prometheus, instance: localhost:9090, method: GET, status: 200 }, metrics: { http_requests_total: 1027.0 } }所有来自 Prometheus 时间序列的 labels包括name都会被写入 labels.* 字段。指标值则写入 metrics.metric_name其中 metric_name 是namelabel 的值。如果时间序列中没有namelabel则会被完全丢弃其 samples 会被计为失败。非有限值NaN、Infinity、负 Infinity会被静默跳过。这也包括 Prometheus 的 staleness 标记它使用一个特殊的 NaN 位模式0x7ff0000000000002来表示某个时间序列已经消失。一个 sample一条文档你可能会疑惑将每个 sample 单独存储为一条文档是否会带来较大的存储开销尤其是在 labels 较多的情况下。一种常见的优化方式是将具有相同 labels 和时间戳的多个指标打包到同一条文档中。随着近期 TSDB 的改进这种优化已不再必要。Elasticsearch 已将单条文档的存储开销降低到一个程度使得将多个指标打包在一条文档中与将每个 sample 单独写入之间几乎没有差异。关于这些 TSDB 存储优化的详细介绍将在后续的专门文章中发布。批量索引Bulk indexing单个 Remote Write 请求中的所有文档会通过一次 bulk 请求发送到 Elasticsearch。每条文档的目标数据流为 metrics-{dataset}.prometheus-{namespace}并以仅追加append-only的 create 操作方式写入。指标类型推断Metric type inferenceRemote Write v1 并不会可靠地随 sample 一起传递指标类型。Prometheus 会通过单独的请求大约每分钟一次发送元数据类型、说明文本、单位而这些请求可能会被路由到与 sample 不同的节点。在分布式系统中等待元数据到达再缓冲 sample 并不现实因此 Elasticsearch 选择基于命名约定来推断类型。以 _total、_sum、_count 或 _bucket 结尾的指标名称会被映射为 counter其余则默认映射为 gauge。这是一个广泛使用的约定其他兼容 Prometheus 的后端系统也采用了类似方式。http_requests_total → counter request_duration_seconds_sum → counter request_duration_seconds_count → counter request_duration_seconds_bucket → counter process_resident_memory_bytes → gauge go_goroutines → gauge这种启发式方法可能出错。例如一个名为 temperature_total 的指标如果有人将一个 gauge 命名为这样会被错误地分类为 counter。目前的主要影响是一些 ES|QL 函数例如 rate()要求指标类型为 counter因此会拒绝被错误分类的 gauge。对于 PromQL我们计划取消这一限制使 rate() 无论声明类型如何都可以工作从而降低错误推断的影响。你可以通过创建 metrics-prometheuscustom component template 并使用自定义 dynamic templates 来覆盖默认推断。例如将所有 *_counter 字段视为 counterPUT /_component_template/metrics-prometheuscustom { template: { mappings: { dynamic_templates: [ { counter: { path_match: metrics.*_counter, mapping: { type: double, time_series_metric: counter } } } ] } } }自定义 dynamic templates 会与内置模板进行合并因此对于你未显式覆盖的指标默认的命名约定规则仍然适用。索引模板Elasticsearch 会安装一个内置的 index template用于匹配 metrics-.prometheus-。该模板使字段类型推断能够在无需手动 mapping 配置的情况下正常工作。启用了TSDS 模式这带来了基于时间的分区、优化的存储、去重能力以及随着数据老化进行降采样的能力。对于 labels 和 metrics 命名空间都使用了 passthrough 对象字段这有三个作用命名空间隔离labels 和 metrics 分别位于独立的对象命名空间labels.* 和 metrics.*因此名为 status 的 label 与名为 status 的 metric 不会发生冲突。维度识别labels 的 passthrough 对象被配置为 time_series_dimension: true这意味着 labels.* 下的每个字段都会自动被视为 TSDS 维度。当 Prometheus 发送一个包含新 label 的时间序列时该字段会自动成为一个维度无需显式定义 mapping。透明查询在 ES|QL 或 PromQL 中你不需要写 labels. 或 metrics. 前缀。例如可以直接使用 job 而不是 labels.job或使用 http_requests_total 而不是 metrics.http_requests_total。passthrough 映射会自动完成解析。针对 metrics 的动态推断会应用前面描述的命名约定规则。当一个新的指标名称首次出现时其字段 mapping 会在 metrics.* 下自动创建并带有正确的 time_series_metric 标注。启用了失败存储failure store。当文档索引失败例如由于 mapping 冲突——同一指标名称出现不兼容类型时这些文档会被路由到单独的 failure store而不是被静默丢弃。数据流路由三种 URL 模式会直接映射到对应的数据流名称URL patternData stream/_prometheus/api/v1/writemetrics-generic.prometheus-default/_prometheus/metrics/{dataset}/api/v1/writemetrics-{dataset}.prometheus-default/_prometheus/metrics/{dataset}/{namespace}/api/v1/writemetrics-{dataset}.prometheus-{namespace}这使你能够将来自不同 Prometheus 实例或不同环境的指标分隔到不同的数据流中。这种隔离带来几个好处生命周期隔离Lifecycle isolation你可以为不同数据流设置不同的保留策略。生产环境指标可能保留 90 天而开发环境指标可能只保留 7 天。访问控制Access control你可以将 API key 限定到特定数据流。例如一个团队的 Prometheus 实例写入 metrics-teamA.prometheus-prod而他们的 API key 只拥有该数据流的访问权限。查询性能Query performancePromQL 查询和 Grafana 仪表板可以限定在特定 index pattern 上避免扫描无关数据。错误处理与 Remote Write 规范Remote Write 规范定义了两类响应可重试5xx、429和不可重试4xx。Prometheus 会根据这一分类决定是否重试或丢弃失败请求。如果 bulk 请求中的任意 sample 因索引压力被拒绝Elasticsearch 会返回 429Too Many Requests。这会通知 Prometheus 进行退避backoff并使用指数退避策略重试。对于部分失败部分 sample 成功写入、部分失败响应中会包含汇总信息报告失败 sample 的数量并按目标索引和状态码分组同时附带每组的示例错误信息。没有namelabel 的时间序列会导致这些 sample 返回 400 错误。非有限值NaN、Infinity会被静默丢弃Prometheus 会收到成功响应并不会重试。NaN 最常见于 summary 分位数在尚未有观测值时例如在任何请求到达之前的 p99 延迟指标以及 staleness 标记。实际影响较小在大多数查询中缺失 sample 与 NaN 的行为类似因为 PromQL 的回溯窗口lookback window会以相同方式用最后已知值填充空缺。更重要的差异在于 staleness 标记这将在下文介绍。下一步Remote Write v2 及其未来Remote Write v2 目前仍处于实验阶段因此当前实现基于 v1。但 v2 解决了 v1 的多个局限性。元数据与 sample 同步Metadata alongside samplesv2 会在同一个请求中将 metric 类型、单位和描述信息与 time series 一起发送。这消除了对命名约定推断的依赖。原生 histogramNative histogramsv2 支持 Prometheus 原生直方图它可以自然映射到 Elasticsearch 的 exponential_histogram 字段类型。传统 histogram每个 bucket 一个 counter不仅冗长还会在查询时丢失精度而 native histogram 更紧凑且更精确。字典编码Dictionary encodingv2 用整数引用替代重复的 label 字符串从而显著减少高基数 label 集合的 payload 大小。创建时间戳Created timestampscounter 在 v2 中包含“创建时间”标记用于表示 counter 初始化时间。这使得后端能够比当前“数值下降即 reset”的启发式方法更准确地检测 counter 重置。除了 v2 之外还有两个未来方向正在考虑中。Staleness marker 支持当前 staleness markerPrometheus 在 scrape 目标消失时写入的特殊 NaN会被丢弃。如果支持它们将可以实现正确的 PromQL 回溯行为并避免“5 分钟残留数据”问题即已消失的序列仍出现在查询结果中。共享 metric 字段Shared metric field当前实现会为每个 metric name 创建一个独立字段例如 metrics.http_requests_total、metrics.go_goroutines 等。虽然可行但会导致字段映射数量随 metric 名称增长因此 Prometheus 数据流的字段上限被设置为 10,000。我们正在考虑另一种方案只在namelabel 中存储 metric 名称而将数值写入单个共享字段。这可以彻底避免字段爆炸问题也更贴近 Prometheus 的内部存储模型。这一方向属于提升 Elasticsearch metrics 存储效率与 Prometheus 兼容性的长期演进。可用性AvailabilityPrometheus Remote Write 端点已在 Elasticsearch Serverless 中可用无需额外配置。对于自管理集群可以使用 start-local 快速启动环境。如果遇到问题或有反馈可以在 Elasticsearch 仓库中提交 issue。原文https://www.elastic.co/observability-labs/blog/prometheus-remote-write-elasticsearch-architecture