14 10 月 2026

运维的终极形态是「不用运维」——备份、巡检、清理、告警全部自动跑,人只在异常时介入。这篇给一套拿来即用的脚本集,覆盖日常运维的高频场景,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
done
crontab -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
fi
crontab -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

八、脚本管理规范

  1. 全进 Git:/opt/scripts 建个 repo,改动有历史,换机器直接 clone
  2. 密钥别进 Git:用环境变量或独立配置文件,.gitignore 排除
  3. 每个脚本顶部注释:干什么用、谁写的、cron 周期、依赖什么
  4. 失败必须告警:set -e + alert,静默失败是运维事故的源头
  5. 先 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 机型省一半以上。

延伸阅读
  1. 网站环境怎么选:LNMP 手动 / 宝塔面板 / Docker / 云开发
  2. 问卷表单系统搭建:SurveyKing / Formbricks,数据自己收
  3. 图床搭建:Lsky Pro / Lychee,自托管图片托管方案
  4. 邮件列表搭建:listmonk 自托管 Newsletter,把读者握在自己手里
  5. 文档站点搭建:MkDocs / VuePress / Docsify,产品文档与技术手册方案
  6. RSS 阅读器搭建:FreshRSS 自托管,把信息流拿回自己手里
  7. 短链接服务搭建:Shlink 自托管,链接数据自己掌握
  8. 网站状态监控页搭建:Uptime Kuma,服务器挂了第一时间知道
更多阅读
  1. 上一篇:tcpdump 抓包实战:从入门到定位网络问题
  2. 下一篇:MySQL 主从复制配置实战:读写分离与故障切换
发表评论 2

取消回复