上次咱们聊了Shell脚本入门,核心是把重复命令打包成脚本,实现简单自动化。但入门脚本在生产环境中往往“不堪一击”:备份脚本因磁盘满静默失败,等到发现时已错过最佳恢复时机;半夜被紧急叫醒排查问题,却发现脚本没有任何日志记录,排查陷入僵局……这些痛点的根源,都是脚本缺少错误处理、日志记录与异常清理机制。今天老唐就带大家升级玩法——把“能跑”的入门脚本,改造为“能扛”的生产级健壮脚本。一、为什么你的脚本需要“强壮”起来?
入门脚本能满足基础自动化需求,但就像普通自行车能在小区遛弯代步,却不敢开上秋名山一样,无法应对生产环境的复杂场景。生产环境对脚本的核心要求有三点:- 遇事不崩,稳定“扛造”:脚本不能像脆皮大学生一样遇到错误就“破防崩溃”,得像打工人的电脑——卡死但绝不蓝屏。关键任务要有重试机制,重要操作要有回滚预案。
- 处处留痕,拒绝“狼人杀”:不能像互联网嘴替只输出不记录。每个操作都要有清晰的“电子脚印”,出了问题不用玩“猜猜谁动了我的文件”这种悬疑游戏。
- 优雅离场,不当“显眼包”:无论正常结束还是异常退出,都要像职场老油条一样会收拾场面——清理临时文件、释放锁定资源,绝不留下“赛博垃圾”让后续脚本踩坑。
二、三大进阶技巧,让脚本脱胎换骨
技巧1:给脚本装上“错误雷达”——用set命令开启全方位防护
基础脚本的最大问题的是:即使某一步命令执行失败,脚本也会继续往下跑,最终导致错误扩散(比如用失败的备份数据覆盖旧的有效备份)。我们可以通过set系列命令,给脚本装上“错误雷达”,一旦检测到异常就及时止损。#!/bin/bash# 遇到任何命令执行失败(返回非0状态码),立即退出脚本,避免错误扩散set -e# 遇到未定义的变量时,直接报错并退出脚本,防止因变量空值导致逻辑错误set -u# 调试模式:执行时显示每一条命令(调试阶段开启,正式运行时注释)# set -x# 管道命令防护:管道中任意一个命令失败,整个管道视为失败(默认管道只判断最后一个命令)set -o pipefail
- set -e:最核心的防护——比如tar备份命令执行失败(返回非0),脚本会立即停止,不会继续执行后续的“覆盖旧备份”命令,避免数据损坏;
- set -u:防止变量漏定义——比如脚本中误写了变量名(如把BACKUP_DIR写成BACKUP_Dir),会直接报错,而不是用空值执行(比如变成“tar -zcf /syslog”,导致备份失败);
- set -o pipefail:补充管道命令防护——比如“command1 | command2”,默认只有command2失败才视为管道失败,开启后command1失败也会触发脚本退出(比如“cat 日志.txt | grep 错误”,如果日志文件不存在导致cat失败,脚本会及时退出)。
小贴士:如果脚本中某一步命令允许失败(比如“检查旧备份是否存在”),可以在命令后加: || true 临时绕过set -e的限制,例如:find $BACKUP_DIR -name "*.tar.gz" -mtime +7 || true技巧2:关键操作留下“日志脚印”——打造专业级日志系统
入门脚本常用echo输出信息,但echo的缺点很明显:没有时间戳、没有级别区分(信息/错误)、无法持久化存储。专业的脚本日志,需要满足“可追溯、易区分、能审计”的要求,推荐用“函数封装+文件持久化+控制台输出”的方式实现。#!/bin/bash# 日志文件路径(建议放在/var/log下,需确保脚本有写入权限)LOG_FILE="/var/log/my_script.log"# 获取脚本名称(用于日志标识,区分多个脚本的日志)SCRIPT_NAME=$(basename "$0")# 日志输出函数:支持级别区分、时间戳、文件+控制台双输出log_message() { local level=$1 # 日志级别:INFO/WARNING/ERROR local message=$2 # 日志内容 local timestamp=$(date "+%Y-%m-%d %H:%M:%S") # 精确到秒的时间戳 # 1. 写入日志文件(持久化存储,用于后续回溯) echo "[$timestamp] [$level] $SCRIPT_NAME: $message" >> "$LOG_FILE" # 2. 同时输出到控制台(交互式运行时,方便实时查看) if [ -t 0 ]; then # 可选:给不同级别日志加颜色(红色=错误,黄色=警告,绿色=信息) case $level in "INFO") color="\033[32m" ;; # 绿色 "WARNING") color="\033[33m" ;; # 黄色 "ERROR") color="\033[31m" ;; # 红色 *) color="\033[0m" ;; # 默认(无颜色) esac # 输出带颜色的日志,最后重置颜色 echo -e "${color}[$level] $message\033[0m" fi}# 日志使用示例log_message "INFO" "脚本开始执行,准备进行系统日志备份"log_message "WARNING" "备份目录空间不足500MB,可能影响备份完整性"log_message "ERROR" "备份源目录/var/log不存在,创建中..."
- 日志轮转:如果脚本高频执行,建议给日志文件配置logrotate(Linux系统自带工具),避免日志文件过大(比如设置保留7天日志,单个文件超过100MB自动切割);
- 错误重定向:将脚本执行中产生的标准错误(stderr)也定向到日志文件,避免遗漏命令自带的错误信息,可在脚本入口加:exec 2>> "$LOG_FILE";
- 敏感信息过滤:如果日志中可能包含密码、密钥等敏感信息,需在输出前过滤(比如用sed替换敏感字段)。
技巧3:异常时自动“打扫战场”——用trap命令清理现场”
脚本执行中可能因各种原因中断(比如Ctrl+C强制停止、系统重启、脚本执行出错退出),此时容易留下临时文件、锁定文件等“垃圾”,导致后续脚本无法正常运行(比如锁定文件未删除,下次执行提示“脚本正在运行”)。用trap命令可以捕获脚本的退出信号,无论脚本是正常结束还是异常中断,都会执行预设的清理操作。#!/bin/bash# 引入前面定义的日志函数(确保log_message可调用)LOG_FILE="/var/log/my_script.log"SCRIPT_NAME=$(basename "$0")log_message() { # 此处省略日志函数实现,同技巧2}# 定义清理函数:脚本退出时执行的操作cleanup() { log_message "INFO" "开始执行清理操作..." # 1. 删除临时文件(比如脚本执行中生成的临时日志、缓存文件) rm -f /tmp/temp_*.txt /tmp/backup_cache.dat # 2. 删除锁定文件(避免下次执行被误判为“正在运行”) rm -f /var/lock/my_script.lock # 3. 其他清理:比如关闭打开的文件描述符、终止子进程等 # pkill -P $$ # 终止当前脚本的所有子进程(按需使用) log_message "INFO" "清理操作完成,脚本退出"}# 捕获退出信号:让cleanup函数在脚本退出时执行# EXIT:正常退出信号;INT:Ctrl+C强制停止信号;TERM:kill命令触发的信号trap cleanup EXIT INT TERM# 脚本主逻辑(示例:创建锁定文件,避免并发执行)if [ -f /var/lock/my_script.lock ]; then log_message "ERROR" "脚本已在运行中(检测到锁定文件),退出执行" exit 1else touch /var/lock/my_script.lock log_message "INFO" "创建锁定文件成功,开始执行主逻辑"fi# 此处省略脚本主逻辑(如备份、数据处理等)
注意:清理函数要“简单可靠”,避免在清理过程中再抛出错误(比如不要用可能失败的命令),否则会覆盖原始的错误信息,增加排查难度。三、实操:升级版日志备份脚本(整合三大技巧)
下面我们把上次的基础备份脚本,整合前面的“错误防护、日志记录、清理机制”,打造一个生产级的日志备份脚本。这个脚本具备以下能力:- 异常立即止损(set -euo pipefail);
升级版备份脚本完整代码:
#!/bin/bash# ==== 1. 错误防护设置(脚本开头优先配置)====set -euo pipefail# 开启错误输出重定向:将标准错误定向到日志文件exec 2>> "$LOG_FILE"# ==== 2. 配置参数(集中管理,方便后续修改)====BACKUP_DIR="/backup/logs" # 备份文件存储目录LOG_DIR="/var/log" # 待备份的日志目录LOG_FILE="/var/log/backup_script.log" # 脚本日志文件RETENTION_DAYS=7 # 旧备份保留天数(超过7天的自动删除)MAX_RETRIES=3 # 备份失败后的最大重试次数REQUIRED_SPACE=1024 # 备份所需最小磁盘空间(单位:MB,即1GB)LOCK_FILE="/var/lock/backup_script.lock" # 锁定文件路径# ==== 3. 日志函数(带颜色区分)====log_message() { local level=$1 local message=$2 local timestamp=$(date "+%Y-%m-%d %H:%M:%S") # 写入日志文件 echo "[$timestamp] [$level] $(basename "$0"): $message" >> "$LOG_FILE" # 带颜色输出到控制台(交互式运行时) if [ -t 0 ]; then case $level in "INFO") color="\033[32m" ;; "WARNING") color="\033[33m" ;; "ERROR") color="\033[31m" ;; *) color="\033[0m" ;; esac echo -e "${color}[$level] $message\033[0m" fi}# ==== 4. 清理函数(脚本退出时执行)====cleanup() { # 只清理存在的锁定文件(避免rm -f报错) if [[ -f "$LOCK_FILE" ]]; then rm -f "$LOCK_FILE" log_message "INFO" "已删除锁定文件:$LOCK_FILE" fi # 删除备份过程中产生的临时文件 rm -f /tmp/backup_temp_*.tar.gz log_message "INFO" "清理操作完成"}# ==== 5. 辅助检查函数====# 检查磁盘空间是否充足check_disk_space() { log_message "INFO" "开始检查备份目录磁盘空间" # 获取备份目录所在磁盘的可用空间(单位:MB) # df -m:以MB为单位显示磁盘空间;awk 'NR==2 {print $4}':提取第二行的第4列(可用空间) local available_space=$(df -m "$BACKUP_DIR" | awk 'NR==2 {print $4}') # 比较可用空间与所需空间(注意:此处用-eq/-lt等,需确保变量为数字) if [[ $available_space -lt $REQUIRED_SPACE ]]; then log_message "ERROR" "磁盘空间不足!备份目录$BACKUP_DIR可用空间:${available_space}MB,所需空间:${REQUIRED_SPACE}MB" exit 1 # 空间不足,直接退出 fi log_message "INFO" "磁盘空间检查通过,可用空间:${available_space}MB"}# ==== 6. 主逻辑函数====main() { # 步骤1:设置清理机制 trap cleanup EXIT INT TERM # 步骤2:检查是否并发执行(通过锁定文件) if [[ -f "$LOCK_FILE" ]]; then log_message "ERROR" "检测到锁定文件$LOCK_FILE,脚本已在运行中,退出执行" exit 1 else touch "$LOCK_FILE" log_message "INFO" "创建锁定文件$LOCK_FILE成功" fi # 步骤3:检查备份目录是否存在,不存在则创建 if [[ ! -d "$BACKUP_DIR" ]]; then log_message "WARNING" "备份目录$BACKUP_DIR不存在,开始创建" mkdir -p "$BACKUP_DIR" log_message "INFO" "备份目录$BACKUP_DIR创建成功" fi # 步骤4:检查磁盘空间 check_disk_space # 步骤5:生成备份文件名(含时间戳,避免重复) local date_stamp=$(date +%Y%m%d_%H%M%S) local backup_file="syslog_backup_${date_stamp}.tar.gz" local full_path="$BACKUP_DIR/$backup_file" # 步骤6:带重试机制的备份操作 local retry_count=0 local backup_success=false log_message "INFO" "开始执行日志备份,最大重试次数:$MAX_RETRIES" while [[ $retry_count -lt $MAX_RETRIES && $backup_success == false ]]; do ((retry_count++)) log_message "INFO" "第${retry_count}次备份尝试:备份$LOG_DIR/syslog到$full_path" # 执行备份命令(tar -zcf:压缩打包;-C:指定源目录;--exclude:排除不需要备份的文件) if tar -zcf "$full_path" -C "$LOG_DIR" syslog --exclude="syslog.*.gz"; then backup_success=true # 输出备份文件大小(方便审计) local backup_size=$(du -h "$full_path" | cut -f1) log_message "INFO" "第${retry_count}次备份成功,备份文件:$full_path,大小:$backup_size" else log_message "WARNING" "第${retry_count}次备份失败" # 失败后删除不完整的备份文件 rm -f "$full_path" # 重试间隔2秒(避免频繁重试) sleep 2 fi done # 步骤7:判断备份是否成功 if [[ $backup_success == false ]]; then log_message "ERROR" "备份失败,已达最大重试次数($MAX_RETRIES)" exit 1 fi # 步骤8:清理旧备份(删除超过RETENTION_DAYS天的备份文件) log_message "INFO" "开始清理${RETENTION_DAYS}天前的旧备份文件" # find命令:-mtime +7:最后修改时间超过7天;-delete:删除符合条件的文件;-print:打印删除的文件(用于计数) local deleted_count=$(find "$BACKUP_DIR" -name "syslog_backup_*.tar.gz" -mtime +$RETENTION_DAYS -delete -print | wc -l) log_message "INFO" "旧备份清理完成,共删除${deleted_count}个旧备份文件"}# ==== 7. 脚本入口(启动主逻辑)====log_message "INFO" "=== 系统日志备份脚本开始执行 ==="mainlog_message "INFO" "=== 系统日志备份脚本执行完成 ==="
- 权限配置:给脚本添加执行权限:chmod +x backup_syslog.sh;
- 测试运行:先开启调试模式(取消脚本开头的set -x注释),手动运行测试:./backup_syslog.sh;
- 定时执行:测试通过后,用crontab设置定时任务(比如每天凌晨3点执行):0 3 * * * /path/to/backup_syslog.sh;
- 日志查看:通过日志文件监控执行情况 tail -f /var/log/backup_script.log
四、调试技巧:快速定位脚本问题
即使脚本加了错误处理和日志,也可能遇到问题。掌握以下3个调试技巧,能快速定位问题根源:- 逐行调试:不需要修改脚本,直接用bash -x运行脚本,会显示每一条执行的命令和变量值,相当于“脚本执行过程录像”适合:排查变量赋值错误、命令参数错误、逻辑流程错误。
- 语法检查(提前避坑):用shellcheck工具自动检查脚本的常见语法错误(如变量未定义、括号不匹配、命令拼写错误等)。
# 工具需安装# Debian/Ubuntu :sudo apt install shellcheck# CentOS/RHEL sudo yum install shellcheck()# Mac osbrew install shellcheck# 运行使用:shellcheck 你的脚本
- 局部调试(精准定位):如果只怀疑脚本中某一段代码有问题,不需要全局开启调试模式,可在怀疑的代码段前后添加set -x(开启调试)和set +x(关闭调试),只输出该段的执行过程。
# 示例:log_message "INFO" "开始执行备份逻辑"set -x # 开启局部调试tar -zcf "$full_path" -C "$LOG_DIR" syslogset +x # 关闭局部调试log_message "INFO" "备份逻辑执行完成"
五、进阶学习路线(持续提升)
掌握了错误处理、日志记录和清理机制,你已经超越了80%的入门级Shell脚本开发者。接下来可以按以下路线继续进阶:
- Shell脚本安全:学习如何防止命令注入(比如处理用户输入时的安全过滤)、安全处理密码和密钥(避免明文存储)、最小权限原则(脚本用普通用户执行,而非root);
- Shell与外部交互:学习用curl调用API、用jq工具解析JSON数据(比如从API返回结果中提取信息)、脚本参数解析(用getopts或getopt处理命令行参数);
- 脚本工程化:将Shell脚本集成到CI/CD流水线(如Jenkins、GitLab CI),实现脚本的版本控制、自动化测试和批量部署,适配大规模集群环境。
好的Shell脚本不是一次写成的,而是通过不断迭代优化(解决实际问题、补充防护机制)逐步完善的。从今天起,给你写的每一个脚本都加上错误处理、日志记录和清理机制——三个月后再回头看,你会发现自己的脚本不仅“能跑”,还能在生产环境中“稳稳当当”,而你也会从“脚本编写者”成长为“自动化运维工程师”。