Prometheus源码专题【左扬精讲】—— 监控系统 Prometheus 3.4.0 源码解析:Discovery 动态服务发现机制
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 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
}
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
}
通知服务发现任务则更侧重于当监控目标发生变化时,能够及时向系统发出通知。当数据源中的目标信息发生新增、删除或修改等变化时,通知服务发现任务会立即感知到这些变化,并将变化信息快速传递给相关组件,以便系统能够迅速做出响应,调整监控策略。
简单来说,采样目标服务发现是主动去获取信息,而通知服务发现任务是被动接收变化通知并传递信息。

浙公网安备 33010602011771号