查询路径 · 数据怎么被读出来

一次查询要同时读内存的 Head 和磁盘的多个 block,还要把结果合并成一条连续的 series。这一章读 storage.Querier 接口和 SeriesSet 的惰性迭代模型,并讲清 native histograms 与远程写这两个扩展,收尾第 3 部分。

作者 David更新于 第 24 篇(共 52 篇)

查询路径 · 数据怎么被读出来

数据存好了(压缩、索引、压实)。现在反过来:怎么读出来?

一次查询“过去 6 小时的 {job="api"}”要面对一个尴尬的现实:这段数据散落在多个地方——最近 2 小时在内存的 Head 里,更早的在若干个 block 里。查询必须把它们缝合成一条连续的 series。

Querier 接口

看真实签名(storage/interface.go:123):

// Querier provides querying access over time series data of a fixed time range.
type Querier interface {
    LabelQuerier
    // Select returns a set of series that matches the given label matchers.
    // Caller can specify if it requires returned series to be sorted.
    // Prefer not requiring sorting for better performance.
    Select(ctx context.Context, sortSeries bool, hints *SelectHints,
           matchers ...*labels.Matcher) SeriesSet
}

三个值得注意的设计:

  • 一个 Querier 覆盖一个固定时间范围 —— 注释说得很清楚:“over time series data of a fixed time range”。查询开始时先“开”一个 Querier(指定 mint/maxt),它内部决定要读哪些 block。时间范围在创建时就固定,这让它能提前规划好数据源。
  • sortSeries bool —— 调用方声明“我需不需要结果有序”,注释还特意提醒:“Prefer not requiring sorting for better performance”。为什么?因为排序不是免费的,而很多操作根本不需要有序(比如 sum() 只是把值加起来,顺序无所谓)。把“要不要排序”的决定权交给调用方,是个精细的性能设计——默认不排,需要时才付代价。
  • hints *SelectHints —— 查询引擎可以给存储层提示(比如“我只要算 rate(),给我 counter 的边界点就行”)。注释很坦诚:“it's up to implementation how this is used if used at all”——这是个可选优化,存储层可以完全忽略。这种“提示而非命令”的设计,让接口保持简单的同时给优化留了口子。

SeriesSet:惰性迭代

Select 返回的不是数据,而是一个 SeriesSet——一个迭代器:

ss := querier.Select(...)
for ss.Next() {           // 逐条 series
    s := ss.At()
    it := s.Iterator(nil)
    for it.Next() {       // 逐个采样点
        t, v := it.At()
        // ...
    }
}

为什么是迭代器而不是“返回一个大数组”?因为数据可能极大,不能一次性装进内存**。**

想想一个查询可能碰到 10 万条 series × 每条几千个点 = 几亿个采样点。如果 Select 返回 []Series 且每条都带全部数据,直接 OOM。

迭代器让数据流式地流过:

  • 按需解压 —— chunk 是压缩的,只在真正迭代到它时才解压。
  • 可以提前退出 —— 比如 topk(5, ...) 可能不需要看完所有数据。
  • 内存恒定 —— 不管查多少数据,同时在内存里的只有“当前这条 series 的当前这个 chunk”。

这是所有大数据处理系统的通用模式(火山模型 / Volcano model):把查询表达成一棵迭代器的树,数据自底向上流,每一层只处理“当前这一个”。第 4 部分的 PromQL 引擎正是建在这个模型上。

缝合:Head + 多个 block

一条 series 的数据可能同时存在于 Head 和多个 block 里,查询要把它们归并成一条连续的时间线。

查询 [T-6h, now] 的 series X:

block A [T-6h, T-4h]:  ● ● ● ●
block B [T-4h, T-2h]:            ● ● ● ●
Head    [T-2h, now]:                      ● ● ● ●
─────────────────────────────────────────────────
缝合后:                ● ● ● ● ● ● ● ● ● ● ● ●   ← 一条连续的 series

这靠一层 merge(storage/fanout.go、storage/merge.go):把多个 SeriesSet 归并成一个。因为每个来源内部都是按时间有序的,归并又是线性的——有序性再一次救了场。

一个微妙的问题:重叠怎么办? 压实期间、或者乱序写入时,block 的时间范围可能重叠。TSDB 支持重叠 block,归并时会去重(同一时刻的点只取一个)。这是为什么 meta.json 里要记录每个 block 的精确时间范围。

两个扩展:native histograms 与远程写

第 3 部分收尾前,补两个重要的扩展。

native histograms:桶自动生成

第 1 部分讲过经典 histogram 的痛:桶要预先定死、选不好就废、桶数还吃基数。Prometheus 3.x 的 native histograms 就是来解这个的。

核心区别:一个采样点不再是一个 float64,而是一整个直方图结构(tsdb/chunkenc/histogram.go、float_histogram.go)。

  • 桶自动生成 —— 按指数规则(如每个桶是前一个的 1.1 倍),覆盖任意量级,不用你预先猜。
  • 精度可调 —— 一个 schema 参数控制桶的密度,想精确就调高。
  • 基数大降 —— 经典 histogram 里每个桶是一条 series(le 是标签);native histogram 整个直方图是一条 series。从 12 条变 1 条。
  • 代价 —— 解析要走 protobuf 格式(文本表达不了这么复杂的结构),生态支持还在铺开。

这是近年 Prometheus 最重要的演进——它同时解决了桶设计的纠结和基数两个痛点。新项目值得直接上。

远程写:把数据送出去

Prometheus 刻意单机自足,但数据总要有个长期的去处——这就是 remote write(storage/remote/)。

它的位置很优雅:remote write 就是 storage.Appender 接口的另一个实现。抓取模块照样往 Appender 里塞数据,完全不知道背后是本地 TSDB 还是“发到远端”。接口解耦的价值在这里兑现——加一条远程写链路,抓取代码一行不改。

这也是 Thanos / Mimir / VictoriaMetrics / 各种云托管服务的接入点:它们通过 remote write 收数据,自己做分布式存储和全局查询,而 Prometheus 只管“抓 + 转发”。

remote write 的工程难点是背压和重试:远端慢了怎么办?本地队列积压怎么办?丢数据还是阻塞抓取?源码里有一整套队列管理(storage/remote/queue_manager.go)——这和 Collector 的背压问题是同一类难题,第 6 部分会再碰到。

小结与下一部分

一次查询要把散落在 Head 和多个 block 里的数据缝合成连续的 series。storage.Querier(storage/interface.go:123)三个设计点:一个 Querier 覆盖固定时间范围、sortSeries 让调用方决定要不要排序(不排更快,很多操作本就不需要有序)、hints 是提示而非命令(实现可忽略,留优化口子但接口保持简单)。返回 SeriesSet 迭代器而非大数组——数据可能几亿个点,必须流式:按需解压、可提前退出、内存恒定;这就是火山模型,PromQL 引擎建在它上面。缝合靠 merge(归并,再一次靠有序性做到线性),支持重叠 block 并去重。两个扩展:native histograms(一个点是整个直方图结构 → 桶自动指数生成、基数从 12 条降到 1 条,代价是走 protobuf——近年最重要的演进)、remote write(就是 Appender 的另一个实现 → 抓取代码零改动,Thanos/Mimir 由此接入;难点是背压与重试)。

TSDB 讲完了。数据能存能读,但用户是用 PromQL 提问的——那门语言怎么被解析和执行?👉 第 4 部分 · PromQL:查询引擎与实战。

参考资料

  • [1] Prometheus 源码 storage/interface.go:123(Querier/Select)、storage/merge.go、storage/fanout.go。
  • [2] Prometheus 源码 tsdb/chunkenc/histogram.go、float_histogram.go(native histograms);storage/remote/queue_manager.go(远程写)。
  • [3] 关联chunk 编码、索引、指标类型。

本页目录