Learn
Prometheus/10-relabel-and-metric-relabeling

relabel 与指标重标记

如果说 PromQL 是 Prometheus 的「读」能力,那 relabel 就是它的「写」能力——在数据进库之前,你有一次机会重新定义它长什么样。改抓取地址、丢弃不想要的目标、把服务发现塞过来的一堆元数据变成有意义的标签、把占了 40% 存储却从没人看过的指标扔掉,全都靠这套机制。

它也是整个 Prometheus 配置体系里最容易写错、错了还最难发现的部分。写错一条 keep,结果不是报错,而是「目标悄悄地一个都不剩」,Targets 页面干干净净什么都没有,看起来倒像是一切正常。

本章把这套机制彻底拆开。

读完本章你会掌握:

  • relabel_configs 与 metric_relabel_configs 的时机差异:一个改「目标」,一个改「样本」
  • 一条规则的六个字段:source_labels、separator、regex、target_label、replacement、modulus
  • 八种 action:replace、keep、drop、labelmap、labeldrop、labelkeep、hashmod、lowercase 与 uppercase
  • 正则默认全串锚定这条最关键的隐藏规则
  • 三个实战套路:只抓特定实例、只保留特定指标、重写标签
  • 经典坑:keep 写错导致抓不到数据、labeldrop 删掉 instance 导致样本冲突

一、时机:两个 relabel 分别站在流水线的哪个位置

先把第 9 章那张流水线图再看一遍,这次只关注两个 relabel 的位置:

服务发现产出候选目标(一组标签)
        到
【relabel_configs】      ← 作用对象:目标(Target)
        到
目标定型:丢弃 __ 开头的标签,确定 instance
        到
发起 HTTP 请求,拉回一大堆样本
        到
【metric_relabel_configs】 ← 作用对象:每一条样本(Sample)
        到
写入 TSDB

两者的对比:

维度relabel_configsmetric_relabel_configs
执行时机抓取之前抓取之后、入库之前
作用对象一个「目标」的标签集每一条「样本」的标签集
执行次数每轮服务发现时,每个目标执行一次每次抓取,每条样本都执行一遍
能看到的标签__address__、__scheme__、__metrics_path__、__param_*、__meta_*、静态 labels__name__ 以及样本自身的标签 + 已合并的目标标签
典型用途决定抓不抓、抓哪个地址、把 SD 元数据变成正式标签丢弃不需要的指标、清洗高基数标签
能省抓取流量吗能(目标都不抓了)不能(数据已经拉回来了,只是不入库)
drop 掉之后该目标从 Targets 页面消失该样本不写入,up 仍然正常
ℹ️一句话记住区别

relabel_configs 回答的是「要不要抓这台机器、去哪儿抓」;metric_relabel_configs 回答的是「抓回来的这堆数据,哪些值得存」。

前者看到的是 __address__ 这种「怎么发请求」的信息,后者压根看不到 __address__(那时目标标签早已定型,__ 开头的都被清掉了)。所以想在 metric_relabel_configs 里改抓取地址是不可能的,反过来想在 relabel_configs 里按 __name__ 丢指标也是不可能的——那时候一条样本都还没拉回来呢。

二、一条规则的六个字段

无论哪种 relabel,单条规则的字段是同一套:

- source_labels: [label_a, label_b]   # 取哪些标签的值(数组,按顺序)
  separator: ';'                       # 拼接分隔符,默认分号
  regex: '(.*);(.*)'                   # 匹配用的正则,默认 (.*)
  target_label: 'new_label'            # 结果写到哪个标签
  replacement: '$1-$2'                 # 写什么,默认 $1
  action: replace                      # 做什么,默认 replace
  modulus: 8                           # 仅 hashmod 使用

执行逻辑严格按这个顺序:

1. 按 source_labels 的顺序取出各标签的值
   (标签不存在时取空字符串,不会报错)
2. 用 separator 把这些值拼成一个字符串
3. 用 regex 去匹配这个字符串(全串锚定)
4. 根据 action 决定动作,replacement 里的 $1 $2 用捕获组填充

举个例子,某目标当前有标签 env="prod"、region="bj":

- source_labels: [env, region]
  separator: '-'
  regex: '(.+)-(.+)'
  target_label: zone
  replacement: '$2.$1'
  action: replace

拼接得到 prod-bj,正则捕获出 $1 = prod、$2 = bj,最终写入 zone="bj.prod"。

⚠️regex 默认是「全串锚定」的,这是第一大坑

Prometheus 会自动在你写的正则两端加上锚点,等价于 ^(?:你的正则)$。也就是说:

  • regex: 'prod' 匹配不上 prod-bj,因为它要求整个字符串就是 prod。
  • 想匹配「以 prod 开头」,必须写 regex: 'prod.*'。
  • 想匹配「包含 prod」,必须写 regex: '.*prod.*'。

这一条规则是 relabel 事故的头号来源。写 keep 的时候尤其致命:正则匹配不上,所有目标全被丢弃,你会得到一个空空如也的 Targets 页面。

💡几个默认值背下来能省很多字
  • separator 默认是 ;(分号)
  • regex 默认是 (.*),即匹配任意内容并整体捕获为 $1
  • replacement 默认是 $1
  • action 默认是 replace

所以「把 A 标签的值原样复制到 B 标签」只需要写三行:

- source_labels: [__address__]
  target_label: instance_addr

因为 regex: (.*) 捕获全部内容,replacement: $1 原样写出,action: replace 是默认。

三、八种 action 逐一详解

replace:改写或新建标签(默认)

# 从 __address__ = "10.0.0.11:9100" 里抽出 IP,写入 host 标签
- source_labels: [__address__]
  regex: '([^:]+):\d+'
  target_label: host
  replacement: '$1'

三条重要行为:

  1. 正则匹配不上时,这条规则什么都不做——目标标签保持原样,不会被清空,也不会报错。这是 relabel 里最「安静」的失败方式。
  2. 匹配上但 replacement 展开后是空字符串时,target_label 会被删除。这是主动删标签的一种写法。
  3. 不写 source_labels 也合法。此时拼接结果是空串,默认 regex: (.*) 能匹配空串,于是 replacement 被原样写入。这就是第 9 章 blackbox 配置里那条「硬编码地址」的原理:
- target_label: __address__
  replacement: 'blackbox-exporter:9115'

keep / drop:决定「要不要」

# 只保留 __meta_kubernetes_pod_annotation_prometheus_io_scrape 为 "true" 的目标
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
  regex: 'true'
  action: keep
 
# 丢弃所有 env 为 dev 或 test 的目标
- source_labels: [env]
  regex: 'dev|test'
  action: drop
  • keep:正则匹配上才留下,匹配不上就丢弃。
  • drop:正则匹配上就丢弃,匹配不上才留下。

两者互为镜像。用哪个取决于哪种写法的正则更简单、更不容易漏。

⚠️keep 与 drop 混用时,顺序决定一切

规则是从上到下顺序执行的,一旦某条 keep 或 drop 判定丢弃,后面的规则就不再执行了。所以:

# 这样写:先只留 prod,再从 prod 里去掉 canary —— 符合预期
- source_labels: [env]
  regex: 'prod'
  action: keep
- source_labels: [role]
  regex: 'canary'
  action: drop

反过来把两条互斥的 keep 叠在一起,结果一定是「一个都不剩」:

# 灾难写法:先 keep 掉只剩 bj,再 keep 只剩 sh,最后什么都没有
- source_labels: [region]
  regex: 'bj'
  action: keep
- source_labels: [region]
  regex: 'sh'
  action: keep

想表达「或」,必须写在同一条规则的正则里:regex: 'bj|sh'。

labelmap:批量重命名标签

labelmap 很特殊:它的 regex 匹配的是标签名,不是标签值。所有名字能匹配上的标签,都会按 replacement 生成一个新标签名,值原样复制过去。

# 把所有 __meta_kubernetes_pod_label_xxx 变成 xxx
- action: labelmap
  regex: '__meta_kubernetes_pod_label_(.+)'
  replacement: '$1'

假设目标有 __meta_kubernetes_pod_label_app="nginx" 和 __meta_kubernetes_pod_label_tier="frontend",执行后会多出 app="nginx" 和 tier="frontend" 两个标签。

ℹ️labelmap 是「复制」不是「移动」

原来的标签依然存在。只不过在 relabel_configs 里这通常无所谓——__meta_* 和其他 __ 开头的标签会在目标定型时被自动清掉。但如果你在 metric_relabel_configs 里用 labelmap 重命名普通标签,就要额外补一条 labeldrop 把旧的删掉,否则会同时留下新旧两份。

labeldrop / labelkeep:按名字删标签

同样作用于标签名:

# 删掉所有以 pod_label_ 开头的标签(通常是清理噪声)
- action: labeldrop
  regex: 'pod_label_.*'
 
# 只保留这几个标签,其余全删(危险,慎用)
- action: labelkeep
  regex: '__name__|job|instance|env'
⚠️labeldrop / labelkeep 只允许写 regex 一个字段

这是 Prometheus 明确做了配置校验的地方。如果你给它们额外写了 source_labels、target_label、replacement 或 modulus,启动时会直接报错:

labeldrop and labelkeep actions require only 'regex', and no other fields

另外 labelkeep 有个致命细节:指标名本身也是一个标签,叫 __name__。如果你的 regex 没把它包含进去,指标名会被一起删掉,样本就废了。所以 labelkeep 的正则里几乎总要写上 __name__。

hashmod:把目标均匀分片

# 把所有目标按地址哈希分成 4 份,本实例只抓第 1 份
- source_labels: [__address__]
  modulus: 4
  target_label: __tmp_shard
  action: hashmod
- source_labels: [__tmp_shard]
  regex: '1'
  action: keep

hashmod 对拼接后的值做哈希,再对 modulus 取模,把结果(0 到 modulus - 1 的整数)写进 target_label。它本身不丢弃任何目标,必须配合后面一条 keep 才能实现分片。

这是水平扩展 Prometheus 的标准手法:部署 4 个 Prometheus 实例,配置完全相同,只有那条 keep 的 regex 分别写 0、1、2、3,每个实例就只负责四分之一的目标。因为哈希是确定的,同一个目标永远落在同一个分片上,不会来回漂移。

💡__tmp 前缀是官方保留给你用的

Prometheus 官方承诺永远不会使用 __tmp 开头的标签名。所以当你需要一个中间变量(比如 hashmod 的结果、多步拼接的临时值)时,用 __tmp_xxx 命名最安全:既不会和内置标签撞车,又会在目标定型时自动被清掉,不会污染最终数据。

lowercase / uppercase:大小写转换

# 把服务发现拿到的主机名统一转成小写
- source_labels: [__meta_consul_node]
  target_label: node
  action: lowercase

这两个 action 在 2.36 加入,用来解决「同一台机器在不同系统里大小写不一致,导致标签值分裂成两条序列」的问题。它们需要 source_labels 和 target_label,不使用 regex。

keepequal / dropequal:比较两个标签

2.41 加入,用于「保留(或丢弃)source_labels 拼接结果等于 target_label 值的目标」:

# 只保留「暴露端口」与「注解里声明的端口」一致的目标
- source_labels: [__meta_kubernetes_pod_container_port_number]
  target_label: __tmp_port
  action: keepequal

用得不多,知道有这么回事即可。

🎯练习 1:预测 relabel 的执行结果

一个目标经过服务发现后带有这些标签:

__address__  = "10.20.30.40:9100"
__scheme__   = "http"
env          = "production"
region       = "bj-1"
role         = "worker"

依次执行下面四条规则,写出每一步之后的标签变化,以及最终这个目标会不会被抓取:

relabel_configs:
  - source_labels: [env]
    regex: 'prod'
    action: keep
 
  - source_labels: [region]
    regex: '([a-z]+)-(\d+)'
    target_label: dc
    replacement: '$1'
 
  - source_labels: [__address__]
    regex: '([^:]+):(\d+)'
    target_label: host
    replacement: '$1'
 
  - action: labeldrop
    regex: 'role'

四、实战套路一:只抓取特定实例

最常见的需求:服务发现返回了一大堆目标,但你只想抓其中一部分。

- job_name: 'node'
  file_sd_configs:
    - files: ['/etc/prometheus/targets/*.yml']
  relabel_configs:
    # 1) 只要生产环境
    - source_labels: [env]
      regex: 'prod.*'
      action: keep
 
    # 2) 但排除掉标记为下线中的机器
    - source_labels: [status]
      regex: 'draining|decommissioned'
      action: drop
 
    # 3) 也排除掉端口不是 9100 的(防止配错)
    - source_labels: [__address__]
      regex: '.*:9100'
      action: keep

在 Kubernetes 里,这个套路演变成大名鼎鼎的「注解驱动」模式:

relabel_configs:
  # 只抓带有 prometheus.io/scrape=true 注解的 Pod
  - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
    regex: 'true'
    action: keep
 
  # 如果注解指定了路径,就用它覆盖 metrics_path
  - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
    regex: '(.+)'
    target_label: __metrics_path__
 
  # 如果注解指定了端口,就把地址里的端口换掉
  - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
    regex: '([^:]+)(?::\d+)?;(\d+)'
    target_label: __address__
    replacement: '$1:$2'

注意第二条:regex: '(.+)' 的作用是「只在注解存在(非空)时才覆盖」。如果 Pod 没有这个注解,取出来是空串,(.+) 要求至少一个字符,匹配失败,规则跳过,__metrics_path__ 保持默认的 /metrics。这个「用 (.+) 做存在性判断」的技巧非常常用。

第三条用了两个 source_labels,拼接后形如 10.244.1.5:9100;8080,正则把 IP 和注解端口分别捕获,重组成 10.244.1.5:8080。

五、实战套路二:重写标签

从一个标签里提取信息

# instance = "web-01.bj.example.com:8080" → 拆出 hostname 和 idc
- source_labels: [__address__]
  regex: '([^.]+)\.([^.]+)\..*'
  target_label: hostname
  replacement: '$1'
- source_labels: [__address__]
  regex: '([^.]+)\.([^.]+)\..*'
  target_label: idc
  replacement: '$2'

合并多个标签

- source_labels: [namespace, pod]
  separator: '/'
  target_label: pod_full
  replacement: '$1'

注意这里 regex 用的是默认的 (.*),它会把整个拼接结果 default/nginx-abc 捕获为 $1,所以直接写 $1 就得到了拼接结果。

删除标签

# 方式 A:labeldrop(推荐,语义清晰)
- action: labeldrop
  regex: 'noisy_label'
 
# 方式 B:replace 成空值
- source_labels: [noisy_label]
  target_label: noisy_label
  replacement: ''
⚠️千万别随手删掉 instance 或其他区分性标签

假设一个 job 下有 20 台机器,你嫌 instance 太长,来了一条:

- action: labeldrop
  regex: 'instance'

后果是:20 台机器的同名指标标签集变得完全一样,它们全都在往同一条时间序列里写数据。同一时间戳上出现 20 个不同的值,Prometheus 会报错:

Error on ingesting samples with different value but same timestamp

只有一台机器的数据能侥幸写进去,其余 19 台全部被丢弃,而且这个错误只出现在日志里,界面上看 up 还是 1,极难察觉。

判定原则:删标签之前先问一句「删掉它以后,不同来源的数据还能区分开吗?」区分不开就绝对不能删。instance 在 99% 的情况下都属于不能删的那一类。

🎯练习 2:用 relabel 完成一次「乾坤大挪移」

你有一批 file_sd 提供的目标,原始标签如下:

__address__ = "10.1.2.3:9100"
service     = "ORDER-API"
zone        = "cn-north-1a"
tier        = "backend"
maintenance = "false"

要求:

  1. 处于维护状态(maintenance 为 true)的机器不抓。
  2. service 统一转成小写,写入新标签 app,并删掉原来的 service。
  3. 从 zone 里提取出地域部分(cn-north-1,即去掉最后的可用区字母),写入标签 region。
  4. 新增一个标签 ip,值是 __address__ 里的 IP 部分(不含端口)。

写出完整的 relabel_configs。

六、实战套路三:只保留特定指标

这是 metric_relabel_configs 的主战场。典型场景:某个 Exporter 暴露了 2000 条指标,你只关心其中 30 条。

按指标名丢弃

- job_name: 'node'
  static_configs:
    - targets: ['10.0.0.11:9100']
  metric_relabel_configs:
    # 丢掉一整批从来不看的指标
    - source_labels: [__name__]
      regex: 'node_(scrape_collector|mountstats|nfsd|infiniband|wifi)_.*'
      action: drop

按指标名白名单(更激进)

metric_relabel_configs:
  - source_labels: [__name__]
    regex: 'node_(cpu_seconds_total|memory_Mem(Total|Available)_bytes|filesystem_(avail|size)_bytes|load1|load5)'
    action: keep
⚠️白名单会连 up 一起干掉吗?

不会。up、scrape_duration_seconds 这几条自监控指标是 Prometheus 在 metric_relabel_configs 之后才附加的,白名单管不到它们。

但要注意另一件事:如果 Exporter 自己暴露的 xxx_up 指标(比如 mysql_up、mq_exporter_up)不在白名单里,就会被丢掉,而你恰恰最需要它。所以写白名单时务必检查一遍,别把健康指标误伤了。

按标签值丢弃

metric_relabel_configs:
  # 只保留真实磁盘的文件系统指标,丢掉临时/容器挂载点
  - source_labels: [__name__, mountpoint]
    separator: ';'
    regex: 'node_filesystem_.*;/(run|sys|proc|var/lib/docker|var/lib/kubelet).*'
    action: drop
 
  # 丢掉 loopback 和虚拟网卡的网络指标
  - source_labels: [__name__, device]
    regex: 'node_network_.*;(lo|veth.*|docker0|br-.*)'
    action: drop

清洗高基数标签

有时候不需要丢整条指标,只要把某个「值域太大」的标签删掉:

metric_relabel_configs:
  # 某个库把 request_id 打进了标签,直接删掉这个标签
  - action: labeldrop
    regex: 'request_id|trace_id|session_id'
💡怎么知道该丢什么?先量化,再动手

不要凭感觉丢指标。按这个顺序做:

  1. 打开 /tsdb-status 页面,看「序列数最多的指标名 Top10」和「取值最多的标签 Top10」。
  2. 用 topk(20, count by (__name__)({__name__=~".+"})) 精确统计每个指标名占了多少条序列。
  3. 在 Grafana 里全局搜索这个指标名,确认没有任何看板或告警在用它。
  4. 先加 drop 规则观察一周,确认没人来投诉数据不见了,再固化。

丢完之后用这个式子验证效果:

scrape_samples_scraped - scrape_samples_post_metric_relabeling

它直接告诉你每次抓取过滤掉了多少条样本。

🎯练习 3:判断该用哪个 relabel

下面六个需求,分别应该写在 relabel_configs 还是 metric_relabel_configs 里?给出理由和示例配置。

  1. 不抓取 env=dev 的机器。
  2. 不存储 go_gc_duration_seconds 这个指标。
  3. 把抓取端口从 9100 改成 9200。
  4. 给所有序列加一个 datacenter="bj" 标签。
  5. 删掉指标上一个基数极高的 user_id 标签。
  6. 只抓 Kubernetes 里带特定注解的 Pod。

七、经典坑合集

坑 1:keep 写错,目标全没了

症状:Targets 页面上这个 job 一个目标都没有,也没有任何报错。查询时对应的指标完全查不到。

原因:多半是正则锚定问题,或者 source_labels 里那个标签根本不存在(取到空串)。

排查:打开 /service-discovery 页面,找到这个 job,把 "Show more" 展开。你会看到:

  • Discovered Labels:服务发现原始产出的全部标签,包括所有 __meta_*
  • Target Labels:relabel 之后的结果;被丢弃的目标这里会显示 Dropped

对着 Discovered Labels 逐字检查你的 source_labels 拼写和 regex 写法。九成的问题在这一步就能看出来——要么标签名拼错了(__meta_kubernetes_pod_annotation_prometheus_io_scrape 这种长名字极易打错),要么正则少了 .*。

坑 2:拿 metric_relabel_configs 改抓取行为

# 完全无效的写法
metric_relabel_configs:
  - source_labels: [__address__]
    target_label: __address__
    replacement: 'other-host:9100'

__address__ 在目标定型时就被清除了,metric_relabel_configs 阶段根本看不到这个标签,source_labels 取到空串,这条规则相当于什么都没做。改地址只能在 relabel_configs 里。

坑 3:以为 metric_relabel_configs 能省抓取开销

不能。数据已经通过网络传过来、已经被解析成样本了,metric_relabel_configs 只是决定「不写进 TSDB」。它节省的是存储和内存,不节省网络和 CPU。

想省抓取开销,只能从源头减:关掉 Exporter 的 collector,或者用 params 让 Exporter 只采集部分模块。

坑 4:YAML 引号把正则搞坏了

# 危险:双引号里的反斜杠会被 YAML 先转义一次
- regex: "(\d+)"      # YAML 解析后变成 (d+),匹配不上任何数字
 
# 安全:单引号是字面量
- regex: '(\d+)'
💡relabel 的正则一律用单引号包起来

YAML 的双引号字符串支持反斜杠转义,会把 \d 吃成 d;单引号字符串是字面量,原样传给正则引擎。而 relabel 的正则里几乎必然出现 \d、\.、\w,所以养成「一律单引号」的习惯能规避一整类问题。

同理,replacement 里的 $1 也建议用单引号包住,避免和某些工具链的变量展开冲突。

坑 5:忘了 relabel 之后 __ 标签会消失

有人想通过 relabel 把 __meta_kubernetes_namespace 留下来当查询维度,写了半天发现查询时根本没这个标签。原因是所有以双下划线开头的标签在目标定型时都会被删除,这是硬规则。正确做法是把它复制到一个不带下划线前缀的名字上:

- source_labels: [__meta_kubernetes_namespace]
  target_label: namespace
🎯练习 4:诊断并修复一份出问题的配置

运维同学提交了下面这份配置,上线后出现两个现象:(a) k8s-pods 这个 job 在 Targets 页面上一个目标都没有;(b) 把 keep 规则临时注释掉之后目标出现了,但 Prometheus 日志里开始大量刷 Error on ingesting samples with different value but same timestamp。

- job_name: 'k8s-pods'
  kubernetes_sd_configs:
    - role: pod
  relabel_configs:
    - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
      regex: true
      action: keep
    - source_labels: [__meta_kubernetes_namespace]
      regex: 'prod'
      action: keep
    - action: labeldrop
      regex: 'instance|pod'
  metric_relabel_configs:
    - source_labels: [__address__]
      regex: '.*'
      action: keep

请找出所有问题并给出修正后的配置。

小结

  • 时机决定能力:relabel_configs 在抓取前作用于「目标」,能改 __address__、决定抓不抓;metric_relabel_configs 在抓取后作用于「样本」,能按 __name__ 和标签值过滤。
  • 单条规则的执行链是:取 source_labels → 用 separator 拼接 → 用 regex 匹配 → 按 action 动作,replacement 里用 $1 引用捕获组。
  • 正则默认全串锚定,prod 匹配不上 production。这是头号事故来源。
  • 八种 action 分三类:作用于标签值的(replace、keep、drop、hashmod、lowercase、uppercase),作用于标签名的(labelmap、labeldrop、labelkeep),以及比较型的(keepequal、dropequal)。
  • labeldrop / labelkeep 只允许配 regex,多写别的字段会启动失败。
  • 「排除少数例外」优先用 drop,比用 keep 列举正常值更不容易误杀缺标签的目标。
  • 用 (.+) 做「标签存在才覆盖」的判断,用 __tmp_ 前缀存中间结果,都是社区标准技巧。
  • 删标签前务必确认剩下的标签还能区分不同数据来源,否则会撞出 same timestamp different value。
  • 排障第一站永远是 /service-discovery 页面,它并排显示 relabel 前后的标签。
  • 下一章我们讲规则文件:怎么用 recording rules 把复杂查询预计算好,怎么用 alerting rules 把「异常」变成「告警」。