Linux 基础体系 · 第 13/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux 定时任务:cron、systemd timer、时区、防重入和错过执行

定时任务看似只是“到点执行一条命令”,但生产问题通常来自四个边界:

  1. 时间表达式到底由谁解释:cron 还是 systemd?
  2. 时间使用哪个时区:系统时区、任务时区,还是应用自己的时区?
  3. 上一次任务尚未结束时怎么办:允许并发、跳过,还是排队?
  4. 机器关机、服务停止或系统休眠期间错过了计划时间怎么办

要准确回答这些问题,不能只记住一行配置语法,还需要区分“计划触发”和“任务执行”。


一、先建立模型:定时器不等于任务进程

一个定时任务通常包含三个组件:

时间规则 ──> 调度器 ──> 启动任务
                         │
                         ├─ 脚本或程序
                         ├─ 日志
                         ├─ 锁
                         └─ 外部资源

例如:

  • cron 读取 crontab,匹配当前时间,然后创建命令进程;
  • systemd timer 根据时间规则激活一个 .service 单元;
  • .service 再启动脚本、二进制程序或其他服务。

因此,下面两个概念必须分开:

1. 触发时间

调度器认为“应该尝试启动任务”的时间点,记为:

S={s1,s2,s3,}S = \{s_1, s_2, s_3, \ldots\}

例如“每天 03:00”在某个时区中会产生一组计划时间。

2. 实际执行

任务真正开始运行的时间点,记为:

E={e1,e2,e3,}E = \{e_1, e_2, e_3, \ldots\}

理想情况下,eie_i 接近 sis_i,但实际可能存在:

  • 调度器延迟;
  • AccuracySec 或随机延迟;
  • 机器关机导致未触发;
  • 上一次任务仍在运行;
  • 任务启动失败;
  • 锁导致本次执行主动退出。

“定时任务是否可靠”不能只看时间表达式,还要观察从 SSEE 的整个路径。


二、cron:按墙上时间匹配的传统调度器

cron 是一类长期运行的调度守护进程。常见实现包括:

  • Debian、Ubuntu 常见的 cron
  • Fedora、RHEL 系常见的 cronie
  • BusyBox 提供的简化实现。

具体选项和扩展会有差异,因此生产环境应以本机的 man 5 crontabman 8 cron 为准。

2.1 用户 crontab 和系统 crontab

编辑当前用户的 crontab:

crontab -e

查看当前用户的配置:

crontab -l

系统级任务可能位于:

/etc/crontab
/etc/cron.d/*
/etc/cron.hourly/*
/etc/cron.daily/*

用户 crontab 的基本格式有五个时间字段:

分钟 小时 日 月 星期 命令

例如:

0 3 * * * /usr/local/sbin/backup.sh

表示在当前 cron 使用的时间基准下,每天 03:00 尝试执行脚本。

/etc/crontab/etc/cron.d/* 通常多一个用户字段:

0 3 * * * backup /usr/local/sbin/backup.sh

这里的 backup 表示以该用户身份运行。把两种格式混用会导致任务完全不按预期执行。

2.2 cron 表达式的“日”语义

cron 的五个字段通常是:

分钟:0-59
小时:0-23
日:1-31
月:1-12 或名称
星期:0-7,通常 0 和 7 都表示星期日

有一个容易误判的规则:当“日”和“星期”同时被限制时,传统 cron 通常采用 二者满足其一即可,而不是逻辑与。

例如:

0 0 1 * 1 /usr/local/bin/report

常见实现会在以下时间执行:

  • 每月 1 日;
  • 每周一。

它不是“每月 1 日且恰好是星期一”。

如果业务需要严格的逻辑与,应让 cron 每天触发,再由脚本显式判断日期,或者使用 systemd 的更清晰日历表达式。

2.3 cron 的命令环境很小

cron 不会继承交互式登录 Shell 的完整环境。典型差异包括:

  • PATH 较短;
  • 当前工作目录通常不是项目目录;
  • 不会自动读取用户的 .bashrc
  • 不能假设交互式终端存在;
  • 重定向、引号和通配符仍由 Shell 解释。

不可靠的写法:

0 3 * * * backup.sh

更可控的写法:

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

0 3 * * * /usr/local/sbin/backup.sh >>/var/log/backup.log 2>&1

脚本自身也应使用绝对路径,明确工作目录,并正确处理错误:

#!/usr/bin/env bash
set -Eeuo pipefail

cd /srv/app
/usr/bin/rsync -a --delete ./ /backup/app/

set -e 并不能代替完整的错误设计。例如管道、条件命令和子 Shell 有特殊规则;这属于 Shell 工程问题,不应把 cron 的成功启动误认为脚本成功完成。


三、cron 的时间语义、时区和错过执行

3.1 cron 通常按本地墙上时间判断

cron 的调度基础是“墙上时钟”,也就是操作系统提供的日历时间,而不是一个简单的“启动后经过了多少秒”。

查看主机时区和时间同步状态:

timedatectl

典型输出可能包含:

Local time: Tue 2025-03-11 10:20:00 CST
Universal time: Tue 2025-03-11 02:20:00 UTC
Time zone: Asia/Shanghai (CST, +0800)
System clock synchronized: yes

这里至少有三个不同概念:

  • UTC:全球统一的时间基准;
  • 系统时区:主机把 UTC 显示成本地时间的规则;
  • 任务时区:某个调度表达式采用的时区。

不要把脚本中的 TZ=... 和 cron 调度时区混为一谈。前者可能只影响进程内的时间库和命令输出,未必改变 cron 什么时候触发任务。

部分 cron 实现支持在 crontab 中指定:

CRON_TZ=Asia/Shanghai
0 3 * * * /usr/local/sbin/backup.sh

这依赖具体实现。CRON_TZ 在常见实现中用于解释调度时间,但它是否作为普通环境变量传给命令、系统级 crontab 是否支持,不能脱离发行版实现假设。部署前应使用本机的 cron 文档验证。

如果任务是跨地域运行、必须在固定业务时区执行,systemd timer 的时区表达能力通常更容易检查;另一种可靠方案是统一用 UTC 调度,再在业务代码中处理展示时区。

3.2 夏令时会破坏“每天某个本地时间一次”的直觉

假设任务规定为当地时间每天 02:30:

  • 春季向前拨钟时,02:30 可能根本不存在;
  • 秋季向后拨钟时,某个本地时间可能出现两次。

因此,下面这个命题并不总成立:

0 2 * * * 每个自然日恰好执行一次。

cron 实现对夏令时跳过或重复时间的处理并不完全统一。某些实现会特殊处理小于数小时的时间跳变,不能把一种实现的行为推广到所有发行版。

需要“每天一次”的任务应设计幂等性,并记录业务日期。例如任务处理的是某个结算日:

业务日期 = 上一次已经成功处理的日期 + 1

而不是简单地假设“本次进程启动时的本地日期就是要处理的日期”。

3.3 cron 默认不会补执行

cron 通常只在守护进程运行并观察到匹配时间时执行任务。

例如:

03:00  机器关机
07:00  机器启动

如果任务是 0 3 * * *,cron 一般不会在 07:00 自动补执行 03:00 的任务。它会等待下一个匹配时间。

这意味着:

  • 重启期间错过的任务不会自动恢复;
  • cron 守护进程停止期间错过的任务通常不会自动恢复;
  • 长时间休眠时是否补执行取决于实现,不能假设;
  • @daily 不是可靠的“每 24 小时一次”补偿机制。

如果业务要求“至少完成每个业务日”,应在任务中持久化进度,启动时扫描缺失日期并补处理,而不是只依赖 cron 的触发次数。


四、systemd timer:用 Unit 和 Service 表达调度

systemd timer 是 systemd 的一种 Unit。它本身通常不执行备份逻辑,而是激活同名的 service:

backup.timer ──> backup.service ──> /usr/local/sbin/backup.sh

例如:

/etc/systemd/system/backup.timer
/etc/systemd/system/backup.service

backup.timer 默认关联:

backup.service

也可以通过 Unit= 显式指定目标服务。

4.1 一个可运行的完整示例

创建服务:

# /etc/systemd/system/backup.service
[Unit]
Description=Application backup

[Service]
Type=oneshot
User=backup
Group=backup
ExecStart=/usr/local/sbin/backup.sh

创建定时器:

# /etc/systemd/system/backup.timer
[Unit]
Description=Run application backup daily

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
AccuracySec=1min

[Install]
WantedBy=timers.target

加载、启用并立即查看:

sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl status backup.timer
systemctl list-timers --all backup.timer

这里每一步的含义是:

  1. daemon-reload 让 systemd 重新读取新文件;
  2. enable 创建开机启动关系;
  3. --now 同时立即启动 timer;
  4. list-timers 显示上次触发时间和下一次预计触发时间;
  5. timer 触发时,systemd 激活 backup.service
  6. Type=oneshot 表示该服务运行一次命令,命令退出后服务结束。

检查服务日志:

sudo journalctl -u backup.service
sudo journalctl -u backup.service -b

backup.timer 的日志和 backup.service 的日志是两个不同的对象。只查看 timer 日志,不能证明脚本成功完成。

4.2 OnCalendar 和单调时间

systemd timer 有两类主要时间:

日历时间

[Timer]
OnCalendar=Mon..Fri *-*-* 03:00:00

它基于日期、小时、分钟等墙上时间,类似 cron,但表达式更丰富。

systemd-analyze 检查表达式,不必等到第二天:

systemd-analyze calendar 'Mon..Fri *-*-* 03:00:00'

该命令会显示解析结果和下一次匹配时间。它适合发现:

  • 日期字段写错;
  • 星期范围不符合预期;
  • 当前时区导致的下一次时间不同;
  • 表达式实际上无法产生计划时间。

也可以指定时区,例如:

OnCalendar=*-*-* 03:00:00 Asia/Shanghai

具体日历语法以本机 man systemd.time 为准。固定业务时区时,应明确写出时区,而不是依赖主机部署时的默认时区。

单调时间

[Timer]
OnBootSec=15min
OnUnitActiveSec=1h

这些规则基于“启动后经过多久”或“单元激活后经过多久”,而不是某个日历时间。

单调时间适合:

  • 开机后延迟启动;
  • 服务激活后一段时间再次检查;
  • 不关心当地日期和夏令时的周期任务。

它不适合表达“每天凌晨三点”。

单调时钟通常不因手工修改系统墙上时间而倒退,但系统休眠对计时的影响需要区分。普通单调计时器可能在挂起期间暂停计时;需要把挂起时间也计入计时的场景,应检查 WakeSystem= 和本机 systemd 文档,并验证底层硬件是否支持唤醒。


五、systemd timer 的延迟、合并和随机化

5.1 AccuracySec 不是“最多延迟多少秒”

[Timer]
AccuracySec=1min

AccuracySec 表示 systemd 允许把定时器触发时间放在一个精度窗口内,以便合并多个定时器、减少唤醒和调度开销。

它不是一个严格的“任务一定在 60 秒内启动”的实时保证,也不是任务执行时长的限制。

如果任务必须尽量靠近计划时间,可以缩小该值,但这会增加唤醒和调度开销;即使设置得很小,也不能消除:

  • CPU 调度延迟;
  • I/O 阻塞;
  • 系统负载;
  • 服务启动失败;
  • 锁竞争。

5.2 RandomizedDelaySec 会主动增加延迟

[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=20min

这适合大量机器同时在整点执行同一任务的场景,可以打散负载。但它改变了“03:00 执行”的语义:任务可能在 03:00 之后随机延迟。

因此不能同时把“严格时间点”和“随机错峰”当作无条件保证。若任务有截止时间,应在业务中记录计划时间和实际开始时间,并监控延迟。


六、错过执行:cron 与 systemd 的本质差异

6.1 systemd 的 Persistent=true

对于基于 OnCalendar= 的 timer,可以写:

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

Persistent=true 会让 systemd 保存该 timer 上次触发的时间戳。timer 停止期间如果错过了计划时间,重新激活时可能立即激活关联 service。

它解决的是:

timer 不活跃期间错过日历事件

它不是完整的任务队列,也不是“把所有错过的每一天逐个补齐”。

例如:

周一 03:00:机器关机
周四 10:00:机器启动

timer 重新启动后,通常只会产生一次补触发,而不会自动分别执行周一、周二、周三的三次历史任务。若业务要求逐日补偿,必须由任务自己根据持久化状态补齐。

此外,Persistent=true 也不能保证以下情况:

  • service 已经被手工禁用;
  • timer 文件被删除;
  • systemd 数据目录不可写;
  • 任务触发了但 service 立即失败;
  • 任务执行成功但业务结果未持久化。

“补触发”只说明 systemd 尝试激活 service,不说明业务已经完成。

6.2 Persistent=true 的验证方式

先查看 timer:

systemctl list-timers --all backup.timer

再查看 timer 属性:

systemctl show backup.timer \
  -p NextElapseUSecRealtime \
  -p LastTriggerUSec \
  -p Persistent

查看 service 是否真的运行过:

journalctl -u backup.service --since today
systemctl status backup.service

如果需要测试补触发,不应直接修改生产主机时间。可以在测试虚拟机中:

  1. 创建测试 timer;
  2. 启动并记录一次;
  3. 停止 timer;
  4. 等待计划时间过去;
  5. 再启动 timer;
  6. 观察关联 service 是否被激活。

修改系统时间可能影响数据库、TLS、日志排序、分布式锁和其他业务,应避免在生产环境进行。


七、防重入:为什么“每分钟启动一次”会产生并发

设计划周期为 PP,单次任务运行时长为 DD

当:

DPD \leq P

且调度器没有额外延迟时,通常不会因为周期本身产生重叠。

当:

D>PD > P

则第一个任务仍在运行时,第二个计划时间已经到达。若调度器每次都直接创建新进程,就会出现并发:

时间轴:
00:00 ├────────────── 任务 A ──────────────┤
00:01       ├────────────── 任务 B ──────────────┤
00:02              ├────────────── 任务 C ...

这会导致:

  • 两个进程同时写同一个文件;
  • 同一批数据被重复处理;
  • 数据库锁竞争;
  • 备份任务读取正在变化的源目录;
  • API 调用次数突然增加;
  • 失败重试和下一次调度叠加。

7.1 cron 中使用 flock

Linux 上常见的防重入方式是使用文件锁:

*/5 * * * * /usr/bin/flock -n 9 /run/lock/app-sync.lock /usr/local/sbin/app-sync.sh

更明确的脚本形式:

#!/usr/bin/env bash
set -Eeuo pipefail

exec 9>/run/lock/app-sync.lock

if ! flock -n 9; then
    echo "another instance is running; skip" >&2
    exit 75
fi

/usr/local/bin/app-sync

这里的关键步骤是:

  1. exec 9>... 打开锁文件,并把文件描述符 9 绑定到它;
  2. flock -n 9 尝试取得排他锁;
  3. -n 表示不等待,锁已被占用时立即失败;
  4. 脚本退出或进程异常终止时,内核会释放文件锁。

文件本身通常不需要删除。删除“锁文件”并不能安全地解除一个仍由进程持有的内核锁,反而可能使不同进程对不同 inode 加锁,导致防重入失效。

锁目录需要正确权限。例如 /run/lock 下的文件可能只能由特定用户创建。应预先创建目录或使用 systemd 管理运行时目录,不能让任务第一次运行时才碰运气。

flock 只解决同一台机器、同一个共享内核文件系统上的互斥。多台机器共享 NFS 时,锁语义依赖 NFS 和挂载实现;跨主机任务应使用数据库锁、租约服务或专门的分布式协调机制。

7.2 systemd 为什么通常不会启动同一个 service 的第二个实例

systemd 的 service 单元是有状态的。对于同一个 backup.service

inactive ──启动──> activating ──成功──> inactive
                         │
                         └─失败──> failed

当 timer 再次触发时,如果该 service 已处于运行状态,systemd 不会为同一个 service 单元创建一个普通的第二实例。它不是任务队列,也不会为每次触发自动复制一个新的 service 名称。

这意味着:

  • Type=oneshot 正在运行时,后续触发不会形成同名 service 并发实例;
  • 任务结束后 service 回到 inactive,下一次触发可以再次运行;
  • 如果设置了 RemainAfterExit=yes,成功退出后 service 会保持 active (exited),后续对同一 service 的启动可能不会再次执行命令。

因此,对周期性的 oneshot 任务通常不要随意设置:

RemainAfterExit=yes

除非确实需要“成功后保持 active”这一状态语义。

不过,不能把 systemd 的单元级行为当成所有防重入需求的替代品:

  • 脚本可能自己派生后台进程;
  • 任务可能启动了另一个不同名称的 service;
  • 多个 timer 可能激活不同 service,但操作同一资源;
  • 任务可能在进程退出后仍有外部工作;
  • 应用层仍可能需要按资源、租户或业务日期加锁。

对于明确的资源互斥,仍可以在 service 中使用 flock

# /etc/systemd/system/app-sync.service
[Unit]
Description=Synchronize application data

[Service]
Type=oneshot
User=appsync
ExecStart=/usr/bin/flock -n /run/lock/app-sync.lock /usr/local/sbin/app-sync.sh

八、失败路径:触发成功不等于业务成功

定时任务至少有三个可观察结果:

调度器触发
    │
    ├─ service/进程成功启动
    │
    ├─ 程序正常退出
    │
    └─ 业务动作成功并持久化

这三个结果不能混淆。

例如 cron 成功创建了 /usr/local/sbin/backup.sh,但脚本因为路径、权限或网络错误立即退出。cron 可能没有能力理解“备份是否完整”。

systemd 可以记录 service 的退出状态:

systemctl show backup.service \
  -p ActiveState \
  -p SubState \
  -p Result \
  -p ExecMainStatus

常见诊断命令:

systemctl status backup.timer
systemctl status backup.service
systemctl list-timers --all
journalctl -u backup.service -b --no-pager
journalctl -u backup.service --since "2025-03-11 00:00:00"

如果 service 失败,timer 通常仍然可以等待下一次计划时间;是否自动重试由 service 配置决定,而不是由 OnCalendar 自动保证。

可以显式配置失败后的重启策略,但必须理解它的含义:

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/backup.sh
Restart=on-failure
RestartSec=5min

这会把“任务失败后的重试”交给 service 生命周期处理。若脚本具有副作用,重试前必须确认它是幂等的,否则可能重复扣款、重复发送消息或破坏部分结果。


九、时区设计:调度时区和业务时区必须分层

一个任务可能同时涉及四种时间:

  1. systemd 或 cron 判断触发的时间;
  2. Shell 中 date 显示的时间;
  3. 应用运行时默认时区;
  4. 数据库保存和查询所使用的时间。

例如服务器采用 UTC:

date
date -u

应用却以 Asia/Shanghai 显示账单。如果 timer 使用 UTC 的 03:00,它相当于北京时间 11:00;这不是错误,但必须是明确设计的结果。

9.1 以 UTC 作为基础的适用场景

UTC 适合:

  • 数据中心跨多个地域;
  • 任务按固定间隔运行;
  • 日志和事件需要全球排序;
  • 不希望夏令时改变触发次数。

例如:

[Timer]
OnCalendar=*-*-* 00:00:00 UTC

它表达的是 UTC 每天零点,而不是主机本地时间零点。

9.2 以业务时区作为规则的适用场景

“每天当地时间 03:00 生成报表”属于业务时区规则。此时应:

  • 在 systemd OnCalendar 中明确时区,或;
  • 用统一 UTC 调度后,由应用按业务时区计算业务日;
  • 处理夏令时中不存在或重复的本地时间;
  • 记录计划业务日期,而不只记录进程启动时间。

不能仅靠:

[Service]
Environment=TZ=Asia/Shanghai

来改变 timer 的调度时区。这个设置主要影响 service 进程中的环境;timer 在何时触发由 timer 的时间表达式和 systemd 的时区解析决定。


十、cron 和 systemd timer 的选择

10.1 cron 更适合什么

cron 的优点是:

  • 配置简单;
  • 系统普遍存在;
  • 适合简单的用户级周期命令;
  • 对旧系统和轻量环境兼容性好。

它的边界是:

  • 环境和日志约束较弱;
  • 服务生命周期表达能力有限;
  • 错过执行通常不补偿;
  • 依赖扩展来表达时区、锁和复杂调度;
  • 故障、资源限制和安全边界需要额外实现。

10.2 systemd timer 更适合什么

systemd timer 的优势是可以和 service 管理统一起来:

  • 明确的 service 用户和权限;
  • journal 日志;
  • 依赖关系;
  • TimeoutStartSec 等超时;
  • Restart= 和失败状态;
  • 资源控制和安全沙箱;
  • Persistent=true
  • 日历时间和单调时间;
  • systemctllist-timersjournalctl 统一诊断。

例如可以给任务增加基本资源和权限限制:

[Service]
Type=oneshot
User=backup
Group=backup
ExecStart=/usr/local/sbin/backup.sh
TimeoutStartSec=2h
NoNewPrivileges=yes
PrivateTmp=yes

这些选项不是无条件安全。PrivateTmp、只读文件系统、网络限制等沙箱设置可能破坏脚本对特定路径或设备的访问,必须先验证任务实际依赖。

systemd timer 也不是工作流引擎。它不会天然提供:

  • 持久化的多次触发队列;
  • 分布式锁;
  • 业务级重试状态;
  • 复杂依赖图中的数据补偿;
  • “每一次历史计划都必须执行”的完整审计。

这些能力仍要由任务程序、数据库或队列系统实现。


十一、一个完整的生产思路:计划、互斥、幂等、补偿

假设需求是:

每天业务时区 03:00 处理前一天数据;机器关机后启动要补做;任务不能并发;失败后可重试。

可以拆成四层:

第一层:systemd timer 负责日历触发

[Timer]
OnCalendar=*-*-* 03:00:00 Asia/Shanghai
Persistent=true
AccuracySec=1min

它负责产生“该尝试处理某个业务日”的触发。

第二层:service 负责进程身份和生命周期

[Service]
Type=oneshot
User=report
ExecStart=/usr/bin/flock -n /run/lock/report.lock /usr/local/sbin/report.sh
TimeoutStartSec=1h

它负责:

  • 用低权限用户运行;
  • 约束最长运行时间;
  • 防止同机并发;
  • 把结果接入 journal。

第三层:脚本负责幂等

脚本不能假设“触发一次就只会处理一次”。它应使用业务唯一键或状态表,例如:

report_status(
    business_date primary key,
    status,
    started_at,
    finished_at
)

处理前用事务或唯一约束抢占某个 business_date,已成功的日期直接跳过,失败的日期可以安全重试。

第四层:程序负责补偿历史缺口

启动时计算:

待处理日期 = 业务规则要求的日期 - 已成功日期

然后逐个处理缺口,而不是依赖 Persistent=true 产生多次历史触发。

这四层分别解决不同问题:

OnCalendar       -> 何时尝试
Persistent        -> 停机后是否产生一次补触发
flock             -> 同机是否允许并发
业务状态与幂等   -> 重试和历史补偿是否安全

把这四个职责混成一个 cron 表达式,最终通常会得到难以诊断的重复执行或数据遗漏。


十二、排查清单:从“没执行”定位到具体层

12.1 cron 任务没有出现

先确认配置属于哪个用户:

crontab -l
sudo crontab -l

确认 cron 服务状态:

systemctl status cron      # Debian/Ubuntu 常见
systemctl status crond     # Fedora/RHEL 常见

查看服务日志:

journalctl -u cron
journalctl -u crond

再检查:

  • 命令是否使用绝对路径;
  • 脚本是否可执行;
  • 重定向目标是否可写;
  • 脚本的 Shell 解释器是否存在;
  • /etc/cron.d 文件是否包含正确的用户字段;
  • 配置文件权限和格式是否满足本机实现要求。

12.2 systemd timer 没有触发

systemctl is-enabled backup.timer
systemctl is-active backup.timer
systemctl list-timers --all
systemctl cat backup.timer

重点区分:

  • timer 没有启用;
  • timer 没有启动;
  • OnCalendar 表达式没有下一次匹配;
  • timer 已触发但 service 启动失败;
  • service 正在运行,新的同名激活没有形成第二实例。

检查表达式:

systemd-analyze calendar '03:00 Asia/Shanghai'

检查 timer 触发和 service 结果:

systemctl show backup.timer -p LastTriggerUSec -p NextElapseUSecRealtime
systemctl show backup.service -p Result -p ExecMainStatus
journalctl -u backup.timer -u backup.service

12.3 任务只执行了一次

优先检查:

RemainAfterExit=yes

如果 Type=oneshot 的 service 成功后保持 active (exited),后续 timer 激活同一 service 时可能不会再次运行命令。周期任务通常应让 oneshot 在命令结束后回到非 active 状态。

然后检查:

  • timer 是否仍然 active;
  • service 是否持续运行;
  • 脚本是否自己进入后台;
  • 是否有锁文件导致每次都跳过;
  • 下一次触发时间是否因时区或夏令时变化而改变。

12.4 任务重复执行

cron 场景优先检查任务耗时是否大于周期,并添加 flock

systemd 场景检查:

  • 是否是不同 service 名称导致的多个实例;
  • 是否有多个 timer 指向不同 service;
  • 脚本是否创建后台进程;
  • 是否跨主机运行;
  • 是否把“重试”误认为“重复调度”。

十三、最终边界

cron 和 systemd timer 都是调度工具,不是业务可靠性本身。

  • cron 主要解决“守护进程运行时,匹配某个日历时间后启动命令”;
  • systemd timer 解决“用 systemd Unit 管理日历或单调时间,并激活 service”;
  • 时区决定计划时间如何映射到实际时间线;
  • 防重入决定同一资源是否允许并发;
  • Persistent=true 只能提供有限的停机补触发;
  • 真正的补偿、幂等、重试和业务审计必须由任务程序或持久化系统承担。

在简单任务中,cron 足够清晰;当任务需要明确的用户、日志、超时、资源限制、失败状态、时区和停机补触发时,systemd timer 通常能提供更完整的生命周期模型。但无论选择哪一种,都应把“计划触发、进程运行、业务完成”分别验证,而不能仅凭配置文件中的时间表达式判断任务可靠。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。