接手一台Debian服务器,最头疼的不是初始配置,而是日复一日的维护。磁盘满了、日志不清理、备份忘记做、证书过期了、服务挂了没发现,这些问题消耗大量精力。把常见维护场景写成脚本,定时执行或一键调用,才能真正解放双手。下面直接分享我生产环境里正在用的自动化脚本,覆盖系统健康检查、日志清理、自动备份、证书续期、进程守护五个核心场景。
系统健康检查与资源监控脚本服务器出问题往往有前兆:磁盘使用率飙升、内存不足、CPU负载异常。这个脚本把这些指标一次性采集,超过阈值自动告警。告警方式可以根据实际情况改成企业微信、钉钉或者邮件。
#!/bin/bash
# 系统健康检查脚本
HOSTNAME=$(hostname)
DATE=$(date "+%Y-%m-%d %H:%M:%S")
LOG_FILE="/var/log/health_check.log"
# 阈值设置
CPU_THRESHOLD=80
MEM_THRESHOLD=90
DISK_THRESHOLD=85
LOAD_THRESHOLD=$(nproc)
# CPU使用率
CPU_USAGE=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)
CPU_INT=${CPU_USAGE%.*}
# 内存使用率
MEM_TOTAL=$(free -m | awk '/Mem:/{print $2}')
MEM_USED=$(free -m | awk '/Mem:/{print $3}')
MEM_USAGE=$((MEM_USED * 100 / MEM_TOTAL))
# 磁盘使用率
DISK_USAGE=$(df -h / | awk 'NR==2 {print $5}' | sed 's/%//')
# 系统负载
LOAD_AVG=$(uptime | awk -F'load average:' '{print $2}' | awk '{print $1}' | sed 's/,//')
LOAD_INT=${LOAD_AVG%.*}
# 记录日志
echo "[$DATE] CPU:${CPU_USAGE}% MEM:${MEM_USAGE}% DISK:${DISK_USAGE}% LOAD:${LOAD_AVG}" >> $LOG_FILE
# 告警判断
if [ "$CPU_INT" -gt "$CPU_THRESHOLD" ]; then
echo "告警:$HOSTNAME CPU使用率超过${CPU_THRESHOLD}%,当前${CPU_USAGE}%" >> $LOG_FILE
fi
if [ "$MEM_USAGE" -gt "$MEM_THRESHOLD" ]; then
echo "告警:$HOSTNAME 内存使用率超过${MEM_THRESHOLD}%,当前${MEM_USAGE}%" >> $LOG_FILE
fi
if [ "$DISK_USAGE" -gt "$DISK_THRESHOLD" ]; then
echo "告警:$HOSTNAME 磁盘使用率超过${DISK_THRESHOLD}%,当前${DISK_USAGE}%" >> $LOG_FILE
fi
if [ "$LOAD_INT" -gt "$LOAD_THRESHOLD" ]; then
echo "告警:$HOSTNAME 系统负载过高,当前${LOAD_AVG}" >> $LOG_FILE
fi
把这个脚本加入crontab,每10分钟执行一次。告警信息会写入日志文件,后续可以配合日志采集系统做集中告警。磁盘检查部分只看了根分区,如果你的服务器挂载了多个数据盘,需要把df那行改成循环遍历所有分区。CPU阈值设80是经验值,实际业务中如果经常触发,要么优化应用,要么升级配置,别让告警变成狼来了。
智能日志清理与轮转脚本日志文件是磁盘占用的头号元凶。logrotate虽然能用,但配置繁琐,而且有些自定义日志路径它管不到。下面这个脚本按时间和大小双重策略清理,同时打包归档最近7天的日志,万一需要回溯还能找到。
#!/bin/bash
# 日志清理脚本
BASE_DIR="/var/log/app"
ARCHIVE_DIR="/data/logs/archive"
RETENTION_DAYS=30
ARCHIVE_DAYS=7
MAX_SIZE_MB=500
mkdir -p $ARCHIVE_DIR
# 清理超过保留天数的日志
find $BASE_DIR -type f -name "*.log" -mtime +$RETENTION_DAYS -exec rm -f {} \;
echo "$(date) 已清理${RETENTION_DAYS}天前的日志文件"
# 归档最近几天的日志
find $BASE_DIR -type f -name "*.log" -mtime -$ARCHIVE_DAYS -mtime +1 | while read file; do
dir_name=$(dirname "$file")
app_name=$(basename "$dir_name")
tar -czf "${ARCHIVE_DIR}/${app_name}_$(date +%Y%m%d).tar.gz" "$file" 2>/dev/null
done
# 清理超过阈值的单个大日志文件
find $BASE_DIR -type f -name "*.log" -size +${MAX_SIZE_MB}M | while read file; do
cat /dev/null > "$file"
echo "$(date) 已清空超大日志文件:$file"
done
# 清理归档目录中超过60天的压缩包
find $ARCHIVE_DIR -type f -name "*.tar.gz" -mtime +60 -exec rm -f {} \;
这个脚本的精髓在于分层处理。超过30天的直接删除,1到7天内的归档压缩,超过500MB的大文件直接清空但保留文件句柄,不影响正在写入的服务。实际使用时把BASE_DIR改成你的应用日志目录,多个目录可以写循环。归档目录建议放在单独的数据盘,避免日志把系统盘撑爆导致服务器无法登录。
数据库与文件全量增量备份脚本备份是运维的生命线。这个脚本同时处理MySQL数据库和指定目录的文件,支持全量备份和增量差异备份,备份完成后自动校验完整性,保留最近7份全量备份,多余的自动删除。
#!/bin/bash
# 全量备份脚本
BACKUP_BASE="/data/backup"
DB_USER="backup_user"
DB_PASS="your_password"
DB_NAME="your_database"
WEB_DIR="/var/www"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="${BACKUP_BASE}/${DATE}"
mkdir -p $BACKUP_DIR
# MySQL全量备份
mysqldump -u$DB_USER -p$DB_PASS --single-transaction --routines --triggers $DB_NAME | gzip > "${BACKUP_DIR}/db_${DB_NAME}_${DATE}.sql.gz"
# 校验数据库备份完整性
if [ ${PIPESTATUS[0]} -ne 0 ]; then
echo "数据库备份失败!"
exit 1
fi
# 文件增量备份,使用rsync配合硬链接实现增量
LATEST_LINK="${BACKUP_BASE}/latest"
if [ -d "$LATEST_LINK" ]; then
rsync -a --delete --link-dest="$LATEST_LINK" $WEB_DIR "${BACKUP_DIR}/www/"
else
rsync -a $WEB_DIR "${BACKUP_DIR}/www/"
fi
rm -f "$LATEST_LINK"
ln -s "${BACKUP_DIR}/www" "$LATEST_LINK"
# 生成备份清单
find $BACKUP_DIR -type f -exec md5sum {} \; > "${BACKUP_DIR}/checksum.md5"
# 保留最近7份全量备份
cd $BACKUP_BASE
ls -dt */ | tail -n +8 | xargs rm -rf
echo "$(date) 备份完成,备份目录:$BACKUP_DIR"
这个备份方案用到了rsync的--link-dest参数,这是节省空间的利器。每次全量备份时,未变化的文件会创建硬链接指向上一份备份,实际只占用变化文件的空间。看起来每份备份都是完整的,但磁盘占用远小于预期。数据库备份用--single-transaction保证InnoDB表的一致性,如果你的数据库有MyISAM表,需要换成--lock-tables。备份完成后记得把脚本加入crontab,每天凌晨执行,同时配置异地同步,把备份文件传输到另一台服务器或对象存储。
Let's Encrypt证书自动续期与部署脚本SSL证书过期导致网站无法访问的事故太常见了。certbot官方工具虽然能自动续期,但续期后往往需要重启Nginx才能加载新证书。这个脚本把续期、部署、重载服务串起来,同时增加续期失败的重试机制。
#!/bin/bash
# 证书续期脚本
DOMAINS=("example.com" "api.example.com")
EMAIL="admin@example.com"
RETRY_COUNT=3
WEBROOT="/var/www/html"
for domain in "${DOMAINS[@]}"; do
attempt=0
while [ $attempt -lt $RETRY_COUNT ]; do
certbot certonly --webroot -w $WEBROOT -d $domain --non-interactive --agree-tos --email $EMAIL --quiet
if [ $? -eq 0 ]; then
echo "$(date) $domain 证书续期成功"
break
else
attempt=$((attempt + 1))
echo "$(date) $domain 证书续期失败,第${attempt}次重试"
sleep 60
fi
done
if [ $attempt -eq $RETRY_COUNT ]; then
echo "$(date) $domain 证书续期最终失败,请手动处理!"
fi
done
# 检查Nginx配置并重载
nginx -t && systemctl reload nginx
echo "$(date) Nginx已重载,新证书已生效"
certbot默认的续期命令是certbot renew,它会检查所有已签发证书,到期前30天内才会真正续期。我习惯用certonly指定域名,这样更可控。webroot方式验证不需要暂停Nginx,比standalone模式更友好。如果你的Nginx配置里证书路径写死了,续期后不需要额外操作,certbot会自动替换文件。但有些安装方式证书存在自定义路径,需要加--cert-path和--key-path参数指定。重试机制很重要,Let's Encrypt的验证服务器偶尔抽风,三次重试间隔60秒能覆盖大部分临时故障。
服务进程守护与自动重启脚本即使用了systemd,某些进程还是可能异常退出而不自动重启。这个脚本监控关键进程,发现挂了立即拉起,同时记录重启次数,短时间内频繁重启则发出告警,避免陷入死循环。
#!/bin/bash
# 进程守护脚本
PROCESS_LIST=("nginx" "php-fpm" "redis-server" "supervisor")
RESTART_COUNT_FILE="/tmp/restart_count.log"
MAX_RESTART=5
CHECK_INTERVAL=60
check_and_restart() {
local process=$1
if ! pgrep -x "$process" > /dev/null; then
echo "$(date) 检测到 $process 未运行,尝试重启"
systemctl restart $process 2>/dev/null || service $process restart 2>/dev/null
# 记录重启次数
local count=$(grep -c "$process" $RESTART_COUNT_FILE 2>/dev/null || echo 0)
echo "$(date) $process 已重启" >> $RESTART_COUNT_FILE
# 检查短时间内重启次数
local recent_count=$(grep "$process" $RESTART_COUNT_FILE | wc -l)
if [ "$recent_count" -gt "$MAX_RESTART" ]; then
echo "告警:$process 在短时间内重启超过${MAX_RESTART}次,可能存在严重问题!"
fi
fi
}
while true; do
for process in "${PROCESS_LIST[@]}"; do
check_and_restart "$process"
done
sleep $CHECK_INTERVAL
done
这个脚本建议用systemd来管理它本身,保证守护进程自己不会挂。创建/etc/systemd/system/process-guard.service,设置Restart=always。PROCESS_LIST里填你需要监控的进程名,必须是pgrep能精确匹配的名字。频繁重启的判定逻辑比较粗糙,实际使用可以改成统计最近10分钟内的重启次数,用时间戳过滤更准确。如果你的服务是用Docker跑的,把systemctl restart换成docker restart命令即可。
一键安全加固脚本新装完系统,安全配置往往被忽略。这个脚本把常见的安全加固操作汇总,包括修改SSH端口、禁用root密码登录、配置fail2ban、设置防火墙规则、开启自动安全更新。
#!/bin/bash # 安全加固脚本 SSH_PORT=2222 # 修改SSH端口并禁用root密码登录 sed -i "s/#Port 22/Port $SSH_PORT/" /etc/ssh/sshd_config sed -i 's/PermitRootLogin yes/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config systemctl restart sshd # 安装并配置fail2ban apt-get update && apt-get install -y fail2ban cat > /etc/fail2ban/jail.local << EOF [DEFAULT] bantime = 3600 findtime = 600 maxretry = 5 [sshd] enabled = true port = $SSH_PORT logpath = /var/log/auth.log EOF systemctl enable fail2ban && systemctl restart fail2ban # 配置iptables基础规则 iptables -F iptables -A INPUT -i lo -j ACCEPT iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT iptables -A INPUT -p tcp --dport $SSH_PORT -j ACCEPT iptables -A INPUT -p tcp --dport 80 -j ACCEPT iptables -A INPUT -p tcp --dport 443 -j ACCEPT iptables -A INPUT -j DROP apt-get install -y iptables-persistent && netfilter-persistent save # 开启自动安全更新 apt-get install -y unattended-upgrades dpkg-reconfigure -plow unattended-upgrades echo "$(date) 安全加固完成,SSH端口已更改为$SSH_PORT"
这个脚本执行前务必确认你已经有其他方式登录服务器,比如已经配置好SSH密钥。改完SSH端口后,新会话用新端口连接,当前会话不会断开,这是测试的好机会。fail2ban的配置可以根据需要增加Nginx、MySQL等服务的防护规则。iptables规则比较基础,只开放了SSH、HTTP、HTTPS端口,如果你的服务器还跑其他服务,记得加上对应端口。unattended-upgrades只安装安全更新,不会升级系统大版本,相对安全。
定时任务集中管理脚本服务器上的crontab条目多了以后很难管理,不知道哪些任务在跑、什么时候跑的、执行结果如何。这个脚本提供一个集中管理框架,所有定时任务统一放在一个目录,每个任务独立脚本,执行结果统一记录,失败任务自动告警。
#!/bin/bash
# 定时任务调度器
TASK_DIR="/opt/cron_tasks"
LOG_DIR="/var/log/cron_tasks"
FAILED_LOG="${LOG_DIR}/failed.log"
mkdir -p $LOG_DIR
run_task() {
local task_script=$1
local task_name=$(basename "$task_script")
local task_log="${LOG_DIR}/${task_name}_$(date +%Y%m%d).log"
echo "========== $(date) 开始执行 $task_name ==========" >> "$task_log"
start_time=$(date +%s)
bash "$task_script" >> "$task_log" 2>&1
exit_code=$?
end_time=$(date +%s)
duration=$((end_time - start_time))
echo "========== $(date) 执行完成,耗时${duration}秒,退出码${exit_code} ==========" >> "$task_log"
if [ $exit_code -ne 0 ]; then
echo "$(date) $task_name 执行失败,退出码${exit_code}" >> "$FAILED_LOG"
fi
}
# 遍历任务目录执行所有脚本
for task in $TASK_DIR/*.sh; do
if [ -x "$task" ]; then
run_task "$task"
fi
done
使用方式很简单:把具体的任务脚本放在/opt/cron_tasks目录下,赋予执行权限,然后在crontab里只加一条记录,每小时或每天调用一次这个调度器。每个任务的日志独立存储,方便排查问题。任务脚本里用exit 0表示成功,exit 1表示失败,调度器会根据退出码判断是否记录到失败日志。这个框架的好处是新增任务不需要改crontab,直接放脚本进去就行,团队协作时也更清晰。
把这些脚本串联起来的思路单个脚本解决单个问题,但真正高效的运维需要把它们组织成体系。我的做法是:健康检查脚本每10分钟跑一次,告警信息接入即时通讯工具;备份脚本每天凌晨3点执行,完成后触发异地同步;日志清理每周日凌晨执行,避开业务高峰;证书续期每周一上午检查一次;进程守护用systemd常驻运行;安全加固只在初始化时执行一次,后续手动维护。所有脚本的输出统一写入/var/log/ops目录,配合日志采集做集中展示。脚本本身用Git管理,修改有记录,多台服务器通过配置管理工具批量下发。这套方案在十几台Debian服务器上稳定运行了两年多,从没出过大纰漏。脚本不是写完就完事了,根据业务变化持续迭代才是关键。
