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.goNewReadHandler创建的处理器
核心逻辑
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、时序流程图

image

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 查询路由

这类路由用于查询Prometheus的监控目标状态,是运维排查监控采集问题的核心工具,源码中依赖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封装,无统一响应结构体
核心设计目标
用户友好,支持复杂查询,适配人工交互场景
高效传输,降低网络开销,适配服务间数据同步场景
posted @ 2025-11-18 15:35  左扬  阅读(18)  评论(0)    收藏  举报