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 在退出前执行清理操作

知识关联

原理讲解

从"能跑"到"生产可用"

很多初学者写的脚本在终端里执行一次似乎没问题,但放到 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 静态分析

练习题

  1. (概念)什么是幂等性?为什么生产脚本应该具备幂等性?举一个非幂等的例子。
  2. (概念)set -euo pipefail-e-u-o pipefail 分别防止什么问题?
  3. (实操)基于案例2 的数据库备份脚本,增加以下功能:备份完成后将备份文件 rsync 到远程备份服务器;支持 -d 参数指定数据库名(默认全库);配置 logrotate 管理备份脚本自身的日志。
  4. (实操)编写一个disk-check.sh 脚本:检查所有挂载点的磁盘使用率,当任意分区使用率超过 80% 时输出 WARNING,超过 90% 时输出 CRITICAL。支持 -t THRESHOLD 自定义阈值。配合 cron 每小时执行一次。使用锁文件防止并发。
  5. (🔍 挑战)综合本章多个案例,编写一个auto-ops.sh 框架脚本:读取一个 YAML/INI 配置文件(如 tasks.ini)定义多个定时任务(每个任务含名称、脚本路径、调度表达式、超时时间);框架自动启动每个任务、监控执行状态、超时自动 kill、日志独立记录。要求使用 trap 信号处理确保优雅退出。
点击查看答案
  1. (概念)幂等性指同一操作执行多次的结果和执行一次相同。生产脚本应具备幂等性,因为 crond/Automation 可能重复执行,非幂等的例子:mkdir /tmp/test 第二次执行会报错,改为 mkdir -p /tmp/test 则幂等。
  2. (概念)-e:命令失败时立即退出(防止错误被忽略);-u:使用未定义变量时报错退出(防止拼写错误导致诡异行为);-o pipefail:管道中任何一个命令失败即返回非零(而非只取最后一个命令的退出码)。
  3. (实操)在备份脚本末尾添加 rsync -avz "$BACKUP_DIR/" user@remote:/backup/。使用 getopts 解析 -d dbname 参数。logrotate 配置:/var/log/backup-script.log { monthly rotate 12 compress }。注意 rsync 的 SSH 密钥需预先配置免密登录。
  4. (实操)核心代码: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 表示非阻塞——如果上次还没跑完就跳过。
  5. (🔍 挑战)INI 配置文件格式示例:[task1] name=health-check cmd=/opt/scripts/health.sh schedule=*/5 * * * * timeout=30。框架用 trap 捕获 SIGINT/SIGTERM 进入优雅退出流程,用 timeout 命令限制子进程执行时间,用 while read 循环读取配置并启动后台进程。注意清理所有子进程的退出逻辑不能遗漏。

本章总结

本章提供了 8 个可直接用于生产环境的 Shell 脚本案例,涵盖健康巡检、数据库备份、日志清理、服务守护、批量执行、自动化部署、日志分析与 SSL 证书巡检。每个案例都遵循"严格模式 + 日志记录 + 错误处理 + 幂等性"的生产级规范。将这些脚本配合 cron 调度,即可搭建起一套"无人值守"的日常运维体系。掌握这些脚本模板后,你可以基于它们快速开发新的自动化工具——遇到新需求时先想想:这是"巡检型""备份型""守护型"还是"部署型",然后套用对应的模板。

速查表

脚本模式核心要点适用场景
巡检型阈值比较 + 日志记录 + 告警健康检查、磁盘监控、网络连通性
备份型数据导出 + 压缩 + 过期清理数据库备份、文件归档、配置快照
守护型状态检查 + 自动恢复 + 限频服务守护、进程保活
部署型代码拉取 + 构建 + 健康检查 + 回滚应用发布、配置更新

学习路径建议

延伸阅读

常见问题

生产环境脚本怎么确保健壮性?
脚本开头加上 set -euo pipefail,定义 ERR 和 EXIT trap。日志输出到文件并附带时间戳:echo "[$(date +%Y-%m-%d %H:%M:%S)] 消息" >> /var/log/script.log。关键操作(rm、格式化)前双检查变量是否为空。用 mktemp 创建临时文件而非硬编码路径。用锁文件或 flock 防止重复执行。
数据库备份脚本有什么注意事项?
mysqldump 导出时用 --single-transaction(不锁表 InnoDB)和 --routines --events。备份文件用日期命名(db_$(date +%F).sql)。压缩后上传到异地(S3、OSS、B2)。保留最近 7 天本地备份 + 30 天云端备份。最重要的是定期做恢复演练——不能恢复的备份毫无价值。
健康检查脚本应该检查哪些指标?
系统层:磁盘使用率(大于 90% 告警)、内存可用量(小于 10% 告警)、CPU load average(超过核数 2 倍告警)。服务层:进程是否运行、端口是否监听、HTTP 端点是否返回 200。自定义检查:数据库能否连接、缓存服务能否 SET/GET、SSL 证书是否在 30 天内过期。
↑ 回到顶部