Prometheus TSDB 库使用指南:Open、Appender 与 Querier 的完整实践
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 外,同目录还提供更底层的格式文档,可作为延伸阅读:
- tsdb/docs/format/wal.md:WAL(Write Ahead Log)格式说明
- tsdb/docs/format/index.md、tsdb/docs/format/chunks.md:块内索引与 chunk 文件格式
- tsdb/docs/format/tombstones.md:删除标记格式
- tsdb/docs/bstream.md、tsdb/docs/refs.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,这就是实际的数据库实例。内部会经过 validateOpts 对 Options 做补默认值与合法性校验,例如:浮点 chunk 编码只接受 xor 或 xor2(未设置时默认 XOR);MinBlockDuration 未设置时取 DefaultBlockDuration;块时长区间由 ExponentialBlockRanges 生成指数级分桶。随后进入 open,按注释所述它会“初始化锁文件、WAL、compactor,并通过回放 WAL 初始化 Head,然后运行数据库”,且同一目录不允许同时打开多个 DB 实例。
2.2 DB 的三大组件
文档明确指出,DB 包含以下主要组件:
- Compactor(压实器):分层压实器
LeveledCompactor,目前是唯一实现的压实器,且自动运行,负责把 Head 中的数据逐级压实为持久化块。 - Head:内存中的可写层,承担了大量职责,详见下文。
- 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。文档强调三条必须记住的规则:
- 必须调用
Commit()才会真正把样本写入 DB 并更新 WAL; - 每次 Commit 之后必须创建新的 appender——appender 是一次性事务边界;
- 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.Open(tsdb/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.go、tsdb/head.go、tsdb/example_test.go 的源码核对,可直接作为将 TSDB 集成为库使用的实施依据。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00