服务器健康哨兵 SOP
> 适用前提(30 秒判断):Linux + systemd + root;只需 bash/curl/openssl(零外部依赖)。
> 单机能用:配置里去掉 PEER_SSH 即单机模式;双机互检需两台机器 SSH 互通(部署脚本自动建互信)。
> 适用:云主机/物理机上的 Web、API、Node/PM2 类服务。不适用:Windows 主机、K8s 集群(应选专门方案)。
> 显式调用:关键词没触发时,直接说「用服务器健康哨兵巡检 <主机/域名>」即可。
快速导航(按需跳读)
| 你想干什么 | 看哪 |
|---|---|
| 赶紧部署跑起来 | 二、快速部署 |
| 只有一台机器 | 二 + FAQ-Q1(去掉 PEER_SSH 即可) |
| 换服务器 / 换环境 | 二·换环境清单 + references/example-configs.md(三套完整示例) |
| 报错了要排查 | 十一、FAQ + 六、故障注入验证 |
| 给用户交付/汇报 | 十、交付清单模板 |
> 核心命题:服务跑起来了 ≠ 有人在看着它。宕机、磁盘满、证书过期、数据损坏,
> 在"没有任何监控"的机器上永远是用户先发现——而用户发现问题时,损失已经发生。
>
> 本技能的目标是用零外部依赖的方式补齐这块,且不增加运维负担(正常时完全安静)。
〇、能力披露与部署前确认(安全必读)
本技能会在目标机器上实际执行以下三类操作,部署前请逐项确认:
| 能力 | 具体动作 | 影响范围 |
|---|---|---|
| SSH 远程执行 | 跨机互检读取对端 state.txt;deploy_sentinel.sh 经 SSH 安装脚本/配置 |
仅你配置的 PEER_SSH 与部署清单内的主机 |
| cron 安装 | 写入 /etc/cron.d/sentinel(每 5 分钟以 root 执行一次巡检) |
仅此一条 cron;撤销方式 rm /etc/cron.d/sentinel |
| 外联 Webhook | 异常告警 POST 到你配置的企微群机器人地址 | 仅异常时外联;不配置 Webhook 则完全不出网 |
不做的事:无遥测、无数据外传、无第三方上报——告警只发到你自己填写的群机器人;本地日志只留异常内容。
部署前必须确认(deploy_sentinel.sh 会交互式要求输入 YES;自动化场景需显式 AUTO_CONFIRM=1):
- 目标主机是你自己的资产(IP / SSH 账户核对无误)
- SSH 使用哨兵专用密钥(默认
~/.ssh/sentinel_ed25519、各节点/root/.ssh/sentinel_ed25519),不复用个人密钥 - 告警接收范围:Webhook 发到哪个群、群里有谁、是否适合出现这些运维信息
SSH 安全默认值(相对"裸免密"的加固,也是安全扫描的整改口径):
- 不硬编码个人密钥:密钥路径一律走配置(
SSH_KEY),默认指向哨兵专用密钥 - 主机密钥校验不关闭:
StrictHostKeyChecking=accept-new(首连记录指纹、之后变更即拒)+ 专用known_hosts文件;高安全环境可预置 known_hosts 后改yes - 互信条目带来源限制:
from=<两节点IP>,no-pty,no-agent-forwarding,no-port-forwarding - 无口令密钥的说明:cron 无人值守巡检需要免密钥,这是自动化前提。缓解手段:专用密钥 + 专用账户 +
from=来源限制 +IdentitiesOnly=yes,退役时删除密钥即可;高安全环境可用 ssh-agent 带口令托管 - 建议专用低权限账户:为哨兵建独立账户(如
sentinel),自愈命令经 sudoers 白名单放开——sentinel ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx, /usr/bin/systemctl restart myapp,并把SSH_USER/PEER_SSH改为该账户;自愈命令越多,白名单越要逐条收紧
一、四条设计原则(先定原则,再写代码)
| 原则 | 含义 | 反面教训 |
|---|---|---|
| 零依赖 | 只用 bash + curl + coreutils + openssl,不装 Prometheus/Zabbix | 小机器(2G 内存)装监控套件本身就是负担 |
| 异常才出声 | 正常时完全静默,不写日志不推送 | 每 5 分钟一条"一切正常",一周后没人再看 |
| 自愈优先 | 服务掉线先尝试自动拉起,拉不起来才告警 | 半夜宕机,等早上人来了再修,白丢 8 小时 |
| 交叉互检 | 每台机器检查对端;一台彻底宕机时由另一台代报 | 单机自检的根本缺陷:自己挂了就无法给自己报警 |
关于"自愈"的一个关键认知
systemd 的 Restart=always 只管"进程退出",管不了"进程活着但不响应"(僵死/hang)。
哨兵必须额外做 HTTP 探测,把僵死也纳入重启触发条件——这是 systemd 覆盖不到的一层。
二、快速部署(3 步)
# 1) 装脚本 + 配置
install -m 755 sentinel.sh /opt/sentinel/sentinel.sh
cp sentinel.conf.example.txt /etc/sentinel.conf
# 2) 挂 cron(每 5 分钟;正常时静默,所以日志不会涨)
cat > /etc/cron.d/sentinel <<'EOF'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
*/5 * * * * root /opt/sentinel/sentinel.sh >> /opt/sentinel/sentinel.log 2>&1
EOF
# 3) 跑一次并看结果
/opt/sentinel/sentinel.sh
cat /opt/sentinel/state.txt
> 现成脚本在本技能 scripts/ 目录下:sentinel.sh、scripts/sentinel.conf.example.txt(配置模板,按节点改 IP/域名/服务名)、deploy_sentinel.sh(一键部署含双向互信、部署前确认目标与告警范围)、
> pull_states.sh + build_report.py + refresh_report.sh(本地仪表盘)。
> Windows 一键刷新启动器源码在 references/refresh-dashboard-bat.md(复制另存为 .bat 双击即用)。
> 部署到新环境时只需改配置里的 IP / 域名 / 证书路径 / 服务名;SSH 一律用哨兵专用密钥(SSH_KEY),勿硬编码个人密钥。
配置模板要点
NODE_NAME="node-a-web"
SERVICES="nginx myapp" # systemd 管的服务
CHECK_PORTS="3001" # ★ 端口探测:覆盖 PM2/裸进程管理的服务
CHECK_URL_LOCAL="https://127.0.0.1/" # 走本地,绕开 DNS 与外网
CHECK_URL_LOCAL_HOST="app-a.example.com" # ★ 必须带 Host 头,否则命中默认站点
CHECK_URL_PUBLIC="https://app-a.example.com/" # 真实用户视角
CERT_FILE="/etc/nginx/cert/app-a.example.com.pem"
JSON_CHECK_FILES="/opt/myapp/data.json" # JSON 损坏会让服务起不来,必须提前发现
LOG_DIRS="/opt/myapp/logs /var/log/nginx"
PEER_SSH="root@192.0.2.12" # 交叉互检的目标
SSH_KEY="/root/.ssh/sentinel_ed25519" # 哨兵专用密钥(勿用个人密钥)
SSH_STRICT="accept-new" # 主机密钥校验:首连记录指纹、之后变更即拒
SSH_KNOWN_HOSTS="/root/.ssh/sentinel_known_hosts"
WECOM_WEBHOOK="" # 群机器人地址,留空则只写本地日志
为什么 CHECK_PORTS 不可省:systemctl 查不到 PM2 / 裸进程管理的服务。
常见的 web(3000) 和 api(8080) 若由 PM2 管理,只查 systemd 会漏检。
换环境改哪里(就 2 处,有清单不怕漏)
| # | 改什么 | 在哪改 | 说明 |
|---|---|---|---|
| 1 | 两台机的 IP | 命令行参数:bash deploy_sentinel.sh deploy <节点A-IP> <节点B-IP>;拉数据 bash pull_states.sh <ip1:tag1> <ip2:tag2> |
脚本里的 192.0.2.x 只是占位示例,不必进脚本里改 |
| 2 | 业务配置 | /etc/sentinel.conf 一个文件:服务名/端口/探测 URL/证书路径/PEER_SSH/Webhook |
换机必改项全在这 |
可选:SSH_USER(专用低权限账户)、NODE_NAME、各类阈值。
三套可直接照抄的完整配置(节点 A / 节点 B / 单机)见 references/example-configs.md。
部署前先跑 bash deploy_sentinel.sh check <A-IP> <B-IP> 做环境自检——每一项都给"过/不过 + 原因"。
三、架构:交叉哨兵与回声放大防护
┌─────────────────┐ ┌─────────────────┐
│ 节点 A 192.0.2.11 │ │ 节点 B 192.0.2.12 │
│ 自检 10 类 │─── SSG 拉取 ─────▶ │ 自检 10 类 │
│ 写 state.txt │◀── 对端 state ──── │ 写 state.txt │
│ 推送企微 │ │ 推送企微 │
└─────────────────┘ └─────────────────┘
发现对方不通 / 停止 / 有告警 → 代为报警
对端检查做三件事:
1. SSH 不通 → 「疑似整机故障」
2. 对端 ts 超过 PEER_STALE_MIN(20 分钟)未更新 → 「哨兵已停摆」(能抓到"机器活着但哨兵死了")
3. 对端有告警 → 本机代为转发
★★★ 回声放大:交叉哨兵的头号陷阱
错误做法:把对端告警也写进自己的 state。结果:
轮1 A 报「B 不可达」
轮2 B 读到 A 的告警 → 报「A 报告 B 不可达」
轮3 A 读到 B 的告警 → 报「B 报告 A 报告 B 不可达」
轮4 ……字符串逐轮变长,永远无法回到正常
正确做法:对端告警放进独立变量 PEER_ALERTS,绝不写入自己的 state;
state 里只保留本机真实状态(另记一个 peer_alerts=<计数> 供仪表盘参考,
但读取方只解析 alert_N,忽略 peer_alerts)。
ALERTS="" # 本机异常 → 写入 state
PEER_ALERTS="" # 对端转来的异常 → 只用于推送,永不写入 state
add_peer() { ... } # 与 add_alert 分离
验证收敛的方法:清空两台 state,连跑 4 轮。
- ❌ 错误实现:每轮字符串变长,永远 ALERT
- ✅ 正确实现:第 1~2 轮可能有一次性提示,第 3 轮起稳定静默、verdict=OK
四、检查项清单(10 类)
| # | 检查项 | 抓什么 | 关键实现 |
|---|---|---|---|
| 1 | 服务存活 | 进程掉了 | systemctl is-active + 失败自动 restart |
| 2 | 端口监听 | PM2/裸进程服务掉了 | ss -lntH 查内核监听表(不产生 TCP 连接) |
| 3 | 本机 HTTP | 进程活着但不响应(僵死) | curl 走 127.0.0.1 + Host 头 |
| 4 | 公网 HTTPS | 域名/证书/带宽/防火墙问题 | curl 走真实域名 + TTFB 阈值告警 |
| 5 | 磁盘 | 撑满 | df -P 取值比 / |
| 6 | 内存 / Swap | OOM 前兆 | /proc/meminfo 的 MemAvailable;Swap 使用率 |
| 7 | 证书到期 | 到期后全站报错 | openssl x509 -enddate 算剩余天数 |
| 8 | 数据完整性 | JSON 损坏致服务起不来 | node -e "JSON.parse(...)" |
| 9 | 日志体积 | 日志吃盘 | du -sm + 无轮转时顺手补清理任务 |
| 10 | 对端互检 | 另一台整机故障 | 见第三节 |
> ⚠️ 检查项 4 的 TTFB 不要只做"是否超时",记录成趋势(ttfb.log),
> 性能退化是渐进过程,只有趋势能提前发现。
4.1 资源快照:采集与告警必须解耦 ★ 2026-09-17 补
踩过的坑:第一版只做阈值告警 —— 磁盘/内存不超阈值就什么都不写。
结果仪表盘上完全看不到机器有多少核、内存多大、负载多少。
用户问"怎么不显示 CPU 配置",才发现"配置运行情况"整体缺失。
正确做法:独立一段无条件采集,写进 state 的 metrics 对象;阈值判断仍归检查项 5/6。
两者职责分离,仪表盘永远有数据,告警逻辑也不被污染。
采集项(全部零依赖,纯 /proc + df + ps):
| 项 | 来源 | 备注 |
|---|---|---|
| CPU 使用率 | /proc/stat 两次采样取差 |
最准;别用 top -bn1(首帧是开机均值,假的) |
| CPU 型号 / 核数 | /proc/cpuinfo model name、nproc |
|
| 负载 1/5/15 | /proc/loadavg |
必须按核数归一化:2 核 load1=2 就是满载 |
| 内存 / Swap | /proc/meminfo |
总量 + 已用 + 可用 + 百分比 |
| 磁盘 | df -P |
注意 -P 保证单行输出(长设备名会折行) |
| 网络速率 | /proc/net/dev |
汇总除 lo 外所有网卡;rx=入 tx=出 |
| TCP 连接数 | /proc/net/tcp[6] 状态 01 |
反映真实并发,比"在线人数"诚实 |
| 进程数 / 运行时长 | /proc 计数、/proc/uptime |
|
| Top N 进程 | ps -eo rss,pmem,pcpu,comm --sort=-rss |
回答"谁在吃资源" |
★ 采样窗口必须复用:CPU 和网卡都要"两次采样取差",共用一个 sleep,别各等一次。
CS1=$(cpu_snap); NS1=$(net_snap); sleep 1; CS2=$(cpu_snap); NS2=$(net_snap)
# 网卡汇总(除 lo):split($2,f," ") 中单空格是 awk 的"按空白切分"特例
net_snap() {
awk -F: '/:/ { iface=$1; gsub(/[ \t]/,"",iface); if (iface=="lo") next;
n=split($2, f, " "); rx+=f[1]; tx+=f[9] }
END { printf "%d %d", rx+0, tx+0 }' /proc/net/dev
}
4.2 资源趋势(res.log)
和 TTFB 同样思路:每次巡检追加一行,保留最近约 2016 行(7 天)。
ts cpu% mem% disk% load1 rx_mbps tx_mbps tcp_est
拿不到小数第二位的取样窗口会导致带宽趋势失真,公式 (Δbytes×8)/秒/1e6。
> ★★ 百分比类指标必须用固定纵轴(0–100)画图。
> 自适应纵轴会把"全程 3% 的平稳线"画得和"90% 的告警线"一样高,完全误导。
> 计数类(TCP)和速率类(带宽)才用自适应。给 sparkline 加 lo/hi 参数即可。
4.3 仪表盘上怎么呈现
- 资源条(三级配色 <70% 绿 / 70–85% 橙 / >85% 红):CPU、内存、磁盘、Swap(有才显示)
- 规格行:CPU 型号×核数 · 负载 · 运行时长 · 进程数 · 出网带宽 · TCP 连接 · Swap
- 无 Swap 时标橙色「未设置」 —— 轻量机常默认无 Swap,是隐患,要显眼 - Top 3 进程表:名称 / 内存 / 占比 / CPU
- 资源趋势:CPU / 内存 / 出网带宽 / TCP 连接数 四张小图,2 列栅格,窄屏单列
验证要验到像素:用 Playwright 读 getBoundingClientRect().width,
确认"条长 ÷ 轨道长 ≈ 百分比"。这是唯一能证明"图表没骗人"的方法。
> 实测:CPU 2% → 21/1034px、内存 24% → 248/1034px、磁盘 32% → 331/1034px,全部吻合。
五、八个致命坑(都极隐蔽,实测踩过)
坑 1|exec 9>file 2>/dev/null 把脚本 stderr 永久丢进黑洞 ★最坑
exec 不带命令时,其重定向永久作用于当前 shell。所以 2>/dev/null 会让
脚本后续所有 stderr 消失。叠加 set -u 后,任何未定义变量都变成"静默死亡":
无任何输出、退出码 1,日志里什么都查不到。
症状:脚本跑到一半就没了;state.txt 只生成 .txt.tmp。
# ❌ 错误
exec 9>"${STATE_DIR}/.lock" 2>/dev/null
# ✅ 正确:只重定向需要的 fd
exec 9>"${STATE_DIR}/.lock"
配套防御:所有计数变量提前初始化(N_ALERT=0; N_PEER=0; ...),
让 set -u 不会因为将来漏赋值而炸掉整轮。
坑 2|在被 ssh host 'bash -s' <<EOF 喂入的脚本里再调 ssh,必须加 -n
子 ssh 默认吃掉父进程的 stdin(即 heredoc 剩余的行),导致外层会话提前 EOF。
症状同样是"跑到一半就没了、退出码异常",极易误判为脚本 bug。
# ✅ 内层 ssh 一律加 -n;密钥走配置(哨兵专用),主机密钥校验不关闭
POUT=$(ssh -n -i "$SSH_KEY" -o IdentitiesOnly=yes -o BatchMode=yes \
-o StrictHostKeyChecking="$SSH_STRICT" -o UserKnownHostsFile="$SSH_KNOWN_HOSTS" \
"$PEER_SSH" "cat $STATE_TXT" 2>/dev/null)
> 推论:测试方式本身会触发真 bug。"换个方式跑就好了"不等于没问题,
> 反而说明脚本对执行环境有隐含依赖,必须修掉。
坑 3|告警去重签名绝不能包含时间戳
消息体里带 HH:MM → 跨分钟后签名就变 → 去重永远失效、每轮都推,等于没做节流。
# ❌ SIG=$(printf '%s' "$MSG" | md5sum ...) # MSG 含时间
# ✅ 签名只取"问题内容"
SIG_SRC="v=$VERDICT a=$N_ALERT w=$N_WARN h=$N_HEAL p=$N_PEER
$ALERTS
$WARNS
$PEER_ALERTS"
SIG=$(printf '%s' "$SIG_SRC" | md5sum | cut -d' ' -f1)
节流策略:新问题 → 立即出声;同一问题 → 每 REALERT_EVERY 轮(默认 6 = 30 分钟)重提一次;
恢复正常 → 发一条"已恢复"并清空签名。
坑 4|同一文件的多处编辑不可并行发出
并行编辑基于同一份快照,后写的会覆盖先写的。后果是部分改动静默丢失,
而且 bash -n 语法检查照样通过,表面完全看不出来。
症状:明明改了、语法也对,运行时却报 unbound variable 或行为不符预期。
规则:
1. 同一文件多处修改严格串行
2. 改完必须做要素点检——grep 关键标识符并对比期望次数:
chk() { local k="$1" exp="$2"; local got=$(grep -c -- "$k" "$F");
[ "$got" -eq "$exp" ] && printf "✅ %s %s\n" "$k" "$got" \
|| printf "❌ %s %s (期望 %s)\n" "$k" "$got" "$exp"; }
chk "port_listening" 2
chk "add_peer" 4
chk "N_PEER" 6
坑 5|{ ...; [ cond ] && cmd; } > tmp && mv tmp final 的退出码陷阱
命令组末尾若是 [ cond ] && cmd 且 cond 为假,整组退出码为 1 → && mv 永不执行,
状态文件静默写不出来。
# ❌ 末尾条件为假时,mv 不执行
{ ...; [ -n "$WARNS" ] && printf ...; } > "$TMP" && mv "$TMP" "$FINAL"
# ✅ 分两行,不依赖退出码
{ ...; true; } > "$TMP" 2>/dev/null
mv -f "$TMP" "$FINAL" 2>/dev/null
坑 6|--dry-run 不是所有工具的通用参数(证书/巡检场景常见)
acme.sh 没有 --dry-run(那是 certbot 的)。
验证要选"零副作用且真能证伪"的方式,例如证书续期就手工实测 HTTP-01 挑战通道:
往 webroot 放随机 token → 从公网 curl 取回 → 比对一致即通道可用。
坑 7|heredoc 里写 JS/JSON 会吞掉反斜杠(2026-09-17)
cat > x.js <<'EOF' ... EOF 即使加了引号,经工具层传递后 \\ 仍会变成 \,
path.replace(/\\/g, '/') 直接变成 /\/g → SyntaxError。
→ 写 Node/JS/Python 脚本一律用文件写入工具,不要用 heredoc 拼。
坑 8|声明了变量却在几百行后才用 —— 用要素点检兜住
本次新增资源快照时,趋势行里引用了一个最终没定义的名字(LOAD_CPU_NORM)。
bash -n 语法检查完全查不出来(它是合法变量名),
靠"grep 关键标识符计数是否等于 0"抓到。
→ 每轮改动后跑一次要素点检(见第五节坑 4 的检查函数),它比语法检查有效得多。
六、验证方法:故障注入(不可省略)
只验"正常时安静"是不够的——必须验"异常时真报得出来"。
# 1) 备份配置,把阈值改成必然触发
cp /etc/sentinel.conf /tmp/sc.bak
sed -i "s/^DISK_THRESHOLD=.*/DISK_THRESHOLD=1/" /etc/sentinel.conf
rm -f /opt/sentinel/last_sig /opt/sentinel/same_count
# 2) 第一次:应出声
/opt/sentinel/sentinel.sh # 期望:报「磁盘 / 使用率 xx%(阈值 1%)」
# 3) 第二次:应被去重节流(静默)
/opt/sentinel/sentinel.sh # 期望:无输出;same_count=2
# 4) 恢复:应静默并清空签名
cp /tmp/sc.bak /etc/sentinel.conf
/opt/sentinel/sentinel.sh
[ -s /opt/sentinel/last_sig ] && echo "❌ 签名未清理" || echo "✅ 已清理"
逐项都要注入验证,不能只测一项就以为整条链路可信:
| 注入手法 | 期望告警 |
|---|---|
DISK_THRESHOLD=1 |
磁盘使用率超阈值 |
CHECK_PORTS 加不存在的 9999 |
端口未在监听 |
JSON_CHECK_FILES 指向损坏文件 |
数据文件 JSON 损坏 |
CERT_FILE 指向不存在的路径 |
证书文件不存在 |
临时 systemctl stop nginx |
服务停止 + 已自动重启(验证自愈) |
断开对端 SSH(改错 PEER_SSH) |
对端不可达(验证交叉互检) |
七、企微告警推送
方式 A:群机器人 Webhook(最简单,推荐)
企微群 → 右上角 → 群机器人 → 添加 → 复制 Webhook 地址,填入配置:
WECOM_WEBHOOK="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxx"
push_wecom() {
[ -z "$WECOM_WEBHOOK" ] && return 0
local body
body=$(printf '%s' "$1" | sed ':a;N;$!ba;s/\n/\\n/g' | sed 's/"/\\"/g')
curl -s --max-time 10 -H 'Content-Type: application/json' \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"$body\"}}" \
"$WECOM_WEBHOOK" >/dev/null 2>&1
}
> JSON 里换行必须转成 \n、双引号要转义,否则推送 400。
方式 B:自建应用消息(复用已有 corpid/secret,需应用有发消息权限)
用已有 corpid/secret 换 access_token 后调发消息接口,touser 填接收人 userid。
方式 C:留空 = 只写本地日志
留空不影响功能(巡检照常、state 照常写),只是不推送。适合先跑起来、后配推送。
八、可视化仪表盘
pull_states.sh 拉两台 state.json + ttfb.log 到本地 data/,
build_report.py 生成自包含 HTML(无 CDN 依赖):总览横幅 + 每机卡片
(verdict 徽章 / 告警清单 / 响应时间 sparkline)+ 自动深浅色适配。
bash refresh_report.sh # 刷新,不打开
bash refresh_report.sh --open # 刷新并自动打开浏览器
> 值得单独做成仪表盘的原因:状态文件是给机器读的,人能一眼看懂的只有图。
> 巡检数据全在 state.json 里,不聚合展示就白采集了。
★ 交付时必须讲清"这是快照,不会自动更新"
这是最容易让用户误解的一点:仪表盘是个静态 HTML,双击打开后数据永远停在生成那一刻,
看着像实时监控,其实不是。不主动讲清,用户会以为"在看着实时状态",出事后才发现看到的是半小时前的。
两条改进(都要做):
1. 页面顶部加"数据时效"横幅:显示"数据截至 MM-DD HH:MM(N 分钟前)";
超过 15 分钟自动变黄底并提示刷新。让用户一眼知道快照有多旧。
2. 给一个双击就能用的入口(见下),别要求用户记命令。
一键刷新入口(给非技术用户的正确交付方式)
真正的实时告警靠服务器上的 cron + 企微推送;本地仪表盘只是"想看时才看"的视图。
所以本地入口必须做到双击即可:
① refresh_report.sh --open —— 用 explorer.exe <Windows路径> 打开,
中文文件名安全(explorer 直接收参数,不经 shell 解析)。
② refresh_dashboard.bat —— 双击即用,三步:拉取 → 生成 → 打开浏览器。
源码随附于 references/refresh-dashboard-bat.md(复制全文另存为 .bat,内容纯 ASCII)。
> ### ★★★ Windows .bat 的唯一铁律:内容必须纯 ASCII
> 中文版 Windows 的 cmd 默认代码页是 GBK(936),而 .bat 若以 UTF-8 写入,
> 里面的中文路径会变成乱码,表现为"双击没反应"或"找不到文件"。
> 对策:bat 里一个中文字都不写 —— 中文文件名通过 bash 内部处理(bash 是 UTF-8 环境),
> bat 只负责"找到 bash 并调用脚本"。开头加 chcp 65001 >nul 让 bash 的中文输出正常显示。
> bat
> @echo off
> chcp 65001 >nul
> set "BASH=C:\Program Files\Git\bin\bash.exe"
> set "SCRIPT=/c/Users/<user>/.../refresh_report.sh"
> "%BASH%" -lc "bash %SCRIPT% --open"
> if errorlevel 1 pause
>
> 校验方法:LC_ALL=C grep -cP '[^\x00-\x7F]' file.bat 必须为 0。
> 另:bat 里要多路径探测(用户自装 Git → 内置 PortableGit),避免单点失效;
> 失败时 pause 让用户看得到错误,成功时不必停留。
③ 桌面入口 —— 把 .bat 复制到桌面即可(脚本内已用绝对路径,放哪都能跑)。
> ⚠️ 不要用 PowerShell 的 WScript.Shell COM 去建 .lnk —— 会被安全策略拦下
> (COM 对象实例化可执行任意代码)。直接 cp 一个 .bat 更简单也更可靠。
Git Bash 调原生 Python 的路径坑
/c/Users/... 形式的路径原生 Python 不认,需转 Windows 形式:
DIR_WIN="$(cd "$(dirname "$0")" && pwd -W 2>/dev/null || echo "$DIR")"
DIR_WIN="${DIR_WIN//\//\\}" # 统一反斜杠:显示规范,explorer 也更稳
"$PY" "$DIR_WIN/build_report.py"
九、顺手要补的运维缺口(有监控也要治理)
发现机器"没有任何自动维护"时,除加哨兵外还应补:
# 1) 应用日志无轮转 → 定期清理(按应用写入格式调整)
cat > /etc/cron.d/app-log-clean <<'EOF'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
0 3 * * * root find /opt/<app>/logs -maxdepth 1 -name "access-*.ndjson" -mtime +30 -delete
EOF
# 2) journald 无上限 → 设限额(老机器上实测能涨到 190M+)
mkdir -p /etc/systemd/journald.conf.d
cat > /etc/systemd/journald.conf.d/limits.conf <<'EOF'
[Journal]
SystemMaxUse=300M
MaxRetentionSec=30day
EOF
systemctl restart systemd-journald
> 注意 /opt/<app>/logs/access-YYYY-MM-DD.ndjson 这类按天命名的日志,
> 不适合用 logrotate(会被重命名成 .1 反而乱),用 find -mtime 定期删更干净。
十、交付清单模板
| 类别 | 内容 |
|---|---|
| 交付物 | 巡检脚本、两份配置、部署脚本、仪表盘生成器、HTML 报告、一键刷新启动器(.bat 源码见 references/,另存即用,可放桌面)、真浏览器校验脚本 |
| 部署位置 | 各机 /opt/sentinel/sentinel.sh + /etc/sentinel.conf + /etc/cron.d/sentinel |
| 运行时产物 | 各机 /opt/sentinel/:state.txt state.json(含 metrics)ttfb.log res.log |
| 验证记录 | ①故障注入逐项结果(正常静默 / 异常出声 / 去重节流 / 恢复清理 / 交叉互检收敛)②仪表盘像素级校验(条长÷轨道长 ≈ 百分比) |
| 治理补建 | 日志清理 cron、journald 限额 |
| 待用户动作 | 提供群机器人 Webhook 即可推送到手机 |
汇报时要说清三件事:①现在出问题会怎么被发现(链路);
②仪表盘是快照、怎么刷新(双击哪里);③还需要他做什么才能收到手机推送(Webhook)。
若用户提"加上 CPU/配置情况",说明第 4.1 节的资源快照没做——采集与告警必须解耦。
十一、常见问题(FAQ)
Q1|只有一台机器,能用吗?
能。/etc/sentinel.conf 里 PEER_SSH="" 留空即单机模式,其余功能(巡检/告警/自愈/仪表盘)全部照常,交叉互检自动跳过。
Q2|换服务器 / 换环境要改哪些地方?
就 2 处:①命令行传新 IP(deploy / pull_states 都支持传参);②/etc/sentinel.conf 里的业务配置。照抄版配置见 references/example-configs.md。先跑 check 自检再部署。
Q3|收不到手机推送,怎么排查?
按顺序三查:①/etc/sentinel.conf 的 WECOM_WEBHOOK 填了没有;②群机器人还在不在群里(被移除则地址失效);③本地是否产生过告警——正常时本来就完全静默,先手动造一个异常(如临时停个服务)验证链路。
Q4|cron 没跑怎么查?
grep sentinel /etc/cron.d/sentinel # 条目在不在
systemctl status cron 2>/dev/null || systemctl status crond # 服务起没起
/opt/sentinel/sentinel.sh && cat /opt/sentinel/state.txt # 手动跑一遍看结果
Q5|跨机互检报"不可达",是什么原因?
看告警括号里的原因:rc=255 是 SSH 连接层(网络不通 / 认证被拒 / 密钥不对 / 主机指纹变更);可达但状态为空 是对端哨兵没装或没跑。bash deploy_sentinel.sh check 能逐项定位。
Q6|Webhook 外联会泄露数据吗?
不会多发一个字节:只在异常时把告警文本 POST 到你自己填写的群机器人地址,无遥测、无第三方上报;WECOM_WEBHOOK 留空则完全不出网。
Q7|关键词没触发技能怎么办?
显式调用:直接说「用服务器健康哨兵巡检 <主机/域名>」。
Q8|怎么停用 / 卸载?
rm /etc/cron.d/sentinel # ① 立即停用(巡检不再跑)
rm -rf /opt/sentinel /etc/sentinel.conf # ② 卸载文件
rm /root/.ssh/sentinel_ed25519* # ③ 删互检密钥,并到对端 authorized_keys 删对应行
Q9|误报太多 / 太吵怎么调?
调 /etc/sentinel.conf 阈值:DISK_THRESHOLD、TTFB_ALERT_MS、MEM_AVAIL_MIN_MB、CERT_WARN_DAYS;REALERT_EVERY 控制同一问题重推间隔(6 = 30 分钟)。
Q10|仪表盘数据为什么不更新?
它是生成时刻的快照,不会自动刷新。Windows 双击 refresh_dashboard.bat(源码见 references/refresh-dashboard-bat.md)或跑 scripts/refresh_report.sh 重新生成。
[2026-10-06] ★★ 应用日志目录过阈值(WARN「日志目录已达 xxxMB」)的标准处置
判别:生产机哨兵 WARN 日志目录 /opt/myapp/logs 已达 520MB(阈值 500MB)。
磁盘通常还很空(本例 49G 用 22%)⟹ 是"该轮转了",不是告急,不用慌。
已落地的轮转设施(生产机 192.0.2.11):
- 脚本:/root/log_rotate.sh [days=2] —— N 天前的 access-*.ndjson 逐个
gzip -c > archive/xx.gz → gzip -t 校验通过才删原文件(失败自动保留原文件);归档保留 90 天。
- cron:30 3 * * *(保留今天+昨天,稳态 ≈ 50~80MB)。
- journald 已限 200M(/etc/systemd/journald.conf.d/size.conf)。
- 归档在 /opt/myapp/logs/archive/,没删数据,zcat/gunzip 随时可查。
其他机器遇到同样 WARN:把 /root/log_rotate.sh 抄过去改 LOGDIR 即可(脚本幂等:.gz 已存在即跳过)。
★ 踩坑:paramiko 长任务超时 ≠ 任务失败
exec_command(cmd, timeout=300) 压 124 个文件(476MB)超了通道超时——
SSH 通道断开,但远端 bash 继续跑完了。处理原则:
1. 超时后先回去查现场(残留 .tmp 数、已完成 .gz 数、pgrep 进程),再决定是否重跑;
2. 重跑也安全(脚本幂等),但别盲目重发覆盖;
3. 更长任务用 nohup cmd > log 2>&1 & 挂后台,再轮询结果。