K3s 跨云集群 VictoriaMetrics 指标缺失排查

一个 K3s 集群的节点分布在不同公有云中,云网络之间没有通过 VPC 对等连接或 VPN 打通。各节点都有云平台分配的私网地址,但这些地址只在各自网络内可达。K3s 官方文档 将这类节点不共享同一私有网络、无法直接连接的部署称为分布式混合云或多云集群。

Kubernetes Node 对象同时记录了节点的私网地址和跨网络可达地址:

InternalIP: 10.0.0.43
ExternalIP: 203.0.113.43

本文使用 RFC 5737 保留的文档示例网段 203.0.113.0/24 隐去真实公网地址。

公网地址是否直接绑定到实例网卡并不重要。触发这个问题的条件是:采集端无法访问 Node 对象中的 InternalIP,却仍默认使用它作为抓取地址。

节点本身是 Readykubectl top nodes 也能正常返回 CPU 和内存使用率,但 VictoriaMetrics 的 Grafana 面板始终缺少该节点的指标。这个现象很容易让人误以为集群网络、Kubelet 和 node-exporter 都没有问题。

真正的问题是:metrics-server 和 vmagent 选择跨云节点地址的方式不同。

故障现象

vmagent 的 target 和日志显示,它尝试访问:

https://10.0.0.43:10250/metrics
http://10.0.0.43:9100/metrics

10.0.0.43 只在目标节点所在的云私有网络中可达,其他集群节点无法直接路由到这个地址,因此两个 target 都会超时。

从 vmagent Pod 所在的采集网络直接测试,可以进一步确认问题:

curl --connect-timeout 3 http://10.0.0.43:9100/metrics
# timeout

curl --connect-timeout 3 http://203.0.113.43:9100/metrics
# returns metrics

这说明 node-exporter 本身运行正常,故障发生在抓取地址的选择上。

公网抓取的安全边界

ExternalIP 路径必须配合主机防火墙,只允许集群节点来源地址访问 Kubelet 和 node-exporter。node-exporter 默认使用 HTTP,在不可信网络上还应考虑加密隧道或认证代理。

为什么 kubectl top 正常

metrics-server 可以通过参数明确设置 Kubelet 地址优先级:

--kubelet-preferred-address-types=ExternalIP,InternalIP,Hostname

因此它会优先访问 203.0.113.43:10250kubectl top 正常只能证明 metrics-server 的采集链路可用,不能证明 vmagent 使用了相同地址。

在本文使用的 vmagent v1.144.0 中,Node discovery 会优先把 InternalIP 写入 __address__。如果跨云集群没有共享私网,这个默认地址不一定从采集端可达。对应的地址选择顺序和 meta labels 生成逻辑可以在 node.go 中确认。

Kubelet 与 node-exporter 的发现链路不同

Kubelet 指标由 VMNodeScrape 采集。Node discovery 会直接提供:

__meta_kubernetes_node_address_InternalIP
__meta_kubernetes_node_address_ExternalIP
__address__

因此可以通过 relabel 将 __address__ 改写为 ExternalIP,同时保留原端口。

node-exporter 原本由 VMServiceScrape 采集,链路则是:

VMServiceScrape
  -> Service
  -> Endpoints
  -> Pod IP

node-exporter 使用 hostNetwork 时,Pod IP 等于节点的 InternalIP,所以发现到的 Endpoint 是 10.0.0.43:9100

一个合理但无效的尝试

最初的思路是给 node-exporter 的 VMServiceScrape 设置:

attach_metadata:
  node: true

然后通过 serviceScrapeRelabelTemplate 使用 __meta_kubernetes_node_address_ExternalIP 改写地址。

实际生成的 vmagent 配置中确实包含 attach_metadata.node: true,但运行时 target 仍然没有 __meta_kubernetes_node_address_ExternalIP。在本文使用的 vmagent v1.144.0 中,endpoints discovery 附加的 Node metadata 只有节点名称、labels 和 annotations,不包含 Node Address;这一行为可以在 pod.go 中直接确认。

最终规则接收到的内容类似:

;10.0.0.43:9100

ExternalIP 为空,正则不会匹配,抓取地址也就不会改变。

最终方案

最终方案分为三部分:

  1. 所有 VMNodeScrape 统一优先使用 ExternalIP,并保留各自端口。
  2. 禁用 node-exporter 原有的 VMServiceScrape
  3. 使用 extraObjects 为 node-exporter 创建 VMNodeScrape
保留现有 extraObjects

extraObjects 是 Helm 数组,通过 values 覆盖时会整体替换。实施下面的配置前,必须同时保留 release 中已有的其他 extraObjects
vmagent:
  spec:
    nodeScrapeRelabelTemplate:
      - action: replace
        sourceLabels:
          - __meta_kubernetes_node_address_ExternalIP
          - __address__
        regex: "([^;]+);.+:([0-9]+)"
        replacement: "$1:$2"
        targetLabel: __address__

prometheus-node-exporter:
  vmScrape:
    enabled: false

extraObjects:
  - apiVersion: operator.victoriametrics.com/v1beta1
    kind: VMNodeScrape
    metadata:
      name: node-exporter
      namespace: monitoring
    spec:
      path: /metrics
      port: "9100"
      metricRelabelConfigs:
        - action: drop
          regex: /var/lib/kubelet/pods.+
          sourceLabels:
            - mountpoint
      relabelConfigs:
        - action: replace
          replacement: node-exporter
          targetLabel: job
        - sourceLabels:
            - __meta_kubernetes_node_name
          targetLabel: node
      scheme: http
      selector:
        matchLabels:
          kubernetes.io/os: linux

nodeScrapeRelabelTemplate 会得到类似下面的输入:

203.0.113.43;10.0.0.43:9100

正则捕获 ExternalIP 和原端口,再将地址改写为:

203.0.113.43:9100

没有 ExternalIP 的节点不会匹配,仍然保留原地址。这条规则也不依赖节点名称、固定 IP 或固定端口,因此可以同时处理 Kubelet 10250、node-exporter 9100 和其他 NodeScrape 端口。

如果旧版本 values 曾经配置过 serviceScrapeRelabelTemplate,并且升级时使用 --reuse-values,还需要显式清空旧值:

vmagent:
  spec:
    serviceScrapeRelabelTemplate: []

全新安装或完整 values 中没有旧规则时,可以省略这个空数组。

通过 Helm 升级

先用 dry-run 检查渲染结果,确认原有 extraObjects 没有丢失:

helm upgrade vmks \
  oci://ghcr.io/victoriametrics/helm-charts/victoria-metrics-k8s-stack \
  --version <CURRENT_CHART_VERSION> \
  --namespace monitoring \
  --reuse-values \
  -f vmks-external-ip.yaml \
  --dry-run=server

确认后执行正式升级:

helm upgrade vmks \
  oci://ghcr.io/victoriametrics/helm-charts/victoria-metrics-k8s-stack \
  --version <CURRENT_CHART_VERSION> \
  --namespace monitoring \
  --reuse-values \
  -f vmks-external-ip.yaml \
  --wait \
  --timeout 10m

整个过程通过 Helm values 管理,不直接 patch Operator 生成的 CR,后续升级时仍然可以追踪配置来源。

验证采集结果

首先确认新的 NodeScrape 已由 Operator 接收:

kubectl --namespace monitoring get vmnodescrape node-exporter

确认旧的 node-exporter ServiceScrape 已删除:

kubectl --namespace monitoring get vmservicescrape -o name \
  | grep node-exporter
# no output

然后把 vmagent 的 targets API 转发到本机:

kubectl --namespace monitoring port-forward \
  service/vmagent-vmks-victoria-metrics-k8s-stack 8429:8429

在另一个终端查看 node-exporter active targets:

curl -fsS http://127.0.0.1:8429/api/v1/targets \
  | jq -r '.data.activeTargets[]
      | select(.labels.job == "node-exporter")
      | [.scrapeUrl, .health, .lastError]
      | @tsv'

预期每个节点只有一个 node-exporter target:

http://203.0.113.11:9100/metrics  up
http://203.0.113.12:9100/metrics  up
http://203.0.113.43:9100/metrics  up

最后查询 VictoriaMetrics:

up{job="node-exporter",node="<NODE_NAME>"}

结果应为:

1

会不会重复采集

不会。最终配置关闭并删除了旧 VMServiceScrape,新的 VMNodeScrape 对每个 Linux 节点只产生一个 node-exporter target。relabel 的 replace 操作只是修改已有 target 的 __address__,不会创建第二个 target。

验证时应确认:

target 数量 = Linux 节点数量
唯一 scrape URL 数量 = target 数量
旧 ServiceScrape target 数量 = 0
down target 数量 = 0

切换后,旧序列和新序列可能使用不同的 instance label。旧序列会保留到 retention 到期,但不会继续写入;在跨越切换时间点的长时间查询中,可能同时看到两组历史序列,这不等于正在重复采集。

经验总结

  • kubectl top 正常不代表其他监控组件使用了相同的节点地址。
  • 排查采集失败时,应直接查看 active target、实际 scrapeUrllastError
  • VMNodeScrapeVMServiceScrape 的 discovery metadata 不相同,不能只根据字段名称推断运行时一定存在某个 meta label。
  • 先检查生成的 vmagent 配置,再检查运行时 discovered labels;两层证据缺一不可。
  • 通用地址策略适合放在 nodeScrapeRelabelTemplate,自定义抓取对象适合放在 extraObjects
  • 使用 --reuse-values 时要特别注意旧值继承和数组整体覆盖。

参考资料