Appearance
10|计划任务
每天凌晨 2 点要备份一次数据库,每小时要清理一次过期日志,每分钟要采集一次监控指标。这些事不可能让人盯着时间手动敲——得让机器自己按时间跑。这就是定时任务。
Linux 上做定时任务主要用 cron 和 systemd timer 两种:
| 机制 | 用途 |
|---|---|
| cron | 周期性定时任务,传统主流 |
| systemd timer | 与 systemd 集成的定时器,可观测性更好,新项目推荐 |
本篇以 cron 为主线,重点讲一个核心陷阱——cron 的执行环境和交互式 shell 不同,这是定时任务失败的头号原因。然后给出 systemd timer 的对照写法。
一、crontab 基础
cron 由 crond 守护进程实现,它在后台按预设的时间表达式扫描所有任务,到点后以对应用户身份执行命令。
查看与编辑用户 crontab
bash
crontab -l # 查看当前用户的定时任务
crontab -e # 编辑当前用户的定时任务
crontab -r # 删除当前用户的所有定时任务(谨慎使用)
crontab -u root -l # root 权限下查看其他用户的 crontabcrontab 格式
每行一个任务,由 5 个时间字段和 1 个命令组成:
分 时 日 月 周 命令5 个时间字段的取值范围:
| 字段 | 取值 | 含义 |
|---|---|---|
| 分钟 | 0-59 | 每小时的第几分钟 |
| 小时 | 0-23 | 每天的第几小时 |
| 日 | 1-31 | 每月的第几日 |
| 月 | 1-12 | 每年的第几月 |
| 周 | 0-7 | 周几(0 和 7 都表示周日) |
时间表达式中的特殊符号:
| 符号 | 含义 | 示例 |
|---|---|---|
* | 任意值 | * * * * * 每分钟 |
*/n | 每 n 个单位 | */5 * * * * 每 5 分钟 |
a,b,c | 列出多个值 | 0 1,5,12 * * * 每天 1 点、5 点、12 点 |
a-b | 连续范围 | 0 9-18 * * * 9 点到 18 点的每个整点 |
a-b/n | 范围内按步长 | 0 0-23/2 * * * 每 2 小时(0,2,4...22 点) |
常见任务示例
cron
# 每 5 分钟执行巡检脚本,输出追加到日志
*/5 * * * * /opt/check.sh >> /var/log/check.log 2>&1
# 每天凌晨 2 点执行备份
0 2 * * * /opt/backup.sh >> /var/log/backup.log 2>&1
# 每周一凌晨 1:30 生成周报
30 1 * * 1 /opt/report.sh
# 每月 1 号凌晨 3 点清理 30 天前的日志
0 3 1 * * find /var/log -type f -mtime +30 -delete
# 每 15 分钟同步配置(只在工作时间)
*/15 9-18 * * 1-5 /opt/sync-config.sh命令必须使用绝对路径
cron 任务中的命令必须使用绝对路径。这是 cron 任务最常见的失败原因之一。
cron 运行时的 PATH 环境变量非常短(通常只有 /usr/bin:/bin),登录 Shell 中可以直接调用的命令(如自己安装在 /usr/local/bin 下的工具、Python 虚拟环境中的 python),在 cron 环境下可能因为找不到可执行文件而失败:
cron
# 错误写法:命令可能找不到
*/5 * * * * python /opt/app/check.py
# 正确写法:使用绝对路径
*/5 * * * * /usr/local/bin/python3 /opt/app/check.py这种失败的特征是"手动执行明明正常,定时任务静默失败"——因为 cron 失败的输出通常被发送到本机邮件队列,如果系统没有配置 MTA,失败信息会完全丢失。
二、系统级 cron
除了每个用户的 crontab,还有系统级的 cron 入口:
| 位置 | 用途 |
|---|---|
/etc/crontab | 系统级 crontab 主文件(传统位置) |
/etc/cron.d/* | 第三方软件/自定义任务的独立 cron 文件 |
/etc/cron.hourly/ | 每小时执行的脚本目录 |
/etc/cron.daily/ | 每天执行的脚本目录 |
/etc/cron.weekly/ | 每周执行的脚本目录 |
/etc/cron.monthly/ | 每月执行的脚本目录 |
cron.hourly/ 等目录非常方便——将可执行脚本放入该目录,无需编写时间表达式,cron 会按目录名称约定的频率自动执行:
bash
cat > /etc/cron.daily/backup << 'EOF'
#!/bin/bash
/opt/backup.sh >> /var/log/backup.log 2>&1
EOF
chmod +x /etc/cron.daily/backup注意:这些目录下的脚本不能带文件扩展名(如 .sh)。某些发行版的 run-parts 工具会忽略带扩展名的文件——这是一个容易踩的隐性规则。
系统级 crontab 的格式差异
系统级 crontab(/etc/crontab 和 /etc/cron.d/ 中的文件)比用户 crontab 多一个用户字段:
cron
# 用户 crontab 格式(5 字段 + 命令)
*/5 * * * * /opt/check.sh
# 系统级 crontab 格式(5 字段 + 用户 + 命令)
*/5 * * * * root /opt/check.sh第六列指定以哪个用户身份执行。
系统级 cron 的目录组织建议
系统级任务推荐放在 /etc/cron.d/服务名 这种独立文件中,而不是堆在 root 用户的 crontab -e 里:
cron
# /etc/cron.d/myapp
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 每 5 分钟巡检
*/5 * * * * deploy /opt/myapp/bin/check.sh >> /var/log/myapp/check.log 2>&1
# 每天凌晨备份
0 2 * * * deploy /opt/myapp/bin/backup.sh >> /var/log/myapp/backup.log 2>&1这种组织方式的优点:服务的所有定时任务集中在一个文件中,接手该服务时一眼能看到所有定时任务的全貌,而不是要去翻 root 用户的 crontab。
三、cron 的执行环境陷阱
cron 运行时的环境变量与交互式 Shell 差异巨大,这是 cron 任务失败的头号原因。
cron 任务执行时:
- 不加载
~/.bashrc、~/.bash_profile、/etc/profile等交互式 Shell 配置 - PATH 极简——通常只有
/usr/bin:/bin - 没有别名(alias)和 shell 函数
- 工作目录是执行用户的家目录,不是脚本所在目录
在 crontab 中声明环境变量
可以在 crontab 文件顶部声明环境变量,这些声明对该文件中的所有任务生效:
cron
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
JAVA_HOME=/usr/lib/jvm/java-17-openjdk
LD_LIBRARY_PATH=/opt/oracle/instantclient_21_5
*/5 * * * * /opt/check.sh >> /var/log/check.log 2>&1更稳妥的做法:脚本内部显式设置
更可靠的做法是在脚本内部显式设置所有需要的环境变量,不依赖 cron 环境继承:
bash
#!/bin/bash
# /opt/myapp/check.sh
# 显式设置 PATH 和关键环境变量
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk
export LD_LIBRARY_PATH=/opt/oracle/instantclient_21_5
# 切换到脚本所在目录(避免相对路径问题)
cd /opt/myapp || exit 1
# 业务逻辑
./bin/check这种写法的好处是脚本独立于 cron 环境——手动执行、cron 执行、systemd timer 执行,行为完全一致。
工作目录陷阱
cron 任务执行时的工作目录是执行用户的家目录($HOME),不是脚本所在目录。如果脚本中使用了相对路径,会以家目录为基准查找,可能找不到目标文件:
bash
#!/bin/bash
# 错误:相对路径会从 $HOME 开始查找
cat conf/app.conf
# 正确:使用绝对路径
cat /opt/myapp/conf/app.conf
# 或者先切换到脚本所在目录
cd "$(dirname "$0")" || exit 1
cat conf/app.conf$(dirname "$0") 会返回脚本所在的目录路径,这种写法让脚本可以从任何位置被调用都能正确工作。
四、避免任务重复执行:flock
如果某个定时任务执行时间长于触发间隔,可能出现"上一次还没结束,下一次又开始了"的并发问题。例如备份任务跑 10 分钟,但配置成 */5 * * * *,就会有多个备份进程同时运行,可能造成数据损坏。
flock 通过文件锁机制实现互斥:
cron
*/5 * * * * flock -n /tmp/check.lock /opt/check.sh >> /var/log/check.log 2>&1参数含义:
-n— 非阻塞模式,获取锁失败立即退出(不等待)/tmp/check.lock— 锁文件路径
执行过程:
- flock 尝试对
/tmp/check.lock加排他锁 - 加锁成功:执行
/opt/check.sh - 加锁失败:直接退出(不会等待,也不会重复执行)
- 脚本执行完成后自动释放锁
-w 60 可以指定"最多等待 60 秒",通常配合长时间任务使用:
cron
*/5 * * * * flock -w 60 /tmp/sync.lock /opt/sync.sh >> /var/log/sync.log 2>&1备份、数据同步、批量变更等不允许并发的任务,加 flock 锁是标准做法。
五、anacron:补跑错过的任务
anacron 解决的是 cron 的一个盲区:任务原本应在特定时间执行,但机器当时关机了,错过的任务不会被自动补跑。anacron 会在系统启动时检测哪些任务被遗漏,然后补跑。
bash
cat /etc/anacrontabanacron 主要适用于不是 24 小时开机的设备——桌面机、笔记本、测试机、边缘节点。生产服务器通常 24 小时运行,这一机制使用较少,了解原理即可。
六、at:一次性延迟任务
at 用于在未来某个时间点执行一次性任务,与 cron 的周期性任务不同。
安装与启用
bash
# 安装 at
yum install at -y # RHEL 系
apt install at -y # Debian 系
# 启动 atd 守护进程
systemctl enable --now atd创建任务
bash
$ at now + 10 minutes
at> /opt/rollback.sh
at> <EOT> # 按 Ctrl+d 结束输入
job 1 at Tue Jun 23 10:50:00 2026也可以一行写完:
bash
echo "/opt/rollback.sh" | at now + 10 minutes
echo "/opt/disable.sh" | at 23:00
echo "/opt/cleanup.sh" | at midnight任务管理
bash
atq # 查看所有待执行的 at 任务
atrm <job-id> # 取消指定任务
at -c <job-id> # 查看任务的具体内容(包括环境变量)适用场景
at 适合临时性的"在某个时刻执行一次"操作:
- 维护窗口结束时自动关闭压测流量
- 灰度发布后定时切回原版本
- 临时调整后自动恢复
涉及重要变更时,除 at 任务本身,还应当同时记录回滚命令、维护日志,不要仅依赖 at 任务。临时操作最容易被遗忘,真出问题时,完整的操作记录比单纯的 at 任务更可靠。
七、systemd timer
systemd timer 是 systemd 提供的定时器机制,相比 cron 有几个明显优势:
| 维度 | cron | systemd timer |
|---|---|---|
| 任务状态可见性 | 低(需翻日志或自己加监控) | 高(systemctl list-timers) |
| 上次执行结果 | 不可见 | 可见(systemctl status) |
| 日志收集 | 自行重定向 | journald 自动收集 |
| 错过任务补跑 | 不支持(需 anacron) | 支持(Persistent=true) |
| 与 service 联动 | 无 | 原生集成 |
| 配置复杂度 | 低(一行) | 较高(需两个文件) |
任务数量少时 cron 足够;当任务多、对可观测性要求高时,systemd timer 的优势开始显现。新项目推荐直接使用 systemd timer。
两个文件的结构
systemd timer 需要两个文件配合工作:
xxx.service— 定义实际执行的任务(Type=oneshot)xxx.timer— 定义触发 service 的时间和策略
完整示例:每 5 分钟巡检
第 1 步:创建 service 文件,定义任务:
ini
# /etc/systemd/system/check.service
[Unit]
Description=Run check script
[Service]
Type=oneshot
ExecStart=/opt/check.sh
User=deploy
WorkingDirectory=/opt
StandardOutput=journal
StandardError=journal第 2 步:创建 timer 文件,定义触发策略:
ini
# /etc/systemd/system/check.timer
[Unit]
Description=Run check script every 5 minutes
[Timer]
OnBootSec=1min # 系统启动后 1 分钟首次执行
OnUnitActiveSec=5min # 上次执行完后 5 分钟再次执行
Unit=check.service # 触发哪个 service
[Install]
WantedBy=timers.target第 3 步:启用 timer:
bash
systemctl daemon-reload
systemctl enable --now check.timer
systemctl status check.timertimer 的时间字段
| 字段 | 含义 |
|---|---|
OnBootSec | 系统启动后多久首次执行 |
OnUnitActiveSec | 上次执行完后多久再次执行(等效于 cron 的固定间隔) |
OnCalendar | 按日历时间执行(类似 cron 表达式) |
Persistent | 错过的任务(如关机期间)是否在开机后补跑 |
RandomizedDelaySec | 在指定时间上加上随机延迟(避免任务集中触发) |
cron 表达式风格:OnCalendar
OnCalendar 字段支持类似 cron 的表达式:
ini
OnCalendar=*-*-* 02:00:00 # 每天凌晨 2 点
OnCalendar=Mon *-*-* 03:00:00 # 每周一凌晨 3 点
OnCalendar=*-*-1 04:00:00 # 每月 1 号凌晨 4 点
OnCalendar=hourly # 每小时整点
OnCalendar=daily # 每天 00:00
OnCalendar=weekly # 每周一 00:00每天凌晨 2 点的备份任务,改用 timer 写法:
ini
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true # 关机错过后,开机补跑一次
Unit=backup.service
[Install]
WantedBy=timers.targettimer 任务管理
bash
# 查看所有 timer 的上次和下次触发时间(最常用!)
systemctl list-timers --all
# 查看具体某个 timer 的状态
systemctl status check.timer
# 查看任务输出
journalctl -u check.service -n 100 --no-pager
journalctl -u check.service --since "1 hour ago"
# 停止 timer(任务不再触发)
systemctl stop check.timer
systemctl disable check.timer
# 手动触发一次任务(用于测试)
systemctl start check.servicesystemctl list-timers --all 的输出示例:
NEXT LEFT LAST PASSED UNIT
Tue 2026-06-23 11:00:00 CST 28min left Tue 2026-06-23 10:55:00 CST 2min ago check.timer
Wed 2026-06-24 02:00:00 CST 15h left Tue 2026-06-23 02:00:00 CST 8h ago backup.timer可以清楚看到每个 timer 的上次执行时间、下次执行时间、是否成功——这是 cron 完全缺失的可观测性。
systemd timer 任务的排查流程
bash
# 第 1 步:确认 timer 是否启用,下次什么时候触发
systemctl status check.timer
# 第 2 步:查看 service 上次执行成功与否
systemctl status check.service
# 第 3 步:全景视图,所有 timer 一目了然
systemctl list-timers --all
# 第 4 步:查看任务输出和错误
journalctl -u check.service -n 100 --no-pager这套排查路径比 cron 直观得多——不需要去找日志文件位置、不需要猜测任务上次什么时候跑过、不需要担心日志被覆盖。