Linux 定时任务实战:cron 与 systemd timer 怎么选怎么写

每天自动备份、定时清理日志、周期跑健康检查——crontab 五字段怎么写?systemd timer 又是什么?这篇文章讲清两种定时任务的写法、坑点和适用场景。

服务器的魅力在于"它能在你睡觉时替你干活"。自动备份、定时清理、周期上报,这些重复劳动都该交给定时任务。Linux 上有两套主流方案:老牌的 cron 和新派的 systemd timer。很多人只知道 crontab -e,结果踩了一堆坑——命令找不到、输出淹没、时区错乱。这篇文章把两种方案都讲透,并告诉你什么场景该用哪个,以及那些"明明写了却不跑"的隐蔽原因。

一、cron 是什么:按分钟精度排程

cron 是最经典的定时守护进程,精度到分钟,适合 7×24 运行的服务器。它的任务表叫 crontab,每个用户有自己的表。crontab -e 编辑当前用户的任务,crontab -l 列出,crontab -r 删除全部(这个很危险,无确认!)。要改别人的任务用 sudo crontab -u 用户名 -e。理解 cron 是"后台默默执行的脚本调度器",是自动化运维的第一步。

crontab -e
crontab -l
sudo crontab -u deploy -e

二、crontab 五字段:分 时 日 月 星期

一行任务由五个时间字段加命令组成:分(0-59) 时(0-23) 日(1-31) 月(1-12) 星期(0-7,0和7都代表周日)。*/5 表示"每5个单位",* 表示"每"。比如 0 2 * * * 是每天凌晨2点,0 2 * * 1 是每周一凌晨2点。还有 @daily、@hourly、@reboot 这种人类友好的简写。最常见的错误是把午夜写成 0 24 * * *——小时最大是23,24无效,午夜应是 0 0。时区方面,cron 默认用系统时区,容器里可能是 UTC,写出来要和你的预期对齐。

*/5 * * * * /path/script.sh
0 2 * * * /path/backup.sh
@daily /path/backup.sh
@reboot /path/start.sh

三、cron 的两大坑:PATH 与输出

新手 90% 的"写了不跑"都栽在这里。第一,cron 的运行环境极简,PATH 只有 /usr/bin:/bin,你在终端里随手敲的命令(比如 python、nginx)在 cron 里可能"找不到"。解决办法:脚本里用绝对路径,或在 crontab 顶部写 PATH=/usr/local/bin:/usr/bin:/bin。第二,任务输出默认发本地邮件,没人看,排错时无迹可寻。务必把输出重定向:>> /var/log/script.log 2>&1,这样 stdout 和 stderr 都进日志,出问题一查便知。

PATH=/usr/local/bin:/usr/bin:/bin
*/5 * * * * /path/script.sh >> /var/log/script.log 2>&1

四、crontab 末尾必须有空行

这个小细节坑过无数人:cron 要求 crontab 文件的最后一行必须以换行符结尾,否则最后一条任务可能被悄悄忽略。很多编辑器保存时不会自动补末尾换行,于是你"明明写了备份任务却没执行",其实是它被截断了。养成习惯:编辑完在末尾多敲一个回车。重要任务改完先用 crontab -l 看一眼,确认它真的在表里。这个一行空白,值千金。

五、systemd timer:现代发行版的新选择

Ubuntu 22.04、Rocky 9 这类新系统更推荐 systemd timer。它由一对单元文件组成:.service 定义"干什么",.timer 定义"什么时候干"。最大优势是 Persistent=true 能在关机错过的任务补跑(cron 错过就错过了),而且日志统一进 journald,用 journalctl 就能查执行记录。OnCalendar 用 systemd 时间格式,比如 *-*-* 02:00:00 是每天2点,*:0/30 是每30分钟。对需要可靠性的任务,timer 比 cron 省心。

[Unit]
Description=Daily backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
[Unit]
Description=Run backup daily at 02:00

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
Unit=backup.service

[Install]
WantedBy=timers.target

六、管理 systemd timer

写好两个文件放到 /etc/systemd/system/,记得 sudo systemctl daemon-reload 让系统重新读取,再 sudo systemctl enable --now 备份.timer 启用并立即生效。list-timers 能看到所有定时器及下次触发时间,status 看状态,journalctl -u 备份.service 查执行日志。想手动跑一次就 systemctl start 备份.service。timer 的排错比 cron 透明得多,因为每一步都有日志可查。

sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers
journalctl -u backup.service

七、怎么选:cron 还是 timer

简单、一次性的周期任务,crontab -e 一行搞定,学习成本低,适合个人小脚本。需要补跑、需要统一日志、需要依赖关系或资源控制的,用 systemd timer,它和现代 systemd 系统浑然一体。anacron 则面向笔记本这类不常开机的机器,按"天"错峰补跑。一句话:能接受"错过就错过"用 cron,要"绝不漏跑"用 timer。两种都会,才是完整的自动化能力。

补充与延伸:自动化是运维的放大器

定时任务的价值,不在于"省下手动点的那一下",而在于它把"应该做但容易忘"的事情变成了系统的一部分。备份、日志清理、证书续期、数据同步……这些事只要有一次忘了做,后面就是事故。所以真正成熟的运维,会把所有重复动作都交给 cron 或 timer 托管,自己只负责定义"什么时候做什么"和"失败了怎么办"。这一点认知上的转变,比记住任何一条命令都重要:你不是在写脚本,你是在给机器安装条件反射。

但要注意,自动化是把双刃剑。一个写错路径的 rm 脚本被定时执行,可能半小时就清空整块磁盘;一个没设超时的任务卡死后堆积,会占满进程表拖垮整机。所以给定时任务加"护栏"是基本功:关键操作前先备份或 dry-run;用 flock 防止上一次没跑完、下一次又启动;脚本里用 set -euo pipefail 让错误及时暴露而不是悄悄吞掉;失败时发邮件或写日志告警。把这些机制内置进去,自动化才是帮手,而不是定时炸弹。

还有一个常见误区:把定时任务写进 crontab 就以为万事大吉,从不去看它到底跑没跑成功。务必养成习惯,让脚本把输出重定向到日志文件(>> /var/log/mytask.log 2>&1),或者 systemd 下直接用 journalctl -u 看状态,定期去翻一眼。你会发现,真正出问题的往往不是命令本身,而是"它早就失败了,而你一个月后才发现"。自动化只有可观测,才可信赖。

最后给个实用建议:cron 时间表达式容易写错,上线前用在线工具(如 crontab.guru)把表达式翻译回人话核对一遍;systemd timer 的 OnCalendar 也支持人类可读写法(如 daily、Mon..Fri 09:00),别硬写陌生的格式。再小的定时任务,也值得你花两分钟确认它"对不对"和"有没有在跑"。这两分钟,能省下你未来几个小时的救火。

再聊一个真实场景:用 cron 跑需要环境变量的脚本。cron 的环境极其精简,你在终端里 export 的变量它一概没有,所以脚本里用到的 PATH、JAVA_HOME、NODE_ENV 要么在脚本里自己 export,要么写进 crontab 顶部的环境变量区。这也是"本地手动跑得好好的,cron 一跑就找不到命令"的头号原因。记住:cron 不继承你的 shell 环境,它只认自己那一亩三分地,凡是依赖环境的地方都要在脚本里说清楚。

还有一个细节值得记:cron 默认把任务的标准输出和错误都邮件发给属主,很多服务器没配邮件,这些输出就消失在 /var/spool/mail 里无人看。更好的做法是脚本自己把输出重定向到日志,或者在 crontab 里写 MAILTO="" 关掉邮件、改由日志文件记录。总之,让任务的"声音"有去处,你才能在它出错时听见,而不是等到下游业务崩了才回头查是哪一步静默失败。

定时任务是你和"遗忘"之间的屏障。人总会忘,机器不会——只要你把任务交代清楚、把护栏设好、把输出看着。把本文的命令整理成你的常用清单,下次需要"每天凌晨三点做个事"时,你不会再打开搜索引擎现查,而是从容地写一行 cron 或一个 timer。这种从容,就是自动化给你的红利。

最后留一道思考题:当你有几十台机器、上百个定时任务时,单台 crontab 就管不过来了。那时你会自然地走向配置管理(Ansible 统一下发 cron)或分布式调度。本文打下的手动基础,正是你理解那些高级工具的前提——先把一台机器上的 cron 玩透,再谈规模化的事,步子才稳。