跳到主要内容

服务发现与 relabeling

上一篇用 static_configs 把几台虚拟机抓了起来——机器少、IP 固定时这么干没问题。但进了云和 K8s 的世界,Pod 重建一次 IP 就变一次,弹性伸缩组里的机器说没就没,手工维护目标列表注定追不上变化。这篇笔记记录 Prometheus 的服务发现机制(重点是 kubernetes_sd)和 relabel_configs 标签重写的实战配方,解决「目标自己来、标签我来定」的问题。示例环境:Prometheus 3.x(2.5x 同样适用),K8s 1.30 集群。

为什么需要服务发现​

static_configs 的前提是「目标列表稳定、且我知道它是谁」。云环境下这两个前提都不成立:

  • 虚拟机弹性伸缩,Auto Scaling 忙时扩一批、闲时缩掉;
  • K8s 里 Pod 是消耗品,重建、漂移、滚动发布都会换 IP;
  • 新上一个服务,还得想起去改 prometheus.yml 再 reload——人是最不可靠的环节。

服务发现(Service Discovery,SD)的思路:让 Prometheus 自己去问「权威数据源」要目标列表——K8s 的 API Server、云厂商的 API、Consul 注册中心,或者一个约定好的文件。目标增删自动跟进,配置文件只描述「抓哪一类」,不写死「抓哪几个」。

服务发现机制总览​

Prometheus 内置了几十种 SD,常用的就这几个:

SD 机制数据源适用场景
kubernetes_sd_configsK8s API Server集群内的节点、Pod、Service、Endpoints
ec2_sd_configsAWS API按标签、地域发现 EC2(azure_sd、gce_sd 同理)
consul_sd_configsConsul服务注册进 Consul 的传统微服务架构
file_sd_configs本地 YAML/JSON 文件通用兜底:CMDB、Ansible、任意脚本生成目标文件
dns_sd_configsDNS SRV/A 记录用 DNS 做服务注册的老系统

file_sd:最通用的兜底​

file_sd 值得单独一说:它不依赖任何外部系统,Prometheus 定期读指定文件里的目标列表,文件一变就生效,不用 reload。CMDB、Ansible、crontab 脚本,谁都能生成这个文件:

scrape_configs:
- job_name: vm-nodes
file_sd_configs:
- files:
- /etc/prometheus/targets/*.yml
refresh_interval: 30s # 默认 5m,调短点感知更快

目标文件长这样(YAML、JSON 两种格式都认):

# /etc/prometheus/targets/prod-nodes.yml
- targets:
- 10.0.1.11:9100
- 10.0.1.12:9100
labels:
env: prod
role: web
- targets:
- 10.0.2.11:9100
labels:
env: prod
role: db
[
{
"targets": ["10.0.3.11:9100"],
"labels": { "env": "staging", "role": "cache" }
}
]

本质上 file_sd 是把 static_configs 挪进独立文件并支持热更新。混合场景常见用法:K8s 里的目标走 kubernetes_sd,集群外的虚拟机由 Ansible 渲染 file_sd 文件下发。

kubernetes_sd​

kubernetes_sd_configs 让 Prometheus 以指定「角色(role)」watch API Server,自动发现目标。role 决定发现粒度:

role一个目标对应典型用途
node每个节点一个(kubelet 地址)抓 kubelet、cAdvisor,或按节点抓 DaemonSet 形态的 node_exporter
service每个 Service 一个(ClusterIP)黑盒式探测:Service 整体通不通
pod每个 Pod 的每个声明端口一个注解驱动的应用抓取
endpoints每个 Service 的每个后端端口一个官方推荐的服务抓取姿势
endpointslice同 endpoints,走 EndpointSlice APIendpoints 的新版替代,K8s 1.21+ 推荐
ingress每个 Ingress 的每个 path 一个探测对外暴露的 URL

基于 endpoints 抓服务的原理​

最常用的姿势:Service 打注解,endpoints 角色发现,relabel 筛选。链条是这样的:

  1. Service 上打 prometheus.io/scrape: "true" 等注解,声明「抓我」;
  2. kubernetes_sd 以 endpoints 角色发现这个 Service 的所有后端,每个后端 Pod 的对应端口生成一个目标;
  3. 每个目标自动携带一串 __meta_kubernetes_* 元标签(namespace、Pod 名、注解值……);
  4. relabel_configs 按元标签筛选、改写,生成最终抓取目标。

Service 侧只要加注解:

apiVersion: v1
kind: Service
metadata:
name: demo-app
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: /metrics
spec:
selector:
app: demo-app
ports:
- name: http
port: 8080

之后扩容出来的新后端、滚动发布换掉的 Pod,都会被自动纳入抓取,全程不动 Prometheus 配置。生产上这套逻辑通常由 kube-prometheus-stack 的 ServiceMonitor 自动生成,手写配置的原理和它一模一样,见 监控、日志与可观测性。

集群内外的权限

Prometheus 跑在集群内:给它的 ServiceAccount 配 nodes、pods、services、endpoints 的 list/watch 权限(kube-prometheus-stack 已配好)。跑在集群外:在 kubernetes_sd_configs 里用 kubeconfig_file 指定 kubeconfig,权限要求相同。

relabel_configs 入门​

relabel_configs 写在 job 下,在抓取发生之前,对每个目标的标签集合逐条加工。先认识三类标签:

  • 元标签 __meta_kubernetes_*:SD 发现目标时附带的原始信息,如 __meta_kubernetes_namespace、__meta_kubernetes_pod_name、__meta_kubernetes_pod_annotation_prometheus_io_scrape(注解名里的点和横线被转成下划线)。抓取结束后全部丢弃、不落地——想留就得用 relabel 改名。
  • 内部标签 __address__、__scheme__、__metrics_path__、__param_*:控制「怎么抓」——连谁、走 http 还是 https、抓哪个路径、带什么参数。
  • 普通标签:最终留在时序上的标签,如 job、instance、namespace。

每条规则四个关键字段:

  • source_labels:取哪几个标签的值,多个值默认用 ; 拼接后再匹配;
  • regex:对拼接结果做匹配,RE2 语法、隐含首尾锚点(全匹配);
  • target_label / replacement:要写入的标签名和值,replacement 里可用 $1 引用捕获组,默认就是 $1;
  • action:本条规则干什么,默认 replace。
regex 是全匹配,不是包含

这是 relabel 第一大坑:regex: prod 只匹配值恰好是 prod,匹配不了 prod-web;想匹配「含 prod」要写 .*prod.*;想匹配「kube 开头」要写 kube-.*。另外 RE2 不支持回溯和 lookahead,写法上有别于 PCRE。

常用 action 逐个看(以下都是 relabel_configs 下的片段)。

keep / drop:按标签值保留或丢弃整个目标:

# 只保留打了 prometheus.io/scrape: "true" 注解的 Pod
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: "true"

# 丢掉 kube-system 命名空间的目标
- source_labels: [__meta_kubernetes_namespace]
action: drop
regex: kube-system

replace:把 source 的值(经 regex 捕获、replacement 拼装)写进 target_label,最常用来「把元标签落成正式标签」:

# 把命名空间写成正式标签 namespace
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: namespace

# 自定义 metrics 路径:注解有值(捕获组非空)才命中
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)

labelmap:批量映射,把标签名匹配 regex 的部分按捕获组改名,一步把 Pod 的所有 label 落成时序标签:

# Pod 上所有 label 原样映射为时序标签(app、version……)
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+)

labeldrop / labelkeep:按标签名(不是值)删/留标签,注意它俩不看 source_labels:

# 删掉 K8s 自动生成的模板哈希类标签,减少无意义的标签 churn
- action: labeldrop
regex: __meta_kubernetes_pod_label_(pod_template_hash|controller_revision_hash)

实战配方​

配方一:只抓打了注解的 Pod​

最经典的 kubernetes-pods job 骨架:

scrape_configs:
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: "true"

role: pod 会为每个 Pod 的每个声明端口生成一个目标,keep 规则把没打注解的全部筛掉。Pod 侧注解写法:

metadata:
annotations:
prometheus.io/scrape: "true"

配方二:把 namespace 和 Pod 名写成标签​

默认目标只有 job、instance 两个标签,排查时看不出是哪个 Pod。把元标签落下来:

    relabel_configs:
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: namespace
- source_labels: [__meta_kubernetes_pod_name]
action: replace
target_label: pod

之后查询就能按维度聚合:

sum by (namespace) (rate(container_cpu_usage_seconds_total[5m]))

配方三:自定义 metrics path 与端口​

应用指标不在 /metrics,或容器声明的端口不是指标端口时,用注解指定:

    relabel_configs:
# 路径:注解 prometheus.io/path 有值就替换默认的 /metrics
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
# 端口:注解 prometheus.io/port 有值就改写抓取地址的端口
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__

端口这条值得细看:两个 source_labels 的值用 ; 拼接(地址在前、注解在后),regex 把「IP[:原端口];新端口」拆开,replacement 拼回 IP:新端口。regex 里那个分号就是拼接符,不是笔误。

配方四:丢弃 kube-system​

    relabel_configs:
- source_labels: [__meta_kubernetes_namespace]
action: drop
regex: kube-system

别忘全匹配语义:想把 kube-public、kube-node-lease 一起丢,写 kube-.* 或 kube-system|kube-public|kube-node-lease。

配方五:scheme 改成 https​

目标只暴露 https 时,用注解声明并改写内部标签 __scheme__:

    relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scheme]
action: replace
target_label: __scheme__
regex: (https?)

注解值是 https 才命中替换;没打注解的目标不受影响,保持默认 http。自签证书还要在 job 级配 tls_config:

  - job_name: kubernetes-pods
tls_config:
insecure_skip_verify: true # 内网自签的临时方案,生产建议挂 CA
# ...其余配置省略

组合起来​

五段配方按顺序拼在一起,就是生产可用的完整 job:

scrape_configs:
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
relabel_configs:
# 1. 只抓打了注解的
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: "true"
# 2. 自定义路径
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
# 3. 自定义端口
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
# 4. 落下 namespace 和 pod 名
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: namespace
- source_labels: [__meta_kubernetes_pod_name]
action: replace
target_label: pod
# 5. 丢弃系统命名空间
- source_labels: [__meta_kubernetes_namespace]
action: drop
regex: kube-system

规则自上而下逐条执行:keep 放最前面尽早筛掉无关目标;改路径、改端口的规则要赶在抓取消费 __address__ 之前。配方里的顺序就是推荐顺序。

metric_relabel_configs​

relabel_configs 管「抓谁、怎么抓」,metric_relabel_configs 管抓回来的指标本身:在样本入库之前逐条加工,能干的事包括丢指标、改指标名、删样本上的标签。

典型用途一:丢弃高基数或不关心的指标,省存储省内存:

  - job_name: cadvisor
# ...kubernetes_sd 与 relabel_configs 省略
metric_relabel_configs:
# go 运行时指标对排障帮助不大,丢
- source_labels: [__name__]
action: drop
regex: go_.*
# cAdvisor 的块设备读写细分指标量太大,丢
- source_labels: [__name__]
action: drop
regex: container_fs_(reads|writes)_.*

__name__ 是「指标名」这个特殊标签,对样本做匹配基本靠它。

典型用途二:改指标名(升级 Exporter 后老面板不用跟着改):

    metric_relabel_configs:
- source_labels: [__name__]
action: replace
regex: old_requests_total
target_label: __name__
replacement: app_requests_total

两者的分工:

对比项relabel_configsmetric_relabel_configs
执行时机抓取之前抓取之后、入库之前
作用对象目标(target)的标签集合每个样本的标签(含 __name__)
可用标签__meta_*、__address__ 等元/内部标签目标最终标签 + 指标自带标签
keep/drop 的效果整个目标不抓单条指标系列不入库
典型用途筛目标、改地址/路径/scheme、落 namespace 等标签丢高基数指标、改指标名、删 churn 标签
能丢在源头就别留到入库

cAdvisor、envoy 这类高基数来源,用 metric_relabel_configs 丢弃用不上的系列是标准操作。反过来,如果一个 Exporter 暴露的指标九成都不看,也可以在 relabel_configs 阶段就整个不抓——抓得少比丢得快更省资源。

常见坑​

  • 规则顺序敏感:规则自上而下执行,后面的规则看到的是前面改完的结果。典型翻车:先 replace 改了 __address__,后面想按注解改端口时取到的已经是旧地址。记住顺序:先 keep/drop 筛选,再改地址/路径,最后落正式标签。
  • keep 的正则忘了全匹配:想抓「prod 开头的命名空间」写成 regex: prod,结果一个都抓不到(prod 只匹配恰叫 prod 的);反过来想精确匹配注解值 true,写了 tr.* 这类宽匹配,把打了 trusted 之类注解的也抓了进来。写完规则去 Service Discovery 页对照一眼最稳妥。
  • labelmap 前缀写错:regex: __meta_kubernetes_pod_label_(.+) 里捕获组才是新标签名,漏写 (.+) 一条都映射不出来;把 pod_label 手误成 pod_annotation 这类前缀写错,也是静默不生效。另外 labelmap 映射出来的是目标标签,想删指标上的标签请去 metric_relabel_configs。
  • 目标不 UP 先看标签变化:Web UI 的 Status → Service Discovery 页能看到 SD 发现的所有目标(含被 keep 掉的)和它们的元标签;Status → Targets 页显示的是 relabel 之后的存活目标。排查路径固定为:发现页确认元标签对不对 → 目标页确认改写结果对不对 → curl 目标地址确认网络通不通。
  • 改了配置不生效:改完 prometheus.yml 要 reload(kill -HUP,或 POST /-/reload,需启动参数 --web.enable-lifecycle)。已入库的历史数据保持旧标签不变,新旧标签会查出两条曲线——时序身份的概念见 核心概念与架构。

小结​

  • 服务发现让目标列表自动跟进:K8s 内用 kubernetes_sd,云主机用云厂商 SD,其他场景 file_sd 兜底。
  • kubernetes_sd 的 role 决定发现粒度;抓应用服务用 endpoints/pod 角色 + 注解驱动,是官方姿势。
  • relabel_configs 在抓取前重写标签:keep/drop 筛目标,replace 改地址落标签,labelmap 批量映射;记住 regex 全匹配、多 source 用分号拼接、__meta_* 标签抓完即丢。
  • metric_relabel_configs 在抓取后处理指标本身:按 __name__ 丢弃高基数系列是省资源的标配。
  • 排查 relabel 问题盯两个页面:Service Discovery(看发现的原始目标)和 Targets(看最终抓取目标)。
  • 目标抓回来了,下一篇讲怎么查:PromQL 查询实战。