Linux Shell 脚本自动化运维实战:备份清理、存活监控与排错调试


<!DOCTYPE html>

Linux Shell 脚本自动化运维实战:备份清理、存活监控与排错调试

Linux Shell 脚本自动化运维实战:备份清理、存活监控与排错调试

运维干久了你会发现,80% 的日常操作是重复劳动:备份数据库、清理日志、检查网站是否挂、磁盘满了没……这些完全可以交给 Shell 脚本 + crontab 自动完成,把时间省下来干更有价值的事。本文不讲枯燥语法,直接给三个能立刻上线的实战脚本:日志备份清理、MySQL 自动备份、网站存活监控告警,并附上调试排错方法论,让脚本写得出来、跑得稳、坏了能查。

一、Shell 脚本基本功:写出规范脚本的 5 条铁律

先立规矩,脚本才能长期稳定运行:

1. 首行写 #!/bin/bash,明确解释器,避免用 sh 踩坑
2. 开头加 set -euo pipefail:出错即停、变量未定义即报错、管道失败即报错
3. 变量全部加引号,防止路径带空格时炸掉
4. 日志打点:每条关键步骤 echo 时间戳,出问题能定位到第几步
5. 先做前置检查(目录存在?命令存在?),再干正事

一个标准脚本骨架:

#!/bin/bash
set -euo pipefail

# 基础变量
BASE_DIR="/var/log/myapp"
LOG_FILE="/var/log/scripts/cleanup.log"
STAMP=$(date +%Y-%m-%d_%H%M%S)

# 日志函数
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "${LOG_FILE}"
}

# 前置检查
[ -d "${BASE_DIR}" ] || { log "目录不存在: ${BASE_DIR}"; exit 1; }

log "脚本开始执行"
# ... 业务逻辑 ...
log "脚本执行完成"

二、实战脚本 1:日志备份清理脚本

需求:每天把应用日志按天归档,只保留最近 30 天,超过的删除,防止磁盘被日志塞满。

#!/bin/bash
set -euo pipefail

APP_LOG="/var/log/myapp/access.log"
ARCHIVE_DIR="/var/log/myapp/archive"
KEEP_DAYS=30
STAMP=$(date +%Y%m%d)
LOG_FILE="/var/log/scripts/log_cleanup.log"

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

mkdir -p "${ARCHIVE_DIR}"

# 1. 把昨天的日志重命名归档(用 mv 比 copy+truncate 更省 IO)
if [ -f "${APP_LOG}" ]; then
    mv "${APP_LOG}" "${ARCHIVE_DIR}/access_${STAMP}.log"
    log "归档完成: access_${STAMP}.log"
fi

# 2. 通知应用重新打开日志文件(应用如支持 USR1 信号则最优雅)
# kill -USR1 $(cat /var/run/myapp.pid) 2>/dev/null || true

# 3. 删除超过保留天数的归档日志
find "${ARCHIVE_DIR}" -name "access_*.log" -mtime +${KEEP_DAYS} -type f -delete
log "已清理超过 ${KEEP_DAYS} 天的归档日志"

# 4. 统计磁盘占用并记录
du -sh "${ARCHIVE_DIR}" >> "${LOG_FILE}"

把这个脚本和 crontab 结合,每天凌晨执行:

crontab -e
# 每天 00:30 执行
30 0 * * * /usr/local/bin/log_cleanup.sh

进阶提示:这一步其实也可以用 logrotate 完成(按大小/天切割、压缩、保留份数一行配置),但手工脚本的优势是"想干什么都行"——比如归档后还要 rsync 到异地,脚本里加一行即可。两者结合使用效果最好。

三、实战脚本 2:MySQL 自动备份脚本

需求:每天凌晨全量备份所有数据库,压缩后保留 7 天,并把备份同步到异地服务器,防止服务器挂了备份也没了。

#!/bin/bash
set -euo pipefail

DB_USER="backup"
DB_PASS="Backup@2026"
BACKUP_DIR="/backup/mysql"
KEEP_DAYS=7
REMOTE_HOST="backup@192.168.1.200"
REMOTE_DIR="/backup/mysql"
STAMP=$(date +%Y%m%d_%H%M%S)
LOG_FILE="/var/log/scripts/mysql_backup.log"

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

mkdir -p "${BACKUP_DIR}"
log "开始备份 MySQL"

# 1. 全量备份:single-transaction 保证备份期间数据一致且不锁表
#    --routines/--triggers/--events 带上存储过程、触发器、事件,恢复才完整
mysqldump -u"${DB_USER}" -p"${DB_PASS}" \
    --single-transaction --routines --triggers --events \
    --databases --all-databases 2>/dev/null \
    | gzip > "${BACKUP_DIR}/mysql_full_${STAMP}.sql.gz"

log "备份完成: mysql_full_${STAMP}.sql.gz"

# 2. 删除超过 7 天的本地备份
find "${BACKUP_DIR}" -name "mysql_full_*.sql.gz" -mtime +${KEEP_DAYS} -delete

# 3. 异地同步(关键!防止同机房故障/勒索病毒把备份一起带走)
#    用 rsync 增量传输,传完再删本地旧备份更稳妥
rsync -avz --timeout=300 "${BACKUP_DIR}/" "${REMOTE_HOST}:${REMOTE_DIR}/" >> "${LOG_FILE}" 2>&1 \
    && log "异地同步完成" || log "异地同步失败,请检查网络"

log "备份流程结束"

关键点:

1. --single-transaction 对 InnoDB 表用一致性快照,备份时不锁表,线上直接跑没压力
2. 先 rsync 到异地、确认成功后再删本地旧备份,避免"本地删了、异地也没传上去"的尴尬
3. 备份文件别只留一份,7 天轮换 + 异地副本,覆盖"误删、损坏、机房故障"三种场景

恢复演练(定期做,别等出事才想起来):

# 解压并导入
gunzip -c /backup/mysql/mysql_full_最新.sql.gz | mysql -uroot -p
# 验证:查表数量、行数与生产对比,确认恢复可用
mysql -uroot -p -e "USE mydb; SELECT COUNT(*) FROM users;"

四、实战脚本 3:网站存活监控告警脚本

需求:每分钟检查网站首页是否正常返回 200,连续 3 次失败就发钉钉/飞书/微信告警,恢复后再通知。

#!/bin/bash
set -euo pipefail

URL="https://www.example.com/"
FAIL_FILE="/tmp/site_down_count"
NOTIFY_FILE="/tmp/site_notified"
WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=你的token"

# 检查一次
check() {
    # -s 静默、-o /dev/null 丢弃响应体、-w 只输出状态码、--max-time 防卡死
    code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "${URL}" || echo "000")
    echo "${code}"
}

notify() {
    local msg="$1"
    curl -s -H "Content-Type: application/json" -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"${msg}\"}}" "${WEBHOOK}" > /dev/null
}

code=$(check)

if [ "${code}" = "200" ]; then
    # 网站恢复:清空失败计数;如果之前告警过,发一条恢复通知
    if [ -f "${NOTIFY_FILE}" ]; then
        notify "✅ 网站已恢复: ${URL}"
        rm -f "${NOTIFY_FILE}"
    fi
    rm -f "${FAIL_FILE}"
else
    # 记录连续失败次数
    count=0
    [ -f "${FAIL_FILE}" ] && count=$(cat "${FAIL_FILE}")
    count=$((count + 1))
    echo "${count}" > "${FAIL_FILE}"

    if [ "${count}" -ge 3 ] && [ ! -f "${NOTIFY_FILE}" ]; then
        notify "⚠️ 网站连续 ${count} 次不可用: ${URL} (HTTP ${code})"
        touch "${NOTIFY_FILE}"
    fi
fi

配合 crontab 每分钟执行:

* * * * * /usr/local/bin/site_monitor.sh

思路值得借鉴:连续失败 N 次才告警(避免单次网络抖动误报),用标记文件防止重复告警轰炸,恢复后自动通知。这类逻辑同样适用于端口检查(nc)、磁盘使用率检查(df)、进程检查(pgrep)。

五、排错与调试:脚本坏了怎么快速定位

1. 开启调试模式 set -x

最常用的调试手段:执行时加 bash -x,脚本会逐行打印执行过程,变量值一目了然:

bash -x /usr/local/bin/site_monitor.sh

# 输出形如:
# + code=$(curl -s -o /dev/null -w %{http_code} --max-time 10 https://www.example.com/)
# ++ curl -s -o /dev/null -w %{http_code} ...
# + [ 200 = 200 ]

也可以在脚本某段前后手动开关:

set -x      # 打开调试
# ... 可疑代码 ...
set +x      # 关闭调试

2. 用 shellcheck 做静态检查

写脚本必备工具,能自动找出未加引号、可移植性、语法陷阱等几百种问题:

# 安装
apt install -y shellcheck

# 检查
shellcheck /usr/local/bin/mysql_backup.sh
# 输出形如:
# In mysql_backup.sh line 23:
# mysqldump -u"${DB_USER}" ...
# ^-- SC2086: Double quote to prevent globbing and word splitting.

3. 常见问题排查清单

1. 脚本手动执行成功、crontab 不执行 -> 大概率是 PATH 环境问题:
   crontab 环境 PATH 很精简,脚本里命令写绝对路径(/usr/bin/xxx)或开头 export PATH
2. 中文注释乱码 -> 文件编码必须是 UTF-8,且首行 #!/bin/bash 后无 BOM
3. 脚本里有\r 回车符报错 -> 在 Windows 编辑过,用 sed -i 's/\r$//' script.sh 去掉
4. 变量没加引号,路径带空格 -> "xxx" 包裹所有变量
5. 日志没输出 -> 检查日志目录权限,log 函数里的 >> 需要可写
6. 用 set -e 后"明明第2步失败了脚本还在跑" -> 确认是 set -euo pipefail 且管道各段都加了处理

4. 排错三板斧

1. bash -x 看执行过程
2. 看日志文件(脚本里 log() 留下的时间戳打点)
3. 手工拆分:把脚本复制一份,一段一段去掉,二分定位到具体语句

六、结语

三个脚本覆盖了运维最常碰的三件事:清日志、备数据库、盯存活。写脚本的核心不是记住每个语法,而是养成三个习惯:脚本规范化(set -euo pipefail + 日志打点)、关键操作留痕迹(日志文件 + 标记文件)、坏了有方法论(bash -x + shellcheck + 二分排除)。把重复劳动交给脚本,把精力留给真正需要判断力的事,这才是自动化的意义。


手机扫码阅读

微信或手机浏览器扫一扫,随时随地随心阅读与分享

Linux 磁盘在线扩容实战:LVM 逻辑卷扩容与云硬盘热扩容排错指南

Nginx 防盗链与静态资源缓存实战:referer 校验、expires 缓存与防滥用排错

评 论
更换验证码