Prometheus源码专题【左扬精讲】—— 监控系统 Prometheus 3.4.0 源码解析:Discovery 动态服务发现机制
https://github.com/prometheus/prometheus/tree/v3.4.2/discovery/
一、引言
二、动态服务发现的核心 ——Discovery 管理器
2.1、核心概念
2.1.1、Discovery 接口
https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/discovery.go#L35
Discoverer 接口:定义了发现 TargetGroup 的基本行为。实现该接口的组件(如 Consul、DNS 等)负责发现目标组,并在检测到潜在变化时通过通道发送更新后的目标组。在 https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/discovery.go#L35 中,Discoverer 接口定义如下:
// Discoverer 接口定义了发现监控目标组的规范,它维护一组来源,从中可以获取TargetGroup信息 // 当发现提供者检测到潜在变更时,会通过通道发送TargetGroup // 注意:Discoverer并不保证每次发送的TargetGroup都实际发生了变化 // 但确保只要发生变化,就会发送新的TargetGroup // 并且Discoverer实现应该在启动时发送一次完整的可发现TargetGroup集合 type Discoverer interface {// Run方法用于启动发现过程,它接收一个context和一个用于发送更新的通道// 参数说明:// - ctx: 用于控制发现过程的生命周期,当ctx被取消时,Run方法应该返回// - up: 发现者通过这个通道向消费者发送更新后的target group列表// 实现要求:// - 必须在ctx被取消时立即返回// - 不得关闭up通道,由调用者负责关闭// 典型工作流程:// 1. 初始化时发送一次全量的target groups// 2. 持续监听来源变化(如服务注册中心、文件系统等)// 3. 检测到变化时发送增量或全量更新Run(ctx context.Context, up chan<- []*targetgroup.Group) }
关键说明:
-
-
-
- 异步通信机制:通过 Go 的 channel 实现异步通信,避免阻塞
- 上下文控制:使用 context.Context 实现优雅的取消机制
- 单向数据流:up chan<- 表示只写通道,确保数据流向的一致性
- 批量更新:一次可以发送多个目标组 []*targetgroup.Group
-
-
2.1.2、Config 接口
https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/discovery.go#L85
Config 接口:每个服务发现机制都需要实现 Config 接口:
// Config 接口定义了发现器的配置规范,负责提供配置信息和创建发现器实例 type Config interface {// Name 返回该发现机制的名称(如"consul"、"kubernetes"等)// 该名称用于配置文件识别和日志标识Name() string// NewDiscoverer 根据配置创建并返回一个Discoverer实例// 参数 opts 包含发现器运行所需的通用选项(如日志、客户端等)// 返回的Discoverer实例将负责实际的服务发现工作NewDiscoverer(opts DiscovererOptions) (Discoverer, error)// NewDiscovererMetrics 创建并返回该发现机制使用的指标收集器// 参数 registerer 用于注册指标,instantiator 用于创建刷新相关指标// 实现应返回一个DiscovererMetrics接口的实现,用于监控发现过程NewDiscovererMetrics(registerer prometheus.Registerer, instantiator RefreshMetricsInstantiator) DiscovererMetrics }// Configs 是 Config 接口的切片类型,提供了自定义YAML编解码功能 // 它允许将多种不同类型的发现配置组合在一起,并序列化为YAML格式 // 反序列化时会根据配置类型自动映射到对应的Config实现 type Configs []Config
关键说明:
-
-
- Config 接口:
- 解耦了配置数据和发现器实现
- 通过工厂模式(NewDiscoverer)创建具体发现器
- 支持为每种发现机制定制监控指标
- 关键方法作用:
- Name(): 作为配置类型的唯一标识,用于配置文件解析,也是通过该方法标识不同的发现机制
- NewDiscoverer(): 实现依赖注入,将通用选项注入到具体发现器
- NewDiscovererMetrics(): 支持为不同发现机制定制监控指标
- Configs 切片:
- 支持混合多种发现机制配置(如同时使用 Consul 和 Kubernetes 发现)
- 通过自定义 YAML 编解码实现灵活的配置格式
- 典型用法:在 Prometheus 配置文件中定义多个发现源
- Config 接口:
-
例如,在 https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/nomad/nomad.go#L86 中,SDConfig 结构体实现了 Config 接口,通过 NewDiscoverer 方法创建了 Discovery 实例,该实例实现了 Discoverer 接口:
// SDConfig is the configuration for nomad based service discovery. type SDConfig struct {AllowStale bool `yaml:"allow_stale"`HTTPClientConfig config.HTTPClientConfig `yaml:",inline"`Namespace string `yaml:"namespace"`RefreshInterval model.Duration `yaml:"refresh_interval"`Region string `yaml:"region"`Server string `yaml:"server"`TagSeparator string `yaml:"tag_separator,omitempty"` }// NewDiscovererMetrics implements discovery.Config. func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {return newDiscovererMetrics(reg, rmi) }// Name returns the name of the Config. func (*SDConfig) Name() string { return "nomad" }// NewDiscoverer returns a Discoverer for the Config. func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {return NewDiscovery(c, opts.Logger, opts.Metrics) }
例如,在 https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/consul/consul.go#L90 中,SDConfig 结构体实现了 Config 接口,通过 NewDiscoverer 方法创建了 Discovery 实例,该实例实现了 Discoverer 接口:
// SDConfig is the configuration for Consul service discovery. type SDConfig struct {Server string `yaml:"server,omitempty"`PathPrefix string `yaml:"path_prefix,omitempty"`Token config.Secret `yaml:"token,omitempty"`Datacenter string `yaml:"datacenter,omitempty"`Namespace string `yaml:"namespace,omitempty"`Partition string `yaml:"partition,omitempty"`TagSeparator string `yaml:"tag_separator,omitempty"`Scheme string `yaml:"scheme,omitempty"`Username string `yaml:"username,omitempty"`Password config.Secret `yaml:"password,omitempty"`// See https://www.consul.io/docs/internals/consensus.html#consistency-modes,// stale reads are a lot cheaper and are a necessity if you have >5k targets.AllowStale bool `yaml:"allow_stale"`// By default use blocking queries (https://www.consul.io/api/index.html#blocking-queries)// but allow users to throttle updates if necessary. This can be useful because of "bugs" like// https://github.com/hashicorp/consul/issues/3712 which cause an un-necessary// amount of requests on consul.RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"`// See https://www.consul.io/api/catalog.html#list-services// The list of services for which targets are discovered.// Defaults to all services if empty.Services []string `yaml:"services,omitempty"`// A list of tags used to filter instances inside a service. Services must contain all tags in the list.ServiceTags []string `yaml:"tags,omitempty"`// Desired node metadata. As of Consul 1.14, consider `filter` instead.NodeMeta map[string]string `yaml:"node_meta,omitempty"`// Consul filter string// See https://www.consul.io/api-docs/catalog#filtering-1, for syntaxFilter string `yaml:"filter,omitempty"`HTTPClientConfig config.HTTPClientConfig `yaml:",inline"` }// NewDiscovererMetrics implements discovery.Config. func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {return newDiscovererMetrics(reg, rmi) }// Name returns the name of the Config. func (*SDConfig) Name() string { return "consul" }// NewDiscoverer returns a Discoverer for the Config. func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {return NewDiscovery(c, opts.Logger, opts.Metrics) }
2.1.3、目标组 (TargetGroup)
https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/targetgroup/targetgroup.go#L24
目标组 (TargetGroup):目标组是服务发现的基本数据单元,其结构体定义如下:
// Group 表示一组具有共同标签集合的目标(如生产环境、测试环境、预发环境等)。 type Group struct {// Targets 是由标签集合标识的目标列表。// 组内的每个目标通过其address标签唯一标识。Targets []model.LabelSet// Labels 是该组内所有目标共享的标签集合。Labels model.LabelSet// Source 是一个标识符,用于描述一组目标的来源。Source string }
2.1.3、供应者(Provider)
https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/manager.go#L37
// Provider 管理一个 Discoverer 实例及其生命周期,包括配置、取消函数和订阅者。 type Provider struct {name string // 提供者的唯一名称d Discoverer // 负责服务发现的实例config interface{} // 存储发现器的配置信息cancel context.CancelFunc // 取消函数,用于终止发现器的运行// done 在取消提供者并清理相关资源后调用done func()mu sync.RWMutex // 读写锁,保护并发访问subs map[string]struct{} // 当前订阅者集合(使用空结构体节省内存)// newSubs 用于临时存储配置重新加载完成后要使用的订阅者newSubs map[string]struct{} }// Discoverer 返回提供者持有的服务发现实例 func (p *Provider) Discoverer() Discoverer {return p.d }// IsStarted 判断服务发现实例是否已启动 // 通过检查 cancel 函数是否存在来确定 func (p *Provider) IsStarted() bool {return p.cancel != nil }// Config 返回提供者的配置信息 func (p *Provider) Config() interface{} {return p.config }
2.2、架构设计模式
2.2.1、插件化架构
Prometheus 的服务发现采用了插件化架构(https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/discovery.go#L139):
// A StaticConfig is a Config that provides a static list of targets. // StaticConfig 是一个实现了 Config 接口的结构体,用于提供静态的目标列表。 type StaticConfig []*targetgroup.Group// Name returns the name of the service discovery mechanism. // Name 方法返回服务发现机制的名称,该名称用于在配置文件和日志标记等场景中唯一标识此发现机制。 // 这里返回 "static",表示这是静态服务发现机制。 func (StaticConfig) Name() string { return "static" }// NewDiscoverer returns a Discoverer for the Config. // NewDiscoverer 方法根据提供的 StaticConfig 和 DiscovererOptions 创建并返回一个 Discoverer 实例。 // 参数 DiscovererOptions 包含了创建 Discoverer 所需的一些选项,如日志记录器、指标注册器等。 // 这里将 StaticConfig 转换为 staticDiscoverer 类型并返回,不返回错误。 func (c StaticConfig) NewDiscoverer(DiscovererOptions) (Discoverer, error) {return staticDiscoverer(c), nil }// NewDiscovererMetrics returns NoopDiscovererMetrics because no metrics are // needed for this service discovery mechanism. // NewDiscovererMetrics 方法返回用于服务发现机制的指标对象。 // 由于静态服务发现机制不需要额外的指标(如失败计数、持续时间等),所以返回 NoopDiscovererMetrics 实例。 // NoopDiscovererMetrics 是一个空操作的指标实现,不会记录任何实际的指标数据。 // 参数 prometheus.Registerer 是用于注册指标的注册器,RefreshMetricsInstantiator 是用于实例化刷新指标的实例化器。 func (c StaticConfig) NewDiscovererMetrics(prometheus.Registerer, RefreshMetricsInstantiator) DiscovererMetrics {return &NoopDiscovererMetrics{} }
关键说明:
-
-
-
- 统一接口:所有发现机制都实现相同的接口
- 配置驱动:通过配置文件指定使用哪种发现机制
- 热插拔:可以在运行时动态添加或移除发现机制
-
-
2.2.1、管理器模式
Manager 类负责协调多个 Discoverer(https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/manager.go#L143):
// Manager 维护一组服务发现提供者,并将每次更新发送到映射通道。 // 目标按目标集名称分组管理。 type Manager struct {logger *slog.Logger // 日志记录器name string // 管理器名称httpOpts []config.HTTPClientOption // HTTP客户端配置选项mtx sync.RWMutex // 保护管理器状态的读写锁ctx context.Context // 管理生命周期的上下文// 部分发现器(如k8s)仅发送目标组的增量更新,// 使用 map[tg.Source]*targetgroup.Group 跟踪需更新的组targets map[poolKey]map[string]*targetgroup.GrouptargetsMtx sync.Mutex // 保护targets的并发访问// providers 维护所有注册的服务发现提供者providers []*Provider// syncCh 作为同步通道,将更新以映射形式发送,键为抓取配置中的job值syncCh chan map[string][]*targetgroup.Group// updatert 控制发送更新到通道前的等待时间,仅在单元测试中修改updatert time.Duration// triggerSend 通道用于触发更新发送,当接收到提供者的新更新时发出信号triggerSend chan struct{}// lastProvider 记录管理器生命周期内注册的提供者总数lastProvider uint// registerer 用于注册所有服务发现指标registerer prometheus.Registerermetrics *Metrics // 管理器自身指标sdMetrics map[string]DiscovererMetrics // 各发现器的指标 }// Providers 返回当前配置的所有服务发现提供者 func (m *Manager) Providers() []*Provider {return m.providers }// UnregisterMetrics 注销管理器指标。注意: // 不注销服务发现或刷新指标,这些指标生命周期独立管理 func (m *Manager) UnregisterMetrics() {m.metrics.Unregister(m.registerer) }// Run 启动管理器的后台处理逻辑: // 1. 启动sender协程处理更新发送 // 2. 等待上下文结束信号 // 3. 取消所有发现器的运行 // 4. 返回上下文错误(通常是取消原因) func (m *Manager) Run() error {go m.sender() // 启动更新发送协程<-m.ctx.Done() // 等待上下文结束m.cancelDiscoverers() // 取消所有发现器return m.ctx.Err() // 返回取消原因 }
-
- 结构体字段注释详细解释了各组件的用途和设计意图
- 对复杂数据结构如 targets 进行了特别说明,解释了两级映射的使用逻辑
- 方法注释明确了功能、实现逻辑和注意事项
- 对并发控制机制和生命周期管理关键点进行了说明
- 解释了指标管理的特殊处理方式(为何部分指标不注销)
- 对 Run 方法的执行流程进行了步骤分解说明
三、总体工作流程
3.1、创建 Discovery 管理器
使用 NewManager(https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/manager.go#L84) 函数创建一个 Discovery 管理器实例。该函数接受上下文、日志记录器、注册器、服务发现指标等参数,并初始化管理器的各种属性。
// NewManager 是 Discovery Manager 的构造函数,用于创建和初始化服务发现管理器。 // 参数说明: // - ctx: 控制管理器生命周期的上下文 // - logger: 日志记录器,若为nil则使用空实现 // - registerer: Prometheus指标注册器 // - sdMetrics: 服务发现指标集合 // - options: 可选配置函数,用于自定义管理器行为 func NewManager(ctx context.Context, logger *slog.Logger, registerer prometheus.Registerer, sdMetrics map[string]DiscovererMetrics, options ...func(*Manager)) *Manager {// 确保日志记录器不为空,避免空指针异常if logger == nil {logger = promslog.NewNopLogger()}// 初始化管理器基础配置mgr := &Manager{logger: logger, // 设置日志记录器syncCh: make(chan map[string][]*targetgroup.Group), // 创建同步通道targets: make(map[poolKey]map[string]*targetgroup.Group), // 初始化目标组映射ctx: ctx, // 设置上下文updatert: 5 * time.Second, // 默认更新间隔为5秒triggerSend: make(chan struct{}, 1), // 创建带缓冲的触发通道registerer: registerer, // 设置指标注册器sdMetrics: sdMetrics, // 设置服务发现指标}// 应用所有可选配置函数,允许自定义管理器行为for _, option := range options {option(mgr)}// 注册管理器指标(必须在所有选项设置完成后执行,确保管理器名称已设置)metrics, err := NewManagerMetrics(registerer, mgr.name)if err != nil {logger.Error("Failed to create discovery manager metrics", "manager", mgr.name, "err", err)return nil}mgr.metrics = metrics // 保存指标收集器return mgr // 返回初始化完成的管理器实例 }
3.2、ApplyConfig
https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/manager.go#L204
使用 ApplyConfig 函数应用新的配置。该函数会检查现有的发现提供者,并根据新的配置启动或停止相应的提供者。
// ApplyConfig 检查是否已有配置的发现提供者正在运行,保留它们不变。 // 剩余提供者将被停止,并使用提供的配置启动新的所需提供者。 func (m *Manager) ApplyConfig(cfg map[string]Configs) error {m.mtx.Lock()defer m.mtx.Unlock()var failedCount int// 遍历配置中的每个名称及其对应的配置集合for name, scfg := range cfg {// 注册提供者并累加失败计数failedCount += m.registerProviders(scfg, name)}// 设置失败配置的指标值m.metrics.FailedConfigs.Set(float64(failedCount))var (wg sync.WaitGroup // 用于等待提供者停止的等待组newProviders []*Provider // 存储需要保留的提供者)// 遍历当前所有提供者,处理保留、停止和更新逻辑for _, prov := range m.providers {// 取消过时的提供者(没有新订阅者的)if len(prov.newSubs) == 0 {wg.Add(1)prov.done = func() {wg.Done()}prov.cancel() // 触发提供者的取消逻辑continue}// 保留需要的提供者newProviders = append(newProviders, prov)// 用于引用目标的映射,为新订阅者提供初始目标状态var refTargets map[string]*targetgroup.Groupprov.mu.Lock() // 锁定提供者以修改其状态m.targetsMtx.Lock() // 锁定目标映射以进行并发安全修改// 处理旧订阅者的目标for s := range prov.subs {refTargets = m.targets[poolKey{s, prov.name}]// 移除过时订阅者的目标if _, ok := prov.newSubs[s]; !ok {delete(m.targets, poolKey{s, prov.name})m.metrics.DiscoveredTargets.DeleteLabelValues(m.name, s)}}// 为新订阅者设置指标和目标for s := range prov.newSubs {if _, ok := prov.subs[s]; !ok {m.metrics.DiscoveredTargets.WithLabelValues(s).Set(0)}// 复制引用目标到新订阅者if l := len(refTargets); l > 0 {m.targets[poolKey{s, prov.name}] = make(map[string]*targetgroup.Group, l)for k, v := range refTargets {m.targets[poolKey{s, prov.name}][k] = v}}}m.targetsMtx.Unlock()// 更新提供者的订阅者集合prov.subs = prov.newSubsprov.newSubs = map[string]struct{}{} // 清空临时新订阅者集合prov.mu.Unlock()// 如果提供者尚未启动,则启动它if !prov.IsStarted() {m.startProvider(m.ctx, prov)}}// 立即触发发送更新,确保下游管理器尽快获取最新状态if len(m.providers) > 0 {select {case m.triggerSend <- struct{}{}: // 发送触发信号default: // 通道已满时跳过,避免阻塞}}// 更新提供者列表为保留的提供者m.providers = newProviderswg.Wait() // 等待所有被取消的提供者完成清理return nil }
3.2.1、核心作用:动态协调配置与服务发现提供者的关系
ApplyConfig 的 核心功能 是将新的服务发现配置应用到运行中的系统,通过对比新旧配置,实现“保留有用的、清理过时的、启动新增的”服务发现提供者(Provider),同时维护目标(target)的状态一致性。具体可拆解为以下几个关键作用:
3.2.1.1、维护配置与运行时的一致性
Prometheus 的服务发现配置(如 Kubernetes 服务发现规则、Consul 地址、文件路径等)可能通过热更新(如 reload 信号)动态修改。ApplyConfig 需要确保:
-
- 已有的 Provider 中,配置未变化的继续运行(避免重复创建销毁,减少资源消耗);
- 新配置中新增的 Provider 被启动(如新增了一个 Consul 服务发现源);
- 新配置中移除的 Provider 被安全停止并清理资源(如关闭连接、释放句柄)。
- 通过这种 “增量更新” 逻辑,避免了全量重启 Provider 导致的服务中断。
3.2.1.2、管理目标(target)状态的平滑过渡
服务发现的最终目的是输出 “待监控的目标列表”(targets),这些目标会被下游的抓取模块(scraper)使用。ApplyConfig 需要在配置变更时确保目标状态的一致性:
-
- 对于新添加的订阅者(subs,可理解为 “使用该服务发现结果的模块”),复制现有目标状态作为初始值,避免下游模块突然丢失目标;
- 对于移除的订阅者,清理其关联的目标数据,避免无效目标占用内存或导致抓取错误;
- 通过 triggerSend 立即触发目标更新,确保下游模块(如抓取器)能及时获取新配置下的目标列表。
3.2.1.3、保障系统稳定性与可观测性
-
- 资源安全清理:通过 sync.WaitGroup 等待过时 Provider 完成资源释放(如关闭 HTTP 连接、Kubernetes API 客户端),避免内存泄漏或资源句柄泄露;
- 指标跟踪:更新 FailedConfigs(配置失败计数)、DiscoveredTargets(发现的目标数)等指标,帮助用户监控服务发现的健康状态(如通过 Grafana 面板查看配置是否生效、目标是否正常发现);
- 并发安全:通过 sync.RWMutex 和 sync.Mutex 保护配置更新、Provider 状态修改、目标数据读写等操作,避免多线程环境下的竞态条件(如配置更新时同时修改目标列表导致的数据错乱)。
3.2.2、为什么需要这一步?
Prometheus 作为动态监控系统,其服务发现模块必须满足两个核心需求:动态适配环境变化和无中断运行。ApplyConfig 正是为了实现这两个需求而存在:
3.2.2.1、应对动态变化的监控目标与配置
在云原生、容器化环境中,监控目标(如 Pod、容器、服务)是动态创建 / 销毁的(例如 Kubernetes 扩缩容);同时,用户的服务发现配置也可能频繁变更(如新增一个 Consul 集群作为发现源、调整 Kubernetes 的标签选择器)。如果没有 ApplyConfig,每次配置变更都需要重启 Prometheus 才能生效,这会导致:
-
-
-
-
- 监控中断(重启期间无法抓取指标);
- 目标数据丢失(重启后需要重新全量发现目标,可能有延迟);
- 生产环境不可接受的停机时间。
-
-
-
ApplyConfig 实现了配置的 “热更新”,让 Prometheus 无需重启即可适配新配置。
3.2.2.2、确保服务发现的连续性与一致性
服务发现是 Prometheus 监控的“数据源入口”,其输出的目标列表直接决定了抓取模块的工作内容。如果配置更新时处理不当(如突然销毁所有 Provider 再重建),会导致:
-
-
-
-
- 下游抓取模块短暂丢失所有目标,出现 “无数据” 告警;
- 重复创建 Provider 导致资源浪费(如重复建立 Kubernetes API 连接);
- 旧配置的 Provider 未清理,继续产生无效的目标更新,干扰正常监控。
-
-
-
3.3、启动发现提供者
https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/manager.go#L296
使用 startProvider函数启动一个发现提供者。该函数会为提供者创建一个上下文和更新通道,并启动提供者的 Run 方法和更新处理函数。
// startProvider 启动指定的服务发现提供者,并设置其生命周期管理 // 参数: // - ctx: 继承的上下文,用于控制提供者的生命周期 // - p: 需要启动的提供者实例 func (m *Manager) startProvider(ctx context.Context, p *Provider) {// 记录调试日志,包含提供者名称和订阅者信息m.logger.Debug("Starting provider", "provider", p.name, "subs", fmt.Sprintf("%v", p.subs))// 创建可取消的上下文,用于控制提供者的生命周期ctx, cancel := context.WithCancel(ctx)// 创建更新通道,用于接收提供者发现的目标组更新updates := make(chan []*targetgroup.Group)// 设置提供者的取消函数,以便后续可以终止该提供者p.cancel = cancel// 启动提供者的发现逻辑:// - 在独立协程中运行提供者的Run方法// - Run方法负责持续发现目标并将更新发送到updates通道go p.d.Run(ctx, updates)// 启动更新处理器:// - 在独立协程中运行updater方法// - updater负责处理从updates通道接收到的目标更新go m.updater(ctx, p, updates) }
在 Prometheus 的服务发现(discovery)机制中,startProvider 是连接配置与实际目标发现逻辑的核心函数,其作用是将配置中定义的 "发现提供者"(如 Kubernetes、Consul、文件等不同来源的发现组件)实例化并启动,使其能够执行具体的目标探测工作。它的存在是为了确保服务发现过程能够动态、安全、有序地运行,并与 Prometheus 的整体生命周期管理协同工作。
3.3.1、核心作用
startProvider 的核心作用可以概括为:为一个 "发现提供者"(Provider)创建运行环境、启动其发现逻辑,并建立与管理器(Manager)的通信管道,使其能够持续发现目标并将结果反馈给 Prometheus 核心组件。具体可拆解为以下三点:
3.3.1.1、初始化运行环境
为发现提供者创建专用的上下文(context)和取消函数(cancel),用于精确控制提供者的生命周期(启动、运行、停止)。通过 context.WithCancel 生成的上下文,既能继承上层管理器的生命周期(如整个 Prometheus 进程的退出信号),又能独立取消(如配置更新时停止旧提供者),避免资源泄漏。
3.3.1.2、启动目标发现逻辑
启动提供者的核心发现逻辑(p.d.Run),该方法由具体的发现实现(如 kubernetes.SD、consul.SD 等)提供,负责从数据源(如 k8s API、Consul 服务端)持续探测目标(如 Pod、服务实例),并将发现的目标组(targetgroup.Group)通过 updates 通道发送出去。
3.3.2.3、建立目标更新通道与处理流程
创建 updates 通道作为发现结果的传递媒介,并启动 updater 协程处理通道中的目标更新。updater 会将新发现的目标同步到管理器的全局目标集合中,同时更新监控指标(如 DiscoveredTargets),最终让 Prometheus 知道需要抓取哪些目标的指标。
3.3.2、为什么需要这一步?
startProvider 是服务发现机制从"配置定义" 到 "实际运行" 的关键桥梁,其必要性体现在以下几个方面:
3.3.2.1、隔离与并发控制
不同的发现提供者(如同时使用 Kubernetes 和 Consul 发现)需要独立运行,避免相互干扰。startProvider 通过为每个提供者创建独立的协程(p.d.Run 和 updater)和上下文,实现了并发隔离,确保某个提供者的故障或退出不会影响其他提供者。
3.3.2.2、生命周期管理的统一性
服务发现的目标是动态的(如容器重启、服务扩缩容),且配置可能随时更新(如新增一个 Consul 发现源)。startProvider 通过 context 和 cancel 函数,将所有提供者的启动、停止逻辑标准化,使得管理器(Manager)能通过 ApplyConfig 动态控制提供者的生命周期(启动新提供者、停止旧提供者),无需关心具体的发现实现细节。
3.3.2.3、数据流转的安全性
目标发现的结果(targetgroup.Group)需要从提供者传递到管理器的全局目标集合中,而 updates 通道的创建和 updater 协程的启动,确保了这一过程的线程安全(通过通道的序列化特性避免并发读写冲突)。同时,updater 还会处理新旧目标的衔接(如为新订阅者初始化目标状态),保证 Prometheus 抓取逻辑能获取到最新的目标列表。
3.3.2.4、适配动态目标场景
没有 startProvider,发现提供者无法被激活,Prometheus 就无法感知动态变化的目标(如 k8s 中新建的 Pod)。它是将 "静态配置" 转化为 "动态发现能力" 的关键步骤,让 Prometheus 能适应云原生环境中目标频繁变更的场景。
3.4、处理更新
https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/manager.go#L321
使用 updater 函数处理发现提供者发送的更新。该函数会在上下文取消时清理资源,并将更新后的目标组存储到管理器中。