家里现在有四台“机器”:笔记本上的 Proxmox 宿主机、NAS、Docker 虚拟机,还有跑 AdGuard Home 的小容器。平时都是出了问题才登上去看,比如上个月 Docker 虚拟机的磁盘悄悄涨到了 95%,还是某个服务报错我才发现。smartd 和 NUT 只在出问题时通知,我还想看到趋势:磁盘每个月涨多少、夏天 CPU 温度有多高、停电时电池掉得多快。

于是上了最经典的组合:每台机器跑 node_exporter 暴露指标,Prometheus 定时去抓取并存起来,Grafana 负责画图和告警。

在每台机器上装 node_exporter

Debian 仓库里就有,宿主机、NAS、Docker 虚拟机都直接 apt install prometheus-node-exporter,装完它会监听 9100 端口,curl -s http://localhost:9100/metrics 能看到一大堆指标就对了。AdGuard Home 那个 LXC 我没装,容器里看到的很多数据和宿主机是一回事,意义不大。

用 Compose 跑 Prometheus 和 Grafana

服务端放在 Docker 虚拟机上,目录照旧按compose 目录约定来,放在 /srv/compose/monitoring:

services:
  prometheus:
    image: prom/prometheus:v3.5.0
    restart: unless-stopped
    command:
      - --config.file=/etc/prometheus/prometheus.yml
      - --storage.tsdb.path=/prometheus
      - --storage.tsdb.retention.time=90d
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./data/prometheus:/prometheus
    ports:
      - "192.168.10.21:9090:9090"

  grafana:
    image: grafana/grafana:12.1.0
    restart: unless-stopped
    environment:
      GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD}
    volumes:
      - ./data/grafana:/var/lib/grafana
    ports:
      - "192.168.10.21:3000:3000"

prometheus.yml 里列出要抓取的目标:

global:
  scrape_interval: 30s

scrape_configs:
  - job_name: node
    static_configs:
      - targets:
          - 192.168.10.5:9100    # Proxmox 宿主机
          - 192.168.10.20:9100   # NAS
          - 192.168.10.21:9100   # Docker 虚拟机自己

docker compose up -d 之后,打开 http://192.168.10.21:9090,在 Status 菜单里查看各个抓取目标的状态,三个都是 UP 就成了。

Grafana 里添加 Prometheus 数据源,地址填 http://prometheus:9090(同一个 compose 项目里的容器可以用服务名互相访问),然后导入社区里很有名的 Node Exporter Full 仪表盘(ID 1860),CPU、内存、磁盘、网络、温度一下子全有了。

把 UPS 数据接进来

node_exporter 有个 textfile 收集器,会读取指定目录里的 .prom 文件,把里面的内容当成指标一起暴露出来。Debian 打包的版本默认读的是 /var/lib/prometheus/node-exporter 目录。所以只要写个小脚本,把 upsc 的输出转换一下就行。在连着 UPS 的 NAS 上:

#!/bin/sh
# /usr/local/bin/nut-textfile.sh,用 cron 每分钟跑一次
OUT=/var/lib/prometheus/node-exporter/nut.prom

upsc homeups@localhost 2>/dev/null | awk -F': ' '
  $1 == "battery.charge"  { print "nut_battery_charge_percent " $2 }
  $1 == "battery.runtime" { print "nut_battery_runtime_seconds " $2 }
  $1 == "ups.load"        { print "nut_ups_load_percent " $2 }
  $1 == "ups.status"      { print "nut_on_battery " ($2 ~ /OB/ ? 1 : 0) }
' > "$OUT.tmp" && mv "$OUT.tmp" "$OUT"

先写临时文件再 mv,是为了避免 node_exporter 读到写了一半的文件。硬盘的 SMART 数据也可以用同样的思路采集,Debian 还有个 prometheus-node-exporter-collectors 包,里面自带了 smartmon 等几个现成的采集脚本。

常用的查询和告警

自己画面板时用得最多的几个查询:

# 各挂载点的使用率(排除 tmpfs 之类的虚拟文件系统)
100 * (1 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|squashfs"}
         / node_filesystem_size_bytes{fstype!~"tmpfs|overlay|squashfs"})

# 哪台机器抓取失败了
up == 0

# 现在是不是在用电池
nut_on_battery == 1

告警我只配了三条:磁盘使用率超过 85%、有目标连续 5 分钟抓不到、UPS 切到电池供电。用的是 Grafana 自带的告警功能,联系方式选 Webhook,推到家里的 ntfy 上,消息格式粗糙了一点,但能用。告警规则贵精不贵多,一开始我加了一堆 CPU、内存的告警,半夜被吵醒几次之后全删了。

踩过的坑

  1. 数据目录权限。 第一次启动时两个容器都起不来,日志里是 permission denied。prom/prometheus 镜像里的进程以 nobody(UID 65534)运行,Grafana 是 UID 472,而 bind mount 的目录是 root 创建的。分别执行 chown -R 65534:65534 data/prometheus 和 chown -R 472:472 data/grafana 就好了。
  2. 容器里的 localhost 不是宿主机。 我最初把 Docker 虚拟机自己写成了 localhost:9100,结果一直是 DOWN:在 Prometheus 容器里,localhost 指的是容器自己。改成虚拟机的局域网 IP 就好了;也可以在 compose 里加上 extra_hosts: ["host.docker.internal:host-gateway"],然后用 host.docker.internal 访问宿主机。
  3. 抓取间隔别太短。 一开始我设的是 5 秒,数据量大了好几倍,家用根本用不着这么细。现在三台机器、30 秒一次、保留 90 天,Prometheus 的数据目录也就一两 G。
  4. node_exporter 别暴露到不该去的地方。 它默认监听所有网卡,在家里倒无所谓,但如果机器还有别的网络接口,最好在 /etc/default/prometheus-node-exporter 里用 --web.listen-address 限制监听地址。

小结

这套东西搭起来不过一个晚上,回报却很直接:现在打开 Grafana 就能看到每台机器的状态。还顺手发现了两件事:笔记本在午后室温高的时候 CPU 温度会到七十多度;NAS 的数据盘使用率每个月稳定涨 3%,可以提前规划扩容了。