Prometheus源码专题【左扬精讲】—— 监控系统 Prometheus 3.4.0 源码解析:scrape/manager.go 与 scrape/scrape.go
Prometheus源码专题【左扬精讲】—— 监控系统 Prometheus 3.4.0 源码解析:scrape/manager.go 与 scrape/scrape.go
Prometheus 作为开源监控系统的标杆,其数据采集机制是整个系统的核心组件。在 Prometheus 3.4.0 版本中,scrape(百度翻译:英[skreɪp] 美[skreɪp])manager(采样管理器)负责管理监控数据的采集流程,是连接服务发现和数据存储的桥梁。
本专题将深入剖析 scrape manager 的实现原理,包括其总体工作流程、结构体定义以及对监控目标数据的加工过程,特别是以 scrapeLoop 为核心的样本数据处理机制。
通过本文的学习,你将掌握:
-
- Prometheus 3.4.0 中 scrape manager 的设计架构与核心数据结构
- 监控目标数据的接收、处理和同步机制
- scrapeLoop 的实现原理与工作流程
- 样本数据从采集到写入存储的完整路径
一、scrape manager:监控目标的 “总调度中心”
scrape manager 的 核心职责 是统筹所有监控目标的生命周期管理与采集任务调度,其工作过程可拆解为三个关键阶段:
1.1、初始化与配置加载
1.2、目标动态管理与调度
1.3、异常监控与资源控制
二、scrapeLoop:数据采集的 “执行单元”
每个监控目标对应一个 scrapeLoop 协程,其工作过程聚焦于“数据采集-解析-存储”的完整链路:
2.1、HTTP 请求构建与发送
根据配置的 scrape_timeout、headers 等参数,scrapeLoop 会构建 HTTP 请求(默认 GET 方法),并发送至目标的 /metrics 端点。源码中通过 client.Do 方法执行请求,同时支持 Basic Auth、TLS 加密等安全认证方式(由 config.ScrapeConfig 的 Authorization、TLSConfig 字段配置)。
2.2、指标解析与格式转换
接收到响应后,scrapeLoop 会调用 text.Parse 函数解析文本格式的指标数据,将其转换为 Prometheus 内部的 metric 结构体(包含名称、标签、值、时间戳等信息)。解析过程中会对指标名称进行合法性校验(如替换非法字符为下划线),并自动添加 job、instance 等默认标签(由 relabel_configs 配置可自定义)。
2.3、数据过滤与存储提交
三、解决的核心痛点
- 动态目标管理难题:通过服务发现与 scrapeLoop 动态创建 / 销毁,解决了容器、云环境中目标 IP 频繁变化的问题(大家都是老司机,不举例了)。
- 采集可靠性保障:超时控制、健康状态标记、并发限制机制,避免了单个故障目标拖垮整个监控系统(大家可以扩展,并发限制的影响因素,如网络延迟、目标相应速度过慢等下,最佳并发数应该设置多少)。
- 数据一致性与标准化:统一的指标解析、标签处理流程,确保不同类型目标的监控数据格式一致,为后续查询、告警提供可靠基础。
- 资源高效利用:基于时间轮的调度算法与并发控制,平衡了采集频率与系统资源消耗,支持单节点监控数万个目标。
-
从源码设计来看,scrape manager 与 scrapeLoop 的 分工明确:前者负责 “战略调度”,后者专注 “战术执行”,二者的协同既保证了大规模监控的灵活性,又确保了数据采集的稳定性,这也是 Prometheus 能够成为云原生监控事实标准的核心原因之一。
四、scrape manager 架构与核心数据结构
从 架构演进、字段联动、生命周期管理、并发控制 等多个维度进行深化理解。
本节旨在详细介绍 Prometheus scrape manager 的架构设计及其核心数据结构,强调其在监控系统中的角色及与如何管理和协调多个 scrapePoll。
4.1、scrap manager 的总体架构
- 功能概述:作为整个监控系统的调度中心,负责管理所有监控任务的生命周期,包括初始化配置、服务发现集成、采集任务调度等。
- 主要流程:
- 配置加载与初始化
- 动态服务发现与目标管理
- 定期抓取与数据处理
- 自我监控与健康检查
4.2、Manager 结构体定义与关键字
https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/manager.go
在 Prometheus 3.4.0 源码中,scrape manager 的核心结构体是 Manager,定义在 https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/manager.go#L99 文件中:
// Manager 维护一组抓取池(scrape pools),并在从发现管理器(discovery manager)
// 接收到新的目标组时管理启动/停止周期
type Manager struct {
opts *Options // 全局配置选项(如默认超时、日志级别)
logger *slog.Logger // 用于日志记录的 logger 实例
append storage.Appendable // 用于存储抓取数据的可追加存储接口
graceShut chan struct{} // 用于优雅关闭的通道,关闭时出发资源清理
offsetSeed uint64 // 高可用场景下的偏移种子,用于分散集群采集时间
mtxScrape sync.Mutex // 互斥锁,用于保护scrapeConfigs、scrapePools等字段的并发锁
scrapeConfigs map[string]*config.ScrapeConfig // 以 job_name 为键的抓取配置映射
scrapePools map[string]*scrapePool // 以 job_name 为键的抓取配置应射(每个job一个池)
newScrapeFailureLogger func(string) (*logging.JSONFileLogger, error) // 创建新的抓取失败日志记录器的函数
scrapeFailureLoggers map[string]FailureLogger // 抓取失败日志记录器的映射,键为相关标识
targetSets map[string][]*targetgroup.Group // 服务发现的目标组,以 job_name 为键
buffers *pool.Pool // 用于管理缓冲区的池实例
triggerReload chan struct{} // 配置重载触发通道(如prometheus.yml更新时)
metrics *scrapeMetrics // 采集相关指标(如scrape_duration_seconds)
}
关键说明:
# 这里举个例子来讲,更容易懂
scrape_configs:
- job_name: "node_exporter" # job 名称作为键
static_configs:
- targets: ["localhost:9100"]
scrape_interval: 15s
- job_name: "prometheus" # 另一个 job
static_configs:
- targets: ["localhost:9090"]
- scrapeConfigs:存储所有 job 的抓取配置信息。
- 这些配置会被解析后存储到scrapeConfigs中,以job_name为键(如node_exporter、prometheus),每个值对应一个config.ScrapeConfig结构体,包含该job的所有配置细节(目标列表、抓取间隔、超时时间等)
- scrapePools:每个 job 对应一个 scrapePool 实例,用于实际执行抓取操作。
- 例如,上述配置会生成两个scrapePool:
- 键为node_exporter的池,管理对localhost:9100的抓取任务;
- 键为prometheus的池,管理对localhost:9090的抓取任务。
- scrapePool是运行时实体,负责实际调度该job下的目标抓取、处理抓取结果等。
- 例如,上述配置会生成两个scrapePool:
- targetSets:从服务发现模块获取的目标列表,scrapePool 根据这些目标进行抓取。。
- scrapePools:每个job对应一个scrapePool实例,管理该job下的所有监控目标。
- graceShut:优雅关闭信号,确保在停止时能够正确清理资源。
- triggerReload:触发重新加载配置的通道。
- 当prometheus.yml被修改并触发重载时,通过该通道通知Manager重新解析配置。
- 当prometheus.yml被修改并触发重载时,通过该通道通知Manager重新解析配置。
- metrics:采集相关的指标。
- 对应prometheus.yml中global区块的监控指标配置(如evaluation_interval),以及scrape_configs中与指标相关的设置(如metric_relabel_configs),用于记录抓取成功率、耗时等指标。
关键字段联动说明:
配置变更处理流程说明:
在 Prometheus 3.4.0 中,scrape/manager.go 的 ApplyConfig 方法是处理配置变更的核心入口,负责将新的抓取配置(scrape_configs)应用到运行时,实现 scrapePool 的动态更新。
其核心目标是:在不中断整体监控的前提下,增量更新抓取任务,处理新增、修改、删除的 Job 配置。
4.2.1、ApplyConfig 方法的核心流程1 —— 加锁保护并发资源
https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/manager.go#L243
由于 Manager 的 scrapeConfigs、scrapePools 等字段会被多个 goroutine 并发访问(如抓取任务、配置重载、服务发现更新等),首先需要通过互斥锁 mtxScrape 确保配置更新的原子性:
m.mtxScrape.Lock() defer m.mtxScrape.Unlock()
这一步是并发控制的关键,避免在配置更新过程中,其他操作(如目标抓取、服务发现推送目标)对共享资源的干扰。
4.2.2、ApplyConfig 方法的核心流程2 —— 处理抓取失败日志记录器(https://github.com/prometheus/prometheus/blob/main/scrape/manager.go#L260)
Prometheus 支持对抓取失败的目标进行日志记录(通过 scrape_failure_log 配置)。ApplyConfig 需要同步更新 scrapeFailureLoggers:
-
- 构建新配置的 scrapeFailureLoggers 映射:遍历新配置的每个 Job(scfg),为需要日志记录的 Job 创建或复用 scrapeFailureLoggers。
- 清理旧的 scrapeFailureLoggers:对于旧配置中存在但新配置中已删除的 Job 对应的 scrapeFailureLogger,关闭它们并从 scrapeFailureLoggers 中移除。
// 构建新配置的 scrapeFailureLoggers 映射 scrapeFailureLoggers := map[string]FailureLogger{ "": nil, // 空文件名对应 nil 日志记录器 } for _, scfg := range scfgs { // 按 scrape_failure_log 配置的文件路径区分日志记录器 if _, ok := scrapeFailureLoggers[scfg.ScrapeFailureLogFile]; !ok { var ( logger FailureLogger err error ) // 若有创建日志记录器的函数,则新建 if m.newScrapeFailureLogger != nil { logger, err = m.newScrapeFailureLogger(scfg.ScrapeFailureLogFile) if err != nil { return err } } scrapeFailureLoggers[scfg.ScrapeFailureLogFile] = logger } } // 清理旧的 scrapeFailureLoggers(关闭不再使用的日志记录器) oldScrapeFailureLoggers := m.scrapeFailureLoggers for _, s := range oldScrapeFailureLoggers { if s != nil { defer s.Close() // 延迟关闭旧日志记录器 } } // 更新 scrapeFailureLoggers 映射 m.scrapeFailureLoggers = scrapeFailureLoggers // 为现有 scrape pool 关联新的日志记录器 for poolName, pool := range m.scrapePools { cfg, ok := m.scrapeConfigs[poolName] if ok { if l, ok := m.scrapeFailureLoggers[cfg.ScrapeFailureLogFile]; ok { pool.SetScrapeFailureLogger(l) } else { pool.logger.Error("No logger found. This is a bug in Prometheus that should be reported upstream.", "scrape_pool", poolName) } } }
4.2.3、ApplyConfig 方法的核心流程3 —— 构建新配置的映射(以 JobName 为键)
https://github.com/prometheus/prometheus/blob/main/scrape/manager.go#L259
将输入的新配置列表 scfgs 转换为以 job_name 为键的映射(newScrapeConfigs),方便后续与旧配置对比:
func (m *Manager) ApplyConfig(cfg *config.Config) error {
……
// 构建新配置的映射(以 job_name 为键)
c := make(map[string]*config.ScrapeConfig)
// ...(处理抓取失败日志器,略)
for _, scfg := range scfgs {
c[scfg.JobName] = scfg // 新配置映射:job_name -> ScrapeConfig
}
m.scrapeConfigs = c // 更新 Manager 的配置映射为新配置
……
}
4.2.4、ApplyConfig 方法的核心流程4 —— 处理删除的 Job(新配置中不存在的 Job)
https://github.com/prometheus/prometheus/blob/main/scrape/manager.go#L317
当遍历旧的 scrapePools 时,若某个 Job(poolName)在新配置(m.scrapeConfigs)中不存在(!ok),则执行:
- sp.stop():停止该 Job 对应的 scrapePool(终止其管理的所有抓取循环、释放资源);
- toDelete.Store(name, struct{}{}):将该 Job 标记为待删除;
- 最终通过 toDelete.Range 从 m.scrapePools 中删除该 Job 的记录。
https://github.com/prometheus/prometheus/blob/main/scrape/manager.go#L317 case !ok: // 新配置中无此 job sp.stop() // 停止旧的 scrapePool toDelete.Store(name, struct{}{}) // 标记为待删除
4.2.5、ApplyConfig 方法的核心流程5 —— 处理删除的 Job(新旧配置不一致)
https://github.com/prometheus/prometheus/blob/main/scrape/manager.go#L320
若 Job 在新配置中存在(ok),但新旧配置内容不一致(通过 reflect.DeepEqual 对比 sp.config 和新 cfg),则:
- 调用 sp.reload(cfg):重载 scrapePool,内部会根据新配置重建抓取逻辑(如更新抓取间隔、目标处理规则等);
- 若重载失败,记录错误并标记配置应用失败。
case !reflect.DeepEqual(sp.config, cfg): // 配置有变化 err := sp.reload(cfg) // 重载 scrapePool if err != nil { m.logger.Error("error reloading scrape pool", "err", err, "scrape_pool", name) failed.Store(true) } fallthrough // 继续执行下方的日志器更新
4.2.6、ApplyConfig 方法的核心流程5 —— 处理新增 Job(新配置有,旧的配置无)
https://github.com/prometheus/prometheus/blob/main/scrape/manager.go#L126
https://github.com/prometheus/prometheus/blob/main/scrape/manager.go#L177
ApplyConfig 方法仅处理旧有 Job 的删除和修改,新增 Job 的 scrapePool 创建由 reload 方法完成。当新配置中的 Job 在旧 scrapePools 中不存在时:
- 服务发现模块推送目标集(tsets)后,triggerReload 通道被触发,调用 reload 方法;
- reload 方法遍历 targetSets 时,若发现某个 Job(setName)的 scrapePool 不存在,则创建新的 scrapePool。
// Run 接收来自服务发现模块的目标组更新,并触发采集任务的重新加载。 // 该方法在一个独立的 goroutine 中运行,负责监听目标变更并异步触发重载, // 以确保不会阻塞目标更新的接收,从而提升系统的响应性和稳定性。 // // 参数 tsets:一个只读 channel,持续接收从 discovery.Manager 发送过来的 // map[job_name][]*targetgroup.Group 类型的目标集合更新。 // 返回值:当 manager 被优雅关闭时返回 nil。 func (m *Manager) Run(tsets <-chan map[string][]*targetgroup.Group) error { // 启动一个后台 goroutine,专门负责处理配置重载逻辑(即重新构建 scrapePools)。 // 这样可以将“接收目标更新”和“执行重载”解耦,避免阻塞关键路径。 go m.reloader() // 进入主事件循环,持续监听目标更新和关闭信号。 for { // 使用 select 监听多个 channel 事件 select { // 情况 1:从 tsets channel 接收到新的目标组更新 case ts, ok := <-tsets: // 检查 channel 是否已关闭(非正常情况,通常不会发生) if !ok { // 如果 channel 关闭,跳出 select,随后退出 for 循环 break } // 将接收到的新目标集合并保存到 m.targetSets 中(内存中缓存) // updateTsets 是线程安全的操作,会加锁保护 m.targetSets m.updateTsets(ts) // 尝试向 triggerReload channel 发送一个信号,触发重载流程 // 使用 select + default 实现非阻塞发送: // - 如果 triggerReload 有空位,立即写入 struct{}{} // - 如果 channel 已满(表示重载已在排队),则跳过,防止阻塞 // 这是一种“节流”机制,避免频繁的目标更新导致过多的重载请求。 select { case m.triggerReload <- struct{}{}: // 成功触发重载 default: // 重载请求已存在,无需重复触发,直接丢弃本次信号 }
- m.reloader() 是一个长期运行的 goroutine,它监听 m.triggerReload channel,一旦收到信号就调用 m.ApplyConfig() 或直接同步 scrapePools。
- updateTsets(ts) 只是暂存目标,真正的目标同步(Sync)是在 reloader 中触发 scrapePool.Sync() 完成的。
- 这种设计使得 “接收目标” 和 “应用目标” 解耦,提升了系统的鲁棒性与可扩展性。
func (m *Manager) reload() 方法 是 scrape manager 实现动态目标更新的核心逻辑之一,负责根据最新的 targetSets 为每个 job 创建或复用 scrapePool,并触发目标同步Sync。
// reload 是 scrape manager 的核心重载方法。
// 它在后台 goroutine 中被调用(通常由 m.reloader() 触发),用于:
// - 为新增的 job 创建新的 scrapePool
// - 对所有 job 的 scrapePool 执行目标同步(Sync)
// 注意:此方法在执行期间会持有 m.mtxScrape 锁,确保配置和 scrapePools 的一致性。
func (m *Manager) reload() {
// 获取互斥锁,防止在重载过程中发生并发的配置变更或目标更新
m.mtxScrape.Lock()
// 声明一个 WaitGroup,用于等待所有并发的 Sync 操作完成
var wg sync.WaitGroup
// 遍历当前所有已知的 target set(即 job_name -> []*targetgroup.Group 映射)
// m.targetSets 是通过 m.updateTsets() 从服务发现模块更新进来的
for setName, groups := range m.targetSets {
// 检查当前 job 是否已有对应的 scrapePool
if _, ok := m.scrapePools[setName]; !ok {
// 如果没有,则尝试从 m.scrapeConfigs 中查找该 job 的配置
scrapeConfig, ok := m.scrapeConfigs[setName]
if !ok {
// 如果配置不存在,说明这是一个无效的 job 名称,记录错误并跳过
m.logger.Error("error reloading target set", "err", "invalid config id:"+setName)
continue
}
// 检查 histogram 转换与 created timestamp 零值摄入的兼容性问题
// 这是一个临时限制(TODO),防止因 issue #15137 导致数据异常
if scrapeConfig.ConvertClassicHistogramsToNHCBEnabled() && m.opts.EnableCreatedTimestampZeroIngestion {
m.logger.Error("error reloading target set",
"err", "cannot convert classic histograms to native histograms with custom buckets and ingest created timestamp zero samples at the same time due to https://github.com/prometheus/prometheus/issues/15137")
continue
}
// 自增指标:记录成功创建的 scrape pool 数量
m.metrics.targetScrapePools.Inc()
// 根据 scrapeConfig 创建一个新的 scrapePool 实例
// 参数包括:配置、追加接口、偏移种子、日志、缓冲池、选项、指标等
sp, err := newScrapePool(
scrapeConfig,
m.append,
m.offsetSeed,
m.logger.With("scrape_pool", setName), // 为每个 pool 添加上下文日志标签
m.buffers,
m.opts,
m.metrics,
)
if err != nil {
// 如果创建失败,记录失败指标和错误日志
m.metrics.targetScrapePoolsFailed.Inc()
m.logger.Error("error creating new scrape pool", "err", err, "scrape_pool", setName)
continue
}
// 将新创建的 scrapePool 存入 m.scrapePools 映射中,以后可通过 job_name 访问
m.scrapePools[setName] = sp
// 尝试为新 pool 设置抓取失败日志记录器(如果配置了 scrape_failure_log_file)
if l, ok := m.scrapeFailureLoggers[scrapeConfig.ScrapeFailureLogFile]; ok {
sp.SetScrapeFailureLogger(l)
} else {
// 理论上不应该发生:配置了日志文件但找不到 logger
// 属于 bug 级别的警告,应上报
sp.logger.Error("No logger found. This is a bug in Prometheus that should be reported upstream.",
"scrape_pool", setName)
}
}
// 到这里说明 m.scrapePools[setName] 已存在(无论是旧的还是刚创建的)
// 准备对这个 scrapePool 执行 Sync(groups) 操作
// 增加 WaitGroup 计数,表示有一个并发任务将要启动
wg.Add(1)
// 使用 goroutine 并发执行 Sync 操作,提升高负载下的响应速度
// 因为 Sync 可能涉及大量目标的增删、loop 启动/停止,耗时较长
go func(sp *scrapePool, groups []*targetgroup.Group) {
// 调用 scrapePool 的 Sync 方法:
// - 对比现有 activeTargets 与新目标列表
// - 新增目标 → 创建 scrapeLoop 并启动
// - 删除目标 → 停止并清理 scrapeLoop
sp.Sync(groups)
// 当 Sync 完成后,通知 WaitGroup
wg.Done()
}(m.scrapePools[setName], groups)
}
// 所有 scrapePool 的 Sync 启动完成后,释放锁
// 注意:此时 Sync 可能仍在后台执行,但不再持有 m.mtxScrape
m.mtxScrape.Unlock()
// 等待所有并发的 Sync 操作全部完成
// 这保证了本次 reload 的完整性,在此之前不会开始下一次 reload
wg.Wait()
}
4.3、scrapePool 和 scrapeLoop 的关系
在 Prometheus 中,scrapePool 和 scrapeLoop 是数据采集(scraping)流程中两个核心组件,二者为管理与被管理的层级关系,共同协作完成对目标实例(target)的指标抓取工作。
-
-
- scrapePool(抓取池):是管理单元,对应配置文件中一个 scrape_config(即一个 “job”),负责管理该 job 下所有目标实例的生命周期和配置统筹。
- scrapeLoop(抓取循环):是执行单元,每个 scrapeLoop 对应一个具体的目标实例(instance),负责按周期实际执行指标抓取、解析、处理等操作。
-
简单说:一个 scrapePool 管理多个 scrapeLoop,每个 scrapeLoop 对应一个目标实例。
详细关系与交互流程:
- scrapePool 的角色
-
对应配置:每个 scrape_config(如 job_name: "node_exporter")会被解析为一个 scrapePool,包含该 job 的所有配置(如抓取间隔 scrape_interval、超时时间 scrape_timeout、目标列表 static_configs 或服务发现配置 kubernetes_sd_configs 等)。
-
目标管理:负责通过服务发现(如 k8s、Consul 等)动态获取目标实例列表,或维护静态配置的目标列表。当目标新增 / 移除时,scrapePool 会相应地创建 / 销毁对应的 scrapeLoop。
-
配置统筹:统一管理该 job 下所有 scrapeLoop 的共享配置(如抓取间隔、标签重写规则 relabel_configs 等),确保所有子 scrapeLoop 遵循一致的规则。
-
状态监控:聚合该 job 下所有 scrapeLoop 的抓取状态(如成功率、耗时等),作为 Prometheus 自身监控指标(如 prometheus_target_sync_length_seconds)的数据源。
-
- scrapeLoop 的角色
-
绑定目标:每个 scrapeLoop 对应一个具体的目标实例(如 10.0.0.1:9100),从 scrapePool 继承配置并专注于该目标的抓取逻辑。
-
周期执行:按照 scrapePool 定义的 scrape_interval 周期性触发抓取:
- 建立 HTTP 连接,发送抓取请求(如 GET /metrics);
- 处理响应(解析指标、应用标签重写、过滤无效指标等);
- 将处理后的指标传递给 Prometheus 的存储层(本地时序数据库)。
- 错误处理:独立处理单个目标的抓取错误(如超时、连接失败),并记录状态(如 up 指标标记目标是否存活),不影响同scrapePool 下其他 scrapeLoop 的执行。
-
五、scrapePool 与 scrapeLoop:采集任务的“管理层”与“执行层”
https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go
在 Prometheus 3.4.0 的数据采集架构中,scrapePool 与 scrapeLoop 构成了“任务池-执行单元”的经典分层模型。二者分工明确、协作紧密,共同实现了对海量监控目标的高效、稳定、动态管理。
本节将围绕其设计哲学、核心交互机制与生命周期管理,深入剖析这一分层架构如何支撑 Prometheus 的高可扩展性与强健性。
5.1、分层解耦
scrapePool 与 scrapeLoop 的设计体现了典型的“控制平面”与“数据平面”分离思想:
-
- scrapePool(控制平面):作为 job 级别的管理中枢,它不直接参与 HTTP 请求或指标解析,而是专注于策略制定、资源统筹与生命周期协调。它持有该 job 的完整配置(config.ScrapeConfig)、共享的 HTTP 客户端(client)、目标状态映射(activeTargets)以及并发控制机制。其核心职责是“管人、管事、管资源”。
- scrapeLoop(数据平面):作为 target 级别的执行引擎,它不关心全局配置或兄弟 loop 的状态,而是专注于单个目标的周期性抓取任务。它从所属的 scrapePool 继承配置,独立执行 HTTP 请求、响应解析、标签重写与数据提交。其核心职责是“干活、报错、自保”。
这种分层设计带来了显著优势:
-
- 配置一致性:所有 scrapeLoop 共享 scrapePool 的 config,确保同一 job 下所有目标遵循相同的 scrape_interval、timeout、relabel_configs 等规则。
- 资源复用:scrapePool 创建的 http.Client 被其管理的所有 scrapeLoop 复用,避免了为每个目标单独创建连接池,显著降低了资源开销。
- 动态伸缩:scrapePool 可以根据服务发现推送的目标列表,动态创建或销毁 scrapeLoop,实现对目标规模的弹性响应,而无需重启整个 job。
5.2、核心交互机制:Sync 方法的核心流程
https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go#L459
https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go#L505
scrapePool.Sync 方法是 scrapePool 与 scrapeLoop 协同工作的核心接口,它实现了目标列表的增量同步,确保运行时状态与配置期望一致。
5.2.1、scrapePool 结构体定义
// scrapePool 用于管理一组目标的 scrape(指标抓取)任务
type scrapePool struct {
appendable storage.Appendable // 用于存储抓取到的指标数据的接口
logger *slog.Logger // 日志记录器,用于输出 scrapePool 相关日志
cancel context.CancelFunc // 上下文取消函数,用于终止 scrapePool 的运行
httpOpts []config_util.HTTPClientOption // HTTP 客户端选项,用于配置抓取请求的客户端
// 注意:获取 mtx 锁后不能再获取 targetMtx 锁(避免死锁)
mtx sync.Mutex // 保护 config、client 等字段的互斥锁
config *config.ScrapeConfig // 当前 scrapePool 对应的抓取配置
client *http.Client // 用于发送 HTTP 抓取请求的客户端
loops map[uint64]loop // 存储目标对应的 scrape 循环,key 为目标的哈希值
symbolTable *labels.SymbolTable // 标签符号表,用于优化标签存储和比较
lastSymbolTableCheck time.Time // 上次检查符号表的时间
initialSymbolTableLen int // 符号表初始长度
targetMtx sync.Mutex // 保护 activeTargets、droppedTargets 等目标相关字段的互斥锁
// activeTargets 和 loops 必须始终保持同步(具有相同的哈希集)
activeTargets map[uint64]*Target // 当前活跃的目标,key 为目标的哈希值
droppedTargets []*Target // 被丢弃的目标(受 KeepDroppedTargets 配置限制)
droppedTargetsCount int // 所有被丢弃的目标总数
newLoop func(scrapeLoopOptions) loop // 用于创建新 scrape 循环的构造函数(便于测试)
metrics *scrapeMetrics // 用于记录 scrapePool 相关指标的结构体
scrapeFailureLogger FailureLogger // 抓取失败日志记录器
scrapeFailureLoggerMtx sync.RWMutex // 保护 scrapeFailureLogger 的读写锁
validationScheme model.ValidationScheme // 指标验证方案
escapingScheme model.EscapingScheme // 标签转义方案
}
5.2.2、Sync 方法(这里注意了,这是大写的 S)
- 目标解析与处理:接收目标组(targetGroups),解析出有效目标和失败目标,记录失败日志并更新失败指标;
- 目标分类:将标签非空的目标归为有效目标,标签为空的归为丢弃目标(受配置的保留数量限制);
- 指标更新:更新符号表项目数指标,记录同步耗时和同步次数指标;
- 调用 sync 方法:将有效目标传入 sync 方法,完成实际的抓取循环同步;
- 符号表清理:检查并清理符号表,避免内存泄漏。
// Sync 将目标组(targetGroups)转换为实际的 scrape 目标,
// 并将当前运行的 scraper 与结果集同步,返回所有已抓取和被丢弃的目标
func (sp *scrapePool) Sync(tgs []*targetgroup.Group) {
sp.mtx.Lock() // 加锁保护 config 等字段
defer sp.mtx.Unlock() // 方法结束时解锁
start := time.Now() // 记录同步开始时间
sp.targetMtx.Lock() // 加锁保护目标相关字段
var all []*Target // 存储所有有效目标
var targets []*Target // 临时存储从目标组解析的目标
// 创建带有符号表的标签构建器,优化标签处理
lb := labels.NewBuilderWithSymbolTable(sp.symbolTable)
// 重置被丢弃的目标列表和计数
sp.droppedTargets = []*Target{}
sp.droppedTargetsCount = 0
// 遍历所有目标组,解析并处理目标
for _, tg := range tgs {
// 从目标组中解析目标,返回解析后的目标和失败列表
targets, failures := TargetsFromGroup(tg, sp.config, targets, lb)
// 记录解析失败的日志
for _, err := range failures {
sp.logger.Error("Creating target failed", "err", err)
}
// 更新目标同步失败的指标(按 job 维度)
sp.metrics.targetSyncFailed.WithLabelValues(sp.config.JobName).Add(float64(len(failures)))
// 处理每个解析后的目标
for _, t := range targets {
// 检查目标标签是否非空(避免生成垃圾,手动遍历替代 t.Labels().IsEmpty())
nonEmpty := false
t.LabelsRange(func(_ labels.Label) { nonEmpty = true })
switch {
case nonEmpty:
// 标签非空的目标加入有效目标列表
all = append(all, t)
default:
// 标签为空的目标视为被丢弃
// 若配置允许保留被丢弃的目标,且未超过保留上限,则加入丢弃列表
if sp.config.KeepDroppedTargets == 0 || uint(len(sp.droppedTargets)) < sp.config.KeepDroppedTargets {
sp.droppedTargets = append(sp.droppedTargets, t)
}
// 累加被丢弃目标的总数
sp.droppedTargetsCount++
}
}
}
// 更新符号表项目数的指标(按 job 维度)
sp.metrics.targetScrapePoolSymbolTableItems.WithLabelValues(sp.config.JobName).Set(float64(sp.symbolTable.Len()))
sp.targetMtx.Unlock() // 解锁目标相关字段
// 同步有效目标(启动新循环、停止旧循环)
sp.sync(all)
// 检查并清理符号表(避免内存泄漏)
sp.checkSymbolTable()
// 记录同步耗时的指标(按 job 维度)
sp.metrics.targetSyncIntervalLength.WithLabelValues(sp.config.JobName).Observe(
time.Since(start).Seconds(),
)
// 累加同步次数的指标(按 job 维度)
sp.metrics.targetScrapePoolSyncsCounter.WithLabelValues(sp.config.JobName).Inc()
}
5.2.3、sync 方法(这里是小写的 s)
- 目标去重:通过目标哈希值去重,确保每个目标只对应一个抓取循环;
- 新目标处理:为新目标(哈希不在活跃列表中)创建抓取器(targetScraper)和抓取循环(loop),配置抓取参数(间隔、超时、限制等),添加到活跃目标和循环映射中;
- 旧目标处理:对已消失的目标(哈希不在新列表中),停止其抓取循环并从活跃列表中移除,等待所有旧循环终止;
- 指标与限制检查:更新当前目标数量指标,检查是否超过目标数量限制并设置强制错误;
- 启动新循环:启动所有新创建的抓取循环,开始指标抓取。
// sync 处理可能重复的目标列表,去重后为新目标启动 scrape 循环,
// 为已消失的目标停止 scrape 循环,并等待所有停止的循环终止
func (sp *scrapePool) sync(targets []*Target) {
var (
uniqueLoops = make(map[uint64]loop) // 存储唯一的 scrape 循环,key 为目标哈希
interval = time.Duration(sp.config.ScrapeInterval) // 抓取间隔(从配置获取)
timeout = time.Duration(sp.config.ScrapeTimeout) // 抓取超时时间(从配置获取)
bodySizeLimit = int64(sp.config.BodySizeLimit) // 响应体大小限制
sampleLimit = int(sp.config.SampleLimit) // 样本数量限制
bucketLimit = int(sp.config.NativeHistogramBucketLimit) // 原生直方图桶限制
// 根据原生直方图最小桶因子选择最大 schema
maxSchema = pickSchema(sp.config.NativeHistogramMinBucketFactor)
// 标签限制配置(数量、名称长度、值长度)
labelLimits = &labelLimits{
labelLimit: int(sp.config.LabelLimit),
labelNameLengthLimit: int(sp.config.LabelNameLengthLimit),
labelValueLengthLimit: int(sp.config.LabelValueLengthLimit),
}
honorLabels = sp.config.HonorLabels // 是否保留目标自带的标签
honorTimestamps = sp.config.HonorTimestamps // 是否使用目标的时间戳
enableCompression = sp.config.EnableCompression // 是否启用压缩
trackTimestampsStaleness = sp.config.TrackTimestampsStaleness // 是否跟踪时间戳陈旧性
mrc = sp.config.MetricRelabelConfigs // 指标重标签配置
// 降级的抓取协议的媒体类型
fallbackScrapeProtocol = sp.config.ScrapeFallbackProtocol.HeaderMediaType()
alwaysScrapeClassicHist = sp.config.AlwaysScrapeClassicHistograms // 是否始终抓取传统直方图
// 是否将传统直方图转换为原生直方图计数桶
convertClassicHistToNHCB = sp.config.ConvertClassicHistogramsToNHCBEnabled()
)
sp.targetMtx.Lock() // 加锁保护活跃目标相关字段
// 遍历所有目标,处理新目标或更新已有目标
for _, t := range targets {
hash := t.hash() // 计算目标的哈希值(用于去重和标识)
// 若目标不在活跃目标中,说明是新目标,需要创建 scrape 循环
if _, ok := sp.activeTargets[hash]; !ok {
// 从目标中获取实际的抓取间隔和超时时间(可能被重标签修改)
var err error
interval, timeout, err = t.intervalAndTimeout(interval, timeout)
// 创建目标抓取器(负责实际发送 HTTP 请求抓取指标)
s := &targetScraper{
Target: t, // 目标实例
client: sp.client, // HTTP 客户端
timeout: timeout, // 抓取超时时间
bodySizeLimit: bodySizeLimit, // 响应体大小限制
acceptHeader: acceptHeader(sp.config.ScrapeProtocols, sp.escapingScheme), // 接受的媒体类型头
acceptEncodingHeader: acceptEncodingHeader(enableCompression), // 接受的编码头
metrics: sp.metrics, // 指标结构体
}
// 创建新的 scrape 循环(使用配置的循环构造函数)
l := sp.newLoop(scrapeLoopOptions{
target: t,
scraper: s,
sampleLimit: sampleLimit,
bucketLimit: bucketLimit,
maxSchema: maxSchema,
labelLimits: labelLimits,
honorLabels: honorLabels,
honorTimestamps: honorTimestamps,
enableCompression: enableCompression,
trackTimestampsStaleness: trackTimestampsStaleness,
mrc: mrc,
interval: interval,
timeout: timeout,
alwaysScrapeClassicHist: alwaysScrapeClassicHist,
convertClassicHistToNHCB: convertClassicHistToNHCB,
fallbackScrapeProtocol: fallbackScrapeProtocol,
})
// 若获取间隔/超时时间出错,设置循环的强制错误
if err != nil {
l.setForcedError(err)
}
// 设置循环的抓取失败日志记录器
l.setScrapeFailureLogger(sp.scrapeFailureLogger)
// 将新目标添加到活跃目标和循环映射中
sp.activeTargets[hash] = t
sp.loops[hash] = l
// 记录唯一循环(用于后续启动)
uniqueLoops[hash] = l
} else {
// 目标已存在(可能重复),去重处理
if _, ok := uniqueLoops[hash]; !ok {
uniqueLoops[hash] = nil // 标记为已存在,避免重复启动
}
// 更新目标的 ScrapeConfig(用于 Service Discovery 页面显示标签)
sp.activeTargets[hash].SetScrapeConfig(sp.config, t.tLabels, t.tgLabels)
}
}
var wg sync.WaitGroup // 用于等待所有旧循环停止
// 停止并移除已消失的目标及其 scrape 循环
for hash := range sp.activeTargets {
// 若目标不在新的唯一循环中,说明已消失
if _, ok := uniqueLoops[hash]; !ok {
wg.Add(1) // 增加等待计数
// 启动 goroutine 停止循环(避免阻塞)
go func(l loop) {
l.stop() // 停止循环
wg.Done() // 减少等待计数
}(sp.loops[hash])
// 从循环和活跃目标映射中删除
delete(sp.loops, hash)
delete(sp.activeTargets, hash)
}
}
sp.targetMtx.Unlock() // 解锁活跃目标相关字段
// 更新当前 scrapePool 中目标数量的指标(按 job 维度)
sp.metrics.targetScrapePoolTargetsAdded.WithLabelValues(sp.config.JobName).Set(float64(len(uniqueLoops)))
// 检查是否超过目标数量限制,生成强制错误(若有)
forcedErr := sp.refreshTargetLimitErr()
// 为所有循环设置强制错误(如超过限制)
for _, l := range sp.loops {
l.setForcedError(forcedErr)
}
// 启动所有新的 scrape 循环
for _, l := range uniqueLoops {
if l != nil { // 仅启动新创建的循环(非重复目标)
go l.run(nil)
}
}
// 等待所有已停止的循环终止
// 避免目标频繁波动时,旧循环仍在插入数据导致冲突
wg.Wait()
}
5.3、生命周期管理:从创建到销毁的完整闭环
scrapeLoop 的生命周期完全由 scrapePool 管控,形成一个清晰的闭环。
5.3.1、创建(Creation)
-
-
- 触发时机:scrapePool.Sync(https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go#L533)
发现新目标,或 scrapePool 初始化时从静态配置加载目标。
for _, t := range targets { hash := t.hash() if _, ok := sp.activeTargets[hash]; !ok { // 新目标发现,创建新的 scrapeLoop var err error interval, timeout, err = t.intervalAndTimeout(interval, timeout) s := &targetScraper{ Target: t, client: sp.client, timeout: timeout, bodySizeLimit: bodySizeLimit, acceptHeader: acceptHeader(sp.config.ScrapeProtocols, sp.escapingScheme), acceptEncodingHeader: acceptEncodingHeader(enableCompression), metrics: sp.metrics, } l := sp.newLoop(scrapeLoopOptions{ target: t, scraper: s, sampleLimit: sampleLimit, bucketLimit: bucketLimit, maxSchema: maxSchema, labelLimits: labelLimits, honorLabels: honorLabels, honorTimestamps: honorTimestamps, enableCompression: enableCompression, trackTimestampsStaleness: trackTimestampsStaleness, mrc: mrc, interval: interval, timeout: timeout, alwaysScrapeClassicHist: alwaysScrapeClassicHist, convertClassicHistToNHCB: convertClassicHistToNHCB, fallbackScrapeProtocol: fallbackScrapeProtocol, }) if err != nil { l.setForcedError(err) } l.setScrapeFailureLogger(sp.scrapeFailureLogger) sp.activeTargets[hash] = t sp.loops[hash] = l - 实例化:调用 sp.newLoop(https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go#L142)
sp.newLoop = func(opts scrapeLoopOptions) loop { // Update the targets retrieval function for metadata to a new scrape cache. cache := opts.cache if cache == nil { cache = newScrapeCache(metrics) } opts.target.SetMetadataStore(cache) return newScrapeLoop( ctx, opts.scraper, logger.With("target", opts.target), buffers, func(l labels.Labels) labels.Labels { return mutateSampleLabels(l, opts.target, opts.honorLabels, opts.mrc) }, func(l labels.Labels) labels.Labels { return mutateReportSampleLabels(l, opts.target) }, func(ctx context.Context) storage.Appender { return app.Appender(ctx) }, cache, sp.symbolTable, offsetSeed, opts.honorTimestamps, opts.trackTimestampsStaleness, opts.enableCompression, opts.sampleLimit, opts.bucketLimit, opts.maxSchema, opts.labelLimits, opts.interval, opts.timeout, opts.alwaysScrapeClassicHist, opts.convertClassicHistToNHCB, options.EnableNativeHistogramsIngestion, options.EnableCreatedTimestampZeroIngestion, options.ExtraMetrics, options.AppendMetadata, opts.target, options.PassMetadataInContext, metrics, options.skipOffsetting, sp.validationScheme, sp.escapingScheme, opts.fallbackScrapeProtocol, ) }函数(通常指向 newScrapeLoop)创建 scrapeLoop 实例。此函数接收 scrapeLoopOptions,包含 sp.config、sp.client、sp.appendable 等必要依赖。
- 注册:将新创建的 scrapeLoop(https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go#L571)
按哈希键存入 sp.loops 映射,并将目标存入 sp.activeTargets。
sp.activeTargets[hash] = t sp.loops[hash] = l
- 启动:通过 go scrapeLoop.run() 启动一个独立的 goroutine(https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go#L571),
进入周期性抓取循环。
// 更新指标:记录当前目标抓取池添加的目标数量,标签为当前任务名称,值为唯一循环的数量 sp.metrics.targetScrapePoolTargetsAdded.WithLabelValues(sp.config.JobName).Set(float64(len(uniqueLoops))) // 检查并获取由于目标数量限制导致的错误(如果有) forcedErr := sp.refreshTargetLimitErr() // 为所有循环实例设置强制错误(如果存在) for _, l := range sp.loops { l.setForcedError(forcedErr) } // 遍历所有唯一的循环实例,启动goroutine运行循环 for _, l := range uniqueLoops { if l != nil { // 确保循环实例不为空,避免空指针错误 go l.run(nil) // 以goroutine方式启动循环的run方法,参数为nil } } - 关键代码说明
- 新目标发现:在 sp.sync() (https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go#L493)方法中遍历目标列表,检查 sp.activeTargets[hash](https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go#L533) 是否存在
- 创建 scraper:为每个新目标创建 targetScraper(https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go#L772) 实例
- 实例化 loop:调用 sp.newLoop() (https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go#L99)创建 scrapeLoop 实例
- 注册管理:将目标存入 sp.activeTargets(https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go#L571),将 loop 存入 sp.loops(https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go#L572)
- 启动运行:通过 go l.run(nil) (https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go#L611)启动独立的 goroutine 执行抓取循环
- 这个设计确保了每个目标都有独立的抓取循环,并且整个生命周期由 scrapePool 统一管理,形成了清晰的创建-注册-启动的闭环流程。
- 触发时机:scrapePool.Sync(https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go#L533)
-
5.3.2、运行(Execution)
该运行机制确保了每个目标都能按照配置的间隔时间进行周期性抓取,并且具备完善的错误处理和监控能力。
- 定时器启动:使用 time.NewTicker(sl.interval) 创建定时器
- 偏移等待:通过 sl.scraper.offset() 实现抓取时间偏移,避免所有目标同时抓取
- 主循环执行:在 mainLoop 中循环执行抓取操作
- HTTP 抓取:调用 sl.scraper.scrape() 发送 HTTP 请求
- 响应处理:读取响应内容,支持 gzip 压缩
- 数据解析:调用 sl.append() 解析和存储指标数据
- 错误处理:处理抓取失败、解析错误等各种异常情况
- 指标报告:记录抓取持续时间、样本数量等监控指标
5.3.2.1、主运行循环:run()方法
https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go#L1321
// 运行抓取循环,errc用于传递错误信息(可选)
func (sl *scrapeLoop) run(errc chan<- error) {
// 如果不需要跳过时间偏移,则等待指定的偏移时间后再开始
if !sl.skipOffsetting {
select {
// 等待计算出的偏移时间(基于抓取间隔和偏移种子)
case <-time.After(sl.scraper.offset(sl.interval, sl.offsetSeed)):
// 偏移时间过后继续执行
// 如果父上下文被取消,则关闭stopped通道并返回
case <-sl.ctx.Done():
close(sl.stopped)
return
}
}
// 记录上一次抓取时间
var last time.Time
// 初始化对齐的抓取时间为当前时间(精确到纳秒)
alignedScrapeTime := time.Now().Round(0)
// 创建一个定时器,按照指定的抓取间隔触发
ticker := time.NewTicker(sl.interval)
// 函数退出时停止定时器,释放资源
defer ticker.Stop()
// 主循环Loop,用于跳出多层循环
mainLoop:
for {
select {
// 如果父上下文被取消,关闭stopped通道并返回
case <-sl.parentCtx.Done():
close(sl.stopped)
return
// 如果当前上下文被取消,跳出主循环
case <-sl.ctx.Done():
break mainLoop
// 默认情况,继续执行循环体
default:
}
// 临时解决Go定时器抖动导致TSDB磁盘空间增长的问题
// 参考:https://github.com/prometheus/prometheus/issues/7846
// 调用Round确保使用的是 wall clock 时间,否则time.Time的.Sub和.Add方法行为会不同(见time包文档)
scrapeTime := time.Now().Round(0)
// 如果启用了抓取时间戳对齐功能
if AlignScrapeTimestamps {
// 容差限制为抓取间隔的最大1%
tolerance := min(sl.interval/100, ScrapeTimestampTolerance)
// 某些情况下定时器可能跳过了一个tick,这时需要多次累加间隔时间
for scrapeTime.Sub(alignedScrapeTime) >= sl.interval {
alignedScrapeTime = alignedScrapeTime.Add(sl.interval)
}
// 如果当前时间在容差范围内,则将抓取时间对齐到alignedScrapeTime
if scrapeTime.Sub(alignedScrapeTime) <= tolerance {
scrapeTime = alignedScrapeTime
}
}
// 执行抓取并报告结果,更新上一次抓取时间
last = sl.scrapeAndReport(last, scrapeTime, errc)
select {
// 父上下文被取消,关闭stopped通道并返回
case <-sl.parentCtx.Done():
close(sl.stopped)
return
// 当前上下文被取消,跳出主循环
case <-sl.ctx.Done():
break mainLoop
// 等待定时器触发,进入下一次循环
case <-ticker.C:
}
}
// 关闭stopped通道,通知其他协程该循环已停止
close(sl.stopped)
// 如果启用了运行结束时的陈旧性标记功能
if !sl.disabledEndOfRunStalenessMarkers {
// 处理运行结束时的陈旧性标记
sl.endOfRunStaleness(last, ticker, sl.interval)
}
}
5.3.2.2、核心抓取逻辑:scrapeAndReport() 方法
https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go#L1393
// scrapeAndReport 执行一次抓取操作,然后将结果追加到存储中,同时报告指标,
// 尽可能少地使用appender(数据追加器)。在正常情况下,只会使用一个appender。
// 注意:此函数特意使用sl.appenderCtx而非sl.ctx,因为抓取操作只应在关闭时被取消,而非重新加载时
func (sl *scrapeLoop) scrapeAndReport(last, appendTime time.Time, errc chan<- error) time.Time {
// 记录当前函数开始执行的时间
start := time.Now()
// 只在第一次抓取之后记录指标(last不为零值时)
if !last.IsZero() {
// 记录目标抓取间隔长度的指标:标签为间隔时间字符串,值为当前时间与上一次抓取时间的差值(秒)
sl.metrics.targetIntervalLength.WithLabelValues(sl.interval.String()).Observe(
time.Since(last).Seconds(),
)
}
// 定义统计变量:total(总样本数)、added(新增样本数)、seriesAdded(新增时间序列数)、bytesRead(读取的字节数)
var total, added, seriesAdded, bytesRead int
// 定义错误变量:err(通用错误)、appErr(appender操作错误)、scrapeErr(抓取过程错误)
var err, appErr, scrapeErr error
// 获取一个appender实例(用于向存储写入数据),使用appenderCtx作为上下文
app := sl.appender(sl.appenderCtx)
// 延迟处理:在函数退出前根据错误状态决定回滚或提交appender
defer func() {
if err != nil {
// 若存在错误,回滚appender(放弃本次写入)
app.Rollback()
return
}
// 无错误则提交appender(确认写入)
err = app.Commit()
if err != nil {
// 提交失败时记录错误日志
sl.l.Error("Scrape commit failed", "err", err)
}
}()
// 延迟处理:在函数退出前报告抓取结果(包括指标、日志等)
defer func() {
// 调用report方法报告结果,若报告失败记录警告日志
if err = sl.report(app, appendTime, time.Since(start), total, added, seriesAdded, bytesRead, scrapeErr); err != nil {
sl.l.Warn("Appending scrape report failed", "err", err)
}
}()
// 检查是否存在强制错误(如目标数超限等配置限制导致的错误)
if forcedErr := sl.getForcedError(); forcedErr != nil {
// 将抓取错误设为强制错误
scrapeErr = forcedErr
// 添加陈旧标记(stale markers,用于标记时间序列已过时)
if _, _, _, err := sl.append(app, []byte{}, "", appendTime); err != nil {
// 若添加失败,回滚当前appender并重新获取一个新的appender
app.Rollback()
app = sl.appender(sl.appenderCtx)
sl.l.Warn("Append failed", "err", err)
}
// 若存在错误通道,将强制错误发送出去
if errc != nil {
errc <- forcedErr
}
// 返回本次函数开始时间,结束流程
return start
}
// 定义响应相关变量:contentType(响应内容类型)、resp(HTTP响应)、b(响应字节数据)、buf(字节缓冲区)
var contentType string
var resp *http.Response
var b []byte
var buf *bytes.Buffer
// 创建带超时的抓取上下文(超时时间为配置的sl.timeout)
scrapeCtx, cancel := context.WithTimeout(sl.parentCtx, sl.timeout)
// 执行实际的抓取操作(通过scraper获取目标数据)
resp, scrapeErr = sl.scraper.scrape(scrapeCtx)
if scrapeErr == nil {
// 若抓取成功,从缓冲区池获取一个字节数组(大小为上一次抓取的大小,优化内存使用)
b = sl.buffers.Get(sl.lastScrapeSize).([]byte)
// 使用完后将缓冲区放回池
defer sl.buffers.Put(b)
// 创建字节缓冲区
buf = bytes.NewBuffer(b)
// 读取响应内容到缓冲区,并获取内容类型
contentType, scrapeErr = sl.scraper.readResponse(scrapeCtx, resp, buf)
}
// 取消抓取上下文(释放资源)
cancel()
// 处理抓取成功的情况
if scrapeErr == nil {
// 获取缓冲区中的字节数据
b = buf.Bytes()
// 注意:过去存在客户端偶尔返回空结果的问题,我们不希望这种情况错误地重置缓冲区大小
if len(b) > 0 {
// 更新上一次抓取的大小(用于下一次缓冲区分配优化)
sl.lastScrapeSize = len(b)
}
// 记录读取的字节数
bytesRead = len(b)
} else {
// 抓取失败时,记录调试日志
sl.l.Debug("Scrape failed", "err", scrapeErr)
// 读取失败日志器的锁(避免并发问题)
sl.scrapeFailureLoggerMtx.RLock()
// 若存在失败日志器,记录错误
if sl.scrapeFailureLogger != nil {
slog.New(sl.scrapeFailureLogger).Error(scrapeErr.Error())
}
// 释放锁
sl.scrapeFailureLoggerMtx.RUnlock()
// 若存在错误通道,发送抓取错误
if errc != nil {
errc <- scrapeErr
}
// 若错误是由于响应体大小超限,标记字节数为-1
if errors.Is(scrapeErr, errBodySizeLimit) {
bytesRead = -1
}
}
// 失败的抓取等同于空抓取,仍需调用sl.append触发陈旧标记
total, added, seriesAdded, appErr = sl.append(app, b, contentType, appendTime)
if appErr != nil {
// 若append失败(可能是解析错误或样本数超限),回滚当前appender并重新获取
app.Rollback()
app = sl.appender(sl.appenderCtx)
sl.l.Debug("Append failed", "err", appErr)
// 再次调用append,传入空数据以触发陈旧标记
if _, _, _, err := sl.append(app, []byte{}, "", appendTime); err != nil {
app.Rollback()
app = sl.appender(sl.appenderCtx)
sl.l.Warn("Append failed", "err", err)
}
}
// 若抓取本身无错误,但append有错误,则将appErr赋值给scrapeErr
if scrapeErr == nil {
scrapeErr = appErr
}
// 返回本次抓取开始时间
return start
}
关键代码解读
- 核心功能:
- scrapeAndReport 是 Prometheus 抓取目标数据的核心函数,负责执行抓取、处理响应、写入存储、记录指标和错误处理的完整流程。
- 指标记录:
- 通过 sl.metrics.targetIntervalLength 记录两次抓取的间隔时间,用于监控抓取频率是否符合预期。
- 最终通过 sl.report 方法汇总本次抓取的统计信息(如样本数、字节数、耗时等),更新相关监控指标。
- Appender 机制:
- 使用 sl.appender() 获取数据追加器,用于将抓取到的样本写入存储(如 TSDB)。
- 通过 defer 确保在函数退出前处理 appender 的提交(Commit)或回滚(Rollback),保证数据一致性。
- 若中间过程出现错误,会回滚当前 appender 并重新创建,避免错误累积。
- 错误处理:
- 强制错误(forcedErr):如目标数超限等配置限制导致的错误,此时会添加陈旧标记并终止抓取。
- 抓取错误(scrapeErr):如网络超时、响应解析失败等,会记录日志并通过 errc 通道向外传递。
- Append 错误(appErr):如样本格式错误、超出存储限制等,会重试 append 空数据以触发陈旧标记,确保过时序列被正确标记。
- 性能优化:
- 使用缓冲区池(sl.buffers)管理字节数组,避免频繁分配内存,优化性能。
- 通过 sl.lastScrapeSize 记录上一次抓取的响应大小,下一次从池中获取相近大小的缓冲区,减少内存浪费。
- 陈旧标记(Stale Markers):
- 无论抓取成功与否,都会调用 sl.append 方法,确保为未更新的时间序列添加陈旧标记,这是 Prometheus 处理数据时效性的关键机制,避免展示过时数据。
5.3.2.3、HTTP抓取实现:targetScraper.scrape()
https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go#L821
// scrape 方法执行实际的 HTTP 抓取操作,返回 HTTP 响应或错误
func (s *targetScraper) scrape(ctx context.Context) (*http.Response, error) {
// 检查是否已经创建过 HTTP 请求,如果没有则创建
if s.req == nil {
// 创建一个 GET 方法的 HTTP 请求,URL 为目标地址
req, err := http.NewRequest(http.MethodGet, s.URL().String(), nil)
if err != nil {
// 创建请求失败时返回错误
return nil, err
}
// 设置请求头:指定接受的数据类型
req.Header.Add("Accept", s.acceptHeader)
// 设置请求头:指定接受的编码方式(通常用于压缩)
req.Header.Add("Accept-Encoding", s.acceptEncodingHeader)
// 设置请求头:用户代理标识(Prometheus 客户端)
req.Header.Set("User-Agent", UserAgent)
// 设置请求头:告知目标服务本次抓取的超时时间(秒)
req.Header.Set("X-Prometheus-Scrape-Timeout-Seconds", strconv.FormatFloat(s.timeout.Seconds(), 'f', -1, 64))
// 将创建好的请求保存到结构体中,避免重复创建
s.req = req
}
// 使用带上下文的请求执行 HTTP 客户端请求,返回响应结果
// 上下文用于支持超时控制和取消操作
return s.client.Do(s.req.WithContext(ctx))
}
5.3.2.4、响应读取:targetScraper.readResponse()
https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go#L838
// readResponse 读取HTTP响应内容并写入指定的io.Writer,返回内容类型和可能的错误
// 参数:_ context.Context(未使用的上下文),resp HTTP响应,w 用于写入响应内容的输出流
func (s *targetScraper) readResponse(_ context.Context, resp *http.Response, w io.Writer) (string, error) {
// 延迟执行:确保响应体被完全读取并关闭,避免资源泄漏
defer func() {
// 将剩余的响应体内容拷贝到丢弃流(确保连接可重用)
io.Copy(io.Discard, resp.Body)
// 关闭响应体
resp.Body.Close()
}()
// 检查HTTP响应状态码是否为200 OK,非200则返回错误
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("server returned HTTP status %s", resp.Status)
}
// 如果未设置响应体大小限制,将其设为最大整数值(实际上无限制)
if s.bodySizeLimit <= 0 {
s.bodySizeLimit = math.MaxInt64
}
// 处理非gzip编码的响应
if resp.Header.Get("Content-Encoding") != "gzip" {
// 将响应体内容(限制在bodySizeLimit内)拷贝到输出流
n, err := io.Copy(w, io.LimitReader(resp.Body, s.bodySizeLimit))
if err != nil {
return "", err
}
// 如果读取的字节数达到或超过限制,记录指标并返回错误
if n >= s.bodySizeLimit {
s.metrics.targetScrapeExceededBodySizeLimit.Inc()
return "", errBodySizeLimit
}
// 返回响应的Content-Type
return resp.Header.Get("Content-Type"), nil
}
// 处理gzip编码的响应
// 如果gzip读取器未初始化,则创建它
if s.gzipr == nil {
// 创建带缓冲的读取器包装响应体
s.buf = bufio.NewReader(resp.Body)
var err error
// 创建gzip读取器
s.gzipr, err = gzip.NewReader(s.buf)
if err != nil {
return "", err
}
} else {
// 重用已有的缓冲读取器和gzip读取器
s.buf.Reset(resp.Body)
// 重置gzip读取器以处理新的响应体
if err := s.gzipr.Reset(s.buf); err != nil {
return "", err
}
}
// 将解压后的gzip内容(限制在bodySizeLimit内)拷贝到输出流
n, err := io.Copy(w, io.LimitReader(s.gzipr, s.bodySizeLimit))
// 关闭gzip读取器
s.gzipr.Close()
if err != nil {
return "", err
}
// 如果读取的字节数达到或超过限制,记录指标并返回错误
if n >= s.bodySizeLimit {
s.metrics.targetScrapeExceededBodySizeLimit.Inc()
return "", errBodySizeLimit
}
// 返回响应的Content-Type
return resp.Header.Get("Content-Type"), nil
}
关键代码解读
- 核心功能:readResponse 是 Prometheus 处理抓取响应的关键函数,负责读取 HTTP 响应内容(支持普通文本和 gzip 压缩)、验证响应状态、限制响应大小,并将内容写入指定输出流。
- 资源管理:使用 defer 确保响应体被完全消费(io.Copy(io.Discard, resp.Body))和关闭(resp.Body.Close()),这是 HTTP 客户端最佳实践,可避免连接泄漏并确保 TCP 连接可重用。
- 响应验证:首先检查 HTTP 状态码是否为 200 OK,非 200 状态会直接返回错误,确保只处理正常响应。
- 大小限制机制:
- 通过 bodySizeLimit 限制读取的响应体大小,防止超大响应耗尽内存。
- 使用 io.LimitReader 包装响应体,确保不会读取超过限制的内容。
- 若达到大小限制,会递增 targetScrapeExceededBodySizeLimit 指标并返回特定错误 errBodySizeLimit,便于监控和排查问题。
- 压缩处理:
- 支持处理 gzip 编码的响应,通过 gzip.Reader 自动解压内容。
- 采用对象重用机制(s.gzipr 和 s.buf),避免频繁创建和解压相关对象,优化性能。
- 灵活性设计:
- 通过 io.Writer 接口接收输出目标,使函数可以将响应内容写入任意实现了该接口的对象(如内存缓冲区、文件等),增强了代码的复用性。
- 动态处理内容编码类型(普通文本 /gzip),适应不同目标服务的响应格式。
5.4、并发控制与资源隔离
scrapePool 通过多种机制平衡性能与稳定性:
-
- 锁分离:使用 mtx 保护 config、client 等池级状态,使用 targetMtx 保护 activeTargets、loops 等目标级状态,减少锁竞争。
- 并发限制:虽然 scrapePool 本身不直接限制并发数,但可通过 scrape_config 的 scrape_limit(在 config 中)间接控制该 job 下的目标数量,从而影响并发采集数。
- 错误隔离:单个 scrapeLoop 的抓取失败(如超时、解析错误)不会影响同 scrapePool 下其他 scrapeLoop 的运行,故障被限制在局部。
5.5、scrapePool 数据结构核心字段介绍
https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go
每个 job 对应一个scrapePool,负责该 job 下所有目标的采集调度与资源控制。
// scrapePool 管理一组目标(targets)的抓取(scrape)任务。
// 它负责创建、维护和停止针对这些目标的抓取循环(scrape loops)。
type scrapePool struct {
// appendable 是一个接口,用于接收抓取到的样本数据并将其追加到存储系统中(如 TSDB)。
appendable storage.Appendable
// logger 用于记录此 scrapePool 的日志信息。
logger *slog.Logger
// cancel 用于取消与此 scrapePool 相关的所有后台上下文操作(如 scrape loops)。
cancel context.CancelFunc
// httpOpts 存储创建 HTTP 客户端时使用的选项,例如 TLS 配置、代理设置等。
httpOpts []config_util.HTTPClientOption
// mtx 是一个互斥锁,用于保护 scrapePool 的内部状态。
// 注释提醒:在代码中获取 mtx 锁时,不能在已经持有 targetMtx 的情况下再获取 mtx,以避免死锁。
mtx sync.Mutex
// config 指向当前 scrapePool 使用的抓取配置(*config.ScrapeConfig),定义了如何抓取目标。
config *config.ScrapeConfig
// client 是用于执行 HTTP 请求以抓取目标的 HTTP 客户端。
client *http.Client
// loops 是一个映射,存储当前正在运行的抓取循环(loop),键是目标的哈希值。
loops map[uint64]loop
// symbolTable 是标签(labels)符号表,用于高效地管理和去重标签键和值。
symbolTable *labels.SymbolTable
// lastSymbolTableCheck 记录上一次检查 symbolTable 的时间,用于定期清理或更新。
lastSymbolTableCheck time.Time
// initialSymbolTableLen 记录 symbolTable 的初始长度,可能用于监控或性能分析。
initialSymbolTableLen int
// targetMtx 是一个互斥锁,用于保护 activeTargets、droppedTargets 等与目标相关的字段。
// 注释提醒:mtx 不应在 targetMtx 之后获取,以避免死锁。
targetMtx sync.Mutex
// activeTargets 存储当前活跃的目标(*Target),键是目标的哈希值。
// activeTargets 和 loops 必须始终保持同步,即拥有相同的哈希集合。
activeTargets map[uint64]*Target
// droppedTargets 存储被丢弃的目标列表(*Target),这些目标因配置原因未被监控。
// 列表长度受 KeepDroppedTargets 配置项限制。
droppedTargets []*Target
// droppedTargetsCount 记录所有曾经被丢弃的目标总数(可能超过 droppedTargets 列表长度)。
droppedTargetsCount int
// newLoop 是一个可替换的构造函数,用于创建新的抓取循环(loop)。
// 这个字段被设计为可设置的,主要是为了方便单元测试时注入模拟的 loop 实现。
newLoop func(scrapeLoopOptions) loop
// metrics 指向一组用于监控 scrapePool 性能和状态的指标(如抓取次数、失败次数等)。
metrics *scrapeMetrics
// scrapeFailureLogger 用于记录抓取失败的日志。
// 当前设计允许在运行时动态替换此记录器。
scrapeFailureLogger FailureLogger
// scrapeFailureLoggerMtx 读写锁,用于在并发环境下安全地读取和写入 scrapeFailureLogger。
scrapeFailureLoggerMtx sync.RWMutex
// validationScheme 定义了抓取到的样本数据在存储前需要进行的验证规则(如标签格式、时间戳有效性等)。
validationScheme model.ValidationScheme
// escapingScheme 定义了标签值在序列化或显示时的转义方式(如处理特殊字符)。
escapingScheme model.EscapingScheme
}
关键说明:
- appendable (百度翻译: 英 [əˈpend] 美 [əˈpɛnd]
- 作用: 接收抓取到的样本数据并将其追加到存储系统中(如 TSDB)。
- 配置: 不直接在 prometheus.yml 中配置,而是由 Prometheus 内部初始化。
- logger (百度翻译: 英 [ˈlɒɡə(r)] 美 [ˈlɔːɡər]
- 作用: 记录此 scrapePool 的日志信息。
- 配置: 日志级别和格式可以通过 global.log_level 和 global.log_format 进行配置。
- cancel (百度翻译: 英 [ˈkænsl] 美 [ˈkænsəl]
- 作用: 取消与此 scrapePool 相关的所有后台上下文操作(如 scrape loops)。
- 配置: 不直接在 prometheus.yml 中配置,而是由 Prometheus 内部管理。
- httpOpts (百度翻译: 英 [hʌtˌɑːpt] 美 [hʌtˌɑːpt]
- 作用: 存储创建 HTTP 客户端时使用的选项,例如 TLS 配置、代理设置等。
- 配置: 通过 scrape_configs[].scheme, scrape_configs[].tls_config, scrape_configs[].proxy_url 等进行配置。
- mtx (百度翻译: 英 [emˈtiːks] 美 [emˈtiːks]
- 作用: 互斥锁,用于保护 scrapePool 的内部状态。
- 配置: 不直接在 prometheus.yml 中配置,而是由 Prometheus 内部使用。
- config (百度翻译: 英 [ˈkɒnfɪg] 美 [ˈkɑːnfɪɡ]
- 作用: 指向当前 scrapePool 使用的抓取配置(*config.ScrapeConfig),定义了如何抓取目标。
- 配置: 在 prometheus.yml 中通过 scrape_configs[] 数组进行详细配置,包括 job_name, honor_labels, metrics_path, params, static_configs, relabel_configs 等。
- client (百度翻译: 英 [klɪˈent] 美 [klɪˈent]
- 作用: 用于执行 HTTP 请求以抓取目标的 HTTP 客户端。
- 配置: 不直接在 prometheus.yml 中配置,而是根据 httpOpts 初始化。
- loops (百度翻译: 英 [lʊps] 美 [lʊps]
- 作用: 存储当前正在运行的抓取循环(loop),键是目标的哈希值。
- 配置: 不直接在 prometheus.yml 中配置,而是由 Prometheus 根据 scrape_configs 动态创建。
- symbolTable (百度翻译: 英 [ˈsɪmbəltəbəl] 美 [ˈsɪmbəlbəl]
- 作用: 标签(labels)符号表,用于高效地管理和去重标签键和值。
- 配置: 不直接在 prometheus.yml 中配置,而是由 Prometheus 内部维护。
- lastSymbolTableCheck (百度翻译: 英 [ˈlæst sɪmbəltəbəl tʃek] 美 [ˈlæst sɪmbəlbəl tʃɛk]
- 作用: 记录上一次检查 symbolTable 的时间,用于定期清理或更新。
- 配置: 不直接在 prometheus.yml 中配置,而是由 Prometheus 内部管理。
- initialSymbolTableLen (百度翻译: 英 [ɪˈnɪʃəl sɪmbəltəbəl lɛŋθ] 美 [ɪˈnɪʃəl sɪmbəlbəl læŋθ]
- 作用: 记录 symbolTable 的初始长度,可能用于监控或性能分析。
- 配置: 不直接在 prometheus.yml 中配置,而是由 Prometheus 内部初始化。
- targetMtx (百度翻译: 英 [ˈtɑːrɡɪtmɪks] 美 [ˈtɑːrtɪmtɪks]
- 作用: 互斥锁,用于保护 activeTargets, droppedTargets 等与目标相关的字段。
- 配置: 不直接在 prometheus.yml 中配置,而是由 Prometheus 内部使用。
- activeTargets (百度翻译: 英 [ˈæktɪv tɑːrɡɪts] 美 [ˈæktɪv tɑːrtɪts]
- 作用: 存储当前活跃的目标(*Target),键是目标的哈希值。
- 配置: 在 prometheus.yml 中通过 scrape_configs[].static_configs, scrape_configs[].file_sd_configs, scrape_configs[].kubernetes_sd_configs 等动态发现机制进行配置。
- droppedTargets (百度翻译: 英 [ˈdrɒpt tɑːrɡɪts] 美 [ˈdrɒpt tɑːrtɪts]
- 作用: 存储被丢弃的目标列表(*Target),这些目标因配置原因未被监控。
- 配置: 主要通过 relabel_configs 控制哪些目标会被保留或丢弃。
- droppedTargetsCount (百度翻译: 英 [ˈdrɒpt tɑːrɡɪts kaʊnt] 美 [ˈdrɒpt tɑːrtɪts kaʊnt]
- 作用: 记录所有曾经被丢弃的目标总数(可能超过 droppedTargets 列表长度)。
- 配置: 不直接在 prometheus.yml 中配置,而是由 Prometheus 内部统计。
- newLoop (百度翻译: 英 [nuː lʊp] 美 [nuː lʊp]
- 作用: 可替换的构造函数,用于创建新的抓取循环(loop)。主要用于测试场景。
- 配置: 不直接在 prometheus.yml 中配置,而是由 Prometheus 内部使用。
- metrics (百度翻译: 英 [ˈmɛtrɪks] 美 [ˈmɛtrɪks]
- 作用: 指向一组用于监控 scrapePool 性能和状态的指标(如抓取次数、失败次数等)。
- 配置: 不直接在 prometheus.yml 中配置,而是由 Prometheus 内部初始化。
关键字段联动说明:
- job_name 对应于 scrapePool.config.jobName(https://github.com/prometheus/prometheus/blob/v3.4.0/config/config.go#L669)。
- static_configs 定义了要抓取的目标列表,对应于 scrapePool.activeTargets。
- relabel_configs 用于重新标记标签,影响哪些目标会被实际抓取,从而影响 scrapePool.droppedTargets。
- scrapePool 通过 Sync 方法(https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/scrape.go)管理 scrapeLoop 的生命周期:
- 当服务发现推送新目标组时,Sync方法对比现有activeTargets与新目标列表,计算新增、删除的目标。
- 对新增目标,调用newLoop创建scrapeLoop实例并加入loops映射,同时启动协程执行loop.run()。
- 对删除的目标,通过哈希键从loops中找到对应的scrapeLoop,调用loop.stop()终止采集,并从activeTargets和loops中移除。
- scrapePool实现了对scrapeLoop的集中管理,确保目标增删时采集任务的动态调整,同时通过锁机制保证并发安全性。

浙公网安备 33010602011771号