零依赖服务器监控面板:不用现成工具,自己写一个

2593 字
13 分钟
零依赖服务器监控面板:不用现成工具,自己写一个
AI 智能总结内容由 AI 生成,仅供参考

利用本地 Bash 脚本读取 `/proc` 里的实时数据,配合 Nginx 通过定制的 location 暴露无缓存 JSON,前端 Astro 页面只做 `fetch` 与渲染。脚本通过两次计数差值计算 CPU 使用率、采用可用内存(空闲+缓存)评估占用、磁盘使用按实际可用块算,所有数值以标准口径生成;cron 任务每 30 秒执行一次,保证采样窗口短、准确。Nginx 端点加随机令牌并禁止搜索引擎收录,减少暴露风险。前端页面具容错与进度条色彩变化,配置化到博客全局设置。整套方案约 70 行 Bash、几行 Nginx 与一个页面,内存占用几乎为零,适合小内存服务器或想与博客统一风格的轻量监控。如果需要历史曲线或告警,仍建议使用专业工具。

《从零搭建个人博客》里提过,我的服务器是 2 核 2G 的小机型。想在博客里放一个服务器状态面板,现成方案不少——beszel、netdata、dashdot 都很漂亮——但动辄吃掉几百 MB 内存,还得单独开端口、配登录。2G 内存的机器经不起这么造。

最后的方案:一段 bash 脚本 + 一段 Nginx 配置 + 一个博客页面,零依赖、零端口、零额外内存占用,效果见本站的「状态」页。这篇把完整实现过程和踩过的坑记录下来。

一、整体架构#

先想清楚数据从哪来、到哪去:

┌─ 服务器 ────────────────────────────┐
│ monitor.sh(cron 每 30 秒) │
│ ↓ 读 /proc 内核数据,算出指标 │
│ ↓ 写成 JSON 文件(原子替换) │
│ Nginx 把这个 JSON 以固定路径暴露 │
└──────────────┬──────────────────────┘
↓ HTTP(浏览器 fetch)
┌─ 博客页面 ──────────────────────────┐
│ JS 每 30 秒拉取 JSON → 渲染卡片 │
└────────────────────────────────────┘

关键认识:数据采集根本不需要 SSH,也不需要在浏览器端连服务器。脚本跑在服务器本地,读 /proc 虚拟文件——那是内核自己维护的运行时账本,数据最权威。浏览器端只是一个普通 HTTP 请求拉 JSON。

二、第一步:采样脚本#

完整脚本如下,可以直接抄(保存为 /opt/server-mon/monitor.sh,记得 chmod +x):

#!/usr/bin/env bash
# 读取 /proc 生成服务器状态 JSON,由 cron 定时执行
set -u
OUT=/var/www/mon/status.json
STATE=/var/lib/server-mon/prev.txt
# 自动识别默认网卡
IFACE=$(ip route show default 2>/dev/null | awk '{print $5; exit}')
[ -z "${IFACE:-}" ] && IFACE=eth0
# --- CPU 计数器 ---
read -r _ user nice system idle iowait irq softirq steal _ < /proc/stat
CPU_TOTAL=$((user + nice + system + iowait + irq + softirq + steal + idle))
CPU_BUSY=$((user + nice + system + iowait + irq + softirq + steal))
# --- 内存 ---
memtotal=$(awk '/^MemTotal:/{print $2}' /proc/meminfo)
memavail=$(awk '/^MemAvailable:/{print $2}' /proc/meminfo)
# --- 磁盘(根分区)---
dtotal=$(df -kP / | tail -1 | awk '{print $2}')
dused=$(df -kP / | tail -1 | awk '{print $3}')
davail=$(df -kP / | tail -1 | awk '{print $4}')
# --- 网络计数器 ---
RX=$(grep "^[[:space:]]*$IFACE:" /proc/net/dev | awk '{print $2}')
TX=$(grep "^[[:space:]]*$IFACE:" /proc/net/dev | awk '{print $10}')
# --- 负载 / 运行时长 ---
load1=$(cut -d' ' -f1 /proc/loadavg)
UPTIME=$(cut -d. -f1 /proc/uptime)
TS=$(date +%s)
# --- 读上次采样,算差值 ---
prev_ct=0; prev_cb=0; prev_rx=0; prev_tx=0; prev_ts=0
[ -f "$STATE" ] && read -r prev_ct prev_cb prev_rx prev_tx prev_ts < "$STATE"
dct=$((CPU_TOTAL - prev_ct)); dcb=$((CPU_BUSY - prev_cb))
drx=$((RX - prev_rx)); dtx=$((TX - prev_tx))
dt=$((TS - prev_ts))
[ "$dt" -lt 1 ] && dt=1
[ "$dtx" -lt 0 ] && dtx=0
[ "$drx" -lt 0 ] && drx=0
if [ "$prev_ts" -eq 0 ] || [ "$dct" -le 0 ]; then
cpu_pct=-1; rx_s=0; tx_s=0
else
cpu_pct=$(awk -v b="$dcb" -v t="$dct" 'BEGIN{p=b/t*100; if(p<0)p=0; if(p>100)p=100; printf "%.1f", p}')
rx_s=$(awk -v r="$drx" -v t="$dt" 'BEGIN{printf "%.1f", r/t/1024}')
tx_s=$(awk -v r="$dtx" -v t="$dt" 'BEGIN{printf "%.1f", r/t/1024}')
fi
printf '%s %s %s %s %s\n' "$CPU_TOTAL" "$CPU_BUSY" "$RX" "$TX" "$TS" > "$STATE"
MEM_PCT=$(awk -v t="$memtotal" -v a="$memavail" 'BEGIN{printf "%.1f", (t>0)? (t-a)/t*100 : 0}')
DISK_PCT=$(awk -v u="$dused" -v a="$davail" 'BEGIN{t=u+a; printf "%.1f", (t>0)? u/t*100 : 0}')
TOTRX=$(awk -v r="$RX" 'BEGIN{printf "%.2f", r/1073741824}')
TOTTX=$(awk -v t2="$TX" 'BEGIN{printf "%.2f", t2/1073741824}')
mkdir -p /var/www/mon
# 注意:JSON 里刻意不写主机名、IP、网卡名,避免泄露服务器信息
printf '{"ts":%s,"uptime":%s,"load1":"%s","cpu":{"usage":%s},"mem":{"total_kb":%s,"avail_kb":%s,"used_pct":%s},"disk":{"total_kb":%s,"used_kb":%s,"used_pct":%s},"net":{"rx_kbps":%s,"tx_kbps":%s,"total_rx_gb":%s,"total_tx_gb":%s}}\n' \
"$TS" "$UPTIME" "$load1" "$cpu_pct" "$memtotal" "$memavail" "$MEM_PCT" \
"$dtotal" "$dused" "$DISK_PCT" "$rx_s" "$tx_s" "$TOTRX" "$TOTTX" > "$OUT.tmp"
mv "$OUT.tmp" "$OUT"

几个准确性设计,都是查过资料才定的:

CPU 用两次计数差值,不是瞬时值。 /proc/stat 的 CPU 时间是开机以来的累计值,单次读没有任何意义。脚本把上次计数存在 prev.txt,这次减上次,差值才是这段时间的真实占用率。这也是为什么页面刚上线时 CPU 会显示几秒的 ”…”——第一次运行还没有差值,用 -1 标记,前端识别后显示省略号而不是错误数据。

内存用 MemAvailable 口径。 Linux 会把空闲内存拿去做文件缓存(buff/cache),free 命令里的”可用”就是 MemAvailable:等于空闲 + 可回收缓存。用它算占用率才贴近真实体验,直接拿 MemTotal - MemFree 会把缓存算成”已用”,2G 机器看起来永远 90%+。

磁盘百分比和 df 命令同口径。 有两种算法:used/total 和 used/(used+avail)。后者才是 df 命令显示的那个百分比——因为 total 里含一部分 root 保留块(普通用户写不进去),分母用 used+avail 才不会把面板显示得比实际乐观。

JSON 用「写临时文件 + mv」原子替换。 如果前端 fetch 的瞬间脚本正写到一半,会拿到半截 JSON 直接解析报错。先写 .tmp 再 mv,mv 在同一分区是原子操作,任何时刻文件都是完整的。

计数器回绕保护。 服务器重启后 /proc/net/dev 的流量计数会归零,差值算出负数,[ "$dtx" -lt 0 ] && dtx=0 把它钳到 0,面板不会出现负速率。

三、第二步:定时采样#

cron 最小粒度是一分钟,想要 30 秒采样有个经典技巧——一条规则里执行两次,第二次延后半分钟:

Terminal window
* * * * * /opt/server-mon/monitor.sh >/dev/null 2>&1; sleep 30; /opt/server-mon/monitor.sh >/dev/null 2>&1

用 crontab -e 添加后,crontab -l 确认。采样间隔缩短一半有两个好处:网速/CPU 的统计窗口更短,短时突发流量更能捕捉到;页面每 30 秒刷新时每次都能拿到新数据,而不是同样的数字看一分钟。

脚本的差值逻辑天然支持任意间隔(用实际时间差 dt 做除数),所以就算 cron 延迟几分钟跑,速率计算依然准确。

四、第三步:Nginx 暴露数据#

JSON 文件在 /var/www/mon/status.json,在站点 Nginx 配置里加一个 location:

# 路径里那串随机字符是「路径令牌」,防止端点被扫描,需与博客配置一致
location ^~ /server-status/<随机令牌>/ {
alias /var/www/mon/;
add_header Cache-Control "no-store";
add_header X-Robots-Tag "noindex, nofollow" always;
}

三个细节:

  1. no-store:状态数据永远不能被缓存,否则访客看到的是几分钟前的旧数据;
  2. noindex, nofollow:告诉搜索引擎不要收录这个数据端点;
  3. 路径令牌:JSON 的真实地址里拼一段随机字符串。改完 nginx -t 测试语法,再 systemctl reload nginx 平滑生效,站点不中断。

五、第四步:博客展示页#

页面是 Astro 写的(其他框架同理,核心就是 fetch + 渲染)。骨架:

const DATA_URL = "/server-status/<随机令牌>/status.json";
const REFRESH_SEC = 30;
const STALE_SEC = 180; // 数据超过 3 分钟没更新视为异常
const WARN_THRESHOLD = 60; // 占用 60% 进度条转黄
const HIGH_THRESHOLD = 85; // 占用 85% 转红
async function refresh() {
try {
const res = await fetch(DATA_URL + "?t=" + Date.now(), { cache: "no-store" });
if (!res.ok) throw new Error(res.status);
const d = await res.json();
render(d); // 按 CPU/内存/磁盘/网络分块渲染
const stale = Date.now() / 1000 - d.ts > STALE_SEC;
setStatus(!stale, stale ? "数据延迟(采样服务可能异常)" : "运行正常");
} catch {
setStatus(false, "无法获取状态数据");
}
}
refresh();
setInterval(refresh, REFRESH_SEC * 1000);

?t=时间戳 是防止浏览器或中间层自作聪明缓存请求。渲染上有两个容错设计:每个字段赋值前都判空,某个字段缺失只跳过那一处,不会让整个面板白屏;进度条按占用分三档变色(正常主题色 → 黄 → 红),一眼就能看出哪项吃紧。

六、第五步:配置化#

把所有参数收进博客的统一配置文件(src/config/index.ts 的「服务器状态」模块),页面和导航都从这里读:

export const serverStatusConfig = {
enable: true, // 总开关:关闭后导航隐藏入口,页面跳转 404
title: "服务器状态",
dataUrl: "...", // 由路径令牌拼出
refreshSec: 30,
staleSec: 180,
warnThreshold: 60,
highThreshold: 85,
cards: { cpu: true, mem: true, disk: true, net: true }, // 卡片开关
};

导航栏入口也写成条件渲染——enable 一关,导航上「状态」直接消失,不需要再改第二个文件。

七、关于防泄露#

面板数据是公开的(所有访客都能看),所以做了四层处理:

措施防什么
JSON 路径拼随机令牌扫描器批量猜 /server-status/status.json 这类常见路径,直接 404
noindex 响应头搜索引擎不收录数据端点
JSON 字段脱敏不输出主机名、IP、网卡名、系统版本,只有资源占用指标
总开关联动关闭后整个站不再出现数据地址

要诚实说明边界:令牌会出现在网页源码里,所以它防的是扫描器和顺手试探,不防决心翻你源码的人。这是「隐藏入口」不是「加密认证」,对个人博客是成本收益比最高的一档。真要更严格,可以上 Nginx 的 secure_link 模块做签名校验,或者限制访问 IP——对个人博客来说通常没必要。

另外,想换令牌随时可以:改博客配置里的一处 + Nginx 配置里的一处,reload 即可,旧地址立刻作废,相当于换门牌号。

八、踩过的坑#

记录几个调试时真实撞上的问题:

  1. Windows 编辑的脚本在 Linux 跑不了:报 integer expected。Windows 换行是 CRLF,bash 把 \r 当成了内容的一部分。修复:sed -i 's/\r$//' monitor.sh。写服务器脚本最好直接在服务器上用 nano/vim 写,或传完必跑一次这条命令。
  2. 网速永远是 0:/proc/net/dev 每行行首有空格,直接 awk -F'[: ]+' '{print $2}' 会把空字段算进去取错列。修复:先用 grep "^[[:space:]]*eth0:" 锁定网卡那一行,再用默认分隔符取列。
  3. 磁盘百分比和 df 对不上:算法口径问题,见前文,改用 used/(used+avail)。
  4. 内存显示 1.69 TB:前端单位换算错位——1048576 KB 是 1 GB 不是 1 TB,把 2G 内存显示成了 1.69 TB。修完的换算表:KB → 1024 → MB → 1024 → GB → 1024 → TB,写的时候直接把 1073741824 这类常数标注在代码注释里。
  5. 文章时间偏移 8 小时:CI 在 UTC 时区构建,frontmatter 里的时间不写时区就会被按 UTC 解析。修复:所有 published 明确写 +08:00。

九、小结#

这套面板的全部成本:约 70 行 bash + 一段 Nginx 配置 + 一个页面组件,运行时内存占用约等于零(脚本每 30 秒跑一次,跑完就退)。准确度上因为直接读内核数据、用差值法和标准口径,数字和 free、df 完全对得上。

适合的场景很明确:小内存服务器、想要和博客风格统一的轻量面板、不想多开端口和服务。如果你需要历史曲线、告警通知,那还是上 beszel 这类专业工具——但我 2G 的小机器,这个刚好。

系列文章:《从零搭建个人博客》 · 《blog 日常使用指南》

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

零依赖服务器监控面板:不用现成工具,自己写一个
https://li8.me/posts/3/
作者
八奈奈
发布于
2026-09-21
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
八奈奈
记录技术与生活
公告
欢迎来到「二次元宅男录」!这里记录技术与生活,欢迎常来逛逛。
分类
标签
站点统计
文章
10
分类
4
标签
18
总字数
17,017
运行时长
0 天
最后活动
0 天前
文章目录