Prometheus 源码专题【左扬精讲】—— 监控系统 Prometheus 3.4.0 源码解析:Web API 与联邦架构下,本地查询(JSON 格式)与远程读请求差异化处理全流程 + 多场景查询路由(/query//targets//status/)源码梳理(含流程图)
Prometheus 源码专题【左扬精讲】—— 监控系统 Prometheus 3.4.0 源码解析:Web API 与联邦架构下,本地查询(JSON 格式)与远程读请求差异化处理全流程 + 多场景查询路由(/query/、/targets/、/status/)源码梳理(含流程图)
在Prometheus的核心能力中,Web API是用户与系统交互的核心入口,而查询请求处理则是监控数据价值输出的关键链路。相较于远程读(Remote Read)请求依赖ProtoBuf编码与Snappy压缩的复杂逻辑,本地查询请求以JSON为数据交换格式,其处理流程更贴近用户日常使用场景,但源码层面仍涉及数据来源适配、请求路由分发、差异化逻辑处理等核心设计。
本文将从Prometheus 3.4.0源码视角出发,聚焦Web API与联邦架构下的本地查询请求,梳理其从接收、解析到数据返回的全流程,对比本地查询与远程读请求的处理差异,详解样本数据、标签、监控目标及状态查询等多类路由的源码实现,并通过流程图直观呈现核心逻辑,为新人提供清晰的读码指南。
一、本地查询与远程读请求的核心区别
在深入源码前,需先明确本地查询与远程读请求的本质区别——二者虽同为数据查询类请求,但因应用场景、数据来源及传输效率需求不同,在数据格式、处理链路等方面存在显著差异,这也是源码中两类请求分治处理的核心原因。
1.1、区别对比
|
对比维度
|
本地查询请求
|
远程读请求(Remote Read)
|
|---|---|---|
|
数据格式
|
JSON(源码中通过
encoding/json包解析) |
ProtoBuf(基于
prompb定义的协议)+ Snappy压缩 |
|
核心路由
|
/query、/query_range、/query_example等
|
/api/v1/read(远程读专属路由)
|
|
数据来源
|
本地TSDB优先,支持联邦架构下远程存储适配
|
仅远程存储(如Thanos、Cortex等)
|
|
处理入口
|
web/api/v1/api.go中 Register api注册的路由处理器 |
remote/read_handler.go中NewReadHandler创建的处理器 |
|
核心逻辑
|
QL语句解析→查询计划生成→本地/远程数据聚合→JSON结果封装
|
PB协议解析→Snappy解压→远程存储请求→数据格式转换
|
1.2、本地TSDB与远程存储的适配逻辑
新人易误解“本地查询”即“仅查询本地数据”,但在联邦架构或混合存储场景下,本地查询的样本数据可来自两类数据源,源码中通过storage.Storage接口实现解耦:
-
-
- 本地TSDB:默认数据源,对应源码中tsdb.Storage实现,查询请求直接操作本地时序数据库,通过tsdb.DB.Query方法获取数据,是单节点部署的核心数据来源。
- 远程存储:在联邦架构(如Prometheus联邦集群)或配置远程读存储的场景下,通过remote.Storage实现对接远程存储,本地查询请求会被转发至远程节点,数据返回后经本地聚合处理再响应给用户。
-
源码中这一适配逻辑的核心是cmd/prometheus/main.go中初始化的storage实例——根据配置决定是使用纯本地TSDB,还是融合远程存储的复合实现,这也是本地查询能灵活适配多场景的关键。
二、本地查询请求全流程
本地查询请求的处理流程可分为“路由分发→请求解析→查询执行→结果封装”四个核心阶段,以最常用的/query路由为例,结合源码与流程图拆解如下。
2.1、时序流程图

2.2、阶段1:路由注册与请求接收
Prometheus的Web API路由注册集中在 https://github.com/prometheus/prometheus/blob/v3.4.0/web/api/v1/api.go#L339 的 Register API函数中,本地查询相关路由的注册逻辑如下:
// Register 将 API 的所有端点注册到传入的路由路由器中
func (api *API) Register(r *route.Router) {
// wrap 是一个高阶函数,用于包装 API 处理函数,统一处理通用逻辑
// 接收一个 apiFunc 类型的函数,返回一个 http.HandlerFunc
wrap := func(f apiFunc) http.HandlerFunc {
// 定义实际的 HTTP 处理函数
hf := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 设置 CORS 响应头,允许指定的跨域源访问
httputil.SetCORS(w, api.CORSOrigin, r)
// 执行传入的 API 处理函数,并在 TSDB 未就绪时设置"服务不可用"状态
result := setUnavailStatusOnTSDBNotReady(f(r))
// 如果结果中包含资源清理函数,注册为延迟执行(函数结束时调用)
if result.finalizer != nil {
defer result.finalizer()
}
// 如果处理过程中出现错误,返回错误响应
if result.err != nil {
api.respondError(w, result.err, result.data)
return
}
// 如果结果中包含响应数据,返回成功响应(包含数据、警告信息和查询参数)
if result.data != nil {
api.respond(w, r, result.data, result.warnings, r.FormValue("query"))
return
}
// 既无错误也无数据时,返回 204 No Content 状态码
w.WriteHeader(http.StatusNoContent)
})
// 对包装后的处理函数再进行两层包装:
// 1. 启用压缩处理(httputil.CompressionHandler)
// 2. 检查服务是否就绪(api.ready),最终返回 http.HandlerFunc
return api.ready(httputil.CompressionHandler{
Handler: hf,
}.ServeHTTP)
}
// wrapAgent 是 wrap 函数的变体,专门用于需要屏蔽 Prometheus Agent 访问的接口
wrapAgent := func(f apiFunc) http.HandlerFunc {
// 调用基础 wrap 函数,传入一个包装后的 apiFunc
return wrap(func(r *http.Request) apiFuncResult {
// 如果当前服务运行在 Agent 模式下,直接返回"不支持该操作"的错误
if api.isAgent {
return apiFuncResult{nil, &apiError{errorExec, errors.New("unavailable with Prometheus Agent")}, nil, nil}
}
// 非 Agent 模式下,正常执行原始的 API 处理函数
return f(r)
})
}
// 注册 OPTIONS 预检请求的处理接口,支持所有路径(/*path),用于跨域预检
r.Options("/*path", wrap(api.options))
// 注册查询相关接口:支持 GET 和 POST 方法,使用 wrapAgent 包装(Agent 模式下不可用)
r.Get("/query", wrapAgent(api.query))
r.Post("/query", wrapAgent(api.query))
r.Get("/query_range", wrapAgent(api.queryRange))
r.Post("/query_range", wrapAgent(api.queryRange))
r.Get("/query_exemplars", wrapAgent(api.queryExemplars))
r.Post("/query_exemplars", wrapAgent(api.queryExemplars))
// 注册查询格式化接口:支持 GET 和 POST 方法,Agent 模式下不可用
r.Get("/format_query", wrapAgent(api.formatQuery))
r.Post("/format_query", wrapAgent(api.formatQuery))
// 注册查询解析接口:支持 GET 和 POST 方法,Agent 模式下不可用
r.Get("/parse_query", wrapAgent(api.parseQuery))
r.Post("/parse_query", wrapAgent(api.parseQuery))
// 注册标签相关接口:标签名称查询(GET/POST)、指定标签的值查询(GET),Agent 模式下不可用
r.Get("/labels", wrapAgent(api.labelNames))
r.Post("/labels", wrapAgent(api.labelNames))
r.Get("/label/:name/values", wrapAgent(api.labelValues))
// 注册时间序列相关接口:查询序列(GET/POST)、删除序列(DELETE),Agent 模式下不可用
r.Get("/series", wrapAgent(api.series))
r.Post("/series", wrapAgent(api.series))
r.Del("/series", wrapAgent(api.dropSeries))
// 注册抓取相关状态接口:抓取池列表、目标实例列表、目标元数据,使用基础 wrap 包装(Agent 模式可用)
r.Get("/scrape_pools", wrap(api.scrapePools))
r.Get("/targets", wrap(api.targets))
r.Get("/targets/metadata", wrap(api.targetMetadata))
// 注册 Alertmanager 配置查询接口,Agent 模式下不可用
r.Get("/alertmanagers", wrapAgent(api.alertmanagers))
// 注册指标元数据查询接口,使用基础 wrap 包装
r.Get("/metadata", wrap(api.metricMetadata))
// 注册服务状态相关接口:配置信息、运行时信息、构建信息、启动参数、TSDB 状态
r.Get("/status/config", wrap(api.serveConfig))
r.Get("/status/runtimeinfo", wrap(api.serveRuntimeInfo))
r.Get("/status/buildinfo", wrap(api.serveBuildInfo))
r.Get("/status/flags", wrap(api.serveFlags))
// TSDB 状态查询接口,Agent 模式下不可用
r.Get("/status/tsdb", wrapAgent(api.serveTSDBStatus))
// WAL 回放状态查询接口:直接注册(未经过 wrap 包装,无通用逻辑处理)
r.Get("/status/walreplay", api.serveWALReplayStatus)
// 通知相关接口:通知列表、SSE 实时通知(未经过 wrap 包装)
r.Get("/notifications", api.notifications)
r.Get("/notifications/live", api.notificationsSSE)
// 远程读写接口:检查服务就绪状态(api.ready),无其他通用包装
r.Post("/read", api.ready(api.remoteRead))
r.Post("/write", api.ready(api.remoteWrite))
// OTLP 指标写入接口:检查服务就绪状态,支持 OTLP v1 协议
r.Post("/otlp/v1/metrics", api.ready(api.otlpWrite))
// 告警相关接口:告警列表、规则列表,Agent 模式下不可用
r.Get("/alerts", wrapAgent(api.alerts))
r.Get("/rules", wrapAgent(api.rules))
// Admin 管理类接口(高危操作):使用 wrapAgent 包装(Agent 模式下不可用)
// TSDB 数据删除接口:支持 POST 和 PUT 方法
r.Post("/admin/tsdb/delete_series", wrapAgent(api.deleteSeries))
// TSDB 墓碑清理接口:支持 POST 和 PUT 方法
r.Post("/admin/tsdb/clean_tombstones", wrapAgent(api.cleanTombstones))
// TSDB 快照创建接口:支持 POST 和 PUT 方法
r.Post("/admin/tsdb/snapshot", wrapAgent(api.snapshot))
// 重复注册 Admin 接口的 PUT 方法(与上面 POST 方法功能一致,兼容不同请求方式)
r.Put("/admin/tsdb/delete_series", wrapAgent(api.deleteSeries))
r.Put("/admin/tsdb/clean_tombstones", wrapAgent(api.cleanTombstones))
r.Put("/admin/tsdb/snapshot", wrapAgent(api.snapshot))
}
2.3、阶段2:请求参数解析(JSON格式转内部结构体)
本地查询请求的参数支持URL查询串(GET方式)或请求体(POST方式,JSON格式),以 /query 路由为例,在 web/api/v1/api.go 中的 query 方法处理:
// query 处理即时查询(instant query)请求,执行PromQL查询并返回结果
// 参数 r 是HTTP请求对象,返回值是apiFuncResult(包含响应数据、错误、警告和资源清理函数)
func (api *API) query(r *http.Request) (result apiFuncResult) {
// 解析请求中 "limit" 参数(限制返回结果数量),处理参数解析错误
limit, err := parseLimitParam(r.FormValue("limit"))
if err != nil {
// 返回参数错误(指定错误字段为"limit")
return invalidParamError(err, "limit")
}
// 解析请求中 "time" 参数(查询的时间点),默认使用当前服务时间(api.now())
// 处理时间参数解析错误
ts, err := parseTimeParam(r, "time", api.now())
if err != nil {
// 返回参数错误(指定错误字段为"time")
return invalidParamError(err, "time")
}
// 从HTTP请求中获取上下文(用于传递请求生命周期、超时等信息)
ctx := r.Context()
// 检查请求中是否包含 "timeout" 参数(查询超时时间)
if to := r.FormValue("timeout"); to != "" {
var cancel context.CancelFunc // 上下文取消函数,用于超时后释放资源
// 解析超时时间字符串为Duration类型
timeout, err := parseDuration(to)
if err != nil {
// 返回参数错误(指定错误字段为"timeout")
return invalidParamError(err, "timeout")
}
// 创建带截止时间的上下文:当前时间 + 超时时间
// cancel函数需延迟调用,确保函数退出时释放上下文资源
ctx, cancel = context.WithDeadline(ctx, api.now().Add(timeout))
defer cancel()
}
// 从请求中提取查询选项(如评估时间、部分响应策略等)
opts, err := extractQueryOpts(r)
if err != nil {
// 提取选项失败,返回"无效数据"类型的API错误
return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
}
// 通过查询引擎创建即时查询实例
// 参数:上下文、查询数据源、查询选项、查询语句(r.FormValue("query"))、查询时间点
qry, err := api.QueryEngine.NewInstantQuery(ctx, api.Queryable, opts, r.FormValue("query"), ts)
if err != nil {
// 创建查询实例失败,返回"query"参数错误
return invalidParamError(err, "query")
}
// 延迟函数:确保查询资源最终被释放(避免内存泄漏)
// 逻辑:如果返回结果中未设置finalizer(资源清理函数),则直接调用qry.Close()关闭查询
defer func() {
if result.finalizer == nil {
qry.Close()
}
}()
// 从HTTP请求中提取额外上下文信息(如请求ID、追踪信息等),合并到现有上下文
ctx = httputil.ContextFromRequest(ctx, r)
// 执行查询:传入最终上下文,获取查询结果
res := qry.Exec(ctx)
// 检查查询执行是否出错
if res.Err != nil {
// 返回错误结果:包含错误信息、查询警告、查询资源清理函数(qry.Close)
return apiFuncResult{nil, returnAPIError(res.Err), res.Warnings, qry.Close}
}
// 保存查询返回的警告信息
warnings := res.Warnings
// 如果设置了结果数量限制(limit>0),对查询结果进行截断
if limit > 0 {
var isTruncated bool // 标记结果是否被截断
// 调用truncateResults截断结果集,返回截断后的结果和截断标记
res, isTruncated = truncateResults(res, limit)
// 如果结果被截断,添加"结果因限制被截断"的警告信息
if isTruncated {
warnings = warnings.Add(errors.New("results truncated due to limit"))
}
}
// 处理查询统计信息渲染:如果请求中指定"stats"参数,返回统计信息
sr := api.statsRenderer // 优先使用API配置的统计信息渲染器
if sr == nil {
// 未配置时使用默认渲染器
sr = DefaultStatsRenderer
}
// 调用渲染器生成统计信息:传入上下文、查询统计数据、"stats"参数值(控制统计信息详情)
qs := sr(ctx, qry.Stats(), r.FormValue("stats"))
// 返回成功结果:包含查询数据、无错误、警告信息、查询资源清理函数
return apiFuncResult{
// 构造QueryData对象(查询响应数据结构)
data: &QueryData{
ResultType: res.Value.Type(), // 结果类型(如矩阵、向量、标量等)
Result: res.Value, // 实际查询结果数据
Stats: qs, // 查询统计信息(如执行时间、样本数量等)
},
err: nil, // 无错误
warnings: warnings, // 查询过程中的警告信息
finalizer: qry.Close, // 资源清理函数(由外层wrap函数延迟调用)
}
}
2.4、阶段3:查询执行(QL解析+数据查询)
查询执行分为两个步骤:
-
-
- QL 语句解析:由 promql.Engine 负责,将 PromQL 语句解析为可执行的查询计划
- 数据查询执行:通过 storage.Storage 接口从数据源获取数据
-
关键代码在 promql/engine.go 中的查询执行逻辑 https://github.com/prometheus/prometheus/blob/v3.4.0/promql/engine.go#L478:
// NewInstantQuery 创建并返回一个即时查询(Instant Query)实例,用于在指定时间点评估给定的PromQL表达式
// 参数说明:
// ctx: 上下文对象,用于传递请求生命周期、取消信号等
// q: 存储查询接口,用于从存储层获取指标数据
// opts: 查询选项配置(如评估超时、部分响应策略等)
// qs: 待执行的PromQL查询语句字符串
// ts: 查询的目标时间点(即时查询的核心时间参数)
// 返回值:
// Query: 构建完成的查询实例,可通过其Exec方法执行查询
// error: 构建查询过程中出现的错误(如解析失败、参数校验失败等)
func (ng *Engine) NewInstantQuery(ctx context.Context, q storage.Queryable, opts QueryOpts, qs string, ts time.Time) (Query, error) {
// 调用引擎的内部方法newQuery初始化查询结构体
// 即时查询的时间范围为 [ts, ts](起始时间=结束时间,即单个时间点),步长为0(无采样步长)
// 返回值pExpr是表达式指针(用于后续赋值预处理后的表达式),qry是初始查询实例
pExpr, qry := ng.newQuery(q, qs, opts, ts, ts, 0)
// 将查询实例加入引擎的活跃查询队列,获取队列完成函数(用于后续退出队列)
// 若加入队列失败,直接返回错误
finishQueue, err := ng.queueActive(ctx, qry)
if err != nil {
return nil, err
}
// 延迟执行队列完成函数:确保函数退出时(无论成功失败)将查询从活跃队列移除
defer finishQueue()
// 解析PromQL查询语句字符串,转换为抽象语法树(AST)中的表达式节点
expr, err := parser.ParseExpr(qs)
if err != nil {
return nil, err // 解析失败返回错误
}
// 校验查询选项(opts)与解析后的表达式(expr)是否兼容(如特定表达式对选项的限制)
if err := ng.validateOpts(expr); err != nil {
return nil, err // 校验失败返回错误
}
// 对表达式进行预处理(如替换变量、处理时间范围等),适配即时查询的时间上下文(ts到ts)
// 将预处理后的表达式赋值给pExpr指向的变量(更新查询实例中的表达式)
*pExpr, err = PreprocessExpr(expr, ts, ts)
// 返回构建完成的查询实例和预处理结果(无错误则err为nil)
return qry, err
}
2.5、阶段4:结果封装与响应
https://github.com/prometheus/prometheus/blob/v3.4.0/web/api/v1/api.go#L164
查询结果通过统一的响应格式返回,在 web/api/v1/api.go 中定义了响应结构:
// Response contains a response to a HTTP API request.
type Response struct {
Status status `json:"status"`
Data interface{} `json:"data,omitempty"`
ErrorType errorType `json:"errorType,omitempty"`
Error string `json:"error,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Infos []string `json:"infos,omitempty"`
}
最终以 JSON 格式返回给客户端。
三、Web API核心路由详解:样本、标签、目标与状态查询
除了核心的样本查询路由,Prometheus Web API还提供了标签查询、监控目标查询、状态查询等多类路由,覆盖监控系统运维与使用的全场景。以下从源码视角逐一解析其功能与实现逻辑。
3.1、样本数据查询路由(核心业务路由)
样本数据查询是Web API的核心能力,对应3个核心路由,源码中均通过ql.Engine解析PromQL,差异主要在于查询时间范围与使用场景:
-
-
- /api/v1/query:即时查询,获取指定时间点的样本数据,对应queryHandler处理器,适用于快速查看当前监控指标状态。
- /api/v1/query_range:范围查询,获取指定时间范围内的样本数据(支持按步长采样),对应queryRangeHandler处理器,适用于绘制监控曲线等场景,源码中需额外解析Start、End、Step三个时间参数。
- /api/v1/query_example:查询示例路由,对应queryExampleHandler处理器,主要用于返回PromQL查询的示例响应,方便用户调试,源码逻辑相对简单,仅返回预设的示例数据。
-
3.2、标签查询路由(元数据查询)
标签是Prometheus指标的核心属性,标签查询路由用于获取指标的标签名称或标签值,支持快速定位指标维度,核心路由及源码逻辑如下:
|
路由路径
|
功能描述
|
源码处理器
|
核心逻辑
|
|---|---|---|---|
|
/api/v1/labels
|
获取所有标签名称
|
labelsHandler
|
调用
storage.LabelNames,从TSDB或远程存储获取标签列表 |
|
/api/v1/label/{name}/values
|
获取指定标签的所有值
|
labelValuesHandler
|
解析路径中的标签名,调用
storage.LabelValues获取对应值列表 |
|
/api/v1/metadata
|
获取指标元数据(含标签信息)
|
metadataHandler
|
聚合指标的标签、类型等元数据,支持按job或指标名过滤
|
3.3、/api/v1/targets 查询路由
discovery.TargetManager获取目标信息,核心路由如下:-
- /api/v1/targets:获取所有监控目标的状态(如UP/DOWN)、采集配置等信息,对应targetsHandler处理器,源码中通过targetManager.Targets获取目标列表,按状态(active/dropped)分类返回。
- /api/v1/targets/metadata:获取监控目标的元数据,包括目标的标签、采集指标等细节,对应targetsMetadataHandler,支持按job、instance过滤目标。
实际运维中,通过 curl http://localhost:9090/api/v1/targets 即可快速排查目标是否正常采集,源码中这一路由的核心价值是将服务发现模块的目标信息通过API暴露。
3.4、/status 状态查询路由(主要有6类,运维兄弟们要多多关注)
状态查询路由用于获取Prometheus实例的运行状态,共6类核心路由,分别对应不同维度的状态信息,源码中均为“读取配置/运行信息→封装响应”的简单逻辑,是运维排查问题的关键入口:
|
路由路径
|
状态信息类型
|
源码核心逻辑
|
使用场景
|
|---|---|---|---|
|
/api/v1/status/config
|
当前运行配置
|
读取
cmd/prometheus/main.go中的全局配置实例,返回YAML格式配置 |
验证配置是否生效
|
|
/api/v1/status/runtimeinfo
|
运行时信息
|
通过
runtime包获取Go运行时信息(如GOMAXPROCS、内存使用) |
性能排查
|
|
/api/v1/status/buildinfo
|
构建信息
|
返回编译时的版本、分支、提交哈希等信息(编译时注入)
|
确认版本信息
|
|
/api/v1/status/flags
|
启动参数
|
读取
flag包解析的启动参数,返回键值对 |
验证启动参数是否正确
|
|
/api/v1/status/tsdb
|
TSDB状态
|
调用
tsdb.DB.Status,返回数据块、索引、保留策略等信息 |
TSDB运维与问题排查
|
|
/api/v1/stats/walreplay
|
WAL回放状态
|
读取WAL(预写日志)回放的进度、速度等统计信息
|
重启后WAL回放监控
|
四、本地查询与远程读请求的Web API差异化处理
前文已提及两类请求的核心差异,此处从Web API处理链路的“路由注册→请求解析→数据处理→响应封装”四个环节,结合源码进行对比,明确分治处理的设计思路:
|
处理环节
|
本地查询请求
|
远程读请求
|
|---|---|---|
|
路由注册
|
注册在
web/api/v1/api.go,路由路径含/query等 |
注册在
remote/read_handler.go,路由为/api/v1/read |
|
请求解析
|
JSON格式,用
json.Decode解析为QueryParams结构体 |
PB+Snappy,先Snappy解压,再用
prompb.Unmarshal解析 |
|
数据处理核心
|
依赖
ql.Engine解析PromQL,支持本地/远程数据聚合 |
不解析PromQL,直接将PB请求转发至远程存储,数据格式无聚合
|
|
响应封装
|
JSON格式,遵循
APIResponse统一规范 |
PB格式,用
prompb.Marshal封装,无统一响应结构体 |
|
核心设计目标
|
用户友好,支持复杂查询,适配人工交互场景
|
高效传输,降低网络开销,适配服务间数据同步场景
|

浙公网安备 33010602011771号