一个 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,却仍默认使用它作为抓取地址。
节点本身是 Ready,kubectl 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:10250。kubectl 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 为空,正则不会匹配,抓取地址也就不会改变。
最终方案
最终方案分为三部分:
- 所有
VMNodeScrape统一优先使用 ExternalIP,并保留各自端口。 - 禁用 node-exporter 原有的
VMServiceScrape。 - 使用
extraObjects为 node-exporter 创建VMNodeScrape。
保留现有 extraObjectsextraObjects是 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、实际
scrapeUrl和lastError。 VMNodeScrape和VMServiceScrape的 discovery metadata 不相同,不能只根据字段名称推断运行时一定存在某个 meta label。- 先检查生成的 vmagent 配置,再检查运行时 discovered labels;两层证据缺一不可。
- 通用地址策略适合放在
nodeScrapeRelabelTemplate,自定义抓取对象适合放在extraObjects。 - 使用
--reuse-values时要特别注意旧值继承和数组整体覆盖。