Prometheus Remote Read API 全解:/api/v1/read 端点、外部标签语义与流式 XOR 分块
本文围绕 Prometheus 仓库中的 Remote Read API 文档 展开,系统讲解 /api/v1/read 端点的请求/响应协议(基于 protobuf)、external_labels 在读数据两端的影响机制,以及流式 XOR 分块(STREAMED_XOR_CHUNKS)响应的编码方式;并结合 web/api/v1/api.go、storage/remote/read_handler.go、storage/remote/read.go 等源码,说明服务端的完整处理链路与测试验证方法,帮助读者既能正确对接远端读取协议,也能理解其底层实现与边界约束。
一、接口定位与稳定性说明
Remote Read API 是 Prometheus 对外暴露的数据读取接口,它允许外部系统(如 Thanos、Cortex 等长期存储)直接从 Prometheus 的 TSDB 中拉取原始时序数据。需要注意官方文档中明确给出的稳定性声明:
NOTE: This is not currently considered part of the stable API and is subject to change even between non-major version releases of Prometheus.(Remote Read API 目前不属于稳定 API,甚至可能在小版本之间发生变化。)
这意味着在对接该接口时,客户端实现应做好兼容演进的准备,例如对响应头、字段变化做宽容处理。接口在管理 API 文档中的对应条目见 POST /api/v1/read,其中说明该端点“允许外部系统(如 Thanos)从 TSDB 读取数据”,并要求请求使用 snappy 压缩(该链接为原文档保留的外部参考)。
在 docs/storage.md 中,Remote Read 同样被列为 TSDB 数据暴露通道之一,与 --web.enable-remote-write-receive 等写入类端点共同构成 Prometheus 的远端集成面。
二、端点与请求格式
2.1 调用方式
请求发送到一个固定端点(文档原文保留):
POST /api/v1/read
在仓库源码中,该路由注册于 web/api/v1/api.go#L488:
r.Post("/read", api.ready(api.remoteRead))
其中 api.ready 包装保证了只有服务完全就绪(TSDB 可用)后该端点才响应;remoteRead 本身(web/api/v1/api.go#L2165-L2168)只是把请求转发给真正的处理逻辑:
func (api *API) remoteRead(w http.ResponseWriter, r *http.Request) {
if api.remoteReadHandler != nil {
api.remoteReadHandler.ServeHTTP(w, r)
}
...
}
remoteReadHandler 由 storage/remote/read_handler.go 中的 NewReadHandler 构造(web/api/v1/api.go#L360),构造时接收三个重要的容量约束参数(web/api/v1/api.go#L301-L303):
remoteReadSampleLimit:单次请求允许读取的样本数上限;remoteReadConcurrencyLimit:并发读取的查询上限;remoteReadMaxBytesInFrame:流式模式下单个帧(frame)的最大字节数。
这些参数共同构成服务端的自我保护:当匹配到的样本量或返回字节量超过限额时,端点会返回错误而不是拖垮进程。
2.2 压缩约定
文档明确指出:该接口期望使用 snappy 压缩("This interface expects snappy compression")。在源码中,SAMPLES 模式的响应会显式设置 Content-Encoding: snappy 响应头(storage/remote/read_handler.go#L124);测试用例 storage/remote/read_handler_test.go 中则使用 github.com/golang/snappy 对响应体进行 snappy.Decode 验证(如 read_handler_test.go#L55 与 #L235-L247)。
请求体本身是一个 protobuf 编码的 ReadRequest 消息,且整体经 snappy 压缩后发送。
三、Protobuf 协议定义
文档声明“API 定义位于 prompb/remote.proto”。在当前仓库中,核心消息定义见 prompb/remote.proto,它引用 prompb/types.proto 中的 TimeSeries、LabelMatcher 等基础类型。关键定义如下(摘自源码,字段编号即 protobuf 线格式字段号):
3.1 请求:ReadRequest
// ReadRequest represents a remote read request.
message ReadRequest {
repeated Query queries = 1;
enum ResponseType {
// Server will return a single ReadResponse message with matched series that
// includes list of raw samples. ...
SAMPLES = 0;
// Server will stream a delimited ChunkedReadResponse message that contains
// XOR or HISTOGRAM(!) encoded chunks for a single series. ...
STREAMED_XOR_CHUNKS = 1;
}
// accepted_response_types allows negotiating the content type of the response.
// Response types are taken from the list in the FIFO order. If no response type
// in `accepted_response_types` is implemented by server, error is returned.
// For request that do not contain `accepted_response_types` field the SAMPLES
// response type will be used.
repeated ResponseType accepted_response_types = 2;
}
要点:
ReadRequest支持一次携带多个查询(repeated Query queries),服务端按序返回结果;accepted_response_types是响应类型协商字段:服务端按 FIFO 顺序选取第一个自己实现的类型;若请求未携带该字段,则回退为SAMPLES模式。源码中对应的兜底逻辑见 storage/remote/read_handler.go#L109-L111:“On empty or unknown types in req.AcceptedResponseTypes we default to non streamed, raw samples response.”
3.2 查询与结果:Query / QueryResult
message Query {
int64 start_timestamp_ms = 1; // 查询起始时间(毫秒)
int64 end_timestamp_ms = 2; // 查询结束时间(毫秒)
repeated prometheus.LabelMatcher matchers = 3; // 标签匹配器
prometheus.ReadHints hints = 4; // 读取提示(可选)
}
message QueryResult {
// Samples within a time series must be ordered by time.
repeated prometheus.TimeSeries timeseries = 1;
}
Query 与 PromQL 的 matcher 语义一致(等值/正则等匹配),时间范围以毫秒为单位;QueryResult.timeseries 中每条时序内的样本必须按时间升序排列。
3.3 响应:ReadResponse / ChunkedReadResponse
// ReadResponse is a response when response_type equals SAMPLES.
message ReadResponse {
// In same order as the request's queries.
repeated QueryResult results = 1;
}
// ChunkedReadResponse is a response when response_type equals STREAMED_XOR_CHUNKS.
// We strictly stream full series after series, optionally split by time. ...
message ChunkedReadResponse {
repeated prometheus.ChunkedSeries chunked_series = 1;
// query_index represents an index of the query from ReadRequest.queries
// these chunks relates to.
int64 query_index = 2;
}
两个值得注意的契约:
ReadResponse.results的顺序严格对应请求中queries的顺序;- 流式模式下,
ChunkedReadResponse严格按“整条时序一条接一条”的顺序流式下发,可选按时间切分;一旦开始流式输出新的一条时序,就不会再回头补发之前时序的 chunk。多查询场景下用query_index标明本帧属于哪个查询。
四、external_labels 与 Remote Read:两端都要注意的语义
这是原文档最核心的部分。global 配置中的 external_labels 会同时影响 remote read 的读端(暴露方)和写端(查询方),两端行为在源码中都能找到对应实现。
4.1 暴露方(服务端)行为
文档描述:Prometheus 会在通过 /api/v1/read 返回的每条时序上附加 external_labels。这些标签并不存储于 TSDB,而是在响应组装时动态注入。
源码印证:storage/remote/read_handler.go#L88-L91 在处理请求时从当前全局配置中读取外部标签:
externalLabels := h.config().GlobalConfig.ExternalLabels.Map()
随后在返回样本时把这些标签写入序列。同时,入参中匹配外部标签的等值 matcher 会被改写为匹配空字符串,这样 TSDB 内部(并不存储外部标签)才能满足该查询——例如请求 up{env="prod"}(env 是 external label)时,服务端会把 env="prod" 改写为 env="" 再下推给 TSDB 查询,从而保证只要序列本体存在,该请求仍能命中。
4.2 查询方(客户端)行为
文档描述:作为查询方的 Prometheus 会把自己的 external_labels 作为额外的等值 matcher 加入请求,并在收到响应后把它们从结果中剥离。
对应实现位于 storage/remote/read.go。Select 方法注释(read.go#L136-L137)明确写道:
// Select also adds equality matchers for all external labels to the list of
// matchers before calling remote endpoint.
// The added external labels are removed from the returned series sets.
而 addExternalLabels(read.go#L175-L183)则把每个外部标签转换为等值 matcher 追加到查询条件中。查询方通常配置于 remote_read 段(见 docs/configuration.md),用于把 Prometheus 自己的 TSDB 与远端存储(如 Thanos、VictoriaMetrics)的结果合并。
4.3 一致性要求:为什么可能查不到数据
文档特别警告:
If the querying Prometheus has
external_labelsthat differ from or are absent on the remote server, queries may return no results because the matchers will not match. Ensure that anyexternal_labelson the querying side are consistent with the labels present on the remote side.(如果查询方的 external_labels 与远端不同或远端缺失,查询可能无结果。)
结合两侧机制可以推断其成因:查询方把 env="prod" 作为等值 matcher 发给远端,而远端暴露的序列上若没有 env="prod"(例如远端根本没配置该外部标签,或值不同),matcher 匹配失败,返回空结果。因此部署规范是:查询侧 external_labels 必须是远端序列标签的子集且值一致,这是排查 remote read “查不到数据”问题的第一检查项。
五、服务端实现链路(源码级)
把前面的协议与语义串起来,一次 POST /api/v1/read 在仓库中的处理链路为:
- 路由层:web/api/v1/api.go#L488 注册
POST /read,api.remoteRead(#L2165)委托给remoteReadHandler; - 解析与协商:
readHandler.ServeHTTP(storage/remote/read_handler.go)反序列化ReadRequest,读取accepted_response_types决定响应模式:命中STREAMED_XOR_CHUNKS时调用remoteReadStreamedXORChunks(#L109、#L189),否则走原始样本模式; - 响应头:
- SAMPLES 模式:
Content-Type: application/x-protobuf、Content-Encoding: snappy(见 prompb/remote.proto#L38-L40 与 read_handler.go#L124); - STREAMED_XOR_CHUNKS 模式:
Content-Type: application/x-streamed-protobuf; proto=prometheus.ChunkedReadResponse,不设置 Content-Encoding(流式分块各自编码)(prompb/remote.proto#L47-L49、read_handler.go#L190);
- SAMPLES 模式:
- 限额保护:
remoteReadSampleLimit/remoteReadConcurrencyLimit/remoteReadMaxBytesInFrame在NewReadHandler中注入(read_handler.go#L46-L48),超限即返回错误。
测试侧的端到端验证可见 storage/remote/read_handler_test.go:其中既有对 SAMPLES 模式“响应体 snappy 解压后可反序列化”的断言,也有对流式模式响应头的精确断言(read_handler_test.go#L413-L414):
require.Equal(t, "application/x-streamed-protobuf; proto=prometheus.ChunkedReadResponse", recorder.Result().Header.Get("Content-Type"))
require.Empty(t, recorder.Result().Header.Get("Content-Encoding"))
OpenAPI 层面,/read 路径也注册于管理 API 文档生成器中(web/api/v1/openapi_paths.go#L666-L669,operationId 为 remoteRead),因此它出现在 Prometheus 自动生成的 OpenAPI 规范里。
六、两种响应模式对比
| 维度 | SAMPLES(默认) | STREAMED_XOR_CHUNKS |
|---|---|---|
| 请求协商 | accepted_response_types 留空或含 0 |
accepted_response_types 含 1 且服务端支持 |
| Content-Type | application/x-protobuf |
application/x-streamed-protobuf; proto=prometheus.ChunkedReadResponse |
| Content-Encoding | snappy |
无(空) |
| 载荷 | 单个 ReadResponse,含全部匹配时序的原始样本 |
分隔符切分的 ChunkedReadResponse 流,XOR 编码 chunk |
| 适用场景 | 通用兼容,客户端只需 protobuf 解码 | 大数据量拉取:服务端无需逐样本解压重组,带宽更省 |
| 源码位置 | read_handler.go#L124 | read_handler.go#L189-L190 |
七、Streamed Chunks 与 XOR 编码
文档“Streamed Chunks”一节指出:
- 流式 chunk 采用一种受 Gorilla 压缩算法启发的 XOR 编码来编码 chunk(Gorilla 是 Facebook 在 VLDB 2015 提出的时间序列压缩方法,此处不给出外部链接);
- 与 Gorilla 原始方案按“秒”解析时间戳不同,Prometheus 的版本将时间精度提升到毫秒。
这一点与仓库中 TSDB 的 chunk 编码实现相呼应:XOR 编码块位于 tsdb/chunkenc/ 目录,Prometheus 自身写入 TSDB 的样本即使用该编码;remote read 的流式模式本质上把“块内压缩数据”原样下发,省去样本级序列化开销。对客户端而言,这意味着:
- 需要理解 delimited protobuf 流的分帧格式:每个
ChunkedReadResponse帧前是 varint 长度 + 固定 4 字节大端 CRC32-Castagnoli 校验(prompb/remote.proto#L42-L49); - 解码 chunk 需要实现与 TSDB 相同的 XOR 解压,或使用支持该协议的高层客户端库。
八、实践建议与适用边界
综合文档与源码,对接或排查 Remote Read API 时建议:
- 先核对 external_labels 一致性(第四节):查询侧
global.external_labels必须与远端暴露的标签一致,否则查询静默返回空结果; - 默认用 SAMPLES 模式:不传
accepted_response_types即回退原始样本,兼容性最好;只有在客户端具备 XOR chunk 解码能力时才协商流式模式; - 注意服务端限额:样本数、并发、单帧字节数超限会被拒绝(构造参数见 web/api/v1/api.go#L301-L303),大查询应缩小时间范围或拆分 matcher;
- 记住非稳定 API 声明:协议字段与响应头可能在次版本间演进,客户端应做版本探测与降级(例如流式模式失败时回退 SAMPLES);
- 区分读写方向:本文讨论的是 Prometheus 作为数据暴露方的
/api/v1/read;作为查询方拉取远端数据的配置见remote_read(docs/configuration.md),其 matcher 注入与标签剥离逻辑在 storage/remote/read.go。
相关延伸阅读:查询 API 总览、存储与远端集成说明、协议源文件 prompb/remote.proto。
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