一台服务器,手工运维尚能应付;十台以上,靠人肉登录执行就会开始出错——漏一台、敲错一个参数、半夜三点爬起来重启服务。自动化运维的价值不只是"省事",更在于把操作标准化、可复用、可审计。对中小团队而言,完整的自动化不必一上来就上 Kubernetes 和 Ansible,从"写好 Shell 脚本 + 配好定时任务"起步,就能覆盖备份、巡检、清理、部署、告警这五大高频场景。本文给出脚本编写的工程化模板、四个可直接落地的实战脚本,以及定时任务与 systemd timer 的选型建议。
一、先定规范:脚本该长什么样
运维脚本最容易腐烂,原因是"能跑就行"。建议统一到一套最小规范,成本很低但收益巨大。
#!/bin/bash
#==============================================================
# 名称:mysql_backup.sh
# 功能:数据库全量备份并清理过期归档
# 作者:ops-team
# 版本:1.2
# 更新:2024-06-15
#==============================================================
set -euo pipefail # 出错即退出、未定义变量报错、管道错误可捕获
IFS=$'\n\t' # 收紧默认分隔符
# ---- 可配置区(与逻辑分离,便于复用)----
BACKUP_DIR="/data/backup/mysql"
RETENTION_DAYS=14
LOG_FILE="/var/log/ops/mysql_backup.log"
MYSQL_USER="backup"
MYSQL_PASS_FILE="/etc/ops/mysql_backup.cnf"
# ---- 通用函数 ----
log() {
echo "[$(date '+%F %T')] [$1] $2" | tee -a "$LOG_FILE"
}
die() {
log "ERROR" "$1"
exit 1
}
cleanup() {
local rc=$?
[ $rc -ne 0 ] && log "ERROR" "脚本异常退出,退出码 $rc,行号 $LINENO"
exit $rc
}
trap cleanup EXIT
# ---- 前置检查 ----
[ "$(id -u)" -eq 0 ] || die "必须以 root 执行"
mkdir -p "$BACKUP_DIR" "$LOG_FILE" 2>/dev/null || true
mkdir -p "$(dirname "$LOG_FILE")"
command -v mysqldump >/dev/null || die "未找到 mysqldump"
# ---- 业务逻辑 ----
log "INFO" "开始备份"
DATE=$(date '+%Y%m%d_%H%M%S')
TARGET="$BACKUP_DIR/full_${DATE}.sql.gz"
mysqldump --defaults-extra-file="$MYSQL_PASS_FILE" \
--single-transaction --quick --routines --triggers \
--all-databases | gzip > "$TARGET" || die "备份失败"
SIZE=$(du -h "$TARGET" | awk '{print $1}')
log "INFO" "备份完成:$TARGET($SIZE)"
# ---- 清理过期归档 ----
find "$BACKUP_DIR" -type f -name "full_*.sql.gz" -mtime +$RETENTION_DAYS -print -delete | \
while read -r f; do log "INFO" "已删除过期归档:$f"; done
log "INFO" "全部完成"
模板里的关键点逐个说明:
- set -euo pipefail:三合一的安全开关。
-e让任何命令非零退出立即终止脚本(避免"备份失败却继续删除旧文件"的惨剧);-u让引用未定义变量报错;-o pipefail让管道中任一环节失败都能被捕获(否则cmd1 | cmd2只看 cmd2 的退出码)。 - trap cleanup EXIT:无论正常还是异常退出,都会执行收尾逻辑,便于记录失败状态、清理临时文件、发送告警。
- 配置与逻辑分离:所有可变项集中在顶部,改环境时只改这一块。
- 日志函数统一格式:时间戳 + 级别 + 消息,方便后续用 grep 或日志系统做关键字告警。
- 前置检查:权限、依赖命令、目录存在性,先检查再干活,失败时给出人能看懂的原因。
# 语法与常见错误静态检查(强烈建议纳入流程) yum install -y ShellCheck # 或 apt install -y shellcheck shellcheck /opt/scripts/mysql_backup.sh # 语法自检(不执行) bash -n /opt/scripts/mysql_backup.sh # 调试模式执行 bash -x /opt/scripts/mysql_backup.sh
二、四个可直接落地的实战脚本
2.1 系统巡检脚本(每日体检)
#!/bin/bash
# health_check.sh:采集关键指标,异常项输出 WARN
set -uo pipefail
REPORT="/var/log/ops/health_$(date +%F).txt"
mkdir -p "$(dirname "$REPORT")"
{
echo "===== 巡检报告 $(date '+%F %T') ====="
echo "主机:$(hostname) 内核:$(uname -r)"
echo "运行时长:$(uptime -p)"
echo -e "\n--- 负载 ---"
uptime
echo -e "\n--- 内存 ---"
free -h
echo -e "\n--- 磁盘 ---"
df -hT -x tmpfs -x devtmpfs
echo -e "\n--- 异常判定 ---"
LOAD=$(awk '{print $1}' /proc/loadavg)
CORES=$(nproc)
awk -v l="$LOAD" -v c="$CORES" 'BEGIN{if (l>c) print "WARN: 1分钟负载 " l " 超过CPU核数 " c; else print "OK: 负载正常"}'
df -hP -x tmpfs -x devtmpfs | awk 'NR>1{gsub("%","",$5); if ($5>80) print "WARN: " $6 " 使用率 " $5 "%"}'
free -m | awk 'NR==2{if ($3/$2*100>85) printf "WARN: 内存使用率 %.1f%%\n", $3/$2*100}'
grep -iE 'error|fail' /var/log/messages 2>/dev/null | tail -5
} > "$REPORT"
cat "$REPORT"
2.2 服务自愈脚本(挂了自动拉起)
#!/bin/bash
# service_guard.sh:检测服务状态,异常则重启并记录
set -uo pipefail
SERVICES="nginx php-fpm mysql"
LOG="/var/log/ops/guard.log"
MAX_RETRY=2
for svc in $SERVICES; do
for ((i=1; i<=MAX_RETRY; i++)); do
if systemctl is-active --quiet "$svc"; then
break
fi
echo "[$(date '+%F %T')] $svc 未运行,第 $i 次尝试启动" >> "$LOG"
systemctl start "$svc"
sleep 5
done
systemctl is-active --quiet "$svc" \
&& echo "[$(date '+%F %T')] $svc 已恢复" >> "$LOG" \
|| echo "[$(date '+%F %T')] CRITICAL: $svc 重启失败,需人工介入" >> "$LOG"
done
注意:自愈脚本要限制重试次数并留下告警出口,否则服务持续崩溃时会变成无限重启风暴,反而掩盖了真正的故障原因。同时建议加"冷却期"逻辑(如 10 分钟内最多重启 3 次)。
2.3 磁盘清理脚本(带安全阈值)
#!/bin/bash
# disk_clean.sh:超过阈值才清理,且只碰已知安全目录
set -euo pipefail
THRESHOLD=85
PART="/"
SAFE_DIRS="/var/log /tmp /data/backup"
LOG="/var/log/ops/clean.log"
usage=$(df -hP "$PART" | awk 'NR==2{gsub("%","",$5); print $5}')
echo "[$(date '+%F %T')] 当前使用率 ${usage}%" >> "$LOG"
if [ "$usage" -lt "$THRESHOLD" ]; then
echo "使用率未达阈值 $THRESHOLD%,跳过清理" >> "$LOG"
exit 0
fi
# 1. 清理 systemd 日志
journalctl --vacuum-size=500M >> "$LOG" 2>&1 || true
# 2. 清理包缓存
yum clean all >> "$LOG" 2>&1 || apt-get clean >> "$LOG" 2>&1 || true
# 3. 删除 30 天前的归档与临时文件
find /data/backup -type f -name "*.gz" -mtime +30 -print -delete >> "$LOG" 2>&1 || true
find /tmp -type f -mtime +7 -print -delete >> "$LOG" 2>&1 || true
new_usage=$(df -hP "$PART" | awk 'NR==2{gsub("%","",$5); print $5}')
echo "[$(date '+%F %T')] 清理后使用率 ${new_usage}%" >> "$LOG"
2.4 批量分发与执行(多机场景)
#!/bin/bash
# batch_exec.sh:在多台机器上执行同一命令
set -uo pipefail
HOSTS_FILE="/opt/scripts/hosts.txt"
SSH_OPTS="-o ConnectTimeout=5 -o StrictHostKeyChecking=no -o BatchMode=yes"
CMD="${1:-uptime}"
while IFS= read -r host; do
[ -z "$host" ] && continue
echo "===== $host ====="
ssh $SSH_OPTS "root@$host" "$CMD" || echo "FAILED: $host"
done < "$HOSTS_FILE"
批量脚本的两个纪律:先单机验证再批量、失败要能定位到具体机器并继续跑完其余机器(不要在首次失败就退出)。规模超过 20 台后,建议迁移到 Ansible 这类专业工具。
三、调度:crontab 还是 systemd timer
| 对比项 | crontab | systemd timer |
|---|---|---|
| 最小粒度 | 分钟 | 秒(AccuracySec) |
| 错过补执行 | 不支持(关机时错过就错过) | 支持 Persistent=true,开机后补跑 |
| 环境控制 | 需手动设 PATH、SHELL | 可在 unit 里指定 Environment、User、WorkingDirectory |
| 日志 | 需自己重定向 | 自动进 journald,可用 journalctl -u 查看 |
| 依赖管理 | 不支持 | 可声明 After/Requires(如等网络就绪) |
| 资源管理 | 无 | 可设 CPUQuota、MemoryMax、Nice |
| 上手成本 | 极低 | 中等,需写两个 unit 文件 |
| 推荐场景 | 简单周期任务、老系统兼容 | 关键备份、需补执行、需资源限制的任务 |
# systemd timer 示例:每天 2:10 执行备份 # /etc/systemd/system/mysql-backup.service [Unit] Description=MySQL Backup After=network.target [Service] Type=oneshot User=root WorkingDirectory=/opt/scripts ExecStart=/opt/scripts/mysql_backup.sh # /etc/systemd/system/mysql-backup.timer [Unit] Description=Run MySQL Backup daily [Timer] OnCalendar=*-*-* 02:10:00 Persistent=true RandomizedDelaySec=120 [Install] WantedBy=timers.target
# 启用与查看 systemctl daemon-reload systemctl enable --now mysql-backup.timer systemctl list-timers | grep backup journalctl -u mysql-backup.service -n 50
选型建议:日常小任务继续用 crontab,简单可靠;关键备份类任务改用 systemd timer,因为 Persistent=true 能保证"机器在 2 点关机了,开机后补跑一次",这对备份完整性非常重要,而 crontab 做不到。RandomizedDelaySec 还能避免几十台机器在同一秒集中开跑造成流量尖峰。
四、让自动化真正可靠的五个原则
- 幂等性:脚本执行多次的结果应与执行一次相同。例如创建目录用
mkdir -p,写配置前先判断,加锁防止并发。flock -n /tmp/xxx.lock -c '/opt/scripts/job.sh'是最简单的并发保护。 - 可观测:每个脚本都要写日志(时间+级别+结果),关键任务要有成功/失败的可判定信号。建议让脚本在成功时写标记文件或输出固定关键字,便于监控匹配。
- 失败要报警:无人值守的任务失败却没人知道,比不做自动化更危险。可用
curl回调健康检查地址、调用企业微信/钉钉机器人 webhook,或由监控系统扫描日志关键字。 - 退出码语义明确:0 成功、1 通用错误、2 用法错误。调用方依赖退出码判断,脚本里不要用
exit 0掩盖失败,也不要在管道末尾误吞退出码。 - 版本管理:所有脚本纳入 Git,crontab 也导出到仓库(
crontab -l > ops/crontab.$(hostname))。改脚本走提交与评审,避免线上直接 vi。
# 告警通知示例(企业微信/钉钉机器人)
notify() {
local msg="$1"
curl -s -X POST "$WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"[$(hostname)] $msg\"}}" \
>/dev/null || echo "告警发送失败" >> "$LOG"
}
# 使用
mysqldump ... || { notify "数据库备份失败,请立即检查"; exit 1; }
五、实施步骤清单
- 梳理高频操作:把近三个月重复做过三次以上的操作列出来(备份、清理、重启、巡检、发布),作为自动化的第一批目标。
- 建立脚本目录与规范:统一放 /opt/scripts,含 bin(可执行)、conf(配置)、log(日志)子目录;套用本文开头的模板。
- 编写并静态检查:写完跑
shellcheck与bash -n,修复所有告警。 - 手动试跑:在测试机或低峰期手动执行,检查日志、产出物与退出码。
- 模拟 cron 环境验证:
env -i PATH=/usr/bin:/bin /bin/bash /opt/scripts/xxx.sh,提前暴露环境变量问题。 - 接入调度:crontab 或 systemd timer,输出重定向到日志,长任务加 flock。
- 配置告警与复核:关键任务加失败通知;上线后连续三天人工复核日志,确认稳定。
- 纳入版本库与文档:脚本 + crontab 导出文件 + 一份 README(用途、执行时间、负责人、恢复方法)。
- 定期演练:备份类脚本每季度做一次恢复演练,确认备份真的可用——没验证过的备份等于没有备份。
小结
自动化运维不是"把命令堆进脚本",而是把运维动作工程化:规范化的脚本模板(set -euo pipefail + trap + 日志函数 + 前置检查)、明确的调度选择(简单任务 crontab,关键任务 systemd timer)、以及幂等、可观测、失败告警、版本管理这四条保障。从小处做起——先把最烦人、最容易出错的三件事脚本化,你会立刻感受到差别;等脚本数量和管理规模增长后,再平滑迁移到 Ansible、Prometheus 这类平台化工具,水到渠成。
避坑清单
- 不要用 set -e 之后又随意加 || true,会让错误被静默吞掉;只在明确可以容忍失败的地方使用,并写注释说明。
- 脚本里避免 rm -rf 变量路径:如果变量为空,
rm -rf "$DIR"/*可能变成rm -rf /*。务必先判空并校验路径合法性。 - 密码不要写进脚本,用配置文件(权限 600)或 --defaults-extra-file,脚本本身可能进 Git。
- 自愈脚本必须限流,否则持续崩溃时会形成重启风暴并掩盖真实原因。
- cron 环境下的 PATH 问题必现,脚本内所有命令写绝对路径,或在脚本开头显式 export PATH。
- 备份脚本必须配套清理策略,只备份不清理同样会把磁盘写满。
- 不要在脚本里用交互式命令(如需要确认的 rm -i、需要输密码的 sudo),自动化场景下它们会永久挂起;sudo 配 NOPASSWD 并用
sudo -n。 - 定时任务要与业务低峰对齐,并注意时区(
timedatectl查看),改时区后重启调度服务。
企业QQ咨询




