Skip to content

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 权限下查看其他用户的 crontab

crontab 格式

每行一个任务,由 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 — 锁文件路径

执行过程:

  1. flock 尝试对 /tmp/check.lock 加排他锁
  2. 加锁成功:执行 /opt/check.sh
  3. 加锁失败:直接退出(不会等待,也不会重复执行)
  4. 脚本执行完成后自动释放锁

-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/anacrontab

anacron 主要适用于不是 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 有几个明显优势:

维度cronsystemd 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.timer

timer 的时间字段

字段含义
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.target

timer 任务管理

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.service

systemctl 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 直观得多——不需要去找日志文件位置、不需要猜测任务上次什么时候跑过、不需要担心日志被覆盖。