4.19 Shell 脚本实战案例集(10个生产级脚本)
预计阅读时间:13 分钟
📖 目录
学习目标
学完本章后,你将能够:
- 将 2.1:Shell 脚本入门/2.8:Shell 脚本进阶 所学的 Shell 语法应用到真实的运维场景中
- 编写系统巡检、日志切割、批量备份、服务守护等生产级脚本
- 理解生产脚本的健壮性要求(日志记录、错误处理、幂等性)
- 配合 cron 或 systemd timer 将脚本接入自动化运维体系
核心知识
- 生产级脚本——不仅"能跑",还要具备日志、错误处理、幂等性、安全防护的脚本
- set -euo pipefail——Shell 严格模式,任何错误或未定义变量立即退出
- 幂等性(Idempotence)——多次执行脚本产生相同结果,避免重复操作造成破坏
- 锁文件(Lock File)——通过 flock 或 mkdir 防止同一脚本并发执行
- logrotate——Linux 日志轮转工具,按大小或时间拆分日志文件
- 并行执行——使用后台进程 + wait 或 xargs -P 控制并发数
- 信号处理(trap)——捕获 SIGINT/SIGTERM 在退出前执行清理操作
知识关联
- 前置知识:2.1:Shell 脚本入门 Shell 脚本入门、2.8:Shell 脚本进阶 Shell 脚本进阶
- 后续影响:脚本可直接用于 4.13:服务器初始化规范 服务器初始化规范和 4.9:CI/CD 持续部署 CI/CD 部署流程
- 对比技术:简单脚本用 Shell,复杂场景考虑 4.12:Python 运维脚本 Python 运维脚本或 4.1:Ansible 自动化 Ansible
原理讲解
从"能跑"到"生产可用"
很多初学者写的脚本在终端里执行一次似乎没问题,但放到 cron 里每天执行就会出现各种状况:变量未定义导致删错目录、命令失败但脚本继续执行造成连锁反应、日志未记录导致事后无法排查。生产脚本需要额外加三层防护:严格模式(set -euo)、日志记录(每条关键操作都写日志)、幂等性检查(操作前先检查状态)。
常见的脚本模式
运维脚本虽然场景多样,但几乎都可以归入几个模式:巡检型(检查状态、对比阈值、报告结果)、备份型(打包/导出 + 压缩 + 清理旧数据)、部署型(拉取代码 + 构建 + 替换 + 健康检查)、守护型(循环检查 + 自动恢复 + 降级告警)。掌握这四种模式的编写方式,就能覆盖大多数运维自动化需求。
示例代码
案例1:系统健康巡检
配合 cron 每天执行,检查 CPU 内存磁盘服务状态,异常时写日志并可选告警。
#!/bin/bash -euo pipefail
# 系统健康巡检脚本
HOSTNAME=$(hostname)
LOG_FILE="/var/log/health_check.log"
THRESHOLD_CPU=90
THRESHOLD_MEM=80
THRESHOLD_DISK=85
log() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG_FILE"; }
check_cpu() {
local cpu_idle=$(top -bn1 | grep "Cpu(s)" | awk '{print $8}' | cut -d'.' -f1)
local cpu_used=$((100 - cpu_idle))
if [[ $cpu_used -gt $THRESHOLD_CPU ]]; then
log "WARNING: CPU usage ${cpu_used}% exceeds ${THRESHOLD_CPU}%"
return 1
fi
log "OK: CPU usage ${cpu_used}%"
}
check_memory() {
local mem_used=$(free | grep Mem | awk '{printf "%.0f", $3/$2 * 100}')
if [[ $mem_used -gt $THRESHOLD_MEM ]]; then
log "WARNING: Memory usage ${mem_used}% exceeds ${THRESHOLD_MEM}%"
return 1
fi
log "OK: Memory usage ${mem_used}%"
}
check_disk() {
df -h | awk 'NR>1 {print $5, $6}' | while read -r usage mount; do
local pct="${usage%%%*}"
if [[ $pct -gt $THRESHOLD_DISK ]]; then
log "WARNING: Disk $mount usage ${pct}% exceeds ${THRESHOLD_DISK}%"
fi
done
}
check_services() {
local services=("nginx" "mysql" "docker" "ssh" "cron")
for svc in "${services[@]}"; do
if systemctl is-active --quiet "$svc" 2>/dev/null; then
log "OK: $svc is running"
else
log "ERROR: $svc is not running!"
return 1
fi
done
}
log "======== Health Check Start ========"
# 输出: [2026-07-30 10:30:00] ======== Health Check Start ========
errors=0
check_cpu || ((errors++))
check_memory || ((errors++))
check_disk || ((errors++))
check_services || ((errors++))
log "======== Health Check End ($errors issues) ========"
# 输出: [2026-07-30 10:30:05] ======== Health Check End (0 issues) ========
[[ $errors -gt 0 ]] && exit 1 || exit 0
脚本的通用模板结构
#!/bin/bash -euo pipefail
# 脚本模板 —— 适用于大多数运维场景
# 1. 配置区
THRESHOLD_CPU=90
RETENTION_DAYS=30
LOG_FILE="/var/log/myscript.log"
# 2. 工具函数
log() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG_FILE"; }
die() { log "FATAL: $*"; exit 1; }
# 3. 依赖检查
for cmd in curl jq awk; do
command -v "$cmd" &>/dev/null || die "$cmd 未安装"
done
# 4. 锁机制
LOCKFILE="/tmp/myscript.lock"
exec 200>"$LOCKFILE"
flock -n 200 || die "脚本已在运行"
# 5. 主逻辑
log "开始执行..."
# 输出: [2026-07-30 10:30:00] 开始执行...
# ... 业务代码 ...
log "执行完成"
# 输出: [2026-07-30 10:30:01] 执行完成
# 6. 清理(自动触发)
trap 'rm -f "$LOCKFILE"' EXIT
案例2:数据库自动备份
#!/bin/bash -euo pipefail
# MySQL/MariaDB 自动化备份
DB_USER="backup"
DB_PASS="${DB_PASS:?请在环境变量中设置 DB_PASS}" # 强制显式设置,避免空密码触发交互提示
export MYSQL_PWD="$DB_PASS" # 用环境变量传密码,避免命令行泄漏
BACKUP_DIR="/var/backups/mysql"
RETENTION_DAYS=30
mkdir -p "$BACKUP_DIR"
databases=$(mysql -u "$DB_USER" -e "SHOW DATABASES;" | \
grep -Ev "^(Database|information_schema|performance_schema|mysql|sys)$")
for db in $databases; do
backup_file="${BACKUP_DIR}/${db}_$(date +%Y%m%d).sql.gz"
mysqldump --single-transaction --quick \
-u "$DB_USER" "$db" | gzip > "$backup_file"
echo "备份完成: $db ($(du -h "$backup_file" | cut -f1))"
# 输出: 备份完成: mydb (1.2G)
done
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +$RETENTION_DAYS -exec rm {} +
echo "过期备份清理完成"
# 输出: 过期备份清理完成
案例3:日志轮转清理
#!/bin/bash -euo pipefail
# 自定义日志轮转(适合 Docker 和应用日志)
# 用法: ./log-cleanup.sh [-d 30] [-n]
RETENTION_DAYS=30
DRY_RUN=false
while getopts "d:n" opt; do
case $opt in
d) RETENTION_DAYS="$OPTARG" ;;
n) DRY_RUN=true ;;
esac
done
DIRS=("/var/log/myapp" "/var/log/nginx")
for dir in "${DIRS[@]}"; do
[[ -d "$dir" ]] || continue
find "$dir" -name "*.log.*" -mtime +$RETENTION_DAYS -delete
done
$DRY_RUN || journalctl --vacuum-time="${RETENTION_DAYS}d" --quiet
案例4:服务守护与自动恢复
#!/bin/bash -euo pipefail
# 服务守护脚本,配合 cron 每分钟执行
SERVICE_NAME="myapp"
STATE_FILE="/tmp/${SERVICE_NAME}_guard.state"
MAX_RESTART=3
INTERVAL=300
declare -a restart_times
[[ -f "$STATE_FILE" ]] && readarray -t restart_times < "$STATE_FILE"
now=$(date +%s)
valid_times=()
for t in "${restart_times[@]}"; do
(( now - t < INTERVAL )) && valid_times+=("$t")
done
if ! systemctl is-active --quiet "$SERVICE_NAME"; then
if (( ${#valid_times[@]} >= MAX_RESTART )); then
echo "FATAL: $SERVICE_NAME 短期内已重启 ${#valid_times[@]} 次,放弃"
# 输出: FATAL: myapp 短期内已重启 3 次,放弃
exit 1
fi
systemctl restart "$SERVICE_NAME" && echo "$now" >> "$STATE_FILE"
fi
案例5:批量远程并行执行
#!/bin/bash -euo pipefail
# 多主机批量命令执行,支持并发控制
# 用法: ./batch-exec.sh servers.txt "df -h"
SERVERS_FILE="${1:-servers.txt}" ; COMMAND="${2:-uptime}"
declare -a hosts
mapfile -t hosts < "$SERVERS_FILE"
run() {
local host=$1; local cmd=$2
printf "[%s] " "$host"
ssh -o ConnectTimeout=5 "$host" "$cmd" 2>&1 && echo "OK" || echo "FAIL"
# 输出: [web01] 10:30:00 up 15 days, load average: 0.50, 0.75, 1.20
# 输出: OK
}
export -f run
printf "%s\n" "${hosts[@]}" | xargs -P 5 -I {} bash -c 'run "{}" "'"$COMMAND"'"'
案例6:自动化部署脚本
#!/bin/bash -euo pipefail
# 自动化部署,支持回滚
REPO_URL="git@github.com:myorg/myapp.git"
DEPLOY_DIR="/var/www/myapp"
BRANCH="main"
[[ -d "$DEPLOY_DIR/.git" ]] || git clone "$REPO_URL" "$DEPLOY_DIR"
cd "$DEPLOY_DIR"
git fetch origin && git reset --hard "origin/$BRANCH"
make build
systemctl restart myapp
sleep 5
curl -sf http://localhost:8080/health || {
echo "健康检查失败,回滚中..."
# 输出: 健康检查失败,回滚中...
git reset --hard HEAD@{1}
make build && systemctl restart myapp
exit 1
}
echo "部署完成"
# 输出: 部署完成
案例7:访问日志分析与异常告警
需求:Nginx 访问日志每天增长数 GB,需要每天统计状态码分布、TOP 访问来源和慢请求,异常(5xx 突增、扫描特征)时写入告警文件供监控系统读取。
#!/bin/bash -euo pipefail
# 日志分析脚本,配合 cron 每天 01:00 执行
LOG_DIR="/var/log/nginx"
TODAY=$(date -d yesterday +%Y%m%d)
LOG_FILE="${LOG_DIR}/access.log"
REPORT_DIR="/var/reports"
THRESHOLD_5XX=50 # 5xx 超过 50 条视为异常
mkdir -p "$REPORT_DIR"
# 1. 状态码分布统计
awk '{print $9}' "$LOG_FILE" | sort | uniq -c | sort -rn > \
"$REPORT_DIR/status_${TODAY}.txt"
# 输出示例(写入文件):
# 18432 200
# 1204 304
# 215 404
# 67 502
# 2. 慢请求 TOP10($time_total > 5s,$request_time 是第 9 列时需要调整)
awk '$NF > 5 {print $7, $NF}' "$LOG_FILE" | sort -k2 -rn | head -10 > \
"$REPORT_DIR/slow_${TODAY}.txt"
# 3. 异常检测:5xx 数量
err5xx=$(awk '$9 ~ /^5[0-9][0-9]$/' "$LOG_FILE" | wc -l)
if (( err5xx > THRESHOLD_5XX )); then
echo "ALERT: 昨日 5xx 错误 ${err5xx} 条(阈值 ${THRESHOLD_5XX})" | \
tee "$REPORT_DIR/alert_${TODAY}.log"
awk '$9 ~ /^5[0-9][0-9]$/ {print $1, $7, $9}' "$LOG_FILE" | \
sort | uniq -c | sort -rn | head -5 >> "$REPORT_DIR/alert_${TODAY}.log"
# 输出: ALERT: 昨日 5xx 错误 67 条(阈值 50)
# 47 /api/order/query 502
# 12 /api/pay/callback 504
fi
# 4. 清理 30 天前的报告
find "$REPORT_DIR" -name "*.txt" -mtime +30 -delete
解析:awk 是日志分析的主力(字段定位 + 正则过滤 + 统计);阈值比较放在脚本内而不是输出端,让"是否告警"的决策留在脚本里;报告文件按日期命名天然支持历史回溯。
案例8:SSL 证书到期批量巡检
需求:公司有 20 台服务器、30 个域名证书,证书过期是生产事故的高发原因。需要批量检查所有证书剩余天数,30 天内到期的输出告警。
#!/bin/bash -euo pipefail
# 证书巡检脚本,配合 cron 每周一执行
# 用法: ./cert-check.sh certlist.txt
# certlist.txt 每行格式: domain:port (如 example.com:443)
CERT_LIST="${1:-certlist.txt}"
WARN_DAYS=30
CRIT_DAYS=7
warn_count=0
[[ -f "$CERT_LIST" ]] || { echo "文件不存在: $CERT_LIST"; exit 1; }
check_cert() {
local endpoint=$1
local domain=${endpoint%%:*}
local port=${endpoint##*:}
local end_date
end_date=$(timeout 10 openssl s_client -connect "$endpoint" \
-servername "$domain" /dev/null | \
openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
[[ -n "$end_date" ]] || { echo "[$endpoint] 获取证书失败"; return 2; }
local remain
remain=$(( ( $(date -d "$end_date" +%s) - $(date +%s) ) / 86400 ))
if (( remain < 0 )); then
echo "[$endpoint] 已过期 $(( -remain )) 天 !!!"
warn_count=$((warn_count + 1))
elif (( remain <= CRIT_DAYS )); then
echo "[$endpoint] CRITICAL: 仅剩 ${remain} 天"
warn_count=$((warn_count + 1))
elif (( remain <= WARN_DAYS )); then
echo "[$endpoint] WARNING: 剩余 ${remain} 天"
warn_count=$((warn_count + 1))
else
echo "[$endpoint] OK: 剩余 ${remain} 天"
fi
}
while read -r line; do
[[ -n "$line" ]] || continue
check_cert "$line"
done < "$CERT_LIST"
echo "=== 巡检完成: 共 $warn_count 个证书需要关注 ==="
# 输出示例:
# [example.com:443] OK: 剩余 82 天
# [api.example.com:443] WARNING: 剩余 25 天
# === 巡检完成: 共 1 个证书需要关注 ===
exit $(( warn_count > 0 ? 1 : 0 ))
解析:openssl s_client 是证书巡检的通用手段,跨发行版可用;timeout 10 防止网络不通时脚本挂死;剩余天数用纪元秒差值计算,避免日期格式差异;巡检结果通过退出码传递给 cron 或监控系统触发告警。
常见错误
| 错误表现 | 根因 | 正确做法 |
|---|---|---|
| 脚本在 cron 中运行失败但手动执行正常 | cron 的 PATH 环境变量很短,找不到命令 | 在脚本开头设置完整 PATH:export PATH=/usr/local/sbin:/usr/local/bin:... |
| 同一脚本同时被 cron 调用两次导致数据异常 | 未使用锁机制,前次未完成后次又启动 | 用 flock 或 mkdir 实现互斥锁 |
变量未定义导致 rm -rf /$undefined 删除根目录 | 未设置 set -u,空变量被展开为空字符串 | 始终使用 set -euo pipefail |
| 备份脚本覆盖了前一天的备份 | 备份文件名没有时间戳 | 文件名包含 $(date +%Y%m%d_%H%M%S) |
| 脚本中途失败但 exit code 为 0 | 未使用 set -e,后续命令覆盖了错误返回码 | 使用 set -e + trap 记录错误 |
最佳实践
| 实践 | 原理 | 示例 |
|---|---|---|
所有脚本以 set -euo pipefail 开头 | 及早发现错误,防止级联破坏 | —— |
| 敏感信息从环境变量读取而非硬编码 | 避免密码泄露到脚本仓库和进程列表 | DB_PASS="${DB_PASS:-}",通过 .env 或 Vault 注入 |
| 每条关键操作写日志 | 事后排查时知道脚本执行到哪一步 | log() { echo "[$(date)] $*" >> /var/log/script.log; } |
使用 trap 确保退出时清理临时文件和锁 | 防止脚本异常退出后留下垃圾文件 | trap 'rm -f "$LOCKFILE"' EXIT |
先 bash -n script.sh 做语法检查 | 在测试执行前发现明显语法错误 | CI 流水线中集成 ShellCheck 静态分析 |
练习题
- (概念)什么是幂等性?为什么生产脚本应该具备幂等性?举一个非幂等的例子。
- (概念)
set -euo pipefail中-e、-u、-o pipefail分别防止什么问题? - (实操)基于案例2 的数据库备份脚本,增加以下功能:备份完成后将备份文件 rsync 到远程备份服务器;支持
-d参数指定数据库名(默认全库);配置 logrotate 管理备份脚本自身的日志。 - (实操)编写一个
disk-check.sh脚本:检查所有挂载点的磁盘使用率,当任意分区使用率超过 80% 时输出 WARNING,超过 90% 时输出 CRITICAL。支持-t THRESHOLD自定义阈值。配合 cron 每小时执行一次。使用锁文件防止并发。 - (🔍 挑战)综合本章多个案例,编写一个
auto-ops.sh框架脚本:读取一个 YAML/INI 配置文件(如tasks.ini)定义多个定时任务(每个任务含名称、脚本路径、调度表达式、超时时间);框架自动启动每个任务、监控执行状态、超时自动 kill、日志独立记录。要求使用trap信号处理确保优雅退出。
点击查看答案
- (概念)幂等性指同一操作执行多次的结果和执行一次相同。生产脚本应具备幂等性,因为 crond/Automation 可能重复执行,非幂等的例子:
mkdir /tmp/test第二次执行会报错,改为mkdir -p /tmp/test则幂等。 - (概念)
-e:命令失败时立即退出(防止错误被忽略);-u:使用未定义变量时报错退出(防止拼写错误导致诡异行为);-o pipefail:管道中任何一个命令失败即返回非零(而非只取最后一个命令的退出码)。 - (实操)在备份脚本末尾添加
rsync -avz "$BACKUP_DIR/" user@remote:/backup/。使用getopts解析-d dbname参数。logrotate 配置:/var/log/backup-script.log { monthly rotate 12 compress }。注意 rsync 的 SSH 密钥需预先配置免密登录。 - (实操)核心代码:
df -h | awk '{print $5, $6}'解析使用率;循环比较${usage%\%}与阈值;锁文件用flock -n /tmp/disk-check.lock防止并发。crontab:0 * * * * /usr/local/bin/disk-check.sh -t 80。注意flock的-n表示非阻塞——如果上次还没跑完就跳过。 - (🔍 挑战)INI 配置文件格式示例:
[task1] name=health-check cmd=/opt/scripts/health.sh schedule=*/5 * * * * timeout=30。框架用trap捕获 SIGINT/SIGTERM 进入优雅退出流程,用timeout命令限制子进程执行时间,用while read循环读取配置并启动后台进程。注意清理所有子进程的退出逻辑不能遗漏。
本章总结
本章提供了 8 个可直接用于生产环境的 Shell 脚本案例,涵盖健康巡检、数据库备份、日志清理、服务守护、批量执行、自动化部署、日志分析与 SSL 证书巡检。每个案例都遵循"严格模式 + 日志记录 + 错误处理 + 幂等性"的生产级规范。将这些脚本配合 cron 调度,即可搭建起一套"无人值守"的日常运维体系。掌握这些脚本模板后,你可以基于它们快速开发新的自动化工具——遇到新需求时先想想:这是"巡检型""备份型""守护型"还是"部署型",然后套用对应的模板。
速查表
| 脚本模式 | 核心要点 | 适用场景 |
|---|---|---|
| 巡检型 | 阈值比较 + 日志记录 + 告警 | 健康检查、磁盘监控、网络连通性 |
| 备份型 | 数据导出 + 压缩 + 过期清理 | 数据库备份、文件归档、配置快照 |
| 守护型 | 状态检查 + 自动恢复 + 限频 | 服务守护、进程保活 |
| 部署型 | 代码拉取 + 构建 + 健康检查 + 回滚 | 应用发布、配置更新 |
学习路径建议
- 学完本章后建议阅读 4.4:Shell 自动化实战 Shell 自动化实战(更多进阶脚本模板)
- 复杂自动化场景可看 4.1:Ansible 自动化 Ansible 或 4.12:Python 运维脚本 Python 运维脚本
- 将脚本集成到 CI/CD 流水线可参考 4.9:CI/CD 持续部署 CI/CD 持续部署
延伸阅读
- ShellCheck — Shell 脚本静态分析
- Google Shell 风格指南
- GNU Bash 参考手册
- 推荐书籍:《Linux Shell 脚本攻略(第 3 版)》
- 本书章节:2.1:Shell 脚本入门 Shell 脚本入门 · 2.8:Shell 脚本进阶 Shell 脚本进阶 · 4.4:Shell 自动化实战 Shell 自动化实战 · 4.9:CI/CD 持续部署 CI/CD 持续部署 · 4.12:Python 运维脚本 Python 运维脚本 · 2.10:正则表达式 正则表达式从入门到精通 · 4.13:服务器初始化规范 服务器初始化规范