Prometheus源码专题【左扬精讲】—— 监控系统 Prometheus 3.4.0 源码解析:Discovery 动态服务发现机制

Prometheus源码专题【左扬精讲】—— 监控系统 Prometheus 3.4.0 源码解析:Discovery 动态服务发现机制

https://github.com/prometheus/prometheus/tree/v3.4.2/discovery/ 

一、引言

    在监控系统中,对目标的有效管理至关重要。早期,Prometheus 在没有动态服务发现机制时,依赖静态配置来管理监控目标。

    用户需在配置文件中手动罗列所有待监控目标的详细信息,像 IP 地址、端口号等。然而,当监控目标发生增减、IP 或端口变更等情况时,就必须手动修改配置文件,并且重启 Prometheus 才能让新配置生效

    这种方式在小规模、稳定的环境下或许可行,但在大规模、动态变化的基础设施环境中,操作繁琐,容易出现监控遗漏或错误,几乎无法适用。 Prometheus 的动态服务发现机制,借助 Discovery 管理器,有效解决了这些问题,大大提升了监控的灵活性和可靠性。

二、动态服务发现的核心 ——Discovery 管理器

    Prometheus 的 Discovery 管理器是动态服务发现的核心组件,它能自动发现和识别监控目标。其工作架构涉及多个关键概念和组件,下面结合 Prometheus 3.4.2 源码进行详细分析。 

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 配置文件中定义多个发现源

例如,在 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 syntax
	Filter 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.Group
	targetsMtx 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.Registerer

	metrics   *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.Group
		
		prov.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.newSubs
		prov.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 = newProviders
	wg.Wait()  // 等待所有被取消的提供者完成清理

	return nil
}
关键注释说明:
  • 函数整体逻辑概述:配置应用、提供者管理和状态更新
  • 并发控制:使用互斥锁保护管理器状态
  • 提供者生命周期管理:注册、启动、停止和清理
  • 订阅者处理:新旧订阅者的识别、目标状态迁移
  • 指标管理:失败配置计数、发现目标计数更新
  • 状态同步优化:立即触发更新发送,确保下游管理器及时获取最新状态
  • 资源清理:等待所有被取消的提供者完成清理工作
  • 代码中涉及的特殊处理逻辑:目标状态复制、并发通道操作

在 Prometheus 的服务发现(discovery)模块中,ApplyConfig 函数是实现动态配置更新和服务发现提供者生命周期管理的核心逻辑,其作用和必要性可以从 Prometheus 的核心需求和服务发现的动态特性两方面来理解。

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 函数处理发现提供者发送的更新。该函数会在上下文取消时清理资源,并将更新后的目标组存储到管理器中。

// updater 是处理服务发现提供者目标更新的协程函数
// 负责将提供者发现的目标组更新同步到管理器的全局目标集合,并触发下游更新通知
// 参数:
// - ctx: 控制协程生命周期的上下文
// - p: 关联的服务发现提供者
// - updates: 接收提供者发现的目标组更新的通道
func (m *Manager) updater(ctx context.Context, p *Provider, updates chan []*targetgroup.Group) {
	// 延迟调用cleaner,确保当前提供者的目标资源在协程退出时被清理(如提供者停止时)
	defer m.cleaner(p)
	
	// 无限循环,持续处理目标更新
	for {
		select {
		// 若上下文已结束(如提供者被取消),退出协程
		case <-ctx.Done():
			return
		// 从updates通道接收提供者发现的目标组更新
		case tgs, ok := <-updates:
			// 递增"接收更新"指标,用于监控服务发现的活跃性
			m.metrics.ReceivedUpdates.Inc()
			
			// 若通道已关闭(提供者停止发送更新)
			if !ok {
				m.logger.Debug("Discoverer channel closed", "provider", p.name)
				// 等待上下文完成,确保提供者被完全取消后再清理资源
				<-ctx.Done()
				return
			}

			// 对提供者的订阅者加读锁(避免并发修改订阅者集合)
			p.mu.RLock()
			// 遍历所有订阅者,将目标组更新应用到对应的目标集合中
			for s := range p.subs {
				// 调用updateGroup更新全局目标存储,键为订阅者名称+提供者名称的组合(避免冲突)
				m.updateGroup(poolKey{setName: s, provider: p.name}, tgs)
			}
			// 释放订阅者的读锁
			p.mu.RUnlock()

			// 发送触发信号到triggerSend通道,通知管理器有新的目标更新需同步到下游
			// 若通道已满则跳过(避免阻塞,下一次触发会处理)
			select {
			case m.triggerSend <- struct{}{}:
			default:
			}
		}
	}
}

3.4.1、核心作用

updater(必应翻译:美 ['ʌpdeɪtər] 英 ['ʌpdeɪtər]) 是 Prometheus 服务发现中目标更新的 "处理中枢",负责将服务发现提供者(如 Kubernetes、Consul 等)发现的动态目标组(targetgroup.Group)同步到管理器的全局目标存储中,并触发下游模块(如抓取器)更新目标列表。其核心作用可概括为:

3.4.1.1、目标更新的中转与整合

接收提供者(Provider(百度翻译:英/prəˈvaɪdə(r)/ 美/prəˈvaɪdər/)通过 updates 通道发送的目标组更新(如新增 / 删除 Pod、服务实例),并调用 updateGroup 将这些更新写入管理器的全局目标存储(m.targets),确保全局目标状态与实际环境一致。

3.4.1.2、并发安全与资源隔离

通过读写锁(p.mu.RLock)保护订阅者集合的并发访问,避免在处理更新时订阅者被修改导致的数据错乱;同时,为每个提供者单独启动 updater 协程,实现不同提供者的更新处理隔离(一个提供者的故障不影响其他提供者)。

3.4.1.3、触发下游同步

每次处理完目标更新后,通过 triggerSend 通道通知管理器(Manager),促使其尽快将最新的目标列表发送给下游模块(如抓取器),保证监控目标的实时性。

3.4.1.4、资源清理保障

通过 defer m.cleaner(p) 确保在 updater 协程退出时(如提供者被取消),自动清理该提供者关联的目标资源,避免内存泄漏。

3.4.2、为什么需要这一步?

updater 是服务发现从 "目标发现" 到 "目标可用" 的关键桥梁,其必要性体现在以下几个方面:

3.4.2.1、解耦发现逻辑与目标管理

提供者(Provider)的核心职责是 “发现目标”(如通过 K8s API 监听 Pod 变化),而 updater 负责将发现结果 “落地” 到系统中(写入全局存储、触发下游更新)。这种分工避免了提供者与管理器的强耦合,使不同类型的提供者(如 Kubernetes、Consul)能复用相同的目标处理逻辑。

3.4.2.2、应对动态目标场景

在云原生环境中,目标(如 Pod、容器)会频繁创建 / 销毁(如扩缩容、故障重建)。updater 持续监听提供者的更新通道,确保这些动态变化能被实时捕获并同步到系统中,避免 Prometheus 抓取过时的目标(如已销毁的 Pod)或遗漏新目标。

3.4.2.3、保障系统稳定性
        • 若缺少 updater,提供者发现的目标无法被写入全局存储,下游模块(如抓取器)将无法获取最新目标列表,导致监控失效。
        • 通过锁机制和资源清理逻辑,updater 避免了并发修改目标存储导致的数据冲突,以及提供者退出后未清理资源导致的内存泄漏。
3.4.2.4、支持灵活的订阅机制

一个提供者可能被多个订阅者(subs,如不同的抓取任务)使用,updater 会为每个订阅者同步目标更新,确保不同任务能获取到自己所需的目标列表(通过 poolKey 区分订阅者与提供者的组合)。

3.5、Sender 更新

https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/manager.go#L351

使用 sender 函数将更新后的目标组发送到同步通道。该函数会定期检查是否有新的更新,并将更新后的目标组以映射的形式发送到同步通道。

// sender 函数负责定期将所有发现的目标组更新发送到同步通道。
func (m *Manager) sender() {
    // 创建一个时间间隔为 m.updatert 的 ticker,用于定期触发更新操作。
    ticker := time.NewTicker(m.updatert)
    // 确保在函数退出时停止 ticker,避免资源泄漏。
    defer ticker.Stop()

    // 进入一个无限循环,持续监听不同的通道事件。
    for {
        select {
        // 当上下文被取消时,函数返回,停止发送更新。
        case <-m.ctx.Done():
            return
        // 当 ticker 触发时,执行以下逻辑。
        case <-ticker.C: 
            // 注释说明:一些发现器可能会过于频繁地发送更新,使用 ticker 可以对这些更新进行限流。
            select {
            // 当 m.triggerSend 通道接收到信号时,表示有新的更新需要发送。
            case <-m.triggerSend:
                // 增加发送更新的计数器,表示成功发送了一次更新。
                m.metrics.SentUpdates.Inc()
                select {
                // 尝试将所有目标组更新发送到同步通道 m.syncCh。
                case m.syncCh <- m.allGroups():
                // 如果同步通道已满,无法立即发送更新,则执行以下逻辑。
                default:
                    // 增加延迟更新的计数器,表示有一次更新被延迟发送。
                    m.metrics.DelayedUpdates.Inc()
                    // 记录调试日志,提示发现接收器的通道已满,将在下一个周期重试。
                    m.logger.Debug("Discovery receiver's channel was full so will retry the next cycle")
                    select {
                    // 尝试再次触发发送更新的操作,以便在下一个周期继续尝试发送。
                    case m.triggerSend <- struct{}{}:
                    // 如果无法再次触发,则不做处理。
                    default:
                    }
                }
            // 如果 m.triggerSend 通道没有接收到信号,则不做处理。
            default:
            }
        }
    }
} 

3.5.1、核心作用 

sender 函数的核心作用是定期将所有发现的目标组更新发送到同步通道 m.syncCh。它通过 ticker 控制发送更新的频率,避免发现器过于频繁地发送更新导致资源浪费或处理不及时。同时,它会处理通道已满的情况,将无法立即发送的更新标记为延迟更新,并在下一个周期尝试再次发送。

3.5.2、为什么需要这一步?

      • 限流:不同的服务发现机制可能会以不同的频率发送目标组更新,有些发现器可能会过于频繁地发送更新。使用 ticker 可以对这些更新进行限流,确保更新以合适的频率发送,避免对系统资源造成过大压力。
      • 缓冲和重试:同步通道 m.syncCh 可能会因为接收方处理不及时而满负荷。当通道已满时,sender 函数会将更新标记为延迟更新,并在下一个周期尝试再次发送,确保更新不会丢失。
      • 统一管理:sender 函数将所有发现的目标组更新统一发送到同步通道,方便接收方进行处理,提高了代码的可维护性和扩展性。 

四、并发处理机制:Go 协程与通道

        Prometheus 的 Discovery 管理器通过 Go 协程和通道机制实现并发。对于不同的服务发现机制(如 Kubernetes SD、Consul SD 等),Discovery 管理器会为每种机制启动一个或多个 Go 协程。这些协程独立运行,负责从对应的数据源中获取监控目标信息。通道则被用来在协程之间传递数据和控制信号。

        例如,在 startProvider 函数中,为每个发现提供者启动了两个协程:一个协程调用 p.d.Run(ctx, updates) https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/manager.go#L303 从数据源获取监控目标信息,并将信息通过 updates 通道发送出去;另一个协程调用 m.updater(ctx, p, updates) https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/manager.go#L304 处理 updates 通道中的信息。当某个协程获取到新的监控目标信息后,会通过通道将这些信息发送给主协程,主协程再对这些信息进行汇总、处理和更新。同时,通道也可以用于传递停止信号,以便在需要时优雅地终止各个协程的运行。这种基于协程和通道的方式,使得 Discovery 管理器能够高效地并发处理多个服务发现任务,提高了整体的运行效率。

五、采样目标服务发现与通知服务发现任务的区别

        采样目标服务发现通知服务发现任务存在明显区别。

        采样目标服务发现主要关注于定期或按需采集监控目标的信息,以确定当前需要监控的目标集合。它会按照一定的频率或触发条件去查询数据源,获取目标的最新状态,并将这些信息反馈给系统,用于更新监控目标列表。例如,在 https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/refresh/refresh.go 中,Discovery 结构体 (https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/refresh/refresh.go#L37) 实现了定期调用 refresh 函数来获取目标信息的功能。

// Discovery implements the Discoverer interface.
type Discovery struct {
	logger   *slog.Logger
	interval time.Duration
	refreshf func(ctx context.Context) ([]*targetgroup.Group, error)
	metrics  *discovery.RefreshMetrics
}

https://github.com/prometheus/prometheus/blob/v3.4.2/discovery/refresh/refresh.go#L66

// Run 实现了 Discoverer 接口,负责启动服务发现的主循环,持续获取目标组并发送到通道
// 参数:
// - ctx: 控制整个发现过程生命周期的上下文(如取消、超时)
// - ch: 用于发送发现的目标组的通道(下游处理器通过该通道接收更新)
func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
	// 立即执行一次初始刷新,获取第一组目标(避免等待定时器首次触发的延迟)
	tgs, err := d.refresh(ctx)
	if err != nil {
		// 若错误不是因上下文取消导致(如正常运行中出错),记录错误日志
		if !errors.Is(ctx.Err(), context.Canceled) {
			d.logger.Error("Unable to refresh target groups", "err", err.Error())
		}
	} else {
		// 将初始获取的目标组发送到通道(若通道可写且上下文未取消)
		select {
		case ch <- tgs:
		case <-ctx.Done():
			return // 上下文已结束,直接退出
		}
	}

	// 创建定时器,按指定间隔(d.interval)触发周期性刷新
	ticker := time.NewTicker(d.interval)
	defer ticker.Stop() // 函数退出时停止定时器,释放资源

	// 主循环:持续监听定时器和上下文状态
	for {
		select {
		// 定时器触发(到达刷新间隔)
		case <-ticker.C:
			// 执行刷新操作,获取最新目标组
			tgs, err := d.refresh(ctx)
			if err != nil {
				// 非上下文取消导致的错误,记录日志后继续循环
				if !errors.Is(ctx.Err(), context.Canceled) {
					d.logger.Error("Unable to refresh target groups", "err", err.Error())
				}
				continue
			}

			// 将新获取的目标组发送到通道(若通道可写且上下文未取消)
			select {
			case ch <- tgs:
			case <-ctx.Done():
				return // 上下文已结束,退出
			}
		// 上下文结束(如外部触发取消)
		case <-ctx.Done():
			return // 退出主循环,结束服务发现
		}
	}
}

// refresh 执行单次目标组刷新,获取最新目标并记录性能和错误指标
// 参数 ctx 控制单次刷新的生命周期
// 返回刷新得到的目标组和可能的错误
func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
	// 记录刷新开始时间,用于计算耗时
	now := time.Now()
	// 延迟执行:刷新完成后记录耗时指标(单位:秒)
	defer func() {
		d.metrics.Duration.Observe(time.Since(now).Seconds())
	}()

	// 调用具体的刷新实现(由外部注入的 refreshf 函数执行实际逻辑,如调用API、读取文件等)
	tgs, err := d.refreshf(ctx)
	// 若刷新失败,递增错误计数指标
	if err != nil {
		d.metrics.Failures.Inc()
	}
	// 返回目标组和错误(若有)
	return tgs, err
} 

        通知服务发现任务则更侧重于当监控目标发生变化时,能够及时向系统发出通知。当数据源中的目标信息发生新增、删除或修改等变化时,通知服务发现任务会立即感知到这些变化,并将变化信息快速传递给相关组件,以便系统能够迅速做出响应,调整监控策略。

        简单来说,采样目标服务发现是主动去获取信息,而通知服务发现任务是被动接收变化通知并传递信息。

六、结尾

        Prometheus 的动态服务发现机制通过 Discovery 管理器,解决了静态配置下无法适应动态变化的问题,提高了监控的灵活性和可靠性,降低了运维成本,使得 Prometheus 能够更好地应对大规模、动态的基础设施环境。其基于 Go 协程和通道的并发处理机制,保证了高效的服务发现任务处理。同时,采样目标服务发现和通知服务发现任务的不同职责,进一步优化了监控目标的管理和更新。
        深入理解 Prometheus 3.4.2 源码中的 Discovery 机制,有助于我们更好地使用和扩展 Prometheus 监控系统。
posted @ 2025-07-29 16:09  左扬  阅读(40)  评论(0)    收藏  举报