运维的终极形态是「不用运维」——备份、巡检、清理、告警全部自动跑,人只在异常时介入。这篇给一套拿来即用的脚本集,覆盖日常运维的高频场景,Shell 为主、Python 为辅。
需要云服务器可以看看财富云计算:一键开服面板,NAT 型无独立公网 IP,价格比独立 IP 机型省一半以上,主流游戏快速部署。
一、脚本目录规划
/opt/scripts/
├── backup/ # 备份类
│ ├── mysql-backup.sh
│ ├── files-backup.sh
│ └── upload-cos.sh
├── monitor/ # 监控巡检类
│ ├── inspect.sh
│ ├── disk-alert.sh
│ └── service-watch.sh
├── maintain/ # 维护类
│ ├── log-clean.sh
│ ├── docker-prune.sh
│ └── cert-renew.sh
├── lib/ # 公共函数
│ ├── common.sh # 日志/告警/锁
│ └── notify.py # 多通道通知
└── README.md # 每个脚本干什么,一行注释原则:一个脚本一件事、放 Git 管理、README 说明用途和 cron 周期。
二、公共库:所有脚本的地基
#!/bin/bash
# lib/common.sh —— 所有脚本 source 它
# 日志: 同时输出到终端和文件
LOG_DIR="/var/log/scripts"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/$(basename "$0" .sh)-$(date +%Y%m).log"
log() {
local level="$1"; shift
echo "[$(date '+%F %T')] [$level] $*" | tee -a "$LOG_FILE"
}
info() { log INFO "$@"; }
warn() { log WARN "$@"; }
error() { log ERROR "$@"; }
# 告警: 企业微信 Webhook(按需改)
WEBHOOK="${ALERT_WEBHOOK:-}"
alert() {
local title="$1" content="$2"
error "$title: $content"
[ -n "$WEBHOOK" ] && curl -s "$WEBHOOK" -H 'Content-Type: application/json' -d "{
\"msgtype\": \"text\",
\"text\": {\"content\": \"【$(hostname)】$title\n$content\"}
}" > /dev/null 2>&1
return 0
}
# 防重复运行(脚本还没跑完,cron 又触发)
LOCK_FILE="/tmp/$(basename "$0").lock"
acquire_lock() {
if [ -f "$LOCK_FILE" ] && kill -0 "$(cat $LOCK_FILE)" 2>/dev/null; then
warn "另一个实例正在运行(PID $(cat $LOCK_FILE)),退出"
exit 0
fi
echo $$ > "$LOCK_FILE"
trap 'rm -f "$LOCK_FILE"' EXIT
}
# 失败即退出(严谨模式)
set -euo pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin用法:
#!/bin/bash
source /opt/scripts/lib/common.sh
acquire_lock
info "开始执行"
# ... 业务逻辑
info "执行完成"三、备份类脚本
MySQL 备份 + 外传 + 校验
#!/bin/bash
# backup/mysql-backup.sh —— 每日 MySQL 备份
source /opt/scripts/lib/common.sh
acquire_lock
BACKUP_DIR="/backup/mysql"
KEEP_DAYS=14
DATE=$(date +%Y%m%d-%H%M)
DB_PASS_FILE="/root/.my.cnf" # 凭证放文件,别放命令行
mkdir -p "$BACKUP_DIR"
info "开始备份 MySQL"
# 备份(凭证从文件读,ps 看不到密码)
if ! mysqldump --single-transaction --quick --routines --triggers \
--all-databases 2>>"$LOG_FILE" | gzip > "$BACKUP_DIR/full-$DATE.sql.gz"; then
alert "MySQL 备份失败" "请检查 $LOG_FILE"
exit 1
fi
SIZE=$(du -h "$BACKUP_DIR/full-$DATE.sql.gz" | cut -f1)
info "备份完成: full-$DATE.sql.gz ($SIZE)"
# 校验
gzip -t "$BACKUP_DIR/full-$DATE.sql.gz" || { alert "备份文件损坏" "full-$DATE.sql.gz"; exit 1; }
# 外传对象存储(按需启用)
# /opt/scripts/backup/upload-cos.sh "$BACKUP_DIR/full-$DATE.sql.gz" mysql/ || alert "外传失败" ""
# 清理旧备份
find "$BACKUP_DIR" -name "full-*.sql.gz" -mtime +$KEEP_DAYS -delete
info "已清理 ${KEEP_DAYS} 天前备份"网站/游戏服文件备份(rsync 增量)
#!/bin/bash
# backup/files-backup.sh —— 增量备份(硬链接方案)
source /opt/scripts/lib/common.sh
acquire_lock
SRC="/opt/games" # 按需改
DEST="/backup/games"
DATE=$(date +%Y%m%d)
YESTERDAY="$DEST/$(date -d yesterday +%Y%m%d 2>/dev/null || date -v-1d +%Y%m%d)"
mkdir -p "$DEST"
if [ -d "$YESTERDAY" ]; then
rsync -a --delete --link-dest="$YESTERDAY" --exclude='*.tmp' \
"$SRC/" "$DEST/$DATE/" 2>>"$LOG_FILE"
else
rsync -a --delete --exclude='*.tmp' "$SRC/" "$DEST/$DATE/" 2>>"$LOG_FILE"
fi
SIZE=$(du -sh "$DEST/$DATE" | cut -f1)
info "文件备份完成: $DEST/$DATE ($SIZE)"
# 保留 14 天
ls -1t "$DEST" | grep -E '^[0-9]{8}$' | tail -n +15 | while read d; do
rm -rf "$DEST/$d"
done上传到对象存储
#!/bin/bash
# backup/upload-cos.sh —— 上传备份到腾讯 COS
# 用法: upload-cos.sh <本地文件> <远程路径>
source /opt/scripts/lib/common.sh
FILE="$1"
REMOTE_PATH="$2"
if coscmd upload "$FILE" "$REMOTE_PATH" 2>>"$LOG_FILE"; then
info "上传成功: $REMOTE_PATH"
else
alert "备份外传失败" "$FILE -> $REMOTE_PATH"
exit 1
fi四、监控巡检类
磁盘/内存/服务巡检
#!/bin/bash
# monitor/inspect.sh —— 每日巡检(核心逻辑见上一篇巡检清单)
source /opt/scripts/lib/common.sh
acquire_lock
WARNINGS="()
check_disk() {
df -h | awk 'NR>1 && $5+0 >= 90 {print $6, $5}' | while read fs pct; do
WARNINGS+=("磁盘 $fs 使用率 $pct")
done
}
check_memory() {
local avail=$(free -m | awk 'NR==2 {print $7}')
local total=$(free -m | awk 'NR==2 {print $2}')
[ $((avail * 100 / total)) -lt 20 ] && WARNINGS+=("内存可用不足 20%")
}
check_services() {
for svc in nginx mysql redis palworld; do
systemctl list-unit-files 2>/dev/null | grep -q "^$svc.service" || continue
systemctl is-active --quiet "$svc" || WARNINGS+=("服务 $svc 未运行")
done
}
check_backup() {
local latest=$(ls -1t /backup/mysql/full-*.sql.gz 2>/dev/null | head -1)
if [ -z "$latest" ]; then
WARNINGS+=("没有找到任何备份文件")
else
local age=$(( ($(date +%s) - $(stat -c %Y "$latest")) / 3600 ))
[ "$age" -gt 26 ] && WARNINGS+=("最新备份已 ${age} 小时前")
fi
}
check_ports() {
# 对比端口基线
local cur=$(ss -tuln | awk 'NR>1 {print $1, $5}' | sort)
local base=/opt/scripts/port-baseline.txt
[ -f "$base" ] || echo "$cur" > "$base"
local new=$(comm -13 <(sort "$base") <(echo "$cur"))
[ -n "$new" ] && WARNINGS+=("新监听端口: $new")
}
check_disk; check_memory; check_services; check_backup; check_ports
if [ ${#WARNINGS[@]} -gt 0 ]; then
alert "巡检发现 ${#WARNINGS[@]} 个异常" "$(printf '%s\n' "${WARNINGS[@]}")"
else
info "巡检正常"
fi服务守护:挂了自动拉起
#!/bin/bash
# monitor/service-watch.sh —— 关键进程守护(比 systemd 更通用)
source /opt/scripts/lib/common.sh
acquire_lock
# 格式: 进程名匹配模式|systemd服务名(可选)
SERVICES=(
"PalServer|palworld"
"nginx|nginx"
"mysqld|mysql"
)
for entry in "${SERVICES[@]}"; do
pattern="${entry%%|*}"
svc="${entry##*|}"
if pgrep -f "$pattern" > /dev/null; then
continue
fi
warn "进程 $pattern 不存在,尝试恢复"
# 优先用 systemd 拉起
if [ -n "$svc" ] && systemctl list-unit-files | grep -q "^$svc.service"; then
systemctl start "$svc"
sleep 3
if pgrep -f "$pattern" > /dev/null; then
info "通过 systemd 恢复 $svc 成功"
else
alert "服务恢复失败" "$svc 启动后进程仍不存在,请人工介入"
fi
else
alert "进程缺失" "$pattern 不在运行且无 systemd 配置"
fi
donecrontab -e
# 每 5 分钟检查一次关键进程
*/5 * * * * /opt/scripts/monitor/service-watch.sh五、维护类脚本
日志清理
#!/bin/bash
# maintain/log-clean.sh —— 清理过期日志,防磁盘爆
source /opt/scripts/lib/common.sh
info "开始清理日志"
# 1. journald 压到 500M
journalctl --vacuum-size=500M 2>>"$LOG_FILE"
# 2. 应用日志 30 天前删除
find /var/log -name "*.log" -mtime +30 -delete 2>/dev/null
find /var/log -name "*.gz" -mtime +30 -delete 2>/dev/null
# 3. nginx 日志按日期切割(如果 logrotate 没配)
# (logrotate 是首选,这里是兜底)
# 4. Docker 日志
docker system prune -f --filter "until=168h" >> "$LOG_FILE" 2>&1
AFTER=$(df / | awk 'NR==2 {print $5}')
info "清理完成,当前磁盘使用率 $AFTER"证书自动续期
#!/bin/bash
# maintain/cert-renew.sh —— Let's Encrypt 续期 + reload
source /opt/scripts/lib/common.sh
DOMAIN="你的域名"
if certbot renew --quiet 2>>"$LOG_FILE"; then
# 续期成功,重载 Nginx 让新证书生效
nginx -t 2>>"$LOG_FILE" && systemctl reload nginx
info "证书续期完成,Nginx 已重载"
else
# 没到期也会返回 0,真失败才报警
if ! openssl x509 -in "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" \
-noout -checkend $((30*86400)) 2>/dev/null; then
alert "证书 30 天内过期且续期失败" "手工检查 certbot"
fi
ficrontab -e
# 每天凌晨检查续期(certbot 自己会判断是否该续)
0 3 * * * /opt/scripts/maintain/cert-renew.sh六、Python 辅助脚本
多通道通知
#!/usr/bin/env python3
# lib/notify.py —— 统一告警通知(企业微信/钉钉/Server酱/TG)
import os, sys, json, requests
def send_wecom(title, content, webhook=None):
"""企业微信机器人"""
webhook = webhook or os.environ.get("WECOM_WEBHOOK")
if not webhook: return
requests.post(webhook, json={
"msgtype": "text",
"text": {"content": f"【{title}】\n{content}"}
}, timeout=10)
def send_dingtalk(title, content, webhook=None):
"""钉钉机器人"""
webhook = webhook or os.environ.get("DINGTALK_WEBHOOK")
if not webhook: return
requests.post(webhook, json={
"msgtype": "text",
"text": {"content": f"【{title}】\n{content}"}
}, timeout=10)
def send_telegram(title, content):
"""Telegram Bot"""
token = os.environ.get("TG_BOT_TOKEN")
chat_id = os.environ.get("TG_CHAT_ID")
if not token or not chat_id: return
requests.post(
f"https://api.telegram.org/bot{token}/sendMessage",
json={"chat_id": chat_id, "text": f"【{title}】\n{content}"},
timeout=10
)
if __name__ == "__main__":
title = sys.argv[1] if len(sys.argv) > 1 else "告警"
content = sys.argv[2] if len(sys.argv) > 2 else sys.stdin.read()
send_wecom(title, content)
send_dingtalk(title, content)
send_telegram(title, content)Shell 里调用:
echo "磁盘使用率 92%" | python3 /opt/scripts/lib/notify.py "磁盘告警"批量健康检查
#!/usr/bin/env python3
# monitor/health-check.py —— 批量检查网站/接口可用性
import requests, sys, time
TARGETS = [
("https://z0w.cn", 200),
("https://z0w.cn/wiki/", 200),
# 按需加
]
def check():
bad = []
for url, expect in TARGETS:
try:
start = time.time()
r = requests.get(url, timeout=10)
elapsed = (time.time() - start) * 1000
status = "OK" if r.status_code == expect else f"状态码异常 {r.status_code}"
print(f"{url} -> {status} ({elapsed:.0f}ms)")
if r.status_code != expect:
bad.append(f"{url}: {r.status_code}")
except Exception as e:
print(f"{url} -> 失败: {e}")
bad.append(f"{url}: {e}")
return bad
if __name__ == "__main__":
bad = check()
if bad:
sys.exit(1) # 退出码非 0,Shell 脚本据此告警七、crontab 总览
配好之后,crontab -l 应该是这样:
# 备份
0 2 * * * /opt/scripts/backup/mysql-backup.sh
30 2 * * * /opt/scripts/backup/files-backup.sh
0 4 * * 0 /opt/scripts/backup/upload-cos.sh /backup/mysql/latest.sql.gz mysql/
# 监控
0 9 * * * /opt/scripts/monitor/inspect.sh
*/5 * * * * /opt/scripts/monitor/service-watch.sh
*/10 * * * * /opt/scripts/monitor/health-check.py || python3 /opt/scripts/lib/notify.py "健康检查失败"
# 维护
0 3 * * * /opt/scripts/maintain/log-clean.sh
0 3 * * * /opt/scripts/maintain/cert-renew.sh八、脚本管理规范
- 全进 Git:
/opt/scripts建个 repo,改动有历史,换机器直接 clone - 密钥别进 Git:用环境变量或独立配置文件,
.gitignore排除 - 每个脚本顶部注释:干什么用、谁写的、cron 周期、依赖什么
- 失败必须告警:
set -e+alert,静默失败是运维事故的源头 - 先 dry-run 再上线:手动跑一遍,看日志确认行为符合预期,再挂 cron
# 测试脚本(不真的执行破坏性操作)
bash -x /opt/scripts/backup/mysql-backup.sh # 跟踪执行
# 看输出正常后,再 crontab -e 加上九、常见问题
Q:cron 里脚本不执行?
环境变量不同:脚本里 export PATH、用绝对路径、检查 journalctl -u cron。最常见的是脚本有 Windows 换行符(\r)——dos2unix 修一下。
Q:脚本执行结果和手动跑不一样?
cron 环境极简:没有交互式 shell 的配置。把依赖的环境变量写进脚本,别依赖 .bashrc。
Q:告警太多麻痹了?
分级:P1 电话、P2 微信、P3 日志。非紧急的进日志,别推消息。
Q:多台机器脚本怎么同步?
Ansible 批量推送 + 统一版本(见Ansible 入门)。脚本也是配置,要版本管理。
Q:Shell 还是 Python?
系统操作、文件处理、调用命令 → Shell;复杂逻辑、HTTP 请求、数据处理 → Python。两个都该会,按场景选。
小结
自动化运维的套路就三步:公共库(日志/告警/锁)→ 单功能脚本(备份/巡检/维护)→ crontab 编排。上面的脚本集可以直接用,重点是把「失败要告警」做成肌肉记忆——自动化最怕的不是不执行,是执行失败了没人知道。脚本全进 Git,配好 cron,服务器的日常就真的不用操心了。
更多运维内容见财富云计算知识库:Shell 入门、crontab、巡检清单、监控体系全系列。需要云服务器可以看看财富云计算:一键开服面板,NAT 型无独立公网 IP,价格比独立 IP 机型省一半以上。
延伸阅读
- 网站环境怎么选:LNMP 手动 / 宝塔面板 / Docker / 云开发
- 问卷表单系统搭建:SurveyKing / Formbricks,数据自己收
- 图床搭建:Lsky Pro / Lychee,自托管图片托管方案
- 邮件列表搭建:listmonk 自托管 Newsletter,把读者握在自己手里
- 文档站点搭建:MkDocs / VuePress / Docsify,产品文档与技术手册方案
- RSS 阅读器搭建:FreshRSS 自托管,把信息流拿回自己手里
- 短链接服务搭建:Shlink 自托管,链接数据自己掌握
- 网站状态监控页搭建:Uptime Kuma,服务器挂了第一时间知道

[...]uptime # load做成 cron 每日邮件报告,模板见服务器日常巡检清单与自动化。监控告警体系见Prometheus 监控体系。排查口诀:先 uptime 看 load,再 top 找 CPU,ps 看状态位,iostat 看磁盘,dmesg 看内核,crontab 查根。按顺序来,没有查不到的问题。进程异常大多有迹可循:僵尸找爹、[...]
2026-10-15 回复
[...]netsh advfirewall firewall add rule name="GameRange" dir=in action=allow protocol=UDP localport=26901-26903完整的 Windows 环境准备(远程桌面连接、运行库安装、SteamCMD 用法、开机自启、8 个常见坑)见:《Windows 云服务器开服通用前置教程》→ htt[...]
2026-10-18 回复