
前言在生产环境里面, 定时任务是到处都在的: 比如日志文件的轮换处理、数据库的数据备份、SSL证书的重新颁发更新、以及不同服务器之间的数据同步等等。而在这些自动化操作背后的帮助者, 其实就是cron这个后台运行服务程序和相应的命令工具。因为一个每天都需要跟Linux操作系统来打交道的软件开发人员, 可能已经编写过非常多次的cron时间表达式了, 但是你是否真的明白它内部的运行原理呢?今天我们来深入聊聊命令, 会从底层的实现一直到实战的技巧都讲一下, 把这块知识彻底吃透。cron 的架构设计cron 不是一个简单的命令而是一整套系统┌─────────────────────────────────────────┐│ cron 守护进程 ││ (每分钟唤醒检查所有 crontab 文件) │└─────────────────┬───────────────────────┘│┌─────────────┼─────────────┐┌───────┐ ┌───────────┐ ┌──────────┐│系统级 │ │ 用户级 │ │ /etc/ ││crontab│ │ crontab │ │ cron.d/ │└───────┘ └───────────┘ └──────────┘关键文件位置这个 cron 服务的守护程序, 它每一分钟都会去做这么一些事情, 总共有三件:加载所有文件到内存中, 检查当前时间是否匹配任何任务的时间表达式, 以任务所有者身份执行匹配的任务, 并且进行时间表达式的解析。经典的五字段格式* * * * *│ │ │ │ └─── 星期几 (0-7, 0和7都表示周日)│ │ │ └──────── 月份 (1-12)│ │ └───────────── 日期 (1-31)│ └────────────────── 小时 (0-23)└─────────────────────── 分钟 (0-59)有一些细枝末节的小事, 它们特别容易被人忽视或者忘记考虑到。1. 关于星期的边界情况, 具体是什么事情。在星期这个字段里, 数字 0 以及数字 7, 这两者所表示的指向性都是指周日, 这是符合 POSIX 标准的要求。但是呢, 具体的不同的执行和实现层面, 情况之间是会存在着差异性。# 标准 cron0 和 7 都是周日0 0 * * 0 # 每周日凌晨0 0 * * 7 # 同上# 如果同时指定日期和星期是 OR 关系0 0 1 * 1 # 每月1号 OR 每周一凌晨执行2. 步长表达式的陷阱# 每2小时执行错误写法0 */2 * * * # 会在 0,2,4,6,8,10,12,14,16,18,20,22 点执行# 每2小时执行另一种写法0 0-23/2 * * * # 同上# 每分钟执行常见的错误* */2 * * * # 这是每分钟执行因为分钟字段是 **/2 * * * * # 这才是每2分钟执行3. 关于那些特殊的字符, 它们所拥有的优先权。# 最后一个工作日某些实现不支持0 0 * * 1-5 # 周一到周五# 月末执行扩展语法0 0 L * * # 部分实现支持 L 表示最后一天你要进行查看的操作以及编辑的事情也包括删除, 这些内容都是关于那个核心命令的部分。# 查看当前用户的 crontabcrontab -l# 编辑 crontab会打开默认编辑器crontab -e# 删除所有定时任务crontab -r# 删除前确认crontab -ri# 指定用户操作需要 root 权限crontab -u www-data -lcrontab -u www-data -e从文件导入# 从文件导入 crontabcrontab my_cron_tasks.txt# 这会完全覆盖现有的 crontab不是追加这是一个实用性比较强的小窍门, 具体的操作方法是借助于脚本来实施管理。#!/bin/bash# 导出现有 crontab 到文件crontab -l current_cron.txt# 追加新任务echo 0 3 * * * /opt/scripts/backup.sh current_cron.txt# 重新加载crontab current_cron.txt在执行环境里面, 你可能会遇到一个常见的难题, 这个问题叫做环境变量问题。那个执行 cron 任务的环境, 它的状态是相当“干净”的, 系统里面只提供给了一个最基础的变量 PATH:# 在 crontab 中打印环境变量* * * * * env /tmp/cron_env.txt典型输出HOME/rootLOGNAMErootPATH/usr/bin:/binPWD/rootSHELL/bin/sh这就是为什么你在终端能运行的脚本放到 cron 里就报 not found。解决方案就是这样的。# 方法1在脚本中设置完整 PATHexport PATH/usr/local/bin:/usr/bin:/bin:$PATH# 方法2在 crontab 中使用绝对路径0 3 * * * /usr/local/bin/python3 /opt/scripts/backup.py# 方法3在 crontab 文件顶部设置环境变量PATH/usr/local/bin:/usr/bin:/binSHELL/bin/bash0 3 * * * /opt/scripts/backup.sh输出处理在默认的情况下, Cron 会把这个任务所产生的输出的内容还有错误的信息发给这个用户, 也就是以发送邮件的方式去做的。但是呢, 现在的很多系统当中, 通常是并没有配置好那个邮件方面的服务的:# 静默执行丢弃所有输出0 3 * * * /opt/scripts/backup.sh /dev/null 21# 只记录错误0 3 * * * /opt/scripts/backup.sh /dev/null# 记录所有输出到文件0 3 * * * /opt/scripts/backup.sh /var/log/backup.log 21时区陷阱cron 使用系统时区, 但问题在于夏令时切换。# 查看系统时区timedatectl# 如果任务是 2:00 AM夏令时切换那天可能执行两次或零次# 建议避开 2:00-3:00 这个时间段高级技巧是, 要防止那个任务出现互相重叠着去执行的这种状况。当那个需要花很长时间才能跑完的任务正在进行之中, 还没有到全部结束的那个时间点的时候, 下一回的这个任务可能就已经被启动或者触发起来了。# 使用 flock 实现文件锁0 * * * * flock -n /var/lock/backup.lock /opt/scripts/backup.sh# 或者用 pid 文件检测0 * * * * [ !-f /var/run/backup.pid ] /opt/scripts/backup.sh2. 任务执行日志# 每条任务添加时间戳0 3 * * * echo [$(date %Y-%m-%d %H:%M:%S)] Backup started /var/log/backup.log /opt/scripts/backup.sh /var/log/backup.log 213. 随机延迟执行避免所有的任务都整着劲在同一个点上去同时启动。# 在 0-5 分钟内随机执行0 3 * * * sleep $(($RANDOM \% 300)); /opt/scripts/backup.sh# 使用 systemd timer 更优雅现代 Linux4. 当满足条件的时候, 它就执行。# 只在工作日执行0 9 * * 1-5 /opt/scripts/daily_report.sh# 只在特定月份执行0 0 1 1,4,7,10 * /opt/scripts/quarterly_audit.sh# 检测文件存在再执行0 3 * * * [ -f /data/import.csv ] /opt/scripts/import.sh调试技巧1. 手动触发测试# 最直接的方式复制命令直接执行/opt/scripts/backup.sh# 模拟 cron 环境env -i HOME$HOME PATH/usr/bin:/bin /opt/scripts/backup.sh2. 查看执行日志# 查看系统日志grep CRON /var/log/syslog# 实时监控tail -f /var/log/syslog | grep CRON3. 对表示时间的具体表达方式进行实际性的检验和确认。你可以去使用那种在线的验证工具, 来确认一下那个 cron 表达式到底是不是写对了啊, 就像像是 Cron 这个玩意儿之类的。关于安全方面的那些最佳实践, 其中的第一点叫做权限控制。# /etc/cron.allow 和 /etc/cron.deny 控制谁能使用 cron# 只允许特定用户echo root /etc/cron.allowecho deploy /etc/cron.allow# 或者拒绝特定用户echo guest /etc/cron.deny2. 脚本安全# 设置适当的权限chmod 700 /opt/scripts/backup.sh# 使用绝对路径0 3 * * * /opt/scripts/backup.sh# 避免使用 root 执行不必要任务# 在 crontab 中指定用户# /etc/crontab 格式# m h dom mon dow user command0 3 * * * www-data /opt/scripts/cleanup.sh实战案例自动化备份系统#!/bin/bash# /opt/scripts/db_backup.shset -euo pipefail# 配置DB_NAMEproductionBACKUP_DIR/backup/mysqlRETENTION_DAYS30DATE$(date %Y%m%d_%H%M%S)# 环境检查if [ !-d $BACKUP_DIR ]; thenmkdir -p $BACKUP_DIRfi# 执行备份mysqldump --single-transaction $DB_NAME | gzip $BACKUP_DIR/${DB_NAME}_${DATE}.sql.gz# 清理旧备份find $BACKUP_DIR -name *.sql.gz -mtime $RETENTION_DAYS -delete# 记录日志echo [$(date)] Backup completed: ${DB_NAME}_${DATE}.sql.gz /var/log/backup.log相对应的, 就是:# 每天凌晨 3:30 执行数据库备份30 3 * * * /opt/scripts/db_backup.sh /var/log/backup.log 21小结从表面上来看, 它显得非常简单, 然而如果想把它用到最好的地步的话是需要去进行一定注意的。需要理解时间表达式里面的内容, 特别是要把星期还有日期之间的或者关系进行处理和搞清楚, 在当前的执行环境里面要考虑到 PATH 路径以及环境变量或者是输出重定向这些情况来防止它们出现重叠执行的状况, 可以通过使用文件锁或者是进程标识符也就是 pid 相关文件这样的方式来实现, 并且建立完善的日志系统以便方便排查问题, 同时在安全意识方面一定要遵循最小权限原则。你把上面那些东西全都搞明白了, 那么在实际的生产环境里, 你就能够对定时任务的安排和管理进行轻松的处理了。