服务发现 · 去抓谁
容器来来去去,不可能手工维护目标列表。这一章读 discovery/ ——一个只有一个方法的接口撑起了 30 种服务发现实现,以及「推送全量而非增量」这个反直觉却极聪明的契约设计。
服务发现 · 去抓谁
pull 模型有个前提:Prometheus 得知道去抓谁。在容器时代,pod 来来去去、IP 随时变,手工维护目标列表是不可能的。这就是 discovery/ 的活。
一个方法的接口,30 种实现
先看接口有多小(真实源码 discovery/discovery.go:35):
// 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.
Run(ctx context.Context, up chan<- []*targetgroup.Group)
}就一个方法。 而 discovery/ 目录下有 30 个子目录:kubernetes、consul、dns、file、http、aws、azure、gce、eureka、nomad、marathon、openstack、hetzner、linode、scaleway、ovhcloud、digitalocean、puppetdb、moby(Docker)……
一个只有一个方法的接口,撑起了 30 种服务发现——这是接口即边界的教科书级示范。 每种 SD 只需回答一个问题:“当前该抓的目标有哪些?”,通过一个 channel 源源不断地告诉 Prometheus。
它们内部千差万别(K8s 用 watch API、DNS 定期解析、Consul 用长轮询、file 监听文件变化),但对上只暴露同一个 Run。于是:
- 加一种新 SD = 实现一个方法,核心代码一行不改。
- Prometheus 核心完全不知道 Kubernetes 或 Consul 的存在——它只认 channel 里流出来的
[]*targetgroup.Group。
接口越小,能塞进去的实现就越多。 这个接口小到极致,所以生态长到 30 种。
反直觉的契约:推送全量,不推增量
注释里藏着一个极重要、也极聪明的设计决定,值得逐句读:
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.
契约是:每次变化就把当前全量推过来——不是推“增加了谁、删除了谁”这样的增量事件,而是推“现在应该是这个样子”。而且 Discoverer 不必知道到底变没变(多推几次全量是允许的)。
这看起来很浪费(每次变化都传全量),但它换来了巨大的简化:
- 实现方极简 —— 写一个 SD 不用维护“上次是什么状态、这次 diff 出了什么”。K8s watch 到任何事件?重新列一遍当前所有 pod 推过去就完了。不用管增量逻辑,不会出增量 bug。
- 天生自愈 —— 增量模型最怕“漏掉一个事件”→ 状态永久错位、且没人发现。全量模型下,下一次推送自动修正一切。任何不一致都是暂时的。
- 允许“不知道变没变” —— 这句注释是点睛之笔:Discoverer 可以偷懒,拿不准就推一次全量,反正核心会自己 diff。这让实现方的心智负担降到最低。
这是“声明式”胜过“命令式”的经典案例——和 Kubernetes 的 reconcile 循环同一个哲学:不描述“怎么变”,只描述“该是什么样”,由消费方去对齐。代价是传输和 diff 的开销,但换来的是永远不会状态漂移。对一个必须比被监控系统更可靠的监控系统来说,这笔交易毫无悬念。
目标不只是地址
Discoverer 吐出的是 targetgroup.Group(源码 discovery/targetgroup/targetgroup.go:24):
type Group struct {
// Targets is a list of targets identified by a label set. Each target is
// uniquely identifiable in the group by its address label.
Targets []model.LabelSet
// Labels is a set of labels that is common across all targets in the group.
Labels model.LabelSet
// Source is an identifier that describes a group of targets.
Source string
}注意:目标不是一个 IP 字符串,而是一组标签**(model.LabelSet)。** 这个设计非常关键——SD 不只告诉你“去抓 10.1.2.3:8080”,它还附带一堆元数据标签:
- Kubernetes SD 会带上
__meta_kubernetes_pod_name、__meta_kubernetes_namespace、__meta_kubernetes_pod_label_*(pod 上的所有 label!)、__meta_kubernetes_pod_annotation_*…… - EC2 SD 会带上实例类型、可用区、tag……
这些 __meta_* 标签是**relabeling 的原料**:下一章你会看到,正是靠它们,你才能写出“只抓带某个注解的 pod”、“把 K8s 的 namespace 变成指标的 namespace 标签”这类规则。
Group 里的 Labels 是组级公共标签(整组共享),Targets 里每个目标再带自己的标签——这是个省内存的小设计(公共部分不必每个目标重复一遍)。
实战:最常用的三种
30 种里,现实中你 90% 只会用到这几种:
| SD | 场景 | 关键点 |
|---|---|---|
| kubernetes | K8s 集群里(最常用) | 能发现 pod/service/endpoints/node/ingress,带海量 __meta_kubernetes_* 标签 |
| file | 静态或外部系统生成 | 监听 JSON/YAML 文件变化,由别的系统写这个文件——万能逃生舱 |
| static | 手工写死 | 测试和少量固定目标 |
| http | 自定义 | Prometheus 定期 GET 一个 URL 拿目标列表,你用任何语言实现它 |
file_sd 和 http_sd 是“万能逃生舱”,值得记住。 如果你的目标来源是个奇怪的内部系统(自研的 CMDB、某个数据库表),不需要给 Prometheus 提 PR 写一个新 SD——你只要写个小脚本,定期把目标列表输出成 JSON 文件(file_sd)或暴露成一个 HTTP 接口(http_sd),Prometheus 就能用。
这是“小接口 + 通用逃生舱”的组合拳:核心只认一个接口,而 file_sd/http_sd 把这个接口开放给了整个外部世界——任何系统都能接进来,而 Prometheus 一行代码不用改。
小结与下一章
服务发现回答“去抓谁”。Discoverer 接口只有一个方法 Run(ctx, up chan<- []*targetgroup.Group)(discovery/discovery.go:35),却撑起 30 种实现(接口越小,生态越大)。它的契约反直觉却极聪明:推送全量而非增量,且实现方不必知道到底变没变——换来实现极简(不用维护 diff)、天生自愈(下次推送自动修正,永不状态漂移),是“声明式胜过命令式”的典型。目标不是 IP 而是一组标签(targetgroup.Group),SD 附带的 __meta_* 元数据标签是 relabeling 的原料。实战常用 kubernetes,而 file_sd/http_sd 是万能逃生舱——任何外部系统都能借它接入,核心不用改。
目标发现了,但“发现的”和“该抓的”往往不是一回事。下一章讲那个把两者连起来、也是 Prometheus 里出了名强大、也出了名烧脑的机制。👉 relabeling。
参考资料
- [1] Prometheus 源码
discovery/discovery.go:35(Discoverer接口与契约注释)、discovery/targetgroup/targetgroup.go:24(Group)。 - [2] Prometheus 源码
discovery/下 30 个 SD 实现(kubernetes/、consul/、file/、http/等)。 - [3] Prometheus 官方文档:Configuration —
<kubernetes_sd_config>、<file_sd_config>、<http_sd_config>。