用一个 Prometheus 监控 unRaid、群晖 NAS 和 VPS:我做错的四件事

· 2 min read · case-studies prometheusgrafanaunraidsynologydsmcadvisormonitoringhomelab

为什么这件事重要

当一台家用服务器在跑别人的网站、邮件和文件时,它就不是玩具了。糟糕的一周和糟糕的一个月之间,差别通常只是你多早发现:一块出现重分配扇区的盘、一个只剩 20 GB 的卷、一台被宿主机饿死的虚拟机、一个夜里重启四次的容器。

我有三台机器——unRaid(日常主力)、群晖 NAS(存储与服务)和 VPS(对外)——以及三套各自都不知道它们在干什么的方式。这篇文章讲它们如何变成一块屏,以及我路上做错的四件事——其中三个你也会踩。

架构

一个 Prometheus 加一个 Grafana,跑在永不关机的那台机器上。每台被监控主机跑两个 exporter:

机器主机指标容器指标抓取路径
unRaidnode_exporter (:9100)cAdvisor (:8080)直连
群晖 DSMnode_exporter (:9100)cAdvisor (:8082)局域网
ServerHosh VPSnode_exporter (:9100)cAdvisor (:8081)VPN 隧道 + nginx stream 中转

node_exporter 与 cAdvisor 不是替代关系——这是最容易搞错的一点。 node_exporter 看的是主机:CPU、内存、网络、磁盘、文件系统、温度;它对 cgroup 一无所知,说不出是哪个容器吃掉了内存。cAdvisor 只看容器。想要主机健康和按容器记账,两个都得跑;少一个,那个洞会在几个月后表现为一次无法解释的负载高峰。

错误一:cAdvisor 里那些”不是容器的容器”

我的容器规则开始对不是容器的东西告警。cAdvisor 会为 systemd slice 和整机 cgroup 导出序列——其中一条没有名字的序列报告了约 58 GB 的”内存使用”,却没有对应的容器。那就是整台主机,被描述成了一个容器。

修法是加一个过滤条件,但你得先知道要写它:

# container metrics: anything with a name, and only that
container_memory_working_set_bytes{job=~"cadvisor.*", name!=""}

少了 name!="",一条”容器内存超过 10 GB”的规则会对主机自己告警。

错误二:我的”总容量”是虚构的

聚合面板好看,但就是错的。原因:同一个文件系统会在指标里出现多次。

所以朴素的 sum(node_filesystem_size_bytes) 报出了我并不存在的容量。诚实的写法是明确列出什么算数据存储:

node_filesystem_size_bytes{
  mountpoint=~"/volume[0-9]+|/mnt/ssd|/mnt/disk[0-9]+",
  fstype!~"fuse.*|tmpfs|rootfs"
} or node_filesystem_size_bytes{job="vps-host", mountpoint="/"}

还有两条应该写进面板说明的诚实备注,因为没人能解释的数字比没有数字更糟:

还有那个以后一定会咬我的遗漏:unRaid 的 shfs 联合挂载(/mnt/user)报告 avail=0。把它算进”剩余空间低于 X”的规则里,就会产生一条永远无法解除的告警。

错误三:那台不导出 CPU 频率的虚拟机

为了算全家的”总 CPU 频率”,我直接用 node_exporter 的 cpufreq collector。unRaid 和 NAS 都按核心报出了 node_cpu_scaling_frequency_hertz;VPS 什么都没有——它是 KVM 客户机,而客户机没有 /sys/devices/system/cpu/cpu0/cpufreq。这个指标在那里无法存在。

修法是 textfile collector:一个小脚本读客户机能看到的东西(/proc/cpuinfo),把 Prometheus 格式指标写进 node_exporter 会抓取的目录:

# /opt/node-exporter-textfile/cpu-mhz.sh — cron 每 5 分钟
awk -F: '
  /^processor/ { c = $2; gsub(/[ \t]/, "", c) }
  /^cpu MHz/   { f = $2; gsub(/[ \t]/, "", f); printf "node_cpu_mhz_current_hz{core=\"%s\"} %.0f\n", c, f * 1000000 }
' /proc/cpuinfo

配合:

command:
  - '--collector.textfile.directory=/textfile'
volumes:
  - /opt/node-exporter-textfile:/textfile:ro

两个刻意的决定:指标名故意不同于 node_exporter 自己的(用 node_cpu_mhz_current_hz,不用 node_cpu_scaling_frequency_hertz),这样万一以后宿主机暴露真实 cpufreq,不会出现重复序列冲突;仪表盘则显式合并两个来源:

sum(node_cpu_scaling_frequency_hertz or node_cpu_mhz_current_hz)

错误四:exporter 把容器名当成了主机名

仪表盘的主机下拉框里出现了 9f9afcccc962。那是个容器 ID:node_exporter 的 uname collector 读到的是进程自己的 UTS namespace,而容器的 hostname 默认就是它自己的 ID。指标没错,标签毫无意义。compose 里一行解决:

services:
  node_exporter:
    hostname: 2.hoelee.com   # otherwise `nodename` = the container ID

我下次会怎么做

结果

三台主机、7 个抓取目标、一个 Grafana、16 条告警规则,以及一块屏:47 个 CPU 核心 · 当前 158 GHz(标称上限 205 GHz)· 142 GB 内存 · 64 TB 存储 · 155 个运行中容器,并且每台机器克隆同一套主机/容器面板布局,三台读起来完全一致。告警进 Telegram 和邮件,Prometheus 保留 30 天。

最有用的产出不是仪表盘,而是重写存储规则那天触发的一条告警:一个只剩 21 GB 的 NAS 卷——它一直藏在”99% 满”的百分比阈值背后,而那个巨物早已变成背景噪声。

需要为你的业务做这个吗?

如果你有 NAS、VPS 和几台服务器,却没有一个地方能同时看它们,我可以帮你搭这样的自托管监控流水线——Prometheus + Grafana 覆盖你的机器,主机与容器指标,真正有意义的磁盘健康与容量规则,通知推到 Telegram 或邮件。

WhatsApp:+60 12-797 2969 · 邮箱:[email protected] · hoelee.com

网站设计与开发是我的主业;服务器加固与自托管基础设施是它的另一半。

Lee Teong Hoe

Full-stack developer & DevOps engineer. I build web apps, self-host infrastructure, and automate things — this blog is my living portfolio.