@Greptime: 关于Prometheus远程写入,瓶颈并非网络或memtable——而是Region Worker在dec…时持有&mut
摘要
GreptimeDB v1.0引入了Pending Rows Batcher,这是一个三阶段流水线,将CPU密集型工作从Datanode的关键路径上移开,使Prometheus远程写入吞吐量从120万提升到217万points/sec,并将Datanode的CPU使用率降低20%。
查看缓存全文
缓存时间: 2026/06/03 17:54
关于 Prometheus remote write,瓶颈并不在网络或 memtable —— 而是在于 Region Worker 在解码行数据、编码主键和对齐 schema 时持有 &mut 引用。
GreptimeDB v1.0 将这些工作移出临界区,转移到 Frontend(Pending Rows Batcher)。现在 Datanode 直接将预编码的 Arrow IPC 摄入 BulkMemtable。
1.20M → 2.17M 点/秒。Datanode CPU 降低 20%。
https://greptime.com/blogs/2026-05-26-pending-rows-batcher…
Pending Rows Batcher:GreptimeDB 中 Prometheus 数据摄入提速 80%
来源:https://greptime.com/blogs/2026-05-26-pending-rows-batcher GreptimeDB v1.0 在 Prometheus Remote Write [1] 路径上引入了 Pending Rows Batcher。在一个 16 Region 的物理表上,相同负载下,吞吐量从 1.20M 点/秒提升至 2.17M 点/秒,同时 Datanode CPU 使用率下降约 20%。
代价是 Frontend CPU 大约翻倍。我们将原本位于 Datanode 热路径上的工作移到了 Frontend,从而使批处理数据能以列式格式直接流向存储层。本文深入介绍了新的写入路径及其解决的问题。
为什么 Prometheus 数据摄入对时序数据库来说很困难 (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#why-prometheus-ingestion-is-hard-on-a-time-series-database)
在可观测性工作负载中,Prometheus 通过 Remote Write 协议将指标推送到远程存储。每个请求通常只携带几十到几百个数据点,但请求速率非常高。数据库必须跟上持续不断的小型碎片化写入流。
在原始的逐行写入路径上,每个请求独立触发一组固定的操作:
- 表 schema 解析
- Schema 对齐:缺失列执行
ALTER TABLE,缺失表执行CREATE TABLE - 行编码和 gRPC 传输
- 在 Datanode 上进行逐行解码、主键编码、Memtable 写入和 WAL 追加
其中许多是重复的固定开销。连续写入同一张表会重复触发相同的 schema 检查和格式转换。随着 TPS 上升,这些开销主导了吞吐量。
真正的瓶颈:Region Worker 的临界区 (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#the-real-bottleneck-the-region-worker-s-critical-section)
固定开销只是故事的一半。更深的瓶颈位于 Datanode 内部。
在 GreptimeDB 中,对每个 Region 的写入通过专用的 Region Worker 串行化。为了维护写入顺序,Worker 在处理写入时持有 Region 的 &mut 引用,这构成了一个临界区。在该临界区被持有时,其他任务无法接触该 Region。
在逐行写入路径上,临界区内部的工作包括:
- 行解码与校验
- 主键编码与排序
- Schema 对齐与列转换
- Memtable 写入
- WAL 追加
编码和排序是 CPU 密集型的,它们显著延长了锁持有时间。在高 TPS 下,Region Worker 成为瓶颈:新请求在排队等待正在执行临界区的请求完成。
减少固定开销并缩短临界区——这就是 Pending Rows Batcher 的目的。
解决方案:三阶段流水线 (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#the-solution-a-three-stage-pipeline)
Pending Rows Batcher 在 Frontend 上将来自多个请求的行数据合并,一次性进行 schema 对齐和行到列的转换,然后通过 BulkInsert RPC 将列式批量数据直接写入 Datanode。该路径包含三个部分:
Pending Rows Batcher 整体数据流 图 1:Pending Rows Batcher 整体数据流
- Frontend 上的 Batcher 负责批处理和行到列的转换
- BulkInsert 路径传输列式批量数据
- Datanode 上的 BulkMemtable[2] 以列式形式接收数据
该流程分为三个阶段:在入口提交并对齐 schema,按物理表积累批量数据,然后将列式批量数据批量写入 Datanode。
三阶段批处理流程 图 2:三阶段批处理流程
其思想是将昂贵的操作——解码、编码、排序、schema 对齐——从 Datanode 的临界区移到 Frontend 上。Region Worker 只需消费预编码的 RecordBatches。
写入路径上的关键点 (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#key-points-along-the-write-path)
提交与 Schema 对齐 (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#submit-and-schema-alignment)
当 Prometheus Remote Write 请求到达时,Batcher 将其转换为 Arrow RecordBatch[3] 并在过程中对齐 schema:
- 对于已存在的逻辑表,检测并添加缺失的标签列,通过批量
ALTER TABLE调用完成 - 对于之前未见的指标表,通过批量
CREATE TABLE调用自动创建表 - 所有 DDL 操作按物理表分组并批量执行,避免了每次请求的 DDL 开销
Schema 变更的开销被分摊到整个批次中,而不是逐请求支付。
逻辑表 vs. 物理表 (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#logical-tables-vs-physical-tables)
首先简要介绍 Metric Engine 抽象。
在 Prometheus 工作负载中,每个指标对用户暴露为一张逻辑表——不同的指标名称映射到不同的逻辑表。在内部,多个逻辑表可以映射到同一张物理表,而该物理表负责实际的数据写入、分区和 Region 管理。
Metric Engine:逻辑表映射到单个物理宽表 图 3:Metric Engine 将多个逻辑表映射到一张物理宽表
因此,Batcher 并不是简单地“每个指标一个批次”。它首先在逻辑表级别对齐 schema,然后根据它们映射到的物理表进行合并。共享同一张物理表的多个指标也共享一个 Worker 和刷新节奏,它们最终在发往 Datanode 的途中合并成一个更大的列式批次。
这就是“按物理表分区”这个说法反复出现的原因。逻辑表是用户看到的指标表及其 schema;物理表则决定了数据在底层如何组织和调度以进行批量写入。
Worker 批处理:两个刷新触发器 (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#worker-batching-two-flush-triggers)
每个物理表都有一个专用的后台 Worker。提交给 Worker 的 RecordBatches 持续积累,直到两个触发器之一被触发。总行数达到 max_batch_rows(默认 100,000)时 Worker 立即刷新;或者自上次刷新以来 pending_rows_flush_interval 时间已过,Worker 按定时器刷新。闲置超过 3× 刷新间隔的 Worker 会自动关闭以释放资源。
定时刷新限制了延迟下限;满批次刷新则最大化批次大小。两者一起在低延迟和大批次之间取得平衡。
列式批量写入 (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#columnar-bulk-write)
刷新时,Batcher 合并批次中所有逻辑表的 RecordBatches,编码为 Arrow IPC,然后通过单个 BulkInsert RPC 写入目标 Region。与原始的逐行 Insert 相比,这减少了网络往返次数,让 Datanode 可以直接处理列式数据而无需进行行到列的转换,并且支持基于分区的拆分,从而实现对不同 Region 的并行写入。
临界区如何变短 (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#how-the-critical-section-gets-shorter)
映射回临界区瓶颈,路径上的分工如下:
- Frontend(临界区外):行到列转换、schema 对齐、主键编码、分区拆分。这些 CPU 密集型操作在 Batcher 内部异步执行,不会占用任何 Region Worker。
- Datanode(临界区内):当 Region Worker 收到 BulkInsert 请求时,它直接将已经编码的 Arrow IPC RecordBatch 推入 Region。无需解码、无需排序、无需逐行处理。
临界区内的工作从“解码 + 主键编码 + 排序 + Memtable 写入 + WAL”缩减为“摄入预编码的 RecordBatch”。锁持有时间急剧下降,Region Worker 的吞吐量随之提升。
端到端列式化:BulkMemtable 的作用 (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#columnar-end-to-end-the-role-of-bulkmemtable)
一旦 Frontend 将行数据转换为 Arrow RecordBatch,接下来的问题是这些列式数据在 Datanode 端如何处理。
如果 Datanode 仍然使用原始的基于行的 Memtable(TimeSeriesMemtable),那么列式批次会被重新拆解成逐行的 Mutations,逐个主键编码,然后逐行插入。Batcher 通过合并获得的任何优势都会被抵消。网络虽然节省了几次往返,但存储层又会退回到逐行处理。
原始基于行的路径 vs. Batcher + BulkMemtable 列式路径 图 4:原始基于行的路径 vs. Batcher + BulkMemtable 列式路径
同样在 GreptimeDB v1.0 中引入的 BulkMemtable 弥补了这一差距。它是一个专门为列式批量写入构建的 Memtable:只接受 RecordBatch 输入,内部以列式布局存储数据,并在扫描时暴露 RecordBatch 迭代器。
有了 BulkMemtable,完整的写入路径如下:
Prometheus Remote Write → Batcher(行到列 + 积累) → BulkInsert RPC(Arrow IPC) → BulkMemtable(列式存储) → 刷新(列式 Parquet) → 查询(列式扫描)
在 Batcher 完成单次“行→列”转换后,数据再也不会回到行格式。这带来了:
- 无格式转换开销:BulkMemtable 直接接收 RecordBatches,无需拆解成逐行结构
- 零拷贝刷新:已经编码为内存 Parquet 字节的数据直接进入 SST 文件
- 向量化查询:扫描时发出的 RecordBatches 可以被 DataFusion 直接消费
BulkMemtable 的内部设计——分区层次结构、合并策略、内存 Parquet 编码——本身是一个深入的话题。我们在另一篇文章中进行了介绍:优化高基数时序数据库:GreptimeDB 的 Flat 格式设计 (https://greptime.com/blogs/2025-12-22-flat-format)。本文的要点更简单:Pending Rows Batcher 和 BulkMemtable 共同构成了从入口到存储的列式管道。
性能 (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#performance)
基准测试针对一个包含 16 个 Region 的物理表运行。在相同的 Prometheus Remote Write 负载下,我们测量了默认模式和使用 Pending Rows Batcher 模式下的写入吞吐量和资源使用情况。
写入吞吐量 (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#write-throughput)
写入吞吐量对比:默认模式 vs. Batcher 模式 图 5:写入吞吐量对比——默认模式 vs. Batcher 模式
启用 Pending Rows Batcher 后,写入吞吐量从 1.20M 点/秒提升至 2.17M 点/秒,提升了 81%。
资源使用 (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#resource-usage)
| 指标 | 默认模式 | Batcher 模式 | 变化 |
|---|---|---|---|
| 写入吞吐量 | 1.20M 点/秒 | 2.17M 点/秒 | +81% |
| Datanode CPU | 14.9 核 | 11.95 核 | -20% |
| Datanode 内存 | 7.14 GB | 6.36 GB | -11% |
| Frontend CPU | 5.2 核 | 10.15 核 | +95% |
| Frontend 内存 | 2.09 GB | 2.04 GB | 持平 |
Frontend CPU 增加是预期的。Schema 对齐、行到列转换和主键编码都移到了那里。这种权衡是用 Frontend 计算换取更高的吞吐量和更低的 Datanode 负载。在大多数部署中,Datanode 是受限资源,因为它直接与存储、刷新和查询性能相关,而 Frontend 是无状态的且易于水平扩展。这种权衡在生产环境中得到了回报。
原始监控数据 (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#raw-monitoring-data)
以下两张 Grafana 截图是在基准测试期间捕获的,可作为上述数字的原始参考。
默认模式下的 Grafana 仪表板 图 6:默认模式下的 Grafana 仪表板
Batcher 模式下的 Grafana 仪表板 图 7:Batcher 模式下的 Grafana 仪表板
如何启用 (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#how-to-enable)
批量写入模式在 Frontend 上配置。如果您的负载主要由高 TPS、小批量的 Prometheus Remote Write 请求组成,请启用 Metric Engine 并在 Frontend 配置中设置 Pending Rows Batcher 参数:
[prom_store]
enable = true
with_metric_engine = true
pending_rows_flush_interval = "5s"
max_batch_rows = 20000
max_concurrent_flushes = 256
worker_channel_capacity = 65526
max_inflight_requests = 3000
关键参数:
enable:启用 Prometheus Remote Write 存储入口点。with_metric_engine:通过 Metric Engine 存储 Prometheus 指标。这是逻辑/物理表映射和批量写入路径的前提。pending_rows_flush_interval:批处理时间窗口。设置为 0 以禁用 Batcher。示例中使用 5 秒。max_batch_rows:每批次的最大行数。达到此值后立即刷新。示例中使用 20,000。max_concurrent_flushes:同时进行中的最大刷新次数。worker_channel_capacity:每个 Worker 的通道容量,用于缓冲提交给该物理表 Worker 的请求。max_inflight_requests:最大进行中的请求数,用作背压以防止 Frontend 堆积未完成的写入。
总结 (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#summary)
Pending Rows Batcher 使用三阶段流水线(对齐 schema、积累、列式批量写入),将 Prometheus Remote Write 吞吐量提升了 80% 以上,同时降低了 Datanode 资源使用。
它所做的是重新分配 Frontend 和 Datanode 之间的工作。Frontend 承担了更多的预处理(积累、schema 对齐、行到列转换、主键编码),这样 Datanode 的 Region Worker 可以专注于真正需要在临界区内执行的操作。结合 BulkMemtable 的列式透传,写入路径不再在行格式和列格式之间来回切换。
对于高基数、高 TPS 的可观测性工作负载,启用新路径只需在 Frontend 配置中添加几行。
相关 PR:#7831 (https://github.com/GreptimeTeam/greptimedb/pull/7831)、#7877 (https://github.com/GreptimeTeam/greptimedb/pull/7877)、#7902 (https://github.com/GreptimeTeam/greptimedb/pull/7902)、#8054 (https://github.com/GreptimeTeam/greptimedb/pull/8054)。
参考文献 (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#references)
- Prometheus Remote Write 规范 (https://prometheus.io/docs/specs/remote_write_spec/) ↩︎ (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#fnref1)
- GreptimeDB Flat 格式和 BulkMemtable 设计 (https://greptime.com/blogs/2025-12-22-flat-format) ↩︎ (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#fnref2)
- Apache Arrow RecordBatch (https://arrow.apache.org/docs/cpp/ipc.html) ↩︎ (https://greptime.com/blogs/2026-05-26-pending-rows-batcher#fnref3)
相似文章
@Greptime: GreptimeDB v1.1.0 发布,PromQL rate/increase 查询速度提升高达97%,整体查询时间降低20-40%,TS… 上速度提升高达4.5倍
GreptimeDB v1.1.0 已发布,提供高达97%的PromQL查询加速,整体查询时间降低20-40%,在TSBS扫描密集型查询上性能提升高达4.5倍,并支持对现有表进行在线重分区。
@Greptime: GreptimeDB 六月的大部分工作归结为一个想法:如果过滤器无法到达数据,那么它就没用。在分布式查询中…
GreptimeDB 通过启用远程动态过滤器在运行时下推到 datanode 扫描,并优化优化器使其在 MergeScan 包装远程计划之前运行,从而确保过滤器到达数据,提升了分布式查询性能。JSON v2 列现在支持类型提示。
@Greptime: 重新分区大表通常是一个迁移项目:新建表、双写、回填TB级历史数据、切换。Gr…
GreptimeDB v1.1 通过单个 ALTER TABLE 语句引入了对现有表的在线重新分区,消除了数据迁移、双写或应用程序变更的需求。它利用共享对象存储和逻辑分片来更新清单和路由,而无需在节点之间移动数据。
@Greptime: GreptimeDB v1.1.2 已发布 — 这是一个值得升级的 v1.1 补丁。主要修复:定时 Flows 现在绑定 now()/current_timest…
GreptimeDB v1.1.2 是一个补丁版本,修复了定时 Flows 中 now() 的绑定问题,以确保 EVAL INTERVAL 窗口的确定性。此外还修复了以下问题:Kafka SASL 密码在调试输出中的屏蔽、GC 索引文件列表、parquet 元数据缓存大小、Prometheus 标签发现扫描以及 PromQL 时间二元聚合。建议用户升级。
@Greptime: GreptimeDB v1.1:你不再需要在创建表时锁定分区布局。以前,只有使用 PARTITION ON COLUMNS 创建的表才能…
GreptimeDB v1.1 引入了对现有表的在线重新分区、增量 Flow 读取、面向 LLM 的语义层以及稳定性改进。