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 表示各种错误类型
知识关联
- 前置知识:2.1:Shell 脚本入门 Shell 脚本入门、2.8:Shell 脚本进阶 Shell 脚本进阶、1.9:重定向与管道 重定向与管道
- 后续影响:自动化脚本是 Ansible Playbook 的补充,也是 devops 流水线的基础构件
- 配套工具:cron(3.15:Cron 定时任务 高级定时任务)、4.8:集中式日志管理 集中式日志(脚本日志转发)
原理讲解
防御性脚本设计
运维脚本运行在无人值守环境中,不能依赖用户交互来判断错误。核心设计原则包括:
及早失败——用 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 静态分析 |
练习题
- (概念)
set -euo pipefail中的-e、-u、-o pipefail分别有什么作用? - (概念)什么是幂等脚本?举一个非幂等的 Shell 命令例子并说明如何改写为幂等的。
- (实操)编写一个
disk-check.sh脚本:检查所有挂载点的磁盘使用率,当任何分区使用率超过 80% 时输出警告,超过 90% 时输出 CRITICAL。支持-t THRESHOLD参数自定义阈值。配合 cron 每小时执行一次。 - (实操)基于示例中的
backup.sh模板,扩展支持postgresql备份类型(使用pg_dump)。编写 cron 配置实现每天凌晨 3 点对mydb数据库执行备份。 - (🔍 挑战)编写一个
deploy.sh脚本完成以下工作:从 Git 仓库拉取指定分支代码;执行构建(如npm run build);将构建产物复制到/var/www/app;重新加载 Nginx 配置;支持回滚(--rollback参数恢复上一个版本)。要求全程日志记录且幂等。
点击查看答案
-e遇任何命令失败立即退出;-u使用未定义变量时报错;-o pipefail管道中任一命令失败即视为整体失败(而非只取最后一条的退出码)。- 幂等脚本多次执行结果相同。非幂等例子:
echo "data" >> file.log(每次追加)。改为幂等:grep -q "data" file.log || echo "data" >> file.log。 disk-check.sh:用df -h | awk解析使用率,getopts 解析-t,超过阈值输出 WARNING/CRITICAL。cron:0 * * * * /usr/local/bin/disk-check.sh -t 80。- 在 backup.sh 的 case 中添加
postgresql) pg_dump "$SOURCE" | gzip > "$BACKUP_FILE" ;;。cron:0 3 * * * /path/backup.sh -t postgresql -s mydb -d /backups。 - deploy.sh 用
git clone --branch $BRANCH拉代码;构建前备份当前/var/www/app为app.prev;--rollback时cp -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 将脚本集成到流水线
延伸阅读
- ShellCheck —— Shell 脚本静态分析工具
- GNU Bash 手册
- ShellCheck GitHub 仓库
- 推荐书籍:《Linux Shell 脚本攻略(第 3 版)》
常见问题
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 提供了更完善的依赖管理和日志收集。