Skip to content

计划任务

定时执行脚本的需求很常见——巡检、备份、日志清理、报表生成、数据同步,都得靠定时任务来跑。Linux 上做定时任务的机制有好几种:cron(传统定时任务,最常用)、anacron(补跑错过的任务)、at(一次性延迟执行)、systemd timer(systemd 自带的定时器,功能更强)。实际生产里 cron 和 systemd timer 用得最多,其他几种了解原理就够了。

一、crontab

cron 的核心是 crond 守护进程,它按预设的时间表达式不停扫描,到点了就用对应用户身份去执行命令。cron 出问题,大多不是时间表达式写错,而是执行环境跟你手动跑的时候不一样——这点后面专门讲,是最大的坑。

查看当前用户的定时任务:

bash
crontab -l

编辑:

bash
crontab -e

crontab 的格式是每行一个任务,五个时间字段加一个命令:

分 时 日 月 周 命令

五个时间字段从左到右:分(0-59)、时(0-23)、日(1-31)、月(1-12)、周(0-7,0 和 7 都是周日)。

时间表达式里几个符号要记住:* 表示每个值都匹配、*/5 表示每 5 个单位、1,15,30 表示列出的多个值、1-5 表示连续范围、1-10/2 表示范围内按步长(1,3,5,7,9)。

常见任务示例:

cron
*/5 * * * * /opt/check.sh >>/var/log/check.log 2>&1     # 每 5 分钟巡检
0 2 * * * /opt/backup.sh >>/var/log/backup.log 2>&1     # 每天凌晨 2 点备份
30 1 * * 1 /opt/report.sh                               # 每周一凌晨 1:30 跑报表

cron 里命令一定要写绝对路径。cron 运行时的 PATH 环境变量很短(通常就 /usr/bin:/bin),你在登录 Shell 里能直接敲的命令,放到 cron 里可能因为找不到可执行文件而失败——这种失败还特别难排查,因为你手动跑明明是好的。

二、系统级 cron

除了每个用户自己的 crontab(crontab -e 编辑的),还有系统级别的定时任务入口,主要在 /etc/crontab/etc/cron.d/、以及 /etc/cron.hourly//etc/cron.daily//etc/cron.weekly//etc/cron.monthly/ 这几个目录里。后面那几个目录特别方便——把脚本扔进 /etc/cron.daily/ 就每天执行一次,不用写时间表达式。

系统级 crontab(/etc/crontab/etc/cron.d/ 里的文件)比用户 crontab 多一个用户字段:

cron
*/5 * * * * root /opt/check.sh

第六列 root 指定以哪个用户身份执行。系统级任务放在 /etc/cron.d/服务名 这种独立文件里,比散落在 root 用户的 crontab -e 里好维护得多——交接的时候别人一眼能看到这个服务有哪些定时任务,不用翻 root 的 crontab。

三、cron 的执行环境——最大的坑

cron 运行时的环境变量和交互式 Shell 差别巨大,这是 cron 任务失败的头号原因。它不会自动加载 ~/.bashrc~/.bash_profile 这些交互式配置文件,所以你在 Shell 里设的环境变量、别名、函数,在 cron 里全都不存在。

可以在 crontab 文件顶部声明环境变量:

cron
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

*/5 * * * * /opt/check.sh >>/var/log/check.log 2>&1

更稳妥的做法是在脚本内部显式设置所有需要的环境变量。Java 的 JAVA_HOME、Python 虚拟环境的激活路径、Node.js 的 nvm 路径、数据库客户端的库路径(LD_LIBRARY_PATH)——这些在脚本里都得写死,别指望从环境继承。

还有个工作目录的坑:cron 执行时的工作目录是执行用户的家目录,不是脚本所在目录。脚本里如果用了相对路径(比如 cat conf/app.conf),会以家目录为基准去找,找不到就报错。所以定时任务脚本开头通常先切目录:

bash
#!/bin/bash
cd /opt/myapp || exit 1    # 切换到脚本所在目录,失败就退出

四、避免重复执行

如果上一次任务还没跑完,下一个周期又到了,可能导致多个实例同时操作同一份数据——比如备份任务跑得慢,上一次还没结束下一次又开始了,两个备份同时写同一个文件,数据可能损坏。用 flock 加文件锁就能解决:

cron
*/5 * * * * flock -n /tmp/check.lock /opt/check.sh >>/var/log/check.log 2>&1

-n 表示非阻塞——拿不到锁就直接退出,不等待。这样上一次任务还在跑时,这一次就直接跳过,不会并发。备份、数据同步、批量变更这类不适合多实例并发的任务,加 flock 锁是标准做法

五、anacron

anacron 解决的是 cron 的一个盲区:任务本该在特定时间执行,但机器当时关机了,错过了怎么办。cron 错过就是错过了,不会补跑;anacron 会在开机后补跑错过的任务。

bash
cat /etc/anacrontab

anacron 主要适合不是 24 小时开机的桌面机、测试机、边缘节点。服务器通常一直开着,这个场景用得不多,了解就行。

六、at 一次性延迟任务

at 用于"在未来某个时间点执行一次性任务",跟 cron 的"周期执行"不一样。

bash
yum install at -y
apt install at -y
systemctl enable --now atd

创建任务:

bash
at now + 10 minutes

然后交互式输入要执行的命令,按 Ctrl+d 结束输入。管理任务用 atq(查看待执行队列)和 atrm <job-id>(取消任务)。

at 适合"10 分钟后回滚""今晚零点关压测流量""一小时后切回来"这类临时操作。涉及重要变更的时候,除了 at 任务本身,日志记录和回滚命令也要写清楚,别光靠脑子记——临时操作最容易忘,等想起来的时候可能已经出事了。

七、systemd timer

systemd timer 相比 cron 有几个明显优势:状态可查、日志能集中追踪(journald)、上次执行结果可见、能和 service 的生命周期联用。任务一多,cron 的"黑盒"特性就开始烦人——你不知道某个任务上次什么时候跑的、跑成功没有,得去翻各处日志;systemd timer 这些信息都集中在 systemctl 和 journalctl 里。

timer 需要两个文件配合:xxx.service 定义实际要执行的任务(Type=oneshot),xxx.timer 定义触发 service 的时间和策略。

一个定时巡检的 service:

ini
# /etc/systemd/system/check.service
[Unit]
Description=Run check script

[Service]
Type=oneshot
ExecStart=/opt/check.sh

对应的 timer:

ini
# /etc/systemd/system/check.timer
[Unit]
Description=Run check script every 5 minutes

[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
Unit=check.service

[Install]
WantedBy=timers.target

timer 几个关键字段:OnBootSec 开机后多久第一次执行、OnUnitActiveSec 上次执行完多久后再执行、OnCalendar 按日历时间执行(类似 cron 表达式)、Persistent 错过了(关机时)开机后是否补跑、Unit 这个 timer 触发哪个 service。

每天凌晨 2 点的备份任务,改用 timer 写法:

ini
# /etc/systemd/system/backup.timer
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
Unit=backup.service

[Install]
WantedBy=timers.target

启用和管理:

bash
systemctl daemon-reload
systemctl enable --now check.timer
systemctl list-timers --all                        # 查看所有 timer 的上次和下次触发时间
journalctl -u check.service -n 100 --no-pager      # 看任务输出

排查 timer 任务失败,套路基本固定:systemctl status xxx.timer 看 timer 是否启用、下次啥时候触发;systemctl status xxx.service 看上次任务执行成功没;systemctl list-timers --all 看全景,所有 timer 的上次和下次执行一目了然;journalctl -u xxx.service 看任务的标准输出和错误。

简单场景 cron 够用,但任务一多、对可观测性要求一高,systemd timer 的状态透明度和日志可追踪性就显出价值了。新项目建议直接用 systemd timer。