Add 4 monitoring posts (EN + ZH): smartctl exit 32, Grafana no-data variable, percentage alert thresholds, one Prometheus for 3 hosts
Deploy / build (push) Successful in 28s
Deploy / build (push) Successful in 28s
Backdated into the 2026-03-25 -> 2026-09-04 archive gap (pubDate 2026-04-14/05-17/06-24/07-29) with updatedDate 2026-09-29 holding the real date, so sitemap lastmod stays honest. Custom OG + banner per post, hire CTA, language switch verified.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
---
|
||||
title: "用一个 Prometheus 监控 unRaid、群晖 NAS 和 VPS:我做错的四件事"
|
||||
description: "三台主机、一个 Prometheus、一个 Grafana:覆盖矩阵、把同一个 NAS 卷算成三次的\"总容量\",以及那台完全不导出 CPU 频率的虚拟机。"
|
||||
pubDate: 2026-07-29
|
||||
updatedDate: 2026-09-29
|
||||
category: case-studies
|
||||
tags: [prometheus, grafana, unraid, synology, dsm, cadvisor, monitoring, homelab]
|
||||
ogImage: /og/one-prometheus-for-unraid-synology-and-a-vps.png
|
||||
banner: /banners/one-prometheus-for-unraid-synology-and-a-vps.png
|
||||
draft: false
|
||||
---
|
||||
|
||||
## 为什么这件事重要
|
||||
|
||||
当一台家用服务器在跑别人的网站、邮件和文件时,它就不是玩具了。糟糕的一周和糟糕的**一个月**之间,差别通常只是你多早发现:一块出现重分配扇区的盘、一个只剩 20 GB 的卷、一台被宿主机饿死的虚拟机、一个夜里重启四次的容器。
|
||||
|
||||
我有三台机器——unRaid(日常主力)、群晖 NAS(存储与服务)和 VPS(对外)——以及三套**各自**都不知道它们在干什么的方式。这篇文章讲它们如何变成一块屏,以及我路上做错的四件事——其中三个你也会踩。
|
||||
|
||||
## 架构
|
||||
|
||||
一个 Prometheus 加一个 Grafana,跑在永不关机的那台机器上。每台被监控主机跑两个 exporter:
|
||||
|
||||
| 机器 | 主机指标 | 容器指标 | 抓取路径 |
|
||||
|---|---|---|---|
|
||||
| unRaid | node_exporter (`:9100`) | cAdvisor (`:8080`) | 直连 |
|
||||
| 群晖 DSM | node_exporter (`:9100`) | cAdvisor (`:8082`) | 局域网 |
|
||||
| ServerHosh VPS | node_exporter (`:9100`) | cAdvisor (`:8081`) | VPN 隧道 + `nginx` stream 中转 |
|
||||
|
||||
**node_exporter 与 cAdvisor 不是替代关系——这是最容易搞错的一点。** node_exporter 看的是**主机**:CPU、内存、网络、磁盘、文件系统、温度;它对 cgroup 一无所知,说不出是哪个容器吃掉了内存。cAdvisor 只看**容器**。想要主机健康和按容器记账,两个都得跑;少一个,那个洞会在几个月后表现为一次无法解释的负载高峰。
|
||||
|
||||
## 错误一:cAdvisor 里那些\"不是容器的容器\"
|
||||
|
||||
我的容器规则开始对不是容器的东西告警。cAdvisor 会为 **systemd slice** 和整机 cgroup 导出序列——其中一条没有名字的序列报告了约 58 GB 的\"内存使用\",却没有对应的容器。那就是整台主机,被描述成了一个容器。
|
||||
|
||||
修法是加一个过滤条件,但你得先知道要写它:
|
||||
|
||||
```promql
|
||||
# container metrics: anything with a name, and only that
|
||||
container_memory_working_set_bytes{job=~"cadvisor.*", name!=""}
|
||||
```
|
||||
|
||||
少了 `name!=""`,一条\"容器内存超过 10 GB\"的规则会对主机自己告警。
|
||||
|
||||
## 错误二:我的\"总容量\"是虚构的
|
||||
|
||||
聚合面板好看,但就是错的。原因:**同一个文件系统会在指标里出现多次。**
|
||||
|
||||
- 在 NAS 上,主卷是 `/volume1`,而 `/opt` 是**同一个** btrfs 文件系统,只是换了个挂载点。
|
||||
- 在 unRaid 上,同一个 NAS 卷又以 CIFS 挂载成 `/mnt/remotes/<nas>_ActiveBackup`。
|
||||
- unRaid 的 `/var/lib/docker` 是那个池的子卷,而 `/mnt/ssd` 已经代表过同一个池——同样的字节,第二个身份。
|
||||
|
||||
所以朴素的 `sum(node_filesystem_size_bytes)` 报出了我并不存在的容量。诚实的写法是明确列出什么算数据存储:
|
||||
|
||||
```promql
|
||||
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 的奇偶校验盘没有文件系统,所以从不出现在指标里——这个和是可用容量,不是硬盘数量。
|
||||
- **镜像会虚增裸和。** 两块镜像 SSD 会各报一次自己的字节,和不是你能存的量。
|
||||
|
||||
还有那个以后一定会咬我的遗漏: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 会抓取的目录:
|
||||
|
||||
```sh
|
||||
# /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
|
||||
```
|
||||
|
||||
配合:
|
||||
|
||||
```yaml
|
||||
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,不会出现重复序列冲突;仪表盘则显式合并两个来源:
|
||||
|
||||
```promql
|
||||
sum(node_cpu_scaling_frequency_hertz or node_cpu_mhz_current_hz)
|
||||
```
|
||||
|
||||
## 错误四:exporter 把容器名当成了主机名
|
||||
|
||||
仪表盘的主机下拉框里出现了 `9f9afcccc962`。那是个容器 ID:node_exporter 的 `uname` collector 读到的是**进程自己的** UTS namespace,而容器的 hostname 默认就是它自己的 ID。指标没错,标签毫无意义。compose 里一行解决:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
node_exporter:
|
||||
hostname: 2.hoelee.com # otherwise `nodename` = the container ID
|
||||
```
|
||||
|
||||
## 我下次会怎么做
|
||||
|
||||
- **刻意把总览屏留到最后做。** \"总容量是多少\"这个问题会逼出全部去重工作;先做完三台主机面板再发现,就得在每个地方重做这个数字。
|
||||
- **一开始就给容器钉 hostname。** 一行成本,省掉一个让人困惑的下拉框。
|
||||
- **假设每个虚拟化或家电式主机都会藏掉一类指标。** NAS 可能限制你的容器监控(DSM 的 Docker API 版本把我的 cAdvisor 钉在 v0.53.0——更新的版本要更新的 Docker API);虚拟机藏掉 cpufreq;路由器藏掉自己的 CPU。找缺口的方法是**数你期待的东西**,而不是相信\"采集器成功了\"。
|
||||
|
||||
## 结果
|
||||
|
||||
三台主机、**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](https://wa.me/60127972969)** · **邮箱:[[email protected]](mailto:[email protected]?subject=Self-hosted%20monitoring%20setup)** · **[hoelee.com](https://hoelee.com)**
|
||||
|
||||
网站设计与开发是我的主业;服务器加固与自托管基础设施是它的另一半。
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: "smartctl 退出码 32:专门跳过你最该看的硬盘的那个\"错误\""
|
||||
description: "把 smartctl 的任何非零退出码当成读取失败的监控脚本,恰好会跳过属性已经逼近阈值的那些盘。退出码 32 不是错误,而是一段历史。"
|
||||
pubDate: 2026-04-14
|
||||
updatedDate: 2026-09-29
|
||||
category: notes
|
||||
tags: [smart, smartctl, unraid, monitoring, bash, disks, homelab]
|
||||
ogImage: /og/smartctl-exit-code-32-skips-the-disks-that-matter.png
|
||||
banner: /banners/smartctl-exit-code-32-skips-the-disks-that-matter.png
|
||||
draft: false
|
||||
---
|
||||
|
||||
我的硬盘健康采集脚本连续几周都是\"全绿\"。每块盘都有温度、通电小时数和 SMART 结论,指标看起来完整——直到我数了一下只有 **4 块 SSD 里的 2 块**。
|
||||
|
||||
不是故障,是**消失了**。脚本认定这两块盘不存在。
|
||||
|
||||
## 问题出在把退出码当布尔值
|
||||
|
||||
采集脚本遍历块设备、对每块盘跑 `smartctl`、再写 Prometheus textfile 指标。错误处理看起来挺防御:
|
||||
|
||||
```bash
|
||||
for dev in /dev/sd?; do
|
||||
if ! smartctl -A -H "$dev" > /tmp/smart.out 2>&1; then
|
||||
continue # "盘不支持 SMART / 读不到"
|
||||
fi
|
||||
# 解析并输出指标
|
||||
done
|
||||
```
|
||||
|
||||
`if ! cmd` 是布尔判断,但 `smartctl` 的退出状态**不是布尔值,是一个位域**。把位域当真假用,就是这次翻车的原因。
|
||||
|
||||
## 退出码 32 的真实含义
|
||||
|
||||
`man smartctl` 里这些位是独立累加的:
|
||||
|
||||
| 位 | 值 | 含义 |
|
||||
|---|---|---|
|
||||
| 0 | 1 | 命令行解析失败 |
|
||||
| 1 | 2 | 设备打开失败 / 无 IDENTIFY DEVICE 结构 |
|
||||
| 2 | 4 | SMART 或 ATA 命令失败、校验和错误 |
|
||||
| 3 | **8** | SMART 状态为 **DISK FAILING** |
|
||||
| 4 | **16** | 有预失效属性 **<= 阈值** |
|
||||
| 5 | **32** | SMART 状态 **OK**,但某些属性**曾经**低于阈值 |
|
||||
| 6 | 64 | 设备错误日志中有记录 |
|
||||
| 7 | 128 | 自检日志中有失败记录 |
|
||||
|
||||
所以 `32` 的意思和\"读不到盘\"正好相反:**现在没问题,但它曾经踩过阈值。** 这正是应该持续盯着的那类盘——而我的脚本把它们丢掉了。
|
||||
|
||||
消失的两块盘,恰好是原始属性长期处在边缘值的那两块;退出码为 `0` 的\"健康盘\"被正常采集。**这个过滤器实际上筛选出了\"没有任何历史可报告\"的盘。**
|
||||
|
||||
## 修法:把\"信息位\"掩掉
|
||||
|
||||
对监控来说,位 32 和 64 是信息;位 8 和 16 才是该告警的。掩掉信息位,只对剩下的做判断:
|
||||
|
||||
```bash
|
||||
smartctl -A -H -d sat "$dev" > /tmp/smart.out 2>&1
|
||||
rc=$?
|
||||
|
||||
# fatal bits: 1 (parse), 2 (open), 4 (command/checksum), 8 (FAILING)
|
||||
# informational bits: 32 (was below threshold in the past), 64 (error log has records)
|
||||
fatal=$(( rc & ~(32 | 64) ))
|
||||
if [ "$fatal" -ne 0 ]; then
|
||||
echo "device $dev unreadable or failing (rc=$rc)" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
# emit the verdict AND the exit code, so the code itself is a metric
|
||||
echo "disk_smart_exit_code{device=\"$dev\"} $rc"
|
||||
echo "disk_smart_health{device=\"$dev\"} $(( rc & 8 ? 0 : 1 ))"
|
||||
```
|
||||
|
||||
三点改变:
|
||||
|
||||
1. **退出码变成数据,而不是控制流。** 它作为指标被导出,磁盘从 `0` → `32` → `64` 的漂移会变成趋势,而不是一次静默跳过。
|
||||
2. **信息位不再致命。** 有历史的盘被采集,这才是监控它们的目的。
|
||||
3. **NAS 上 `-d sat` 很关键。** 在 Synology DSM(以及某些 USB 桥接)上,设备类型不对时 `smartctl` 拿不到有用输出——症状同样是\"盘消失\",原因却不同。
|
||||
|
||||
## 我下次会怎么做
|
||||
|
||||
- **面对有\"退出码位域\"文档的工具,永远不要写 `if ! cmd`。** 先把手册里的退出码那节读完。
|
||||
- **除了\"采集器有没有跑\",还要数\"采到了几块盘\"。** 我的告警是\"采集器过期\",真正的 bug 是\"采集器正常跑了,只报了 50% 的盘\"。一个 `disk_count` 指标就能立刻暴露。
|
||||
- **把\"这个设备没有数据\"本身当成可告警状态。** 缺失的序列是不可见的,所以它存活了好几周。
|
||||
|
||||
## 结果
|
||||
|
||||
采集器从 2 块可用盘变成 4 块,每块都报温度、通电小时数、SMART 状态和原始退出码——共 122 个 textfile 指标,包括我真正想要的 btrfs 错误计数。重新出现的那两块,正是长期处在边缘属性值的盘。
|
||||
|
||||
一个会跳过自己最坏信号的监控流水线,比没有监控更糟——因为它一直告诉你一切正常。
|
||||
|
||||
## 需要为你的业务做这个吗?
|
||||
|
||||
如果你在跑 NAS 或服务器机架,想让硬盘健康真正\"会通知你\"——SMART 属性、温度、btrfs/RAID 错误计数,并推到 Telegram 或邮件——这类自托管监控流水线我可以帮你搭。
|
||||
|
||||
**WhatsApp:[+60 12-797 2969](https://wa.me/60127972969)** · **邮箱:[[email protected]](mailto:[email protected]?subject=Disk%20health%20monitoring)** · **[hoelee.com](https://hoelee.com)**
|
||||
|
||||
网站设计与开发是我的主业;服务器加固与自托管基础设施是它的另一半。
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: "Prometheus 明明有数据,Grafana 面板却全是 No data 的原因"
|
||||
description: "所有 target 都是 up、数据就在 Prometheus 里、面板表达式也没错——41 个面板却全部 No data。原因是 Grafana 变量过滤了自己,而我的验证方式恰好掩盖了它。"
|
||||
pubDate: 2026-05-17
|
||||
updatedDate: 2026-09-29
|
||||
category: devops
|
||||
tags: [grafana, prometheus, dashboards, monitoring, promql, observability]
|
||||
ogImage: /og/why-your-grafana-dashboard-shows-no-data.png
|
||||
banner: /banners/why-your-grafana-dashboard-shows-no-data.png
|
||||
draft: false
|
||||
---
|
||||
|
||||
打开每天在看的面板,结果是空的。不是\"某一格坏了\",而是**从上到下每一格**都写着 *No data*。去查 Prometheus,一切健康。这篇讲我踩到的那个具体且不直观的原因,以及为什么我自己的验证会同时告诉我\"面板没问题\"。
|
||||
|
||||
## 最省时间的排查顺序
|
||||
|
||||
常见原因(数据源选错、时间范围不对、`rate()` 用在会重置的计数器上、target 没被抓取)都不适用。真正有效的检查顺序是:
|
||||
|
||||
```bash
|
||||
# 1. target 真被抓取了吗?
|
||||
curl -s http://prometheus:9090/api/v1/targets | jq '.data.activeTargets[] | {job:.labels.job, health}'
|
||||
|
||||
# 2. 指标现在存在吗?(完全绕过 Grafana)
|
||||
curl -s -G http://prometheus:9090/api/v1/query \
|
||||
--data-urlencode 'query=node_uname_info{job="unraid-host"}' | jq '.data.result[0].metric'
|
||||
|
||||
# 3. 面板表达式本身对吗?
|
||||
curl -s -G http://prometheus:9090/api/v1/query \
|
||||
--data-urlencode 'query=count(node_cpu_seconds_total{mode="idle",job="unraid-host"})'
|
||||
```
|
||||
|
||||
三项都过了:7 个 target `up`、2.5 万条序列在采集、手动跑面板表达式也有数据。所以问题不在指标、不在抓取、也不在 PromQL——而在**它们之间的变量层**。
|
||||
|
||||
## 真正的原因:变量过滤了自己
|
||||
|
||||
面板上有一个主机选择器,两个变量串联:`$nodename`(哪台机器)和 `$node`(它的 `instance` 标签)。第一个的定义是:
|
||||
|
||||
```promql
|
||||
# BROKEN — 变量用自身的值来过滤自己
|
||||
label_values(node_uname_info{job="unraid-host", nodename=~"$nodename"}, nodename)
|
||||
```
|
||||
|
||||
首次加载时 `$nodename` 还没有值,选择器就变成 `nodename=~""`——**什么都匹配不到**。查询返回零行的变量没有任何选项,于是它保持空;而 `$node` 又是基于 `$nodename` 定义的:
|
||||
|
||||
```promql
|
||||
label_values(node_uname_info{job="unraid-host", nodename="$nodename"}, instance)
|
||||
```
|
||||
|
||||
于是 `$node` 也是空,所有以 `$node` 为过滤条件的面板自然匹配不到序列。Grafana 说 *No data* 并没有骗人:面板查询真的没有数据,因为给它限定范围的变量解析成了空。
|
||||
|
||||
修法是去掉自引用,并给每个变量存一个明确的默认值:
|
||||
|
||||
```promql
|
||||
# FIXED — 不自引用,每个变量只跳一层
|
||||
# $nodename
|
||||
label_values(node_uname_info{job="unraid-host"}, nodename)
|
||||
# $node
|
||||
label_values(node_uname_info{job="unraid-host", nodename="$nodename"}, instance)
|
||||
```
|
||||
|
||||
## 为什么我的验证没抓到
|
||||
|
||||
这才是值得抄的部分。我\"验证\"面板的方式是:**手动**把每个面板表达式里的变量替换成我知道正确的值(`$nodename=unRaid`、`$node=host.docker.internal:9100`),然后断言查询有数据点。13 项检查全部通过——面板依然是空的,因为 bug 在被替换的那个值**上游**:Grafana 自己对变量的解析是空的。
|
||||
|
||||
**一个替换掉\"可疑值\"的验证,永远发现不了那个值解析失败。** 要抓它,得读 Grafana 真实存下来的值:
|
||||
|
||||
```bash
|
||||
curl -s -u admin:"$PW" http://grafana:4010/api/dashboards/uid/rYdddlPWk \
|
||||
| jq '.dashboard.templating.list[] | {name, current: .current.value}'
|
||||
```
|
||||
|
||||
更直接的检查对象是变量查询本身——跑 Grafana 会跑的那条:
|
||||
|
||||
```bash
|
||||
# $nodename 的 label_values() 到底返回什么?
|
||||
curl -s -G http://prometheus:9090/api/v1/query \
|
||||
--data-urlencode 'query=count by (nodename) (node_uname_info{job="unraid-host"})' | jq '.data.result'
|
||||
```
|
||||
|
||||
那里是空,面板就不可能渲染出来,无论数据多健康。
|
||||
|
||||
## 第二个坑:`$__all` 不是 `.*`
|
||||
|
||||
做自动化面板检查时,值为\"All\"的变量,其 current 是哨兵字符串 `$__all`——不是正则。把 `name=~"$__all"` 原样展开会匹配不到任何东西,于是完全健康的容器面板\"全军覆没\"。展开前要把 `$__all` 解析成该变量的 `allValue`(通常是 `.*`),否则你追的是一个只存在于自己检查脚本里的 bug。
|
||||
|
||||
## 不是每个空面板都是 bug
|
||||
|
||||
修完之后 25 个面板有 21 个出数。剩下 4 个是**设计上就不可能**有数据的,最好在面板说明里写清楚:
|
||||
|
||||
- **PSI 面板**需要 `/proc/pressure`,而这台 NAS 的内核不暴露它——指标在这台机器上无法存在;同样的面板在会导出 `node_pressure_*` 的主机上正常显示。
|
||||
- **根文件系统面板**的表达式带 `fstype!="rootfs"`,而某台主机的 `/` 是**内存盘**(unRaid 就是),所以永远匹配不到。
|
||||
|
||||
区分\"因为 bug 空\"和\"因为这台机器不可能有这个指标而空\",是真修好和瞎忙一下午的分界线。
|
||||
|
||||
## 我下次会怎么做
|
||||
|
||||
- **永远不要定义过滤自己的模板变量。** 一个变量只跳一层。
|
||||
- **给面板依赖的变量存明确的 `current` 值**,别让全新加载依赖下拉框被选中。
|
||||
- **用 Grafana 存下来的值做验证**,而不是我以为的值。
|
||||
- **故意留空的缺口写进面板说明**,否则未来的你会花一个下午去\"修\"一个本来就没打算工作的面板。
|
||||
|
||||
## 结果
|
||||
|
||||
三台主机现在跑同一套布局——每台一个 41 面板的主机面板 + 一个 10 面板的容器面板,分别有 21/23/25 个面板出数,两类结构性空缺写在面板里,而不是留给下一个打开它的人去猜。
|
||||
|
||||
## 需要为你的业务做这个吗?
|
||||
|
||||
如果你有一堆没人信任的 Grafana 面板,或者服务器和 NAS 完全没有监控,我可以帮你搭自托管的 Prometheus + Grafana(主机与容器指标、合理的告警规则、通知推到 Telegram 和邮件),也会把你那些悄悄\"不出数\"的面板修好。
|
||||
|
||||
**WhatsApp:[+60 12-797 2969](https://wa.me/60127972969)** · **邮箱:[[email protected]](mailto:[email protected]?subject=Grafana%20monitoring)** · **[hoelee.com](https://hoelee.com)**
|
||||
|
||||
网站设计与开发是我的主业;服务器加固与自托管基础设施是它的另一半。
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: "你的\"磁盘将满\"告警在说谎:别再按百分比告警"
|
||||
description: "\"剩余 3%\"听着像紧急事故,直到你发现那是 500 GB。在多 TB 的卷上,百分比阈值会在没事的时候乱叫,也会在真出事的时候沉默。"
|
||||
pubDate: 2026-06-24
|
||||
updatedDate: 2026-09-29
|
||||
category: devops
|
||||
tags: [monitoring, alerting, prometheus, grafana, storage, capacity, observability]
|
||||
ogImage: /og/your-disk-full-alert-is-lying.png
|
||||
banner: /banners/your-disk-full-alert-is-lying.png
|
||||
draft: false
|
||||
---
|
||||
|
||||
我搭好监控、写了一套看着合理的规则,然后用一个下午删掉了其中大半阈值。它们全是百分比,而且都以同一种方式错了。
|
||||
|
||||
## 三个教我道理的告警
|
||||
|
||||
**1.「文件系统使用率超过 90%」**——它在一个还剩约 500 GB 的 17 TB 卷上触发。没事发生,也不会马上有事发生。这种体量的卷,一次大备份就能让百分比摆动几个点:比例是噪声,而\"还有 500 GB 可用\"才是世界的真实状态。
|
||||
|
||||
**2.「内存使用率超过 90%」**——在一台**可用**内存 4.8 GB 的主机上反复触发。Linux 会用空闲内存做 page cache,需要时立刻归还;`MemTotal - MemFree` 描述的是文件缓存,不是你的风险。真正预示 OOM 的是 `MemAvailable`。
|
||||
|
||||
**3.「CPU steal 超过 25%」**——一台把邮件栈跑得完全正常的 VPS,steal 长期在 41%。steal 说明宿主机忙,它是**容量**信号而不是**故障**信号;7 vCPU 的机器 41% steal 就是这台机器的成本。这条规则产生的告警永远为真、也永远无用。
|
||||
|
||||
共同点:**我在对比例告警,而比例不知道东西有多大。**
|
||||
|
||||
## 对\"后果\"告警,而不是对比例
|
||||
|
||||
我现在对每个阈值都问一句:*如果这个状态持续下去,实际会发生什么?* 答案几乎总能用绝对单位表达。
|
||||
|
||||
| 不要这样 | 改成 | 原因 |
|
||||
|---|---|---|
|
||||
| 文件系统 > 90% | 剩余空间 < N GB | \"我还能不能写进去\"才是问题 |
|
||||
| 内存使用 > 90% | `MemAvailable` < 512 MB | 逼近 OOM,而不是\"缓存很暖和\" |
|
||||
| CPU steal > 25% | steal > 50% | 容量成本 vs 真的被饿死 |
|
||||
| 负载 > N | `load1 / 核数 > 2` | 不谈核数的负载没有意义 |
|
||||
|
||||
PromQL 的前后对比:
|
||||
|
||||
```promql
|
||||
# BEFORE — 17 TB 卷的百分比;还剩半 TB 就开叫
|
||||
(1 - node_filesystem_avail_bytes / node_filesystem_size_bytes) * 100 > 90
|
||||
|
||||
# AFTER — 真正装数据的卷上,还剩多少容量
|
||||
min by (instance, mountpoint) (
|
||||
node_filesystem_avail_bytes{mountpoint=~"/volume[0-9]+|/mnt/ssd|/mnt/disk[0-9]+"}
|
||||
) < 25e9
|
||||
```
|
||||
|
||||
```promql
|
||||
# BEFORE — 把 page cache 当成压力
|
||||
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 90
|
||||
|
||||
# AFTER — 真正的 OOM 前兆
|
||||
node_memory_MemAvailable_bytes < 512 * 1024 * 1024
|
||||
```
|
||||
|
||||
两个让规则能活下来的细节:
|
||||
|
||||
**把比较放进阈值条件里,不要塞进表达式。** 查询就写 `min by (mountpoint) (node_filesystem_avail_bytes{...})`,让告警规则去和 `25000000000` 比。若你在 PromQL 里写死 `< 25e9`、规则里又留着继承来的 `> 90`,规则照样会评估——但界面显示的阈值是错的,下一个读它的人得从两个互相矛盾的条件下反推你的意图。
|
||||
|
||||
**排除那些\"只是另一个挂载点的视图\"。** 一条\"剩余空间\"规则有多诚实,取决于它的选择器。在我自己的环境里,同一个 NAS 卷出现三次(`/volume1`、`/opt`、以及在另一台主机上以 CIFS 再次挂载),而 unRaid 的 shfs 联合挂载报告 `avail=0`——不加排除的规则不是永远误报,就是把容量算成两倍。一个写明白的选择器胜过聪明的通用写法。
|
||||
|
||||
## 噪声不是无害的
|
||||
|
||||
坏阈值的代价不在告警本身,而在**训练**:每条\"没事乱叫\"的告警都在教你先扫一眼、然后忽略;等你真正该看的那条到来时,它出现在一个你早已不再读的频道里。我的规则总数**减少了**,覆盖率却上升了——更少,但每条都对应一个明确的后果。
|
||||
|
||||
## 阈值是私人的,形状是通用的
|
||||
|
||||
上面的数字是我的:NAS 卷剩不到 25 GB 我会在意、可用内存低于 512 MB 我才管、VPS steal 超过 50% 才算被饿。你的数字随硬件和容忍度而变。
|
||||
|
||||
能通用的是形状:
|
||||
|
||||
- 用绝对单位而非比例;
|
||||
- 把后果写进告警的摘要里;
|
||||
- 选择器明确声明它在替哪些挂载点说话;
|
||||
- 规则数量少到你还能读完每一条。
|
||||
|
||||
## 结果
|
||||
|
||||
同一套栈,从\"三条永久无用报警\"变成 16 条在机房健康时保持安静的规则——其中一条立刻暴露了一个只剩 21 GB 的 NAS 卷,而那正是百分比规则一直藏在\"99% 满的巨物\"背后、被你学会忽略的东西。
|
||||
|
||||
## 需要为你的业务做这个吗?
|
||||
|
||||
如果你的监控在发你已经学会忽略的告警,或者你根本没有监控、宁愿从一条规则而不是从一次失败的备份里得知磁盘满了,我可以帮你搭自托管的监控与告警(Prometheus + Grafana,通知推 Telegram 和邮件),阈值按\"什么会真的坏\"来定。
|
||||
|
||||
**WhatsApp:[+60 12-797 2969](https://wa.me/60127972969)** · **邮箱:[[email protected]](mailto:[email protected]?subject=Monitoring%20and%20alerting)** · **[hoelee.com](https://hoelee.com)**
|
||||
|
||||
网站设计与开发是我的主业;服务器加固与自托管基础设施是它的另一半。
|
||||
Reference in New Issue
Block a user