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

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

https://github.com/prometheus/prometheus/blob/v3.4.0/discovery/manager.go

一、背景简介

Prometheus 作为云原生时代的主流监控系统,其强大的“服务发现”能力是自动化监控的基石。服务发现(Service Discovery,简称 SD)模块能够自动感知集群中服务的变化,动态调整监控目标,极大提升了运维效率和系统弹性。

在 Prometheus 源码中,https://github.com/prometheus/prometheus/blob/v3.4.0/discovery/manager.go 是服务发现的核心调度与管理模块。本文将从源码角度,系统学习 manager.go 的设计与实现。

二、整体架构与定位

在 Prometheus 的服务发现体系中,manager 主要承担以下职责:

    • 统一管理所有的服务发现任务(如 Kubernetes、Consul、DNS 等多种发现方式)
    • 负责各类发现任务的生命周期(启动、停止、重载等)
    • 维护目标组(target group)的最新状态,并将变化通知给下游(如 scrape manager)

manager.go 作为“调度中枢”,上承配置,下接各 discovery provider,最终将发现的目标推送给采集模块。

三、深度聊聊 manager 三大核心职责

Prometheus 支持多种服务发现方式(如 Kubernetes、Consul、DNS 等),Manager 作为调度中枢,通过抽象化设计实现了对各类发现任务的统一管理。其核心逻辑体现在对 "服务发现提供者(Provider)" 的集中管控上。

3.1、核心数据结构:通过 Provider 抽象统一类型

https://github.com/prometheus/prometheus/blob/v3.4.0/discovery/manager.go#L37

源代码中定义了 Provider(百度翻译:[prəˈvaɪdə(r)] 美[prəˈvaɪdər]结构体,作为各类服务发现方式的统一封装:

// 定义一个结构体 poolKey,用于作为池(pool)中的键
// 由 setName 和 provider 组成,用于唯一标识一个资源集合和提供者的组合
type poolKey struct {
	setName  string   // 资源集合的名称
	provider string   // 提供者的名称
}

// 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 方法返回该 Provider 所持有的 Discoverer 实例
func (p *Provider) Discoverer() Discoverer {
	return p.d
}

// IsStarted 方法返回 Discoverer 是否已经启动
// 通过检查 cancel 函数是否为 nil 来判断(启动后会设置 cancel 函数)
func (p *Provider) IsStarted() bool {
	return p.cancel != nil
}

// Config 方法返回该 Provider 的配置信息
func (p *Provider) Config() interface{} {
	return p.config
}

其中 Discoverer(https://github.com/prometheus/prometheus/blob/v3.4.0/discovery/discovery.go#L35) 是所有服务发现方式的抽象接口(需实现 Run 方法),无论 Kubernetes 还是 Consul,均通过该接口接入管理器,实现了"多态统一"。

// Discoverer provides information about target groups. It maintains a set
// of sources from which TargetGroups can originate. Whenever a discovery provider
// detects a potential change, it sends the TargetGroup through its channel.
//
// Discoverer does not know if an actual change happened.
// It does guarantee that it sends the new TargetGroup whenever a change happens.
//
// Discoverers should initially send a full set of all discoverable TargetGroups.
type Discoverer interface {
	// Run hands a channel to the discovery provider (Consul, DNS, etc.) through which
	// it can send updated target groups. It must return when the context is canceled.
	// It should not close the update channel on returning.
	Run(ctx context.Context, up chan<- []*targetgroup.Group)  <==== 注意这里的 Run 方法
}  

3.2、管理主体:Manager 中的 providers 集合

https://github.com/prometheus/prometheus/blob/v3.4.0/discovery/manager.go#L143

Manager 结构体通过 providers []*Provider 字段存储所有服务发现任务,实现集中管理:

// Manager 维护一组服务发现提供者(discovery providers),并将每个更新发送到一个映射通道。
// 目标(Targets)按目标集合名称(target set name)进行分组。
type Manager struct {
	logger   *slog.Logger                   // 日志记录器,用于记录Manager的运行日志
	name     string                         // Manager的名称标识
	httpOpts []config.HTTPClientOption      // HTTP客户端选项,用于配置服务发现所需的HTTP客户端
	mtx      sync.RWMutex                   // 读写互斥锁,保护Manager的并发访问
	ctx      context.Context                // 上下文,用于控制Manager及其子组件的生命周期

	// 一些服务发现器(如k8s)仅发送特定目标组的更新,
	// 因此我们使用map[tg.Source]*targetgroup.Group来确定需要更新哪个组
	targets    map[poolKey]map[string]*targetgroup.Group  // 存储目标组的映射,外层键为poolKey,内层键为目标组源标识
	targetsMtx sync.Mutex                                  // 用于保护targets映射的互斥锁

	// providers 用于跟踪服务发现(SD)提供者
	providers []*Provider  // 存储所有服务发现 provider 的切片,按 jobName 分类 <========= 注意这里的 providers,关键成员

	// syncCh 通道用于发送更新,其值为一个映射,键是抓取配置中的job值
	syncCh chan map[string][]*targetgroup.Group  // 目标组变更的同步通道,向下游推送最新的目标组 <====== 关键成员

	// updatert 表示发送更新到通道前的等待时间,该变量仅应在单元测试中修改
	updatert time.Duration  // 更新发送延迟时间

	// triggerSend 通道用于向Manager发出信号,表示已从提供者接收到新的更新
	triggerSend chan struct{}  // 触发发送信号的通道

	// lastProvider 记录Manager生命周期内注册的提供者数量
	lastProvider uint  // 最后一个提供者的计数标识

	// registerer 用于所有服务发现指标的注册器
	registerer prometheus.Registerer  // Prometheus指标注册器

	metrics   *Metrics                   // Manager自身的指标集合
	sdMetrics map[string]DiscovererMetrics  // 服务发现器的指标映射,键为提供者名称
}

// Providers 返回当前配置的服务发现(SD)提供者
func (m *Manager) Providers() []*Provider {
	return m.providers
}

// UnregisterMetrics 注销Manager的指标。它不会注销服务发现或刷新指标,
// 这些指标的生命周期独立于发现Manager进行管理
func (m *Manager) UnregisterMetrics() {
	m.metrics.Unregister(m.registerer)
}

// Run 启动后台处理
func (m *Manager) Run() error {
	go m.sender()  // 启动sender协程处理更新发送
	<-m.ctx.Done() // 等待上下文完成信号(如取消或超时)
	m.cancelDiscoverers()  // 取消所有服务发现器的运行
	return m.ctx.Err()     // 返回上下文的错误信息
}

3.3、注册逻辑:registerProviders 函数实现统一接入

https://github.com/prometheus/prometheus/blob/v3.4.0/discovery/manager.go#L446

registerProviders 函数是服务发现管理器中处理配置注册的核心方法,主要功能是将给定的服务发现配置(Configs)注册到管理器中,并返回注册失败的配置数量。其核心流程与细节处理如下:

3.3.1、检查重复配置,避免重复创建

遍历现有 providers 集合,通过 reflect.DeepEqual 深度对比配置内容,判断当前配置是否已存在。若存在,仅将当前集合名称(setName)添加到该 Provider 的新订阅者列表(newSubs)中,避免重复实例化。

3.3.2、创建新的服务发现实例

若配置不存在,则:

  1. 获取配置类型(如 Kubernetes、Consul 等)
  2. 调用配置的 NewDiscoverer 方法,结合日志、HTTP 客户端选项和指标配置,创建具体的 Discoverer 实例
  3. 若创建失败(如配置错误),记录错误日志并递增失败计数

3.3.3、管理 Provider 生命周期

为新创建的 Discoverer 包装成 Provider 对象,生成唯一名称(类型 + 序号),初始化其订阅者信息,并添加到 providers 集合中,同时更新计数器。

3.3.4、处理边界情况

若所有配置均未成功添加(包括全部失败或配置为空),会主动添加一个空的静态配置(StaticConfig)。这一设计是为了强制刷新对应的抓取池,明确通知接收者 "该目标集合当前无可用目标",保证系统状态的一致性。 

// registerProviders 注册服务发现配置,并返回失败的服务发现配置数量
func (m *Manager) registerProviders(cfgs Configs, setName string) int {
	var (
		failed int  // 记录注册失败的配置数量
		added  bool // 标记是否有配置被成功添加
	)

	// add 是一个内部函数,用于处理单个配置的注册逻辑
	add := func(cfg Config) {
		// 检查该配置是否已存在于现有提供者中
		for _, p := range m.providers {
			// 使用深度相等检查配置是否相同
			if reflect.DeepEqual(cfg, p.config) {
				// 如果配置已存在,将当前集合名称添加到新订阅者中
				p.newSubs[setName] = struct{}{}
				added = true
				return
			}
		}

		// 如果配置不存在,则创建新的服务发现器
		typ := cfg.Name() // 获取配置类型名称
		// 根据配置创建服务发现器实例
		d, err := cfg.NewDiscoverer(DiscovererOptions{
			Logger:            m.logger.With("discovery", typ, "config", setName), // 带有上下文的日志记录器
			HTTPClientOptions: m.httpOpts, // HTTP客户端选项
			Metrics:           m.sdMetrics[typ], // 对应的指标收集器
		})
		if err != nil {
			// 创建失败时记录错误日志
			m.logger.Error("Cannot create service discovery", "err", err, "type", typ, "config", setName)
			failed++ // 增加失败计数
			return
		}

		// 创建新的Provider并添加到管理器中
		m.providers = append(m.providers, &Provider{
			name:   fmt.Sprintf("%s/%d", typ, m.lastProvider), // 生成唯一名称
			d:      d, // 服务发现器实例
			config: cfg, // 对应的配置
			newSubs: map[string]struct{}{ // 初始化新订阅者集合
				setName: {},
			},
		})
		m.lastProvider++ // 递增提供者计数器
		added = true // 标记为已添加
	}

	// 遍历所有配置并调用add函数进行处理
	for _, cfg := range cfgs {
		add(cfg)
	}

	// 如果没有任何配置被添加成功
	if !added {
		// 添加一个空的静态配置,以强制刷新相应的抓取池
		// 并通知接收者该目标集合当前没有目标
		// 这种情况可能发生在服务发现配置集合为空,或者所有配置实例化都失败的情况下
		add(StaticConfig{{}})
	}

	return failed // 返回失败的配置数量
}
通过这一逻辑,无论何种服务发现方式(Kubernetes、Consul 等),均被统一抽象为 Provider 并由 Manager 集中管理,实现了 "多源发现任务的标准化管控"。

四、Manager 的其他关键方法源码阅读

4.1、NewManager 函数

https://github.com/prometheus/prometheus/blob/v3.4.0/discovery/manager.go#L84

作为服务发现管理器(Manager)的构造函数,NewManager 负责初始化并返回一个配置完整的 Manager 实例,是 Prometheus 服务发现系统的 "入口点",为后续服务发现流程提供基础运行环境。

    • 服务发现的 "总控中心":Manager 是 Prometheus 服务发现的核心协调者,负责管理所有服务发现提供者(Provider)、接收目标更新、协调目标组映射关系。NewManager 通过正确初始化 Manager。
    • 适配多源服务发现:通过初始化 sdMetrics 等字段,支持对不同类型的服务发现器(如 Kubernetes、Consul、File 等)进行统一管理和监控,是 Prometheus 兼容多环境服务发现的前提。

    • 保障系统稳定性:合理的默认配置(如更新延迟、缓冲通道)和容错处理,确保服务发现过程在高并发或异常场景下的稳定性,间接保障 Prometheus 抓取目标的连续性。

    • 可观测性基础:指标注册逻辑使服务发现过程本身可被监控,便于运维人员排查 "目标未被发现"" 更新延迟 "等问题,符合 Prometheus" 可观测性优先 " 的设计理念。

// NewManager 是服务发现管理器(Discovery Manager)的构造函数
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()
    }
    // 初始化Manager结构体实例
    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,              // Prometheus指标注册器
        sdMetrics:   sdMetrics,               // 服务发现器的指标映射
    }
    // 应用所有可选配置函数,对Manager进行定制化配置
    for _, option := range options {
        option(mgr)
    }

    // 注册指标
    // 必须在设置所有选项之后进行,以确保Manager的名称已被正确设置
    metrics, err := NewManagerMetrics(registerer, mgr.name)
    if err != nil {
        logger.Error("创建服务发现管理器指标失败", "manager", mgr.name, "err", err)
        return nil
    }
    mgr.metrics = metrics // 将指标实例赋值给Manager

    return mgr // 返回初始化完成的Manager实例
}

4.2、ApplyConfig 方法

https://github.com/prometheus/prometheus/blob/v3.4.0/discovery/manager.go#L204

ApplyConfig 是 Prometheus 服务发现管理器中处理配置更新的核心函数,负责协调新旧配置的切换,实现服务发现提供者的 "平滑更新"—— 保留有用的提供者、停止过时的提供者、启动新需要的提供者,同时保证目标组数据的一致性。

      • 配置热更新:支持在不重启 Prometheus 的情况下更新服务发现配置,保证监控的连续性。
      • 资源高效利用:通过复用已有提供者(避免重复创建)和停止过时提供者(释放资源),优化系统资源占用。
      • 数据一致性:通过同步目标组和订阅者关系,确保配置更新后目标数据的准确性,避免监控数据缺失或重复。
      • 可观测性保障:通过更新相关指标,使配置更新过程和服务发现状态可被监控,便于问题排查。
// 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 {
		// 调用registerProviders注册配置,累加失败数
		failedCount += m.registerProviders(scfg, name)
	}
	// 更新"失败配置数"指标
	m.metrics.FailedConfigs.Set(float64(failedCount))

	var (
		wg           sync.WaitGroup   // 等待组,用于等待被停止的提供者完成资源清理
		newProviders []*Provider      // 存储更新后需要保留的提供者
	)

	// 遍历当前所有提供者,处理需要保留或停止的提供者
	for _, prov := range m.providers {
		// 若newSubs为空,说明该提供者不再被任何目标集合需要(已过时)
		if len(prov.newSubs) == 0 {
			wg.Add(1) // 等待组计数+1
			// 设置done函数,当提供者资源清理完成后通知等待组
			prov.done = func() {
				wg.Done()
			}
			prov.cancel() // 调用cancel函数停止该提供者
			continue      // 跳过后续处理,进入下一个提供者
		}

		// 若提供者需要保留,加入newProviders
		newProviders = append(newProviders, prov)
		// refTargets用于保存参考目标组,新订阅者的目标组应与此保持一致
		var refTargets map[string]*targetgroup.Group

		prov.mu.Lock() // 锁定提供者的读写锁,保护subs和newSubs的修改

		m.targetsMtx.Lock() // 锁定目标组的互斥锁,保护targets映射的修改
		// 遍历当前提供者的旧订阅者
		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 {
			// 若新订阅者不在旧订阅者列表中,初始化其发现目标数指标为0
			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() // 释放目标组互斥锁

		// 更新提供者的订阅者为新订阅者,重置newSubs
		prov.subs = prov.newSubs
		prov.newSubs = map[string]struct{}{}
		prov.mu.Unlock() // 释放提供者的读写锁

		// 若提供者尚未启动,启动它
		if !prov.IsStarted() {
			m.startProvider(m.ctx, prov)
		}
	}

	// 下游管理器期望在配置重载时获取完整的目标状态,因此需要主动触发更新
	// 虽然startProvider也会触发,但这里尽早触发可让下游更快获取状态
	// 参考:https://github.com/prometheus/prometheus/pull/8639 和 https://github.com/prometheus/prometheus/pull/13147
	if len(m.providers) > 0 {
		select {
		case m.triggerSend <- struct{}{}: // 发送触发信号
		default: // 若通道已满则忽略(避免阻塞)
		}
	}

	// 更新管理器的提供者列表为新的保留列表
	m.providers = newProviders
	wg.Wait() // 等待所有被停止的提供者完成资源清理

	return nil
}  

关键代码说明:

      • 配置注册与失败统计
        • 通过遍历新配置 cfg 并调用 registerProviders,完成新配置的注册,并统计失败数量,最后更新监控指标 FailedConfigs,实现配置注册的可观测性。
      • 提供者生命周期管理
        • 对现有提供者,通过检查 newSubs 是否为空判断是否过时(newSubs 为空表示该提供者不再被任何目标集合需要),过时的提供者会被调用 cancel() 停止,并通过 sync.WaitGroup 等待其资源清理完成。
        • 需保留的提供者会被加入 newProviders,后续成为管理器的活跃提供者列表。
      • 目标组与订阅者同步
        • 清理过时订阅者对应的目标组(从 targets 中删除),并更新 DiscoveredTargets 指标。
        • 为新订阅者初始化目标组(若有参考目标组则复制其内容),保证新订阅者能继承合理的初始状态。
        • 更新提供者的订阅者列表(subs 替换为 newSubs),完成订阅关系的切换。
      • 主动触发更新通知
        • 配置更新后主动向 triggerSend 通道发送信号,确保下游组件(如抓取管理器)能及时获取最新的目标状态,避免 stale(过时)目标的残留。

4.3、Run 方法

https://github.com/prometheus/prometheus/blob/v3.4.0/discovery/manager.go#L190

Run 方法是 Manager 的 "启动入口",负责初始化服务发现的后台处理流程,并在管理器终止时协调资源清理,是整个服务发现机制的生命周期控制器。

// Run 启动后台处理流程
func (m *Manager) Run() error {
    // 启动sender协程,负责将目标组更新发送到syncCh通道
    // 该协程会在后台持续运行,处理服务发现的目标更新逻辑
    go m.sender()

    // 阻塞当前协程,等待上下文(m.ctx)被取消或完成
    // 这通常发生在程序关闭、收到中断信号或主动停止管理器时
    <-m.ctx.Done()

    // 当上下文完成后,取消所有运行中的服务发现器(Discoverer)
    // 确保资源被正确释放,避免泄漏
    m.cancelDiscoverers()

    // 返回上下文的错误信息,通常是取消原因(如context.Canceled或context.DeadlineExceeded)
    return m.ctx.Err()
}

关键代码说明:

  • 启动 sender 协程

go m.sender() 是核心操作,启动了一个专门的后台协程处理目标组更新的发送逻辑。sender 方法的主要职责是:

  • 监听 triggerSend 通道的信号(表示有新的目标更新)
  • 按照 updatert 配置的延迟时间(默认 5 秒)批量处理目标更新
  • 将整理后的目标组数据通过 syncCh 通道发送给下游组件(如抓取管理器)
  • 这一设计通过异步处理避免了频繁的目标更新对主线程的阻塞,同时通过延迟批量处理优化了性能。
  • 等待上下文终止

<-m.ctx.Done() 是阻塞操作,使 Run 方法不会立即返回,而是持续等待直到管理器的上下文被取消。这确保了:

  • 后台的 sender 协程可以一直运行
  • 服务发现机制能持续工作直到收到明确的终止信号(如程序退出)
  • 资源清理
  • 当上下文终止后,m.cancelDiscoverers() 会被调用,其内部逻辑是遍历所有 Provider 并调用它们的 cancel 方法,最终停止所有服务发现器(如 Kubernetes、Consul 等发现器的运行),释放网络连接、协程等资源。
  • 返回终止原因
  • return m.ctx.Err() 将上下文的错误信息返回(如取消原因),便于上层调用者了解管理器终止的具体原因(是正常取消还是超时等)。

4.4、SyncCh 方法

https://github.com/prometheus/prometheus/blob/v3.4.0/discovery/manager.go#L198

manager 通过 syncCh 通道,将最新的 target group map 推送给 scrape manager 等下游模块,实现采集目标的动态更新。 

// SyncCh 返回一个只读通道,所有客户端通过该通道接收目标(target)更新
func (m *Manager) SyncCh() <-chan map[string][]*targetgroup.Group {
    // 返回Manager内部的syncCh通道,且限定为只读模式
    // 这样可以防止外部直接向通道发送数据,保证数据流向的安全性
    return m.syncCh
}

五、负责各类发现任务的生命周期(启动、停止、重载等)

Manager 通过精细化的生命周期管理,确保服务发现任务能根据配置动态调整(启动新任务、停止过时任务、重载配置),核心依赖 context 控制和配置对比逻辑。

5.1、启动(Start):startProvider 函数

https://github.com/prometheus/prometheus/blob/v3.4.0/discovery/manager.go#L296

当需要启动一个服务发现任务时,startProvider 函数会完成以下操作:

    • 创建带取消功能的上下文(context.WithCancel),用于后续停止任务;
    • 启动 Discoverer 的 Run 方法(具体发现逻辑的入口,如监听 K8s API 变化);
    • 启动 updater 协程,负责接收 Discoverer 推送的目标组更新并处理。
// 为管理器(Manager)定义一个方法startProvider,接收上下文(context)和提供者(Provider)作为参数
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)
    // 创建一个通道,用于传递目标组(Group)切片的更新
    updates := make(chan []*targetgroup.Group)

    // 将取消函数保存到提供者实例中,以便后续可以取消该提供者的运行
    p.cancel = cancel

    // 启动一个goroutine运行提供者的d属性的Run方法,传入上下文和updates通道
    go p.d.Run(ctx, updates)
    // 启动一个goroutine运行管理器的updater方法,传入上下文、提供者和updates通道
    go m.updater(ctx, p, updates)
}

关键说明:

上下文管理:

    • 使用context.WithCancel创建可取消的上下文,用于控制Provider的生命周期
    • 保存cancel函数到p.cancel,允许外部取消该Provider的所有关联操作

通道通信:

    • updates通道用于在p.d.Run和m.updater之间传递目标组更新数据
    • 类型[]*targetgroup.Group表示这是一个目标组指针的切片,用于批量传递更新

并发设计:

    • 两个go关键字启动了两个并发 goroutine:
    • p.d.Run:负责提供者的核心运行逻辑,会向updates通道发送更新
    • m.updater:负责处理这些更新(可能是应用到系统中)

日志记录:

    • 使用m.logger.Debug记录启动信息,包含提供者名称和订阅信息,便于调试和追踪

5.2、停止(Stop):通过 cancel 函数和 cancelDiscoverers 批量处理

https://github.com/prometheus/prometheus/blob/v3.4.0/discovery/manager.go#L379

    • 单个任务停止:Provider 的 cancel 字段是 context.CancelFunc,调用后会终止对应的 Discoverer 协程(Run 方法会因上下文取消退出);
    • 批量停止:cancelDiscoverers 函数遍历所有 Provider 并调用其 cancel 函数,适用于管理器退出或配置重载时清理过时任务
// 定义Manager的cancelDiscoverers方法,用于取消所有发现者(Discoverers)的运行
func (m *Manager) cancelDiscoverers() {
    // 对m.mtx执行读锁定,允许多个读操作同时进行,但会阻塞写操作
    m.mtx.RLock()
    // 延迟执行读解锁,确保函数退出时释放锁,防止死锁
    defer m.mtx.RUnlock()
    // 遍历管理器中所有的提供者(providers)
    for _, p := range m.providers {
        // 检查当前提供者是否有取消函数(cancel)
        if p.cancel != nil {
            // 调用取消函数,终止该提供者的相关goroutine和操作
            p.cancel()
        }
    }
}

5.3、重载(Reload):ApplyConfig 函数实现配置动态更新

https://github.com/prometheus/prometheus/blob/v3.4.0/discovery/manager.go#L204

配置重载(Reload)是生命周期管理的核心场景,ApplyConfig 函数通过对比新配置与现有 Provider,实现 "增量更新":

  • 保留有用任务:遍历新配置,若已有相同配置的 Provider,则仅更新其订阅者(newSubs),不重复创建;
  • 停止过时任务:对现有 Provider,若新配置中无对应订阅者(newSubs 为空),则调用 cancel 停止,并清理其资源;
  • 创建新任务:对新配置中不存在的 Provider,通过 registerProviders 创建并启动。
    func (m *Manager) ApplyConfig(cfg map[string]Configs) error {
        // ... 注册新配置对应的 Provider
        for _, prov := range m.providers {
            // 停止过时 Provider(无新订阅者)
            if len(prov.newSubs) == 0 {
                wg.Add(1)
                prov.done = func() { wg.Done() }
                prov.cancel()
                continue
            }
            // 保留有用 Provider,更新订阅者
            // ...
            if !prov.IsStarted() {
                m.startProvider(m.ctx, prov) // 启动新创建的 Provider
            }
        }
        // ...
    }
    

    通过这一逻辑,Manager 实现了服务发现任务的 "平滑重载 Reload",无需重启整个进程即可适配配置变化。

六、target group 的最新状态,并将变化通知给下游

Manager 需实时跟踪服务发现产生的目标组(targetgroup.Group)变化,并将最新状态可靠地通知给下游模块(如采集管理器 scrape manager)。

6.1、目标组存储:targets 字段的分层设计

https://github.com/prometheus/prometheus/blob/v3.4.0/discovery/manager.go#L143

Manager 通过 targets 字段存储所有目标组,采用 "双级映射" 确保唯一性和可追溯性:

type Manager struct {
    targets map[poolKey]map[string]*targetgroup.Group // 存储目标组
    // ...
}

// poolKey 由 "任务集名称(setName)" 和 "提供者名称(provider)" 组成
type poolKey struct {
    setName  string // 对应 scrape 配置中的 job 名称
    provider string // 服务发现提供者名称
}
    • 外层 poolKey 确保不同任务集、不同提供者的目标组不冲突;
    • 内层 map[string]*targetgroup.Group 以目标组来源(tg.Source)为键,便于快速更新或删除特定目标组。

6.2、目标组更新:updater 协程处理实时变化

https://github.com/prometheus/prometheus/blob/v3.4.0/discovery/manager.go#L321

updater 协程是目标组更新的核心处理逻辑,负责接收 Discoverer 推送的目标组变化,并更新到 targets 中:

  • 从 updates 通道接收 Discoverer 推送的目标组列表(tgs);
  • 调用 updateGroup 函数,根据 poolKey 定位目标组存储位置,更新或删除目标组(空目标组会被删除,避免内存泄漏);
  • 通过 triggerSend 通道触发下游通知。
func (m *Manager) updater(ctx context.Context, p *Provider, updates chan []*targetgroup.Group) {
    for {
        select {
        case tgs, ok := <-updates: // 接收目标组更新
            m.metrics.ReceivedUpdates.Inc()
            // 处理目标组更新
            p.mu.RLock()
            for s := range p.subs {
                m.updateGroup(poolKey{setName: s, provider: p.name}, tgs)
            }
            p.mu.RUnlock()
            // 触发下游通知
            select {
            case m.triggerSend <- struct{}{}:
            default:
            }
        // ...
        }
    }
}

// 更新目标组到 targets 中
func (m *Manager) updateGroup(poolKey poolKey, tgs []*targetgroup.Group) {
    m.targetsMtx.Lock()
    defer m.targetsMtx.Unlock()
    if _, ok := m.targets[poolKey]; !ok {
        m.targets[poolKey] = make(map[string]*targetgroup.Group)
    }
    for _, tg := range tgs {
        if tg == nil { continue }
        if len(tg.Targets) > 0 {
            m.targets[poolKey][tg.Source] = tg // 更新非空目标组
        } else {
            delete(m.targets[poolKey], tg.Source) // 删除空目标组
        }
    }
}

6.3、下游通知:sender 协程批量推送最新状态

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

为避免频繁更新导致下游压力过大,Manager 通过 sender 协程实现「批量 + 节流」的通知机制:

    • 定时触发:默认每 5 秒(updatert 字段)检查一次是否有更新;
    • 触发信号:若 triggerSend 通道有信号(表示有新更新),则调用 allGroups 函数收集所有目标组,整理为「任务集 -> 目标组列表」的映射;
    • 推送下游:通过 syncCh 通道将整理后的目标组状态发送给下游(如 scrape manager),若通道满则记录延迟指标并重试。
// 定义Manager的sender方法,负责发送更新数据
func (m *Manager) sender() {
    // 创建一个定时器,按照m.updatert指定的间隔触发
    ticker := time.NewTicker(m.updatert)
    // 延迟停止定时器,确保函数退出时释放资源
    defer ticker.Stop()

    // 无限循环,持续执行发送逻辑
    for {
        // 选择语句,等待多个通道操作中的一个完成
        select {
        // 当m.ctx被取消时,退出函数
        case <-m.ctx.Done():
            return
        // 当定时器触发时执行以下逻辑
        case <-ticker.C: // 有些发现者发送更新过于频繁,因此我们用定时器来限制发送频率
            // 嵌套的选择语句,处理触发发送的信号
            select {
            // 接收到触发发送的信号
            case <-m.triggerSend:
                // 发送更新计数加1
                m.metrics.SentUpdates.Inc()
                // 尝试将所有组数据发送到同步通道
                select {
                // 成功发送所有组数据到m.syncCh通道
                case m.syncCh <- m.allGroups():
                // 当通道已满无法发送时执行默认分支
                default:
                    // 延迟更新计数加1
                    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:
                    }
                }
            // 没有触发发送信号时,不执行任何操作
            default:
            }
        }
    }
}

七、与其他模块的关系

    • https://github.com/prometheus/prometheus/blob/v3.4.0/discovery/discovery.go 定义了 provider 接口及各类服务发现实现,manager 负责统一调度。
    • https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/manager.go 采集管理模块,订阅 manager 的 syncCh,动态调整采集目标。
    • https://github.com/prometheus/prometheus/blob/v3.4.0/scrape/manager.go#L242 配置模块,通过 ApplyConfig 实现服务发现配置的热更新。
    • https://github.com/prometheus/prometheus/blob/v3.4.0/discovery/manager.go 这三层逻辑共同构成了 Prometheus 服务发现的核心骨架,支撑起对多源、动态服务的自动化监控能力,如下: 
      • 通过 Manager 与 Provider 的抽象设计,实现了服务发现任务的 "统一管理"
      • 通过 context 控制和 ApplyConfig 函数,实现了任务全生命周期的 "动态管控";
      • 通过 targets 存储与 sender 协程,实现了目标组状态的 "可靠维护与通知"
posted @ 2025-08-02 17:59  左扬  阅读(19)  评论(0)    收藏  举报