接手一台Ubuntu服务器,最怕的就是半夜被报警电话叫醒,原因仅仅是磁盘日志没清理或者一个数据同步进程僵死了。运维的终极目标不是炫技,而是让自己“无事可做”。实现这一目标的核心手段,就是把所有重复性的、需要凌晨执行的操作,全部交给系统定时任务和脚本自动化去完成。在Ubuntu环境中,这主要依赖Cron服务与Shell脚本的深度结合。
理解Cron的调度逻辑与语法细节Cron是Ubuntu服务器上默认安装的后台守护进程,它每分钟检查一次预定的任务列表,一旦时间匹配就立即执行。很多人配置完发现不生效,往往是因为忽略了环境变量。Cron运行的环境极其干净,它不会加载你平时在交互式终端里配置的PATH、JAVA_HOME或者Python虚拟环境。因此,编写定时任务的第一原则是:所有命令必须使用绝对路径,或者在脚本开头重新声明环境变量。
Cron的表达式分为五个字段:分、时、日、月、周。很多新手容易在“日”和“周”的混合使用上栽跟头。如果你同时指定了“日”和“周”,比如每个月的1号且是周三才执行,这通常不是你想要的结果。实际上,当这两个字段都有限定时,Cron执行的是“或”逻辑,也就是1号执行,周三也执行。要避免这种歧义,建议只用一个字段限定日期,把复杂的逻辑判断交给脚本内部去处理。
编辑定时任务使用 crontab -e 命令。这里有个关键点:不要直接去修改 /etc/crontab 或 /var/spool/cron 下的文件,通过 crontab 命令操作才能保证语法检查生效。一个典型的任务条目如下:
# 每天凌晨2点执行数据库备份脚本 0 2 * * * /usr/local/bin/backup_db.sh >> /var/log/backup.log 2>&1
这里的 >> 是追加输出,2>&1 是把错误输出也重定向到同一个日志文件。如果你不重定向输出,Cron会在执行后尝试给系统用户发邮件,如果邮件服务没配置好,这些输出就会堆积在死信队列里,造成磁盘空间的隐性消耗。
脚本设计的健壮性:锁机制与错误处理定时任务最怕的不是执行失败,而是前一次还没跑完,后一次又启动了。比如一个处理大数据的脚本需要跑40分钟,但你设了每10分钟执行一次,瞬间就会产生多个进程把内存和CPU吃光。解决这个问题必须在脚本内部实现互斥锁。
使用flock命令是最高效的锁机制,它利用文件描述符加锁,即便脚本异常退出,锁也会被操作系统自动释放。写法如下:
#!/bin/bash exec 200>/var/lock/myscript.lock flock -n 200 || exit 1 # 这里是实际业务逻辑 sleep 3600
这段代码尝试获取文件锁,如果获取失败说明已有实例在运行,直接退出。exec 200> 是打开一个文件描述符,flock -n 是非阻塞模式。这套机制比用ps grep进程名判断要可靠得多,因为进程名可能被伪造,而文件锁是内核级别的原子操作。
错误处理同样重要。很多脚本默认是遇到错误继续执行,这在运维中非常危险。你必须在脚本开头加上 set -euo pipefail。set -e 让脚本在任何命令返回非零状态时立即退出;set -u 在使用未定义变量时报错;set -o pipefail 让管道中任何一个命令失败都算整个管道失败。这三者组合能防止脚本在错误状态下越跑越偏,比如删错了目录或者覆盖了重要文件。
日志轮转与磁盘空间守护自动化运维最典型的应用场景就是日志管理。应用日志如果不加控制,几天就能撑爆根分区。logrotate是Ubuntu自带的日志轮转工具,但默认配置往往不够精细。你需要理解它的运行机制:logrotate本身并不是守护进程,它依赖Cron每天触发,配置文件在/etc/logrotate.d/目录下。
一个生产级别的日志轮转配置应该包含压缩、延迟压缩、按大小切割和保留份数控制:
/var/log/myapp/*.log {
daily
rotate 7
size 100M
compress
delaycompress
missingok
notifempty
copytruncate
dateext
}
这里有几个容易忽略的参数:delaycompress是延迟压缩,让最近切割的那个日志保持未压缩状态,方便程序继续写入或排查问题;copytruncate是复制后清空原文件,而不是移动文件再创建新的,这对那些不支持重新打开日志文件的应用至关重要;dateext在文件名后加日期后缀,避免同名文件被覆盖。
除了应用日志,系统级的journald日志也会悄悄膨胀。你可以通过修改 /etc/systemd/journald.conf 限制其最大占用空间:
SystemMaxUse=500M SystemKeepFree=1G
修改后执行 journalctl --vacuum-size=500M 立即生效。把这套检查也写进定时任务,每周执行一次磁盘空间巡检,当某分区使用率超过阈值时自动清理并发送告警,才能真正做到高枕无忧。
数据库备份的自动化策略数据库备份是定时任务的重灾区。很多人只备份了数据文件,却不知道在备份期间数据库可能还在写入,导致备份出来的文件不一致。对于MySQL或MariaDB,必须使用mysqldump的--single-transaction参数,它会在备份开始时创建一个快照读,保证数据一致性。对于PostgreSQL,使用pg_dump配合--clean参数。
更稳妥的做法是结合二进制日志做增量备份。全量备份可以每天一次,增量备份每小时一次。脚本逻辑应该是这样的:先判断今天是周几,如果是周日执行全量备份并刷新二进制日志,其他日子只备份二进制日志文件。备份完成后,还要验证备份文件的完整性,比如用gzip -t测试压缩包是否损坏,或者用head查看文件头部是否正常。最后一步是把备份文件同步到远程服务器或对象存储,rsync配合ssh密钥认证是最稳定的传输方案。
这里给出一个备份脚本的核心片段:
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/data/backup/mysql"
DATE=$(date +%Y%m%d)
DB_USER="backup"
DB_PASS="your_password"
mysqldump -u${DB_USER} -p${DB_PASS} --single-transaction --all-databases \
| gzip > ${BACKUP_DIR}/full_${DATE}.sql.gz
# 验证备份
if gzip -t ${BACKUP_DIR}/full_${DATE}.sql.gz; then
find ${BACKUP_DIR} -name "full_*.sql.gz" -mtime +7 -delete
else
echo "Backup verification failed" | mail -s "DB Backup Error" admin@example.com
exit 1
fi
注意这里密码是写在脚本里的,更安全的做法是使用MySQL的加密登录配置文件.my.cnf,把权限设为600,这样脚本里就不需要暴露明文密码。
监控探针与自愈机制定时任务不仅能做备份,还能充当简易的监控探针。每5分钟检查一次Nginx或业务进程是否存活,如果挂了就自动拉起,这比专门搭建一套监控系统轻量得多。但要注意区分“进程死了”和“服务假死”。进程存在不代表服务正常,比如Nginx进程还在,但已经无法响应请求。所以脚本里应该用curl或wget去请求本地健康检查接口,根据HTTP返回码判断。
一个自愈脚本的示例:
#!/bin/bash
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:80/health)
if [ "$HTTP_CODE" != "200" ]; then
systemctl restart nginx
echo "Nginx restarted at $(date)" >> /var/log/autoheal.log
fi
这种自愈机制要加上冷却时间,防止服务频繁重启。可以在重启后touch一个标记文件,下次检查时如果发现标记文件存在且时间在5分钟内,就不再重启,避免陷入死循环。
Ansible与Cron的配合使用当服务器数量超过3台,手动在每台机器上配置Cron就变得不现实了。这时候应该用Ansible来统一管理定时任务。Ansible的cron模块可以直接声明式地定义任务,它会自动处理任务的添加、修改和删除。一个playbook示例如下:
- name: 配置日志清理任务
cron:
name: "clean old logs"
minute: "0"
hour: "3"
job: "/usr/local/bin/clean_logs.sh"
user: root
state: present
这种方式的优势在于版本控制。所有定时任务都变成了代码,存放在Git仓库里,任何变更都有记录,出问题可以快速回滚。你还可以在Ansible里加入cronvar模块来统一设置Cron的环境变量,解决不同机器上路径不一致的问题。
自动化运维的本质是把经验固化成代码。每解决一个线上问题,就应该思考如何把它变成脚本和定时任务,让系统下次遇到同样情况时能自动处理。从Cron的精确调度,到脚本的锁机制和错误处理,再到备份策略和自愈逻辑,这些组件共同构成了一套无需人工干预的运维体系。当你发现自己的服务器连续运行几个月都不需要登录上去敲命令时,这套自动化体系才算真正建成了。
