首页
/ Prometheus TSDB 库使用指南:Open、Appender 与 Querier 的完整实践

Prometheus TSDB 库使用指南:Open、Appender 与 Querier 的完整实践

2026-09-04 16:53:33作者:彭桢灵Jeremy

Prometheus 的 TSDB 包不仅服务于 Prometheus 主程序,也被 Cortex、Thanos、Grafana Mimir 等系统作为底层时序存储库使用。本文以 tsdb/docs/usage.md 为核心脉络,结合当前仓库源码,讲清三件事:如何用 tsdb.Open 实例化一个数据库、如何通过 Appender 写入数据并理解各种拒绝写入的条件、以及如何正确使用 Querier 查询数据,并附上可直接运行的完整示例代码。

1. TSDB 的定位:一个可被独立复用的时序数据库

TSDB(Time Series Database)是 Prometheus 的时间序列数据库实现。除 Prometheus 自身外,它还被其他监控应用(如 Cortex、Thanos、Grafana Mimir)作为库直接集成。tsdb/docs/ 目录下的文档面向两类读者:希望以库的形式使用 TSDB 的开发者,以及希望参与 TSDB 开发的贡献者。

除本文聚焦的 usage.md 外,同目录还提供更底层的格式文档,可作为延伸阅读:

2. 实例化数据库:tsdb.Open 与 DB 的三大组件

2.1 打开数据库

调用方应当使用 tsdb.Open 打开一个 TSDB,目录可以是全新的,也可以是已有数据目录(打开时会自动恢复)。该方法定义在 tsdb/db.go#L901

// Open returns a new DB in the given directory. If options are empty, DefaultOptions will be used.
func Open(dir string, l *slog.Logger, r prometheus.Registerer, opts *Options, stats *DBStats) (db *DB, err error)

Open 返回 *tsdb.DB,这就是实际的数据库实例。内部会经过 validateOptsOptions 做补默认值与合法性校验,例如:浮点 chunk 编码只接受 xorxor2(未设置时默认 XOR);MinBlockDuration 未设置时取 DefaultBlockDuration;块时长区间由 ExponentialBlockRanges 生成指数级分桶。随后进入 open,按注释所述它会“初始化锁文件、WAL、compactor,并通过回放 WAL 初始化 Head,然后运行数据库”,且同一目录不允许同时打开多个 DB 实例

2.2 DB 的三大组件

文档明确指出,DB 包含以下主要组件:

  1. Compactor(压实器):分层压实器 LeveledCompactor,目前是唯一实现的压实器,且自动运行,负责把 Head 中的数据逐级压实为持久化块。
  2. Head:内存中的可写层,承担了大量职责,详见下文。
  3. Blocks(持久化块):已压实落盘的只读块,查询时与 Head 合并读取。

2.3 Head 的内部结构

Head 是 TSDB 的核心,文档列出其四个主要组成部分,当前仓库源码中都能一一对应:

  • WAL(Write Ahead Log):位于 tsdb/wlog/ 目录,写入先落 WAL 再进入内存,保证重启后可回放恢复;
  • stripeSeries:持有所有活跃序列,按 ID(即 "ref",源码中类型为 chunks.HeadSeriesRef)与标签哈希(labels hash)两种索引链接到 memSeries。当前实现见 tsdb/head.go#L2266
// stripeSeries holds series by HeadSeriesRef ("ID") and also by hash of their labels.
// ID-based lookups via getByID() are preferred over getByHash() for performance reasons.
// It locks modulo ranges of IDs and hashes to reduce lock contention.
type stripeSeries struct {
	size   int
	series []map[chunks.HeadSeriesRef]*memSeries // Sharded by ref.
	hashes []seriesHashmap                       // Sharded by label hash.
	locks  []stripeLock
	...
}

源码注释说明:按 ref 查找优先于按哈希查找(性能更好);锁按 ref 与哈希取模分片以减少锁竞争;锁对象做了 40 字节填充以避免落在同一缓存行上。默认分片大小由 DefaultStripeSize 定义为 1 << 14

  • Postings list(倒排索引):对任意“标签名=标签值”对,保存所有对应序列的 ref,用于查询时快速定位序列集合;
  • Tombstones(删除标记):记录被删除(tombstoned)的时间区间,查询时跳过这些区间。

3. 写入数据:Appender 的语义与拒绝条件

3.1 基本用法

通过 db.Appender(ctx) 获取一个“appender”,接口即 Prometheus 的 storage.Appender。文档强调三条必须记住的规则:

  1. 必须调用 Commit() 才会真正把样本写入 DB 并更新 WAL;
  2. 每次 Commit 之后必须创建新的 appender——appender 是一次性事务边界;
  3. Appender 本身不是并发安全的。但抓取(scrape)本身是并发执行的,正确做法是并发使用多个 appender以降低竞争;需要注意所有 appender 的 Commit() 最终都会竞争同一个临界区(写 WAL 是串行化的),多个 appender 同时提交会放大 append 尾延迟。

3.2 Append 会因哪些条件拒绝数据

文档给出了三类拒绝条件,这里结合源码逐一说明:

条件一:timestamp < minValidTime,产生 “out of bounds” 错误。 minValidTime 取以下两者中的较大值(源码中即 Head.minValidTime 字段,见 tsdb/head.go#L83// Mint allowed to be added to the head. It shouldn't be lower than the maxt of the last persisted block.):

  • 最后一个 block 的 maxTime(即 Head 最近一次截断时间),通过 Head.Truncate()DB.compactHead() 更新;
  • 比 Head 块内最大时间早 minBlockDuration / 2 的时刻。虽然 storage.tsdb.min-block-duration 技术上可配置,但它是隐藏选项(cmd/prometheus/main.go#L503 中标注 “For use in testing.”),文档建议假设其取默认 2 小时。

两个条件各有用意:前者保证新生成的块不与已有块时间重叠(简化查询),后者保证样本不会落入“压实窗口”(compaction window)——即可能正在被并发写入磁盘持久化块的那段时间范围,该逻辑与摄入并发执行且不加锁。

条件二:标签集合不合法。 例如标签集为空,或包含重复的标签名。

条件三:样本乱序或时间戳重复。 若某序列(由全部标签确定)上的新样本与已见样本乱序,返回 storage.ErrOutOfOrderSample;若与已见过的最大时间戳相同但值不同,返回 storage.ErrDuplicateSampleForTimestamp

此外文档特别提醒:Commit() 还可能拒绝与通过另一个 appender 写入的样本乱序的数据——因为乱序判断发生在可见的已提交数据之上,跨 appender 的并发提交会互相感知。

4. 查询数据:Querier 的生命周期约束

通过 db.Querier(mint, maxt) 获取一个 querier,同样遵循 storage.Querier 接口。文档要求记住三点:

  • 一个 querier 只能看到它创建时已提交的数据。这一点决定了 querier 的“视野”是创建时刻的快照,也限制了它的生命周期,不应跨请求长期持有;
  • 用完必须调用 Close() 释放资源;
  • 查询时务必用 mint/maxt 时间范围约束,避免加载无关数据。

5. 完整示例代码:写入两个样本并查询回来

官方示例位于 tsdb/example_test.go,覆盖了“打开 → 追加 → 提交 → 查询 → 关闭”的完整闭环,核心代码如下(已按当前仓库源码核对):

// 创建一个临时目录;Open() 不要求目录预先存在
dir, err := os.MkdirTemp("", "tsdb-test")
noErr(err)

// 打开 TSDB(可读可写)
db, err := Open(dir, nil, nil, DefaultOptions(), nil)
noErr(err)

// 打开一个 appender 进行写入
app := db.Appender(context.Background())

series := labels.FromStrings("foo", "bar")

// 首次追加时 ref 传 0,因为我们还不知道该序列的引用号
ref, err := app.Append(0, series, time.Now().Unix(), 123)
noErr(err)

// 一秒后再追加一个样本;复用上面的 ref(同一序列),可以加快追加
time.Sleep(time.Second)
_, err = app.Append(ref, series, time.Now().Unix(), 124)
noErr(err)

// 提交到存储
err = app.Commit()
noErr(err)

// Commit() 之后若还要继续追加,必须获取新的 appender
app = db.Appender(context.Background())

// 打开 querier 进行读取
querier, err := db.Querier(math.MinInt64, math.MaxInt64)
noErr(err)
ss := querier.Select(context.Background(), false, nil,
	labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"))

for ss.Next() {
	series := ss.At()
	fmt.Println("series:", series.Labels().String())

	it := series.Iterator(nil)
	for it.Next() == chunkenc.ValFloat {
		_, v := it.At()
		fmt.Println("sample", v)
	}
	fmt.Println("it.Err():", it.Err())
}
fmt.Println("ss.Err():", ss.Err())
err = querier.Close()
noErr(err)

// 收尾:关闭 DB 与临时目录
err = db.Close()
noErr(err)

预期输出(示例代码中用 // Output: 作为测试断言):

series: {foo="bar"}
sample 123
sample 124
it.Err(): <nil>
ss.Err(): <nil>

两个值得注意的实践细节:

  • 复用 ref 加速追加:首次 Append 返回的 ref 是该序列的内部引用号,同一序列后续追加传入该 ref 可省去按标签哈希查找序列的开销;
  • 迭代类型判断it.Next() 返回 chunkenc.ValFloat 表示当前样本是浮点数(若样本是原生直方图则为其他类型),这正是 TSDB 同时支持 float 与 native histogram 的体现(见 tsdb/example_test.go#L71 处的循环条件)。

tsdb/db_test.go 中还演示了 TSDB 库的更多具体用法(如 Open 各种 Options 组合下的行为),可作为更深入的参考。

6. 小结

  • 打开数据库统一走 tsdb.Opentsdb/db.go#L901),同一目录只允许一个实例;DB 由 Compactor、Head、Blocks 三部分组成,Head 内部由 WAL、stripeSeries、postings 倒排索引与 tombstones 构成。
  • 写入遵循“新 appender → Append(尽量复用 ref)→ Commit”的循环,Commit 串行化写 WAL,并发靠多 appender 分摊;越界(minValidTime)、非法标签、乱序/重复三类情况会被拒绝。
  • 查询使用短生命周期的 Querier,创建即定视野,务必用 mint/maxt 收敛范围并及时 Close。

以上所有结论均来自 tsdb/docs/usage.md 与当前仓库 tsdb/db.gotsdb/head.gotsdb/example_test.go 的源码核对,可直接作为将 TSDB 集成为库使用的实施依据。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
903
1.82 K
docsdocs
暂无描述
Markdown
888
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.51 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341