前言
服务器运维离不开定时任务:日志清理、数据备份、监控告警、报表生成……手动执行既繁琐又容易遗漏。crontab是Linux下最常用的定时任务工具,配合shell脚本可以实现各种自动化需求。
本文整理crontab的使用技巧和常见自动化脚本,附带踩坑经验。
1. crontab基础
1.1 基本语法
* * * * * command │ │ │ │ │ │ │ │ │ └── 星期(0-7,0和7都是周日) │ │ │ └──── 月份(1-12) │ │ └────── 日期(1-31) │ └──────── 小时(0-23) └────────── 分钟(0-59)1.2 常用时间表达式
# 每分钟执行* * * * * /path/to/script.sh# 每小时整点执行0* * * * /path/to/script.sh# 每天凌晨2点执行02* * * /path/to/script.sh# 每周一凌晨3点执行03* *1/path/to/script.sh# 每月1号凌晨4点执行041* * /path/to/script.sh# 每5分钟执行*/5 * * * * /path/to/script.sh# 每天8点到18点,每小时执行08-18 * * * /path/to/script.sh# 工作日每天9点执行09* *1-5 /path/to/script.sh# 每天凌晨1点和13点执行01,13* * * /path/to/script.sh1.3 crontab管理命令
# 编辑当前用户的crontabcrontab-e# 查看当前用户的crontabcrontab-l# 删除当前用户的crontabcrontab-r# 编辑指定用户的crontab(需要root权限)crontab-u username -e# 从文件导入crontabcrontab/path/to/crontab.txt2. crontab踩坑指南
2.1 环境变量问题
crontab执行环境和交互式shell不同,PATH很精简:
# 错误:直接写命令名* * * * * mysql -e"select 1"# 正确:使用绝对路径* * * * * /usr/bin/mysql -e"select 1"# 或者在脚本开头设置PATH#!/bin/bashexportPATH=/usr/local/bin:/usr/bin:/bin查看crontab的环境变量:
# 创建一个临时任务* * * * *env>/tmp/cron_env.txt2.2 输出处理
默认情况下,crontab会把输出发到用户邮箱:
# 丢弃所有输出* * * * * /path/to/script.sh>/dev/null2>&1# 只记录错误* * * * * /path/to/script.sh>/dev/null2>>/var/log/cron_error.log# 记录所有输出* * * * * /path/to/script.sh>>/var/log/cron.log2>&1# 记录输出并带时间戳* * * * * /path/to/script.sh2>&1|whilereadline;doecho"$(date):$line";done>>/var/log/cron.log2.3 特殊字符转义
# 错误:%在crontab中是换行符02* * * /bin/date +%Y%m%d# 正确:转义%02* * * /bin/date +\%Y\%m\%d# 或者放到脚本里执行02* * * /path/to/backup.sh2.4 执行权限
# 脚本需要可执行权限chmod+x /path/to/script.sh# 或者用bash显式执行* * * * * /bin/bash /path/to/script.sh2.5 工作目录
# crontab默认工作目录是用户home# 脚本中使用相对路径可能出问题# 解决:在脚本开头切换目录#!/bin/bashcd/path/to/workdir||exit13. 实用自动化脚本
3.1 日志清理脚本
#!/bin/bash# clean_logs.sh - 清理过期日志LOG_DIRS=("/var/log/app""/data/logs/nginx""/data/logs/tomcat")KEEP_DAYS=30fordirin"${LOG_DIRS[@]}";doif[-d"$dir"];thenecho"清理目录:$dir"find"$dir"-name"*.log"-mtime +$KEEP_DAYS-deletefind"$dir"-name"*.log.gz"-mtime +$KEEP_DAYS-delete# 清理空目录find"$dir"-type d -empty -delete2>/dev/nullfidone# 清理系统日志journalctl --vacuum-time=${KEEP_DAYS}d2>/dev/nullecho"日志清理完成:$(date)"crontab配置:
# 每天凌晨3点清理日志03* * * /opt/scripts/clean_logs.sh>>/var/log/clean_logs.log2>&13.2 数据库备份脚本
#!/bin/bash# mysql_backup.sh - MySQL备份MYSQL_USER="backup"MYSQL_PASS="your_password"BACKUP_DIR="/data/backup/mysql"KEEP_DAYS=7DATE=$(date+%Y%m%d_%H%M%S)# 创建备份目录mkdir-p"$BACKUP_DIR"# 获取所有数据库databases=$(mysql -u"$MYSQL_USER"-p"$MYSQL_PASS"-e"SHOW DATABASES;"|grep-Ev"(Database|information_schema|performance_schema|sys)")fordbin$databases;doecho"备份数据库:$db"mysqldump -u"$MYSQL_USER"-p"$MYSQL_PASS"\--single-transaction\--routines\--triggers\"$db"|gzip>"$BACKUP_DIR/${db}_${DATE}.sql.gz"done# 清理过期备份find"$BACKUP_DIR"-name"*.sql.gz"-mtime +$KEEP_DAYS-delete# 统计备份大小echo"备份完成,总大小:$(du-sh $BACKUP_DIR|cut-f1)"3.3 磁盘空间监控脚本
#!/bin/bash# disk_monitor.sh - 磁盘空间监控THRESHOLD=80ALERT_EMAIL="admin@example.com"# 获取磁盘使用率df-h|awk'NR>1 {print $5,$6}'|whilereadusagemount;do# 去掉百分号usage_num=${usage%\%}if["$usage_num"-gt"$THRESHOLD"];thenmessage="警告:$mount磁盘使用率$usage超过阈值${THRESHOLD}%"echo"$message"# 发送告警(可选)# echo "$message" | mail -s "磁盘告警 - $(hostname)" $ALERT_EMAIL# 或者发送到钉钉/企业微信# curl -X POST "https://webhook.url" -d "{\"text\":\"$message\"}"fidone3.4 服务健康检查脚本
#!/bin/bash# service_check.sh - 服务健康检查SERVICES=("nginx:80""mysql:3306""redis:6379""app:8080")check_port(){localname=$1localport=$2ifnc-z localhost$port2>/dev/null;thenecho"[OK]$name(port$port)"return0elseecho"[FAIL]$name(port$port)"return1fi}forservicein"${SERVICES[@]}";doname=${service%:*}port=${service#*:}if!check_port"$name""$port";then# 尝试重启服务echo"尝试重启$name..."systemctl restart$name2>/dev/nullsleep5if!check_port"$name""$port";thenecho"重启失败,发送告警"# 发送告警逻辑fifidone3.5 SSL证书到期检查
#!/bin/bash# ssl_check.sh - SSL证书到期检查DOMAINS=("www.example.com""api.example.com")WARN_DAYS=30fordomainin"${DOMAINS[@]}";do# 获取证书到期时间expiry_date=$(echo|openssl s_client -servername"$domain"-connect"$domain:443"2>/dev/null|\openssl x509 -noout -enddate2>/dev/null|cut-d=-f2)if[-z"$expiry_date"];thenecho"[ERROR] 无法获取$domain证书信息"continuefi# 计算剩余天数expiry_epoch=$(date-d"$expiry_date"+%s)current_epoch=$(date+%s)days_left=$(((expiry_epoch-current_epoch)/86400))if[$days_left-lt$WARN_DAYS];thenecho"[WARN]$domain证书将在$days_left天后过期"elseecho"[OK]$domain证书剩余$days_left天"fidone4. 多服务器定时任务管理
4.1 问题场景
当管理多台服务器时,定时任务分散在各机器上,维护起来很麻烦:
- 修改任务要逐台登录
- 不知道哪台机器上有什么任务
- 任务执行情况难以统计
4.2 集中管理方案
方案一:Ansible管理crontab
# crontab.yml-hosts:alltasks:-name:部署日志清理任务cron:name:"clean logs"minute:"0"hour:"3"job:"/opt/scripts/clean_logs.sh >> /var/log/clean_logs.log 2>&1"-name:部署磁盘监控任务cron:name:"disk monitor"minute:"*/10"job:"/opt/scripts/disk_monitor.sh"执行:
ansible-playbook -i inventory crontab.yml方案二:集中式任务调度
使用专门的任务调度系统:
- XXL-JOB
- Airflow
- Jenkins Pipeline
这些系统可以集中管理所有服务器的定时任务,还有执行日志、失败重试、依赖调度等功能。
4.3 跨网络批量管理
如果服务器分布在不同网络环境(办公室、家里、云服务器),Ansible连接就比较麻烦。
常见解决方案:
- 跳板机:所有机器都能连接的中转服务器
- VPN:把所有机器组到一个网络
- 组网工具:WireGuard、ZeroTier、星空组网等,配置简单,把机器串成虚拟局域网
用组网工具的好处是配置一次后,后续直接用内网IP管理,不用关心机器实际在哪个网络。Ansible的inventory直接写虚拟IP就行:
# inventory [web] 10.10.0.1 10.10.0.2 [db] 10.10.0.105. 任务执行监控
5.1 记录任务执行日志
#!/bin/bash# 通用任务包装器# usage: task_wrapper.sh <task_name> <command>TASK_NAME=$1shiftCOMMAND="$@"LOG_DIR="/var/log/cron_tasks"LOG_FILE="$LOG_DIR/${TASK_NAME}.log"mkdir-p"$LOG_DIR"START_TIME=$(date'+%Y-%m-%d %H:%M:%S')echo"[$START_TIME] 开始执行:$TASK_NAME">>"$LOG_FILE"# 执行任务OUTPUT=$($COMMAND2>&1)EXIT_CODE=$?END_TIME=$(date'+%Y-%m-%d %H:%M:%S')DURATION=$SECONDSecho"$OUTPUT">>"$LOG_FILE"echo"[$END_TIME] 执行完成,退出码:$EXIT_CODE,耗时:${DURATION}秒">>"$LOG_FILE"echo"---">>"$LOG_FILE"# 如果失败,发送告警if[$EXIT_CODE-ne0];thenecho"任务失败:$TASK_NAME, 退出码:$EXIT_CODE"# 告警逻辑fiexit$EXIT_CODE使用方式:
# crontab02* * * /opt/scripts/task_wrapper.sh mysql_backup /opt/scripts/mysql_backup.sh5.2 任务执行统计
#!/bin/bash# task_stats.sh - 统计任务执行情况LOG_DIR="/var/log/cron_tasks"echo"=== 任务执行统计 ==="echo""forlogin"$LOG_DIR"/*.log;dotask_name=$(basename"$log".log)# 统计成功/失败次数total=$(grep-c"执行完成""$log"2>/dev/null||echo0)success=$(grep"退出码: 0""$log"|wc-l)failed=$((total-success))# 最后执行时间last_run=$(grep"开始执行""$log"|tail-1|grep-oP'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}')printf"%-20s 总计: %-5s 成功: %-5s 失败: %-5s 最后执行: %s\n"\"$task_name""$total""$success""$failed""$last_run"done6. 高级技巧
6.1 防止任务重复执行
#!/bin/bash# 使用flock防止重复执行LOCK_FILE="/tmp/my_task.lock"exec200>"$LOCK_FILE"flock -n200||{echo"任务正在执行中,退出"exit1}# 正常的任务逻辑echo"开始执行..."sleep60echo"执行完成"或者在crontab中直接使用:
* * * * * flock -n /tmp/my_task.lock /path/to/script.sh6.2 任务超时控制
# 使用timeout命令限制执行时间* * * * *timeout300/path/to/script.sh# 超时后发送SIGKILL* * * * *timeout-k10300/path/to/script.sh6.3 随机延迟启动
避免所有机器同时执行任务(比如同时访问某个API):
#!/bin/bash# 随机延迟0-300秒sleep$((RANDOM%300))# 执行实际任务/path/to/actual_script.sh6.4 任务依赖
#!/bin/bash# 先执行备份,成功后再清理/opt/scripts/mysql_backup.sh&&/opt/scripts/clean_old_backup.sh更复杂的依赖关系建议用专门的调度系统。
7. systemd timer替代方案
systemd timer是crontab的现代替代:
# /etc/systemd/system/backup.service [Unit] Description=MySQL Backup [Service] Type=oneshot ExecStart=/opt/scripts/mysql_backup.sh# /etc/systemd/system/backup.timer [Unit] Description=Run backup daily [Timer] OnCalendar=*-*-* 02:00:00 Persistent=true [Install] WantedBy=timers.target启用:
systemctlenablebackup.timer systemctl start backup.timer# 查看所有timersystemctl list-timerssystemd timer优点:
- 日志集成到journalctl
- 支持更复杂的时间表达式
- 可以设置资源限制
- 错过的任务可以补执行(Persistent=true)
总结
| 场景 | crontab表达式 |
|---|---|
| 每分钟 | * * * * * |
| 每5分钟 | */5 * * * * |
| 每小时 | 0 * * * * |
| 每天凌晨2点 | 0 2 * * * |
| 每周一9点 | 0 9 * * 1 |
| 每月1号 | 0 0 1 * * |
| 工作日 | 0 9 * * 1-5 |
最佳实践:
- 使用绝对路径:脚本和命令都用绝对路径
- 处理输出:重定向到日志文件,避免邮件堆积
- 防止重复:使用flock防止任务重叠
- 超时控制:长任务加timeout限制
- 监控告警:记录执行日志,失败时告警
- 集中管理:多服务器用Ansible或调度系统统一管理