4.4 Shell 脚本自动化运维实战

预计阅读时间:10 分钟

📖 目录

学习目标

学完本章后,你将能够:

  • 编写生产运维场景中常用的 Shell 自动化脚本
  • 实现服务健康检查、日志清理、批量远程执行等任务
  • 使用 getopts 解析脚本参数并实现帮助信息
  • 利用 cron 调度自动化任务实现无人值守运维
  • 编写"防御性"Shell 脚本(错误处理、日志记录、幂等性)

核心知识

  • set -euo pipefail——Shell 脚本的"防御性编程"模式:-e 遇错退出、-u 未定义变量报错、-o pipefail 管道中任一步失败即退出
  • getopts——Shell 内置的参数解析工具,支持短选项和参数值
  • 幂等性(Idempotence)——脚本多次执行结果一致,避免重复操作导致问题
  • 锁文件(Lock File)——防止同一脚本多次并发执行的机制
  • cron 调度——Linux 定时任务系统,格式:分 时 日 月 周 命令
  • journalctl——systemd 日志查询工具,支持按时间、服务、优先级过滤
  • exit code——脚本返回码,0 表示成功,非 0 表示各种错误类型

知识关联

原理讲解

防御性脚本设计

运维脚本运行在无人值守环境中,不能依赖用户交互来判断错误。核心设计原则包括: 及早失败——用 set -e 让脚本在第一条出错命令处停止,而不是带着错误继续执行造成更大破坏; 幂等性——在操作前检查状态(如检查服务是否已运行、目录是否已存在),只在状态不匹配时才执行变更; 安全性——使用 -u 捕获未定义变量,避免 rm -rf $undefined_var/ 这类灾难。

脚本模板化

运维场景中大量任务是重复的:检查服务、清理日志、备份数据、部署代码。将这些任务封装为可参数化的脚本模板,每次只需修改少量变量即可复用。使用 getopts 统一参数风格(-d 30 vs 硬编码天数),配合 usage() 函数输出帮助信息,降低后续维护成本。

远程执行模型

批量远程执行有几种方案:一是循环 SSH(本方案),适合小规模集群(几十台);二是 Ansible(大规模,但需额外安装配置);三是 ClusterShell(适合并行执行)。循环 SSH 的优点是零依赖——控制节点只需有 SSH 客户端和目标主机的密钥认证即可。

示例代码

1. 系统初始化脚本

#!/bin/bash
# init-server.sh —— 新服务器初始化
set -euo pipefail

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}

log "开始系统初始化..."

# 1. 更新系统包
apt update && apt upgrade -y

# 2. 安装常用工具
apt install -y vim git curl wget htop ufw fail2ban unattended-upgrades

# 3. 设置时区
timedatectl set-timezone Asia/Shanghai

# 4. 加固 SSH
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl reload sshd

# 5. 配置防火墙
ufw --force reset
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw --force enable

# 6. 配置自动安全更新
cat > /etc/apt/apt.conf.d/20auto-upgrades << 'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
EOF

log "初始化完成,请手动重启以加载新内核(如有更新)"

2. 服务健康检查脚本

#!/bin/bash
# healthcheck.sh —— 服务健康检查与自动恢复
set -euo pipefail

SERVICES=("nginx" "mysql" "docker" "ssh")
LOG_FILE="/var/log/healthcheck.log"

check_service() {
    local svc=$1
    if systemctl is-active --quiet "$svc"; then
        return 0
    else
        return 1
    fi
}

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

for svc in "${SERVICES[@]}"; do
    if check_service "$svc"; then
        log "OK: $svc 正常运行"
    else
        log "WARN: $svc 离线,正在重启..."
        if systemctl restart "$svc"; then
            log "OK: $svc 重启成功"
        else
            log "CRITICAL: $svc 重启失败,需要人工介入"
        fi
    fi
done

# 配合 cron 每 5 分钟执行一次:
# */5 * * * * /usr/local/bin/healthcheck.sh

3. 日志清理脚本(带参数)

#!/bin/bash
# log-cleanup.sh —— 系统日志清理工具
set -euo pipefail

usage() {
    echo "用法: $0 [-d 保留天数] [-n 干运行]"
    echo "  -d DAYS   日志保留天数(默认 30)"
    echo "  -n        干运行模式,只显示将删除的文件不删除"
    exit 1
}

DRY_RUN=false
RETENTION_DAYS=30

while getopts "d:nh" opt; do
    case $opt in
        d) RETENTION_DAYS=$OPTARG ;;
        n) DRY_RUN=true ;;
        *) usage ;;
    esac
done

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}

log "日志清理开始,保留天数: $RETENTION_DAYS"

if $DRY_RUN; then
    # 干运行模式——只列出不删除
    log "[DRY-RUN] 将删除以下 .log.* 文件:"
    find /var/log -name "*.log.*" -mtime +"$RETENTION_DAYS" -type f 2>/dev/null | while read f; do
        log "  $f ($(du -h "$f" | cut -f1))"
    done
    exit 0
fi

# 清理旧日志文件
find /var/log -name "*.log.*" -mtime +"$RETENTION_DAYS" -delete 2>/dev/null || true
log "旧日志文件清理完成"

# 清理 systemd journal
journalctl --vacuum-time="${RETENTION_DAYS}d" --quiet
log "Journal 清理完成"

# 清理 Docker 未使用资源
if command -v docker &>/dev/null; then
    docker system prune -f --filter "until=${RETENTION_DAYS}d" 2>/dev/null || true
    log "Docker 资源清理完成"
fi

log "日志清理完成"

4. 批量远程执行脚本

#!/bin/bash
# batch-exec.sh —— 多主机批量命令执行
set -euo pipefail

SERVERS=("192.168.1.10" "192.168.1.11" "192.168.1.12")
SSH_USER="ubuntu"
SSH_PORT="22"
TIMEOUT="10"

usage() {
    echo "用法: $0 [命令]"
    echo "默认: uptime"
    echo "示例: $0 'df -h /'"
    echo "      $0 'systemctl status nginx | head -5'"
    exit 1
}

CMD="${1:-uptime}"

for host in "${SERVERS[@]}"; do
    echo "=== $host ==="
    if ssh -o ConnectTimeout="$TIMEOUT" -p "$SSH_PORT" "${SSH_USER}@${host}" "$CMD" 2>&1; then
        echo ">>> $host 执行成功"
    else
        echo ">>> $host 执行失败(exit code: $?)" >&2
    fi
    echo
done

5. 脚本复用模板:定时备份

#!/bin/bash
# backup.sh —— 通用备份脚本模板
set -euo pipefail

usage() {
    echo "用法: $0 -t <类型> -s <源路径> -d <目标路径>"
    echo "  -t TYPE    备份类型: mysql|files"
    echo "  -s SOURCE  源路径(数据库名或目录)"
    echo "  -d DEST    备份目录"
    exit 1
}

while getopts "t:s:d:h" opt; do
    case $opt in
        t) TYPE=$OPTARG ;;
        s) SOURCE=$OPTARG ;;
        d) DEST=$OPTARG ;;
        *) usage ;;
    esac
done

[ -z "${TYPE:-}" ] || [ -z "${SOURCE:-}" ] || [ -z "${DEST:-}" ] && usage

BACKUP_FILE="${DEST}/${SOURCE}_$(date +%Y%m%d_%H%M%S).tar.gz"
mkdir -p "$DEST"

case $TYPE in
    files)
        tar -czf "$BACKUP_FILE" -C "$(dirname "$SOURCE")" "$(basename "$SOURCE")"
        ;;
    mysql)
        mysqldump --single-transaction "$SOURCE" | gzip > "$BACKUP_FILE"
        ;;
    *)
        echo "错误: 不支持的备份类型 $TYPE" >&2
        exit 1
        ;;
esac

echo "备份完成: $BACKUP_FILE ($(du -h "$BACKUP_FILE" | cut -f1))"

6. 锁文件防止并发

#!/bin/bash
# 使用锁文件防止脚本并发执行
LOCKFILE="/var/run/my_script.lock"

exec 200>"$LOCKFILE"
if ! flock -n 200; then
    echo "错误: 脚本已在运行" >&2
    exit 1
fi

# ---- 以下为脚本实际逻辑 ----
echo "开始执行任务..."
sleep 60  # 模拟耗时任务
echo "任务完成"

# 脚本退出时自动释放锁

常见错误

错误表现根因正确做法
脚本中途失败但仍返回 exit code 0未使用 set -e,脚本忽略错误继续执行始终在脚本开头添加 set -euo pipefail
rm -rf /var/$undefined 删除了根目录变量未定义,-u 未设置,变量为空时路径变成 /var/使用 set -u;删除前打印确认路径
脚本同一时间被 cron 调用多次前一次执行未完成,后一次又启动,数据冲突使用 flock 锁文件机制
ssh: connect to host timeout 导致整个脚本卡住未设置 ConnectTimeout,默认等待数分钟添加 ssh -o ConnectTimeout=5;超时后继续下一个主机
cron 脚本中 java/git 等命令找不到cron 的 PATH 环境变量很短,不包含 /usr/local/bin 等目录脚本中使用绝对路径,或在开头 export PATH=/usr/local/sbin:/usr/local/bin:...

最佳实践

实践原理示例
所有脚本开头加 set -euo pipefail及早失败,避免级联错误——
关键操作前打印日志到文件事后排查问题时知道脚本执行到哪一步echo "[$(date)] 开始备份 $DB" >> /var/log/backup.log
使用 || true 处理非关键命令的失败允许脚本在非关键操作失败时继续执行docker system prune -f || true
在 cron 中使用完整路径避免环境变量不一致导致命令找不到/usr/bin/find /var/log ... | /usr/bin/xargs ...
脚本版本化并放在统一目录变更可追踪、回滚有方向/usr/local/bin/ + Git 仓库管理
测试脚本用 bash -n script.sh语法检查,不执行脚本发现明显的语法错误CI 流程中集成 ShellCheck 静态分析

练习题

  1. (概念)set -euo pipefail 中的 -e-u-o pipefail 分别有什么作用?
  2. (概念)什么是幂等脚本?举一个非幂等的 Shell 命令例子并说明如何改写为幂等的。
  3. (实操)编写一个 disk-check.sh 脚本:检查所有挂载点的磁盘使用率,当任何分区使用率超过 80% 时输出警告,超过 90% 时输出 CRITICAL。支持 -t THRESHOLD 参数自定义阈值。配合 cron 每小时执行一次。
  4. (实操)基于示例中的 backup.sh 模板,扩展支持 postgresql 备份类型(使用 pg_dump)。编写 cron 配置实现每天凌晨 3 点对 mydb 数据库执行备份。
  5. (🔍 挑战)编写一个 deploy.sh 脚本完成以下工作:从 Git 仓库拉取指定分支代码;执行构建(如 npm run build);将构建产物复制到 /var/www/app;重新加载 Nginx 配置;支持回滚(--rollback 参数恢复上一个版本)。要求全程日志记录且幂等。
点击查看答案
  1. -e 遇任何命令失败立即退出;-u 使用未定义变量时报错;-o pipefail 管道中任一命令失败即视为整体失败(而非只取最后一条的退出码)。
  2. 幂等脚本多次执行结果相同。非幂等例子:echo "data" >> file.log(每次追加)。改为幂等:grep -q "data" file.log || echo "data" >> file.log
  3. disk-check.sh:用 df -h | awk 解析使用率,getopts 解析 -t,超过阈值输出 WARNING/CRITICAL。cron:0 * * * * /usr/local/bin/disk-check.sh -t 80
  4. 在 backup.sh 的 case 中添加 postgresql) pg_dump "$SOURCE" | gzip > "$BACKUP_FILE" ;;。cron:0 3 * * * /path/backup.sh -t postgresql -s mydb -d /backups
  5. deploy.sh 用 git clone --branch $BRANCH 拉代码;构建前备份当前 /var/www/appapp.prev--rollbackcp -r app.prev app 并 reload nginx。全程 set -euo pipefail + 每步日志到文件。

学习检查点

学完本章后,请检验自己是否掌握以下内容:

检查项自测问题验证方法
概念理解能用自己的话解释 Shell 脚本的执行流程和变量作用域尝试向他人讲解
命令操作能不查文档完成 Shell 脚本编写、调试、执行在终端实际执行
原理掌握能说出 Shell 的管道、重定向、信号处理机制画出流程图
故障排查能独立排查 Shell 脚本语法错误、权限问题、环境变量问题模拟故障并修复
最佳实践能说明为什么需要为 Shell 脚本添加错误处理和日志记录对比不同方案

本章总结

Shell 自动化脚本是 Linux 运维的基础工具,填补了 Ansible 等配置管理工具覆盖不到的场景。本章提供的脚本模板覆盖了服务器初始化、服务健康检查、日志清理、批量远程执行、通用备份、锁机制等常见运维需求。编写高质量运维脚本的核心在于:防御性编程(set -euo)、参数化设计(getopts)、日志记录、幂等性和锁保护。将这些脚本配合 cron 调度,即可搭建起"无人值守"的自愈运维体系。

速查表

命令/操作用途
set -euo pipefail防御性 Shell 脚本模式
systemctl is-active --quiet svc无输出检查服务状态
journalctl --vacuum-time=30d清理 30 天前的 journal 日志
flock -n 200文件锁防并发
bash -n script.sh语法检查
crontab -e编辑定时任务
*/5 * * * * /path/script.sh每 5 分钟执行

学习路径建议

  • 学完本章后建议阅读 4.1:Ansible 自动化 Ansible(将自动脚本升级为配置管理代码)
  • 进阶可学习 ShellCheck 静态分析和 bats 测试框架
  • DevOps 方向可结合 4.9:CI/CD 持续部署 CI/CD 将脚本集成到流水线

延伸阅读

常见问题

Shell 脚本中怎么确保只有一个实例运行?
用 flock(文件锁)是最可靠的方式:exec 200>/tmp/myscript.lock || exit 1; flock -n 200 || { echo "脚本已在运行"; exit 1; }。或者在脚本开头检查 PID 文件:if [ -f /var/run/myscript.pid ] && kill -0 $(cat /var/run/myscript.pid) 2>/dev/null; then exit 1; fi。flock 方式更推荐,即使脚本崩溃也会自动释放锁。
什么时候该换 Python 而不是继续用 Bash?
当需要处理复杂数据结构(JSON/YAML/CSV)、调用 REST API、做文本或数据处理、需要跨平台支持,或者脚本超过 200 行时,应该考虑 Python。Bash 强在快速调用系统命令和组合工具,弱在逻辑复杂度和数据解析。一个简单的判断标准:如果脚本中出现多层数组或正则替换,就换 Python。
cron 任务重启后怎么确保自动恢复?
cron 本身是守护进程,随系统启动自动运行。你可以用 systemctl status cron 检查状态。如果你的脚本涉及临时文件,建议添加启动时清理逻辑。关键任务建议用 systemd timer 替代 cron,因为 systemd 提供了更完善的依赖管理和日志收集。
↑ 回到顶部