Linux服务器自动化运维怎么做?Shell脚本与定时任务实践

一台服务器,手工运维尚能应付;十台以上,靠人肉登录执行就会开始出错——漏一台、敲错一个参数、半夜三点爬起来重启服务。自动化运维的价值不只是"省事",更在于把操作标准化、可复用、可审计。对中小团队而言,完整的自动化不必一上来就上 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

对比项crontabsystemd 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 还能避免几十台机器在同一秒集中开跑造成流量尖峰。

四、让自动化真正可靠的五个原则

  1. 幂等性:脚本执行多次的结果应与执行一次相同。例如创建目录用 mkdir -p,写配置前先判断,加锁防止并发。flock -n /tmp/xxx.lock -c '/opt/scripts/job.sh' 是最简单的并发保护。
  2. 可观测:每个脚本都要写日志(时间+级别+结果),关键任务要有成功/失败的可判定信号。建议让脚本在成功时写标记文件或输出固定关键字,便于监控匹配。
  3. 失败要报警:无人值守的任务失败却没人知道,比不做自动化更危险。可用 curl 回调健康检查地址、调用企业微信/钉钉机器人 webhook,或由监控系统扫描日志关键字。
  4. 退出码语义明确:0 成功、1 通用错误、2 用法错误。调用方依赖退出码判断,脚本里不要用 exit 0 掩盖失败,也不要在管道末尾误吞退出码。
  5. 版本管理:所有脚本纳入 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; }

五、实施步骤清单

  1. 梳理高频操作:把近三个月重复做过三次以上的操作列出来(备份、清理、重启、巡检、发布),作为自动化的第一批目标。
  2. 建立脚本目录与规范:统一放 /opt/scripts,含 bin(可执行)、conf(配置)、log(日志)子目录;套用本文开头的模板。
  3. 编写并静态检查:写完跑 shellcheck 与 bash -n,修复所有告警。
  4. 手动试跑:在测试机或低峰期手动执行,检查日志、产出物与退出码。
  5. 模拟 cron 环境验证:env -i PATH=/usr/bin:/bin /bin/bash /opt/scripts/xxx.sh,提前暴露环境变量问题。
  6. 接入调度:crontab 或 systemd timer,输出重定向到日志,长任务加 flock。
  7. 配置告警与复核:关键任务加失败通知;上线后连续三天人工复核日志,确认稳定。
  8. 纳入版本库与文档:脚本 + crontab 导出文件 + 一份 README(用途、执行时间、负责人、恢复方法)。
  9. 定期演练:备份类脚本每季度做一次恢复演练,确认备份真的可用——没验证过的备份等于没有备份。

小结

自动化运维不是"把命令堆进脚本",而是把运维动作工程化:规范化的脚本模板(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 查看),改时区后重启调度服务。