ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

K8S 节点监控:node-exporter 资源清单与镜像文件部署实践

K8S 节点监控:node-exporter 资源清单与镜像文件部署实践

简介:面向Kubernetes运维与监控实践者,这套资料围绕Prometheus生态中的node-exporter组件,整理了在K8s集群内完成主机监控部署所需的全部文件。内容涵盖Service、ConfigMap、Deployment等核心YAML清单,以及配套告警规则与镜像导出包,可直接用于搭建或扩充集群节点监控体系,尤其适合已掌握K8s基础操作、希望快速落地监控告警的工程师。

包体信息:资源共7个文件,以5个yaml清单为主,辅以1个txt说明文件和1个gz镜像压缩包,整体大小约72.83MB。yaml文件负责定义服务暴露、配置挂载与实例调度,txt文件提供使用或排错提示,镜像包免去联网拉取步骤,便于离线环境部署。

该资源当前已有621人学习,较高的关注度也印证了其在日常监控场景中的实用性。获取后可直接参考清单结构、调整镜像地址或告警阈值,减少从零书写配置的时间成本,同时通过自带镜像保证版本一致性,快速获得一套可运行的主机监控方案。

1. 一台节点失联前,node-exporter 能让你提前 10 分钟知道

部署过 K8S 的人应该都有过这种感觉:容器层监控全绿,Prometheus 里的 CPU、内存曲线都正常,可某天节点突然 NotReady,或者磁盘写满导致整个业务 Pod 被驱逐。回头查监控才发现,节点主机的磁盘使用率、网络丢包率从来没采集过。K8S 主机这一层的指标,靠的是这一课的主角 node-exporter。它由 Prometheus 官方维护,安装在每个节点上,把 /proc、/sys 里的节点运行数据转换成标准指标暴露在 9100 端口。标题里的“资源清单”就是把它跑起来所需的 DaemonSet / Service 等 YAML 文件,“镜像文件”则是从镜像仓库拉到离线集群的容器镜像。无论你是刚接触 Prometheus 的运维,还是已经踩过“节点告警查不到数据”的 SRE,本文都值得看完。

2. 用 DaemonSet 把 node-exporter 铺到每个节点:最小资源清单与参数解读

2.1 为什么是 DaemonSet + hostNetwork,而不是 Deployment 加端口转发

node-exporter 做的事情是读取宿主机上的 /proc、/sys、/ 根文件系统。如果把它当普通 Deployment 部署,容器内看到的是容器自己的 /proc,采集出来的数据就变成“容器视角”,进程数、内存、磁盘都是错的。所以必须保证集群里每个节点都且只有一个 exporter。正好 DaemonSet 就是为这类需求设计的控制器:新节点加入自动调一个 Pod,节点删除自动回收。

网络模式方面,我会毫不犹豫地用 hostNetwork: true。原因有两点:第一,抓取地址直接是 nodeIP:9100,Prometheus 静态配置或服务发现都很直观,不需要担心 kube-proxy 转发目标写错;第二,node-exporter 也要经过 hostNetwork 才能可靠拿宿主机网卡层面的指标,比如 node_network_receive_bytes_total,如果走容器网络得到的是虚拟网卡的数据,容易误导排障。缺点也有:9100 端口会在宿主机监听,如果你早有一个类似 exporter 占用了这个端口,就需要改监听地址,这个坑等下单独说。

2.2 最小 DaemonSet 资源清单:三处挂载必须对

下面的 YAML 是我在模拟项目 X 里一直沿用的一套最小清单,去掉了很多花哨的容器安全配置,只保留核心挂载和参数,方便你快速跑通。

apiVersion: apps/v1 kind: DaemonSet metadata: name: node-exporter namespace: monitoring spec: selector: matchLabels: app: node-exporter template: metadata: labels: app: node-exporter spec: hostNetwork: true hostPID: true tolerations: - operator: Exists containers: - name: node-exporter image: quay.io/prometheus/node-exporter:latest args: - --path.procfs=/host/proc - --path.sysfs=/host/sys - --path.rootfs=/host/root - --collector.filesystem.mount-points-exclude=^/(dev|proc|sys|run|var/lib/docker/.+|var/lib/containers/.+)($|/) - --collector.filesystem.fs-types-exclude=^(autofs|binfmt_misc|cgroup|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|overlay|proc|procfs|securityfs|squashfs|sysfs|tracefs)$ ports: - name: metrics containerPort: 9100 volumeMounts: - name: proc mountPath: /host/proc readOnly: true - name: sys mountPath: /host/sys readOnly: true - name: root mountPath: /host/root mountPropagation: HostToContainer readOnly: true resources: requests: cpu: 50m memory: 128Mi limits: cpu: 200m memory: 256Mi volumes: - name: proc hostPath: path: /proc - name: sys hostPath: path: /sys - name: root hostPath: path: /

上面的 args 参数需要逐个说清楚。--path.procfs 和 --path.sysfs 指定 node-exporter 去宿主机的 /proc、/sys 读取数据,所以挂载时把它们映射到容器的 /host/proc 和 /host/sys。--path.rootfs 指向宿主机根目录的映射,它用于文件系统各挂载点、磁盘使用率这类指标。如果你用的是较新版本,还需要把宿主机根目录挂载进来并设置 mountPropagation: HostToContainer,否则容器内看不见宿主机后续新增的挂载点,比如临时插入的磁盘,这属于那种“数据偶尔少一块”的玄学问题,实际上就是挂载传播没配。

后面的两条 --collector 参数是必须吃饭的家伙。第一条 mount-points-exclude 把 /dev、/proc、/sys、/run 以及容器运行时的工作目录排除掉;第二条 fs-types-exclude 把 overlay、tmpfs、autofs 等伪文件系统排除掉。不排除会怎样?你的磁盘指标表里会出现成千上万个 overlay 挂载点,就是每个容器层都当成一个独立磁盘,告警没法写。tolerations 用 operator: Exists 表示容忍所有污点,这样 master / control-plane 节点也能被调度,保证全节点覆盖。hostPID: true 是为了拿到宿主机所有进程的信息,比如进程状态、运行数量,如果不开,node-exporter 只能看到容器内的进程。

2.3 Service 要不要建:Headless 方式与端口命名

很多教程会在 DaemonSet 旁边配一个 Service。我的习惯是:如果没有用 prometheus-operator 的 ServiceMonitor,这个 Service 不是必须的,直接让 Prometheus 配节点 IP 列表即可;如果要用 ServiceMonitor,那就需要一个 Service,而且要建 Headless。

apiVersion: v1 kind: Service metadata: name: node-exporter namespace: monitoring labels: app: node-exporter spec: clusterIP: None selector: app: node-exporter ports: - name: metrics port: 9100 targetPort: 9100

clusterIP: None 会让这个 Service 不加虚拟 IP,DNS 查询直接返回后端 Pod IP 列表,这样 Prometheus 用 kubernetes_sd_configs 做服务发现时,拿到的就是一个个真实 Pod IP,配合后面讲的 relabel 就能把 instance 稳定成节点 IP。port 的 name 我固定写 metrics,这个名字必须和 ServiceMonitor 的 endpoint.port 完全一致,否则 operator 模式下 target 永远是 0。

2.4 资源配额与调度:容忍度、优先类、防止抢占

node-exporter 本身很轻,默认 collector 全开时内存占用在 40~80MB 左右,CPU 也就偶尔波动到几十毫核。我给的 requests/limits 是经过多集群验证的值,其中 memory limit 256Mi 能让它在极端大节点上不至于被 OOMKill,而 cpu limit 200m 又能防止它出问题时拖垮节点。

如果集群比较大,还建议给 node-exporter 加一个 priorityClassName 或直接将它在集群中的 PodDisruptionBudget 做一下保护。实际上 DaemonSet 的 Pod 不经过 eviction 一般不会被驱逐,但节点维护 drain 时它会重新调度,这没关系。真正要注意的是别把 tolerations 写成只容忍某个指定 key,否则节点被打了别的污点,这个节点就漏采了。我有一次就踩过:节点node-role.kubernetes.io/control-plane污点比较隐蔽,某个节点一直 NotReady 却没有任何 node-exporter 的 target,就是因为容忍列表没写全,所以这里直接写operator: Exists,一劳永逸。

3. 镜像文件:从拉取、导出到私有仓库的一次性落地

3.1 拉取镜像并确认 digest:别在版本号上留隐患

镜像文件这个标题,要解决的其实是两类问题:一是集群节点能不能按期从外部镜像仓库拉到镜像;二是离线内网环境怎么把这个镜像搬进去。先看在有网机器上拉取。

docker pull quay.io/prometheus/node-exporter:latest docker images --digests | grep node-exporter

第一行命令正常执行完后,第二行能看到这个镜像的 digest,形如 sha256:xxxx。我建议生产环境固定使用 digest 而不是 tag:latest 会被后续版本刷新,下次 ImagePullPolicy 为 Always 时集群可能自动升级到不兼容版本,导致指标名变化、告警规则失效。正确做法是把 DaemonSet 里的 image 字段写成quay.io/prometheus/node-exporter@sha256:你的digest,这样镜像内容就永远锁死了。

还有一个细节:拉取时尽量选一个和你本机系统架构匹配的镜像。node-exporter 官方仓库支持 multi-arch,默认拉取当前平台对应的架构。但如果你在 x86 管理机上拉镜像,再传到多架构混合的离线集群,就可能出现镜像架构不对、Pod 起不来的问题。常见做法是在拉取时用--platform linux/amd64或linux/arm64显式指定,然后按节点架构分别放置镜像。

3.2 docker save / load 搬运离线环境

离线环境没有外网访问权限,最直接的搬运办法是把镜像保存成 tar 包,拷到内网机器上再加载。

docker save -o node-exporter.tar quay.io/prometheus/node-exporter:latest ls -lh node-exporter.tar scp node-exporter.tar user@offline-host:/tmp/ # 或通过内网传输工具 docker load -i node-exporter.tar docker run --rm quay.io/prometheus/node-exporter:latest --version

docker save 默认把整个镜像所有层打包,后面看到的大小通常有二三十MB,内网拷贝不慢,但要注意传输工具的完整性。如果文件太大且网络不稳定,可以用docker save | gzip再压一道,或者分卷压缩传输,但分卷会引入额外的合并步骤,反而容易漏掉。我更推荐直接在目标机器上逐节点 docker load,因为 kubelet 使用的容器运行时如果是 containerd,也可以用 ctr 命令加载,但很多环境没装 ctr 客户端,还是 docker load 最稳。加载完建议立即运行一次docker run --rm ... --version,确认镜像可执行,避免把损坏的 tar 包一路传到了生产。

3.3 推到私有仓库并在 K8S 中直接引用

如果内网已有 Harbor / Nexus 之类的私有镜像仓库,更好的方式是把镜像推送到那里,让所有 kubelet 从私有仓库拉取,省去逐节点 load。

docker tag quay.io/prometheus/node-exporter:latest registry.example.com/monitoring/node-exporter:latest docker push registry.example.com/monitoring/node-exporter:latest

然后修改 DaemonSet 的 image 字段为私有仓库地址。注意:如果私有仓库启用了认证,需要提前在命名空间里创建 imagePullSecret,并在 DaemonSet 的 spec.template.spec.imagePullSecrets 里引用,否则 Pod 会停留在 ImagePullBackOff。

imagePullSecrets: - name: regcred

3.4 imagePullPolicy 的三种选择与离线后翻车现场

与镜像文件直接相关的还有一个 K8S 参数:imagePullPolicy。三种取值的区别是:Always 每次启动都尝试拉取;IfNotPresent 本地有就使用;Never 只使用本地镜像,没有就报错。默认规则坑人:如果 tag 是 latest,即使你没写 imagePullPolicy,kubelet 也会按 Always 处理。

离线环境踩坑最多的就在这:你明明已经在节点上 docker load 了镜像,DaemonSet 也在运行,但节点重启后 Pod 调度到另一个没 load 过的节点,或者 kubelet 重新拉取时 Pull,直接 ImagePullBackOff。因为离线环境既没有私有仓库,也没有外网,只能用 Never 或 IfNotPresent。我建议:如果整个集群都 offline,在 DaemonSet 里显式写imagePullPolicy: IfNotPresent,并保证每个节点都导入镜像;如果能保证所有节点都预先打了镜像,写 Never 更严格,杜绝任何意外拉取尝试。

另一个和镜像相关的细节是镜像大小占用的节点磁盘空间。node-exporter 镜像本身不大,但 DaemonSet 每个节点都要存一份,算上被遗弃的旧镜像,偶尔会占用几百 MB。在磁盘紧张的节点上,记得定期执行docker image prune清理悬空镜像,这条血泪经验来自一次节点 /var/lib/docker 分区被写满的翻车经历。

4. 让 Prometheus 抓取到 node-exporter:三套接入方式与标签处理

4.1 静态配置:节点少时最直接

如果你的集群只有三五台节点,不需要引入任何服务发现机制,直接在 Prometheus 配置文件的 scrape_configs 里写死目标即可。

scrape_configs: - job_name: node-exporter static_configs: - targets: - 10.0.0.11:9100 - 10.0.0.12:9100 - 10.0.0.13:9100 labels: env: prod

这种方式最稳定,不受 K8S API 状态影响。缺点也明显:节点扩缩容时需要同步维护 targets 列表。如果节点数量超过十台,建议改用文件发现,把 targets 写进一个 YAML 文件,Prometheus 在挂载的 ConfigMap 更新后自动加载。不过对本文场景,先静态配置跑通再说,把注意力放在后面要解决的标签和端口问题上。

4.2 基于 Pod 的服务发现:节点规模大了再用

节点多了以后,最稳的动态方案是让 Prometheus 直接通过 K8S 接口发现 node-exporter 对应的 Pod,然后通过 relabel 规则把 Pod 地址转换成目标地址。

scrape_configs: - job_name: kubernetes-node-exporter kubernetes_sd_configs: - role: pod relabel_configs: - source_labels: [__meta_kubernetes_pod_label_app] regex: node-exporter action: keep - source_labels: [__address__] action: replace regex: (.+):\d+ replacement: $1:9100 target_label: __address__

这段配置需要 Prometheus 所在 Pod 的 ServiceAccount 具备 pods list/watch 权限,通常你会给 Prometheus 配一个 ClusterRole 才能让服务发现生效。第一段 relabel 只保留带有 app=node-exporter 标签的 Pod;第二段是把原始 Pod 地址的端口替换成 9100。注意如果 Pod 没有用 hostNetwork,那么原始address就是一个容器 IP 加一个动态端口,外层再用 Service 做转发,这种方式不仅多一跳,而且 instance 标签会变成容器 IP,对排障很不友好。所以这篇文章里的 DaemonSet 统一用 hostNetwork,这是前面设计决定的。

4.3 ServiceMonitor:prometheus-operator 下最省心

如果这套监控是 prometheus-operator 搭起来的,你根本不用写 scrape_configs,而是创建一个 ServiceMonitor 对象,operator 会自动把对应的服务发现同步到 Prometheus。

apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: node-exporter namespace: monitoring spec: jobLabel: app endpoints: - port: metrics interval: 30s selector: matchLabels: app: node-exporter namespaceSelector: matchNames: - monitoring

这段配置的作用是告诉 operator:注意 monitoring 命名空间里带 app=node-exporter 标签的 Service,并在它的 metrics 端口抓取数据。使用它的前提是上一步那个 Service 的 port name 必须叫 metrics,这里的endpoints.port: metrics指的就是这个。如果 Service 的 label 和 ServiceMonitor 的 selector 匹配不上,target 为空,这是新手必踩。

4.4 relabel 的关键:instance 标签到底保留了谁

抓取回来的数据,标签里最常用的就是 instance。很多同事拿到 node-exporter 数据后第一件事是想按节点名筛,发现 instance 是 IP:9100,而节点名存在 node_uname_info 的 nodename 标签里。我的建议是保留 instance 为节点 IP,不要试图覆盖它,因为 Prometheus 的 up 指标以及很多告警规则都依赖 instance 作为唯一标识。如果你希望告警通知里显示的是节点名,可以在抓取时额外加一个自定义标签。

- source_labels: [instance] action: replace regex: ([\d.]+):\d+ replacement: $1 target_label: node_ip

这段 relabel 把 instance 中的纯 IP 复制成了新标签 node_ip,方便后续 alertmanager 路由时引用,同时也不破坏原有的 instance。如果你真的想把 instance 改成节点名,可以去拉取 node_uname_info 生成一个新的标签映射,但那样会让每个 node-exporter 目标的 instance 随着节点变化而变化,历史数据序列会断开,不推荐。

5. 避坑:node-exporter 监控落地中的 6 个高频翻车点

5.1 CrashLoopBackOff:挂载 rootfs 后报 permission denied

现象:DaemonSet 创建后,Pod 反复重启,describe 看到容器日志里有mount /host/root/proc: permission denied之类的字样。

原因:宿主机根目录挂载进容器时,涉及宿主机挂载点重新关联,普通非特权容器在较严格的 K8S 环境里没有权限执行挂载传播操作。

解决:在容器段增加securityContext: { privileged: true },或者只保留必需的 /host/proc 和 /host/sys 挂载,不挂 /host/root,同时去掉 --path.rootfs 参数。如果你只需要磁盘使用率,rootfs 是必须的,所以一般最终都会开特权。注意这个特权只会放大到该 DaemonSet 的 Pod,不会影响其他工作负载,不需要过度紧张。

5.2 9100 端口被宿主机进程占用

现象:Pod 状态显示 Running,但容器日志里出现listen tcp :9100: bind: address already in use,且 Prometheus 抓取失败。

原因:hostNetwork: true 让 exporter 直接监听了宿主机网络栈,如果节点上原本有另一个 node-exporter 或 tomcat 之类进程占用了 9100,就会冲突。

解决:在 args 里增加--web.listen-address=:9101,同时容器端口、Service 端口都同步改成 9101。这里更要提的是,有些云厂商的节点初始化脚本会偷偷跑一个 node-exporter 占 9100,排查时先ss -lntp | grep 9100看清楚占用者再决定是杀进程还是换端口。

5.3 磁盘指标数量爆炸

现象:查询 node_filesystem_avail_bytes 时返回几千个时间序列,其中大量 mountpoint 是/var/lib/docker/overlay2/xxx/merged,告警阈值根本没法按节点写。

原因:没有配置排除规则,node-exporter 把所有挂载点都暴露出来,overlay、tmpfs、squashfs 这些文件系统都是容器运行时的内部视图。

解决:把 2.2 节里的--collector.filesystem.mount-points-exclude和--collector.filesystem.fs-types-exclude两项参数加上,并让 DaemonSet 滚动重启。加完后可用count(node_filesystem_avail_bytes) by (instance)验证一个节点只剩几个合理条目,比如 /、/boot、/data。这个坑可以说是我见过的 node-exporter 部署里最频繁的翻车点,十个人里九个会踩。

5.4 Prometheus 抓取 connection refused

现象:Target 页面看到 node-exporter 目标的 State 是 Down,错误信息是connection refused,进入 Pod 容器看 exporter 本身没报错。

原因:最常见的是三种:DaemonSet 没有配 hostNetwork,Pod IP 是容器网的 10.244.x.x 而节点防火墙只放行节点 IP;要么是 Service 定位错误;要么节点安全组没放行 9100 入方向。

解决:先用kubectl get pod -o wide -n monitoring | grep node-exporter拿到 Pod IP,然后在 Prometheus 所在机器上curl http://<节点IP>:9100/metrics和curl http://<PodIP>:9100/metrics,两边对比立刻能判断是网络隔离问题还是调度问题。如果是 Pod IP 可通而节点 IP 不通,十有八九是 hostNetwork 没开,回到 DaemonSet 修改。

5.5 ServiceMonitor 创建后 target 一个都没有

现象:prometheus-operator 环境里,ServiceMonitor 没有报错,但 Prometheus 的 Targets 页面里 node-exporter 服务一个 target 都没有。

原因:三处不匹配,一是 Service 的 labels 与 ServiceMonitor 的 selector 不一致;二是 Service 的 port name 与 endpoints.port 不一致;三是 namespaceSelector 没把 Service 所在的命名空间列进来。

解决:逐步检查。kubectl get svc -n monitoring --show-labels查看 Service 标签;kubectl get servicemonitor -n monitoring -o yaml查看 selector 和 endpoints.port;再确认两者都在 monitoring 命名空间。大多数时候只是把app: node-exporter写成了app: node_exporter,差一个下划线,target 就没了,这类字符串匹配问题往往要靠kubectl describe才发现。

5.6 node-exporter 的 CPU 占用异常高

现象:节点 load 不高,但 node-exporter 进程 CPU 占用持续超过 1 核,甚至导致节点卡顿。

原因:默认启用全部 collector,某些 collector 在进程数极大的节点上需要反复遍历 /proc,比如 process collector、threaded 相关指标;另外如果宿主机的 /sys 文件特别多或存在慢存储,sysfs 数据读取也会拖慢。

解决:先用curl http://127.0.0.1:9100/metrics | head -50看打开了哪些 collector,然后按需关闭,args 里加--no-collector.process或--no-collector.softnet等。不过不建议你一上来关太多,先给 node-exporter 提资源 limit,比如 CPU 改成 500m,真扛不住再逐个关闭。node-exporter 是单进程模型,内部其实是串行采集的,这也是为什么资源 limit 设置不合理时它可能被 throttle,表现成抓取超时。

6. 验证与进阶:从「能出图」到「能告警」的收尾习惯

6.1 三个 PromQL 验证节点监控是否干净

配置全部完成后,先在 Prometheus 的 Graph 页面跑三条查询。第一条验证节点数量和覆盖情况:count(count(node_uname_info) by (instance)),结果应该等于集群节点数,大于或小于都说明 DaemonSet 调度或服务发现有偏差。第二条验证磁盘指标:node_filesystem_avail_bytes{fstype!~"tmpfs|overlay",mountpoint="/"} / 1024 / 1024,能看到每个节点的根分区剩余 MB。第三条验证负载与 CPU 关系:node_load1 / count(node_cpu_seconds_total{mode="system"}) by (instance),比值超过 1 说明节点已经处于饱和边缘。这三条查询没问题,就说明抓取、标签、指标完整性都正常。

6.2 节点重启与宕机告警规则示例

监控最终要落到告警。我常用两条基础规则,一是节点失联,二是检测异常重启。节点失联直接看 up 指标,持续 5 分钟为 0 就说明这个 exporter 目标丢掉了。重启检测可以用time() - node_boot_time_seconds < 600,node_boot_time_seconds 记录的是节点开机的时间戳,用当前时间减去它,结果小于 600 秒就说明节点最近 10 分钟内重启过。这两条规则建议配合告警路由区分处理,失联通常是节点宕机或网络隔离,比较紧急;重启则可能是被 OOM 或者内核问题,优先级可以放低一点。

6.3 用 node_uname_info 核对版本号与内核信息

最后分享一个容易被忽略的小技巧:node_uname_info 指标本身携带了内核版本、操作系统、节点名的标签。比如node_uname_info{nodename="node-01"}的 release 标签就是该节点的 kernel 版本。当你要批量升级内核或者排查某些节点内核不一致时,直接在 Prometheus 里跑一句node_uname_info就能列出全集群的节点名和内核版本,比登录每台机器敲 uname 高效得多。我平时习惯把这条查询存到 Grafana 的一个变量面板里,node-exporter 告警带出来的 instance 不好认时,用它关联节点名。

回到实践本身,node-exporter 的价值不在部署那一刻,而在于节点真正出问题时,你手里有没有足够长的历史数据。我曾经因为只关注容器指标、没有提前部署 node-exporter,某次节点磁盘被日志写满,业务 Pod 全部被驱逐,事后连故障曲线都拼不完整。从那以后,我把 node-exporter 的 DaemonSet 和镜像文件当作所有 Prometheus 集群的基线之一,每次搭完先验证前面三条 PromQL,再写告警规则。这个顺序值得你保持,希望帮到你。

本文还有配套的精品资源,点击获取

返回列表