定时任务基础:cron表达式详解、时间调度核心概念
说实话,做运维这些年,我见过太多因为定时任务写错导致的事故。凌晨三点把全量备份跑到了白天业务库上,或者周日的维护窗口因为cron表达式少了一个空格,硬生生提前了1小时执行——嗯,这些都是真实案例。今天咱们就把cron表达式这块硬骨头彻底啃透。
cron表达式长什么样
cron表达式本质上就是一个时间模式串。在Linux系统中,它包含5个字段:分钟、小时、日、月、星期。我习惯用一个口诀记:“分时日,月周”。
# 标准格式
* * * * * command
│ │ │ │ │
│ │ │ │ └─ 星期 (0-7, 0和7都表示周日)
│ │ │ └─── 月份 (1-12)
│ │ └───── 日期 (1-31)
│ └─────── 小时 (0-23)
└───────── 分钟 (0-59)
| 字段 | 允许值 | 通配符 |
|---|---|---|
| 分钟 | 0-59 | * / , - |
| 小时 | 0-23 | * / , - |
| 日期 | 1-31 | * / , - ? L W |
| 月份 | 1-12 或 JAN-DEC | * / , - |
| 星期 | 0-7 或 SUN-SAT | * / , - ? L # |
小提示:日期和星期字段不能同时用数字指定,否则容易产生歧义。建议只用一个,另一个用 ? 表示无限制。我一般只在星期字段用 *,日期字段留 ?。
常用表达式速查
0 2 * * *— 每天凌晨2点整执行*/15 * * * *— 每15分钟执行一次0 0 1 * *— 每月1号0点执行0 0 * * 1— 每周一0点执行0 1-5 9 * *— 每天9点到9点5分之间每分钟执行0 23,0 * * *— 每天23点和0点各执行一次
你想想看,如果我们要在每周五晚上做全量备份,表达式该怎么写?当然是 0 22 * * 5。但我记得有次同事写成了 0 22 * * 0,结果周日凌晨跑了备份——因为0在星期里代表周日。
避坑指南
我曾经踩过一个坑:在cron表达式里写 0 */2 * * *,以为会每2小时执行一次。实际上这个表达式是 每2小时的第0分钟执行,也就是0:00, 2:00, 4:00……但如果服务器在00:01才启动,那第一次执行会延迟到2:00。更安全的做法是用 0 0 */2 * *,但注意这表示每个偶数日期的0点执行,不是每2小时。真正每2小时应该写成多个时间点,比如 0 0,2,4,6,8,10,12,14,16,18,20,22 * * *,或者用系统级的timedatectl控制。
关键点:*/N 在分钟字段上表示每N分钟,但在小时字段上表示每N小时,但跨天会重置。如果想精确控制周期,我建议用 0,30 这种列举方式,可读性更强。
时间调度核心概念
搞清楚了cron表达式,咱们还得理解背后的时间调度逻辑。说白了,调度器的核心只有四个概念:周期、固定时间点、窗口、延迟与偏差。
- 周期 (Period):两次执行之间的固定间隔,比如每5分钟、每小时。cron里的
*/5就是周期调度。 - 固定时间点 (Fixed Time):在每天的特定时刻执行,不考虑上次执行时间。比如每天的2:00。
- 窗口 (Time Window):一个允许执行的时间范围,比如“只允许在 23:00-06:00 执行”。巡检策略里经常用到。
- 延迟与偏差 (Delay & Skew):为了避免多个任务同时爆发,在基准时间上加一个随机偏移。我负责过一个1000台集群的日志收集,如果每台都在整点执行,网络就炸了。加上
RANDOM_MINUTE偏移后,成功率从50%提到了98%。
| 概念 | cron表达式示例 | 实际含义 |
|---|---|---|
| 周期 | */10 * * * * | 每10分钟一次,从整点开始 |
| 固定时间点 | 0 3 * * * | 每天凌晨3:00整 |
| 窗口 | 0 0 * * 6 + 脚本内检查时间 | 周六全天执行,但脚本只允许晚上23点后 |
| 延迟与偏差 | 0 */12 * * * + 程序内sleep(random(0,600)) | 每12小时执行一次,但实际时刻随机偏移0-10分钟 |
实战建议:对于巡检类任务,我推荐固定时间点+随机偏差的组合。比如每天凌晨2:00巡检,但每个节点延时0-300秒,这样能避免所有节点同时打爆监控接口。代码里加个 sleep(random.uniform(0, 300)) 就行。
cron表达式解析流程
来看看一个cron表达式从输入到生成任务的全过程。我画了个简图,你跟着箭头走一趟就明白了。
这张图揭示了核心逻辑:cron引擎每分钟唤醒一次,拿当前时间与表达式里的所有通配符集合做比较。如果全部命中,就拉起任务。你可能会问:“那如果服务器在这一分钟刚好卡住怎么办?” 嗯,这就是为什么大型巡检系统很少直接依赖cron,而是用分布式调度框架(比如xxl-job、Quartz)做补偿。
我自己的双保险设计
最后分享一个我自己在用的cron写法。针对重要的巡检定时任务,我会同时配置两条cron:
- 主任务:标准时间点,比如
0 2 * * * - 兜底任务:主任务执行后5分钟,检测是否成功,失败则重试。兜底的cron写
5 2 * * *,脚本里检查标志文件。
这样就算主任务因为系统负载高延迟了,兜底也能接住。记住,定时任务不是写完就完了,得靠运维经验给它上保险。