服务器运维健康体检
什么时候用 / 不用
用:接手一台跑着服务的机器;要判断"它稳不稳、安全吗";部署前的注意事项;服务刚出过故障想找同类隐患;定期复查。
不用:只看应用代码质量(那走 launch-risk-audit);只看 HTTP 是否 200。
铁律
- 全程只读。体检阶段不改任何配置、不重启任何容器。产出建议等拍板。
- 每条结论必须带证据(命令 + 原始输出),禁止"我觉得应该是"。
- 先备份再谈改造;改造按停机面分批,不按重要性分批。
- "零备份"永远是 P0。任何字段/配置的容错都不如数据本身。
- 对方说"这是设计如此"时,先问它被怎么用——很多缺陷不是实现错,是语义被误用(见下「两个通用陷阱」)。
检查清单(12 项 · 命令已验证)
远程执行建议走 base64 通道,避免多层引号转义:
# 连接信息从环境变量读取(技能包不含任何主机/密钥,按你的环境填写)
export PROD_SSH_HOST=your-server.example.com # 必填:目标主机
export PROD_SSH_USER=deploy # 可选:登录用户,缺省取当前用户
# export PROD_SSH_PORT=22 # 可选:端口,缺省 22
# export PROD_SSH_KEY=~/.ssh/id_ed25519 # 可选:私钥路径
B64=$(base64 -w0 audit.sh) && node scripts/prod-ssh.cjs "printf '%s' '$B64' | base64 -d | bash"
> scripts/prod-ssh.cjs 是包内自带工具(Node.js,零依赖):把上一条命令经 ssh 发往 PROD_SSH_HOST 执行,支持 StrictHostKeyChecking=accept-new 与 -i 指定密钥。若你已有自己的 SSH 封装,直接替换这步即可。
> 若远程脚本输出较长,写文件再读(.workbuddy/_ssh_out.txt 之类);某些环境下 stdout 不回传。
1. 容器与重启策略
docker ps -a --format '{{.Names}}|{{.Image}}|{{.Status}}'
docker inspect <name> --format 'Restart={{.HostConfig.RestartPolicy.Name}} RestartCount={{.RestartCount}} Health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}} OOM={{.State.OOMKilled}} MemLimit={{.HostConfig.Memory}}'
看:Health=none(无健康检查)、RestartCount 大(在反复崩)、OOM=true、Restart 策略是否 unless-stopped/always。
2. 数据备份(最高优先)
docker volume ls | grep -i <proj>
for v in $(docker volume ls -q | grep -i <proj>); do mp=$(docker volume inspect "$v" --format '{{.Mountpoint}}'); echo "$v $mp $(du -sh "$mp"|cut -f1)"; done
ls -la <app>/backups/
crontab -l
grep -l -i 'dump\|backup' <app>/scripts/* 2>/dev/null || echo NO_BACKUP_SCRIPT
判定:backups/ 里若只有「配置 tar」而无数据 dump → 视为零备份(配置能重写,数据不能)。
量化"丢了会丢什么"(说服力最强,也是给决策者的依据):
docker exec <mongo> mongosh --quiet --eval 'const s=db.getSiblingDB("x").stats(); print("dataSize="+(s.dataSize/1048576).toFixed(2)+"MB collections="+s.collections+" objects="+s.objects)'
docker exec <pg> psql -U u -d d -c "select relname, n_live_tup from pg_stat_user_tables order by n_live_tup desc limit 15"
注意:卷占用(如 396M)常是引擎预分配,远大于真实数据(如 0.42MB)。用真实数据量算备份成本,结论会完全不同——"一天一份、一年不到 200MB"比"没有备份"有力得多。
必须有恢复演练:dump 出来不验证 = 没备份。灌进临时实例数一遍集合/表。
3. 健康检查语义(易被忽视,见陷阱一)
grep -rn "health\|ready\|/ping" <app>/src/*.js <app>/src/**/*.js | head
判定:端点只返时间戳/常量 → 是 liveness(进程活着),不是 readiness(能服务)。若它被监控或部署脚本当成就绪信号用 → P0。
正确修法:保留原端点语义(兼容既有探针),新增一个探库/探依赖的端点(如 /ready),配 healthcheck。
验收必须做负向测试:停掉依赖(docker stop <db>)→ /health 仍 200(符合设计)、/ready 必须 503、容器状态转 unhealthy。
4. 目录与文件权限(提权通道)
find <app> -maxdepth 2 -perm -o+w -not -path '*/node_modules/*'
判定:目录 777 比文件 666 更危险——目录可写意味着可以删掉再替换其中的脚本。若该脚本被 root cron 执行 → 完整的 root 提权路径,P0。
修:chmod 755 <dir> + chmod 644 <dir>/*(可执行脚本保 755)。
5. 密钥与敏感文件
ls -la <app>/.env*
sed -E 's/=.*/=***/' <app>/.env | grep -v '^$' # 只看键名,绝不打印值
判定:.env 非 600 → P0(同机任意进程可读;未来容器更甚)。支付/提现/JWT 类密钥泄露可直接伪造业务。
另查旧版残留(.env.bak-*)——轮换时别只换主文件。
6. 日志有无上限
grep -n -A4 'logging' docker-compose*.yml || echo NO_LOGGING
cat /etc/docker/daemon.json 2>/dev/null
for f in /var/lib/docker/containers/*/*-json.log; do echo "$(du -h "$f"|cut -f1)|$f"; done | sort -rh | head
判定:compose 无 logging → 容器日志无上限,迟早吃满磁盘。
7. 定时任务
crontab -l; ls -la /etc/cron.d/; systemctl list-timers --all | head
ls -la <app>/logs/
看:任务在不在(0 字节的日志文件说明重定向失效或脚本自写他处)、日志有无轮转、脚本是否静默失败。
8. 端口与网络暴露
grep -n -A3 'ports:' docker-compose*.yml || echo NO_PORTS_MAPPED
docker exec <c> sh -c 'netstat -ltnp 2>/dev/null || ss -ltnp'
docker network inspect <net> --format '{{.Name}} labels={{.Labels}} containers={{range .Containers}}{{.Name}} {{end}}'
合格:只监听容器内、无 ports: 映射;数据库走 internal: true 网络。
labels={} = 该网络归宿主所有(无 compose 标签),任何一方 down 都删不掉——这是"生命周期隔离"的判据。
9. 数据库认证
docker inspect <db> --format 'Cmd={{.Config.Cmd}} Entrypoint={{.Config.Entrypoint}}'
grep -c 'URI=.*@' <app>/.env # 有无凭据
无认证 + internal: true + 无端口映射 = 可接受;无认证 + 有端口映射 = P0。
10. 证书到期(共享资产常被漏)
for c in <dir>/*.crt <dir>/*.pem; do [ -f "$c" ] && echo "$c $(openssl x509 -in "$c" -noout -enddate)"; done
若多项目共用一份证书 → 到期时一起挂。算剩余天数,进台账 + 到期前 30 天提醒。
11. 资源余量
df -h /; free -m; docker stats --no-stream --format '{{.Name}}|{{.MemUsage}}|{{.MemPerc}}'
算:各容器 limit 之和 vs 物理内存。构建期并发(多镜像 --no-cache)是最容易 OOM 的时刻 → COMPOSE_PARALLEL_LIMIT=1。
12. 监控告警 + daemon 配置
systemctl list-units --type=service --state=running | grep -i -E 'exporter|prometheus|grafana|zabbix|netdata' || echo NO_MONITORING
grep -n 'live-restore' /etc/docker/daemon.json || echo NO_LIVE_RESTORE
最轻的可用性探针:宿主 cron 每分钟探一次就绪端点,失败写日志 + 推 IM 机器人。
live-restore 未开 → daemon 重启会连坐所有容器;但首次开启需重启 dockerd,会中断服务,必须排窗口。
两个通用陷阱(跨项目复用,务必先验证)
陷阱一:健康检查被误用为就绪检查。
端点是 liveness(只探进程)却被当 readiness(判"能服务")用——这不算实现 bug,是语义错配。看到类似注释「不依赖任何外部服务,部署探针用」就要意识到作者是有意的。修法是补端点,不是改原端点。
陷阱二:业务错误编码在 body,不在 HTTP 状态码。
云函数迁移过来的服务常保留"永远返 200、错误写在 body.ok/code"的口径(兼容旧客户端)。后果:任何基于状态码的监控都会把"登录失败/支付失败"算成健康。
→ 扫描:grep -n "res.status(status || 200)\|res.status(status||200)" <src>。
→ 监控/看门狗必须解析 body.ok。
→ 反过来这也有用:走该前缀拿到的 JSON 一定是本服务自己的(而非上层框架兜底页),可作为"请求是否真到达本服务"的判据。
报告结构模板
0 一句话结论(最重的那条风险 + 一个关键澄清)
1 盘点事实表(项 / 实测值 / 判定) ← 全部带数值,无形容词
2 风险清单
P0 必须做(成本极低、后果最重):每条 = 证据 / 影响(量化)/ 动作(可复制代码)/ 验收(含负向测试)/ 成本
P1 应该做(表格:项 / 证据 / 建议 / 影响面)
P2 可以等
3 落地顺序(按停机面分批:零停机 → 一次重启 → 需重启 daemon)
4 部署注意事项(增补到既有的运维告示里)
5 分工与待拍板(谁能做 / 必须对方做 / 需要决策者定的)
6 本次声明(只读,未改任何配置)
分批原则:A 批 = 零停机(改权限位、改 .env 模式);B 批 = 重启应用容器一次;C 批 = 宿主层零停机(cron/日志轮转);D 批 = 需重启 daemon 或会同时影响多个服务 → 单独排窗口。
收尾(必做)
- 报告落
docs/YYYY-MM-DD_<服务>稳定性与安全优化建议.md;登记文档索引。 - 若机器上有常驻运维告示(如
<app>/DEPLOY-NOTICE.md),把可执行部分增补进去——那是后续操作者唯一能看到的位置。 - 服务器操作台账记一行(含只读盘点),标"影响面:无"。
- 记忆只留判据级内容("不读下次会做错"的),不搬事实清单。例:「X 服务的
/health只探进程,验收要用/ready或真实业务端点」。