简介
Linux 上提到定时任务,最先想到的通常是cron。
cron足够简单,也足够稳定,但任务一旦涉及日志、启动依赖、超时控制、错过后补跑、运行用户和资源限制,单独一行 crontab 很快就会变得难以维护。
systemd Timer提供了另一套方案:
.timer 负责决定什么时候执行 .service 负责决定执行什么、以什么方式执行例如,每天凌晨备份一次应用数据,可以拆成两个单元:
myapp-backup.timer | | 到达触发时间 v myapp-backup.service | | 启动备份脚本 v /usr/local/sbin/myapp-backup.sh这样不仅能定时执行,还能直接使用systemctl查看状态,使用journalctl查看日志,并继续使用 systemd 的权限、依赖、超时和资源控制能力。
一句话概括:
systemd Timer 不是把 cron 表达式换一种写法,而是把定时任务纳入 systemd 的服务管理体系。systemd Timer 是什么?
Timer 是 systemd 的一种单元,文件名以.timer结尾。
Timer 本身通常不运行脚本。触发时间到达后,Timer 会激活另一个单元,最常见的目标是.service。
假设存在下面两个文件:
/etc/systemd/system/report.service /etc/systemd/system/report.timer当report.timer没有显式配置Unit=时,默认会触发同名的report.service。
也可以明确指定其他服务:
[Timer] OnCalendar=daily Unit=generate-report.service所以,“Timer 和 Service 必须同名”并不准确。
准确说法是:
没有配置 Unit= 时,foo.timer 默认触发 foo.service。同名是最省事、也最容易维护的做法,但不是硬性要求。
Timer 和 cron 有什么区别?
| 对比项 | cron | systemd Timer |
|---|---|---|
| 配置方式 | 一行 crontab | .timer和.service单元 |
| 日历定时 | 支持 | 支持,语法更丰富 |
| 开机后延时 | 通常需要额外处理 | OnBootSec= |
| 按间隔循环 | 支持有限 | 多种单调时间触发器 |
| 关机期间错过后补跑 | 默认不支持 | Persistent=true |
| 随机延迟 | 通常需要sleep | RandomizedDelaySec= |
| 日志 | 邮件、文件重定向或 syslog | 直接进入 journal |
| 超时和权限控制 | 需要脚本处理 | 交给.service |
| 服务依赖 | 较弱 | 使用 systemd 依赖关系 |
| 查看下次执行时间 | 不够直观 | systemctl list-timers |
systemd Timer 也不是任何场景都比 cron 合适。
临时写一个简单的个人任务,cron 配置更短;服务器已经由 systemd 管理,并且任务需要日志、补跑、超时或依赖控制时,Timer 更容易维护。
最小 Demo:每分钟输出一次当前时间
先用一个最小示例跑通完整流程。
这个示例不写脚本,也不使用额外配置,只做一件事:
每分钟执行一次 date 命令,并把输出写入 systemd 日志。创建 Service
创建/etc/systemd/system/show-time.service:
sudovim/etc/systemd/system/show-time.service内容如下:
[Unit] Description=Show current time [Service] Type=oneshot ExecStart=/usr/bin/date这里只需要关注两个配置:
Type=oneshot:命令执行完成后,服务结束ExecStart=/usr/bin/date:指定需要执行的命令
创建 Timer
创建/etc/systemd/system/show-time.timer:
sudovim/etc/systemd/system/show-time.timer内容如下:
[Unit] Description=Run show-time every minute [Timer] OnCalendar=minutely [Install] WantedBy=timers.targetOnCalendar=minutely表示每分钟触发一次。
Timer 和 Service 都叫show-time,所以show-time.timer会自动触发show-time.service。
启动 Timer
sudosystemctl daemon-reloadsudosystemctlenable--nowshow-time.timer查看下一次执行时间:
systemctl list-timers show-time.timer不想等待下一分钟,可以手动执行一次 Service:
sudosystemctl start show-time.service查看输出:
journalctl-ushow-time.service-n10--no-pager完整流程只有三步:
创建 .service 创建 .timer 启动 .timer理解这个最小示例后,再继续看各种时间规则和进阶配置。
两类时间:日历时间和单调时间
Timer 的时间规则主要分成两类。
日历时间
日历时间关注钟表上的日期和时间,例如:
每天 02:30 每周一 09:00 每月 1 日 00:00 工作日每小时一次对应配置项是OnCalendar=:
[Timer] OnCalendar=*-*-* 02:30:00这种方式和 cron 最接近,会受到系统日期、时区和夏令时影响。
单调时间
单调时间不关心当前是几月几日,而是关心某个事件过去了多久,例如:
开机 5 分钟后执行 Timer 启动 30 秒后执行 服务上次启动 1 小时后再次执行 服务上次结束 10 分钟后再次执行常见配置如下:
[Timer] OnBootSec=5min OnUnitInactiveSec=1h单调时钟不会受到手动修改系统时间的影响,更适合固定间隔任务。
常用配置:.service文件负责什么?
定时任务真正执行的命令写在.service中。
一个最小的 Service 如下:
[Unit] Description=Generate application report [Service] Type=oneshot ExecStart=/usr/local/bin/generate-report.shType=oneshot表示启动命令执行完成后,服务就结束。备份、清理、同步和报表生成等一次性任务通常使用这个类型。
常用配置如下:
| 配置 | 作用 |
|---|---|
User= | 指定任务使用哪个用户运行 |
Group= | 指定运行组 |
WorkingDirectory= | 指定工作目录 |
Environment= | 设置环境变量 |
EnvironmentFile= | 从文件读取环境变量 |
ExecStart= | 指定执行命令 |
TimeoutStartSec= | 限制任务最长执行时间 |
StandardOutput=journal | 把标准输出写入 journal |
StandardError=journal | 把标准错误写入 journal |
ExecStart=不是交互式 Shell,下面这些内容不能想当然地直接使用:
~ $HOME *.log 命令1 | 命令2 命令 > 文件管道、重定向、变量展开等 Shell 语法需要明确调用 Shell:
ExecStart=/bin/bash -c 'date >> /var/log/task.log'复杂逻辑更适合放进独立脚本。单元文件只保留启动参数,排错和复用都会更简单。
常用配置:.timer文件负责什么?
一个常见的 Timer 如下:
[Unit] Description=Run application report every day [Timer] OnCalendar=*-*-* 02:30:00 [Install] WantedBy=timers.target各部分作用如下:
[Unit]保存描述、依赖等通用配置[Timer]保存触发时间和 Timer 行为[Install]决定执行systemctl enable时建立什么启动关系
WantedBy=timers.target表示启用后,系统进入正常运行状态时会拉起这个 Timer。
注意:
enable 负责设置开机自动启动 start 负责当前立即启动 Timer因此最常见的命令是:
sudosystemctlenable--nowreport.timer--now会立即启动 Timer,但不会无条件立即执行 Service。Service 是否马上执行,仍由 Timer 的时间规则决定。
OnCalendar 日历语法
OnCalendar=的完整形式可以写成:
星期 年-月-日 时:分:秒没有用到的部分可以省略,常见关键字也可以直接使用。
| 配置 | 含义 |
|---|---|
OnCalendar=minutely | 每分钟一次 |
OnCalendar=hourly | 每小时一次 |
OnCalendar=daily | 每天 00:00 |
OnCalendar=weekly | 每周一 00:00 |
OnCalendar=monthly | 每月 1 日 00:00 |
OnCalendar=yearly | 每年 1 月 1 日 00:00 |
OnCalendar=*-*-* 02:30:00 | 每天 02:30 |
OnCalendar=Mon *-*-* 09:00:00 | 每周一 09:00 |
OnCalendar=Mon..Fri *-*-* 09:00:00 | 周一到周五 09:00 |
OnCalendar=*-*-01 00:00:00 | 每月 1 日 00:00 |
OnCalendar=*-01-01 00:00:00 | 每年 1 月 1 日 00:00 |
OnCalendar=*-*-* 09,15,21:00:00 | 每天 9、15、21 点 |
OnCalendar=*:0/5 | 每 5 分钟一次 |
Timer 还支持在表达式末尾指定时区:
OnCalendar=Mon..Fri *-*-* 09:00:00 Asia/Shanghai日历表达式容易在通配符、步进值上写错。不要靠肉眼猜,直接让 systemd 解析:
systemd-analyze calendar'*:0/5'输出中会包含规范化后的表达式、下一次触发时间以及距离触发还有多久。
查看连续多次触发时间:
systemd-analyze calendar--iterations=5'Mon..Fri *-*-* 09:00:00'这一步特别适合检查跨天、跨月和工作日规则。
单调时间触发器
常见的单调时间配置如下:
| 配置 | 起算点 |
|---|---|
OnActiveSec=30s | Timer 被激活后 30 秒 |
OnBootSec=5min | 系统启动后 5 分钟 |
OnStartupSec=5min | systemd 服务管理器启动后 5 分钟 |
OnUnitActiveSec=1h | 目标单元上次被激活后 1 小时 |
OnUnitInactiveSec=10min | 目标单元上次进入 inactive 状态后 10 分钟 |
OnUnitActiveSec=和OnUnitInactiveSec=经常被混淆。
假设任务每次运行需要 8 分钟:
OnUnitActiveSec=1h间隔从服务开始运行时计算,理论上的两次启动时间相隔 1 小时。
OnUnitInactiveSec=1h间隔从服务运行结束、进入 inactive 状态时计算,上一轮结束 1 小时后才会开始下一轮。
对于“每轮完成后休息一段时间”的采集或同步任务,OnUnitInactiveSec=更符合直觉。
开机后先执行一次,之后每轮结束 30 分钟再执行,可以这样写:
[Timer] OnBootSec=2min OnUnitInactiveSec=30min同一个 Timer 中可以配置多个触发器。多个触发器采用“任意一个到期就触发”的关系,不需要全部满足。
进阶配置:Persistent 错过任务后补跑
服务器每天 02:30 备份,但 02:00 到 04:00 处于关机状态,普通日历任务会错过这次执行。
加入下面的配置后,Timer 再次启动时会检查上次触发记录:
[Timer] OnCalendar=*-*-* 02:30:00 Persistent=true如果发现关机期间至少错过了一次,会尽快补执行一次。
这里有两个限制:
Persistent=true只对OnCalendar=这类日历触发器生效- 错过多次不会按次数全部补齐,只会触发一次
备份、账单生成和证书检查等任务通常适合开启;高频监控和临时状态采集未必需要补跑。
进阶配置:AccuracySec 精度不是越高越好
Timer 默认允许 systemd 在一定精度窗口内合并唤醒,以减少不必要的 CPU 唤醒和能耗。
AccuracySec=1min这并不表示任务每分钟执行一次,而是表示触发时间允许在 1 分钟精度窗口内被安排。
确实需要接近指定秒执行时,可以缩小精度:
AccuracySec=1s普通备份、清理任务没有必要追求毫秒或微秒精度。系统繁忙、服务依赖未满足或目标服务仍在运行时,即使精度设置很高,也不能保证程序准点开始执行。
进阶配置:RandomizedDelaySec 给任务加一点随机延迟
多台服务器都在整点备份,可能同时请求数据库、对象存储或监控接口,瞬间形成流量尖峰。
下面的配置会在原定时间后增加一个 0 到 15 分钟的随机延迟:
RandomizedDelaySec=15min这个参数适合:
- 集群批量备份
- 软件更新检查
- 日志上传
- 监控数据上报
- 定时调用外部接口
随机延迟不是执行间隔。每天 02:30 的任务设置 15 分钟随机延迟后,实际启动时间会落在大约 02:30 到 02:45 之间。
一个容易忽略的规则:服务还在运行时不会再启动一份
假设 Timer 每 5 分钟触发一次,但任务每次需要 8 分钟。
到达下一次触发时间时,对应 Service 仍是 active 状态,systemd 不会再并发启动一个相同 Service,也不会为每次触发排队补跑。
这能避免同一个 oneshot 服务重叠运行,但也意味着高频触发可能被跳过。
需要严格消费每个时间点的任务时,应该把任务写入消息队列或任务表,再由常驻 Worker 消费,不能把 Timer 当成可靠队列。
完整 Demo:每天备份应用数据
下面搭建一个可直接运行的备份任务:
每天 02:30 触发 最多随机延迟 15 分钟 关机错过后补跑一次 把 /srv/myapp/data 打包到 /var/backups/myapp 清理约 7 天前的旧备份 任务超过 30 分钟自动终止 执行日志进入 journal准备测试目录
sudomkdir-p/srv/myapp/dataecho'systemd timer demo'|sudotee/srv/myapp/data/demo.txtsudoinstall-d-m0700 /var/backups/myapp编写备份脚本
创建/usr/local/sbin/myapp-backup.sh:
sudovim/usr/local/sbin/myapp-backup.sh脚本内容如下:
#!/usr/bin/env bashset-Eeuopipefailumask077SOURCE_DIR="${SOURCE_DIR:-/srv/myapp/data}"BACKUP_DIR="${BACKUP_DIR:-/var/backups/myapp}"KEEP_DAYS="${KEEP_DAYS:-7}"if[[!-d"$SOURCE_DIR"]];thenecho"source directory does not exist:$SOURCE_DIR">&2exit1fiinstall-d-m0700"$BACKUP_DIR"timestamp="$(date'+%Y%m%d-%H%M%S')"archive="$BACKUP_DIR/myapp-$timestamp.tar.gz"temp_file="$(mktemp"$BACKUP_DIR/.myapp-XXXXXX.tar.gz")"cleanup(){rm-f"$temp_file"}trapcleanup EXITtar-C"$SOURCE_DIR"-czf"$temp_file".mv"$temp_file""$archive"trap- EXITfind"$BACKUP_DIR"\-maxdepth1\-typef\-name'myapp-*.tar.gz'\-mtime"+$KEEP_DAYS"\-deleteecho"backup completed:$archive"增加执行权限:
sudochmod0755 /usr/local/sbin/myapp-backup.sh脚本先写临时文件,压缩成功后再通过mv改成正式文件名。压缩中途失败时,不会留下一个看起来正常、实际已损坏的正式备份。
创建 Service
创建/etc/systemd/system/myapp-backup.service:
sudovim/etc/systemd/system/myapp-backup.service内容如下:
[Unit] Description=Back up MyApp data Documentation=man:systemd.service(5) ConditionPathIsDirectory=/srv/myapp/data [Service] Type=oneshot Environment=SOURCE_DIR=/srv/myapp/data Environment=BACKUP_DIR=/var/backups/myapp Environment=KEEP_DAYS=7 ExecStart=/usr/local/sbin/myapp-backup.sh TimeoutStartSec=30min User=root Group=root UMask=0077 Nice=10 IOSchedulingClass=best-effort IOSchedulingPriority=7 NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/var/backups/myapp StandardOutput=journal StandardError=journal这里的安全配置把系统目录设为只读,只允许任务写入/var/backups/myapp。源目录仍然可以读取。
如果实际数据位于/home下,ProtectHome=true会阻止访问,需要删除这一项或按实际目录调整。
创建 Timer
创建/etc/systemd/system/myapp-backup.timer:
sudovim/etc/systemd/system/myapp-backup.timer内容如下:
[Unit] Description=Run MyApp backup every day Documentation=man:systemd.timer(5) [Timer] OnCalendar=*-*-* 02:30:00 Persistent=true RandomizedDelaySec=15min AccuracySec=1min [Install] WantedBy=timers.target文件名都是myapp-backup,因此不需要额外配置:
Unit=myapp-backup.service校验配置
先校验单元文件:
sudosystemd-analyze verify\/etc/systemd/system/myapp-backup.service\/etc/systemd/system/myapp-backup.timer再校验日历表达式:
systemd-analyze calendar--iterations=3'*-*-* 02:30:00'没有错误后,让 systemd 重新读取单元文件:
sudosystemctl daemon-reload先手动测试 Service
不要等到凌晨才判断脚本能不能运行。直接手动启动一次 Service:
sudosystemctl start myapp-backup.service查看执行结果:
systemctl status myapp-backup.servicesudojournalctl-umyapp-backup.service-n50--no-pagersudols-lh/var/backups/myapponeshot 任务成功执行后变成inactive (dead)属于正常现象,不表示任务失败。重点查看Result=success、进程退出码和 journal 日志。
启用并启动 Timer
sudosystemctlenable--nowmyapp-backup.timer查看 Timer:
systemctl status myapp-backup.timer systemctl list-timers myapp-backup.timerlist-timers常见列含义如下:
| 列 | 含义 |
|---|---|
NEXT | 预计下次触发时间 |
LEFT | 距离下次触发还有多久 |
LAST | 上次触发时间 |
PASSED | 距离上次触发过去了多久 |
UNIT | Timer 单元 |
ACTIVATES | 被触发的目标单元 |
常用管理命令
查看所有 Timer
systemctl list-timers systemctl list-timers--all--all会把当前未激活的 Timer 也列出来。
启动、停止和重启
sudosystemctl start myapp-backup.timersudosystemctl stop myapp-backup.timersudosystemctl restart myapp-backup.timer设置或取消开机启动
sudosystemctlenablemyapp-backup.timersudosystemctl disable myapp-backup.timer停止当前 Timer 并取消开机启动:
sudosystemctl disable--nowmyapp-backup.timer手动执行一次任务
sudosystemctl start myapp-backup.service启动.service是立即执行一次,启动.timer是开始等待触发时间,两者不要混淆。
查看实际生效的配置
systemctlcatmyapp-backup.timer systemctlcatmyapp-backup.service查看 systemd 解析后的属性:
systemctl show myapp-backup.timer systemctl show myapp-backup.service查看日志
sudojournalctl-umyapp-backup.servicesudojournalctl-umyapp-backup.service--sincetodaysudojournalctl-umyapp-backup.service-fTimer 日志主要记录触发和状态变化:
sudojournalctl-umyapp-backup.timer业务脚本的标准输出和报错通常在 Service 日志中,所以排错时应该优先查看.service。
修改配置后怎样生效?
修改.service或.timer后,需要重新加载单元文件:
sudosystemctl daemon-reload修改 Timer 时间规则后,再重启 Timer:
sudosystemctl restart myapp-backup.timer如果只修改了外部脚本内容,通常不需要daemon-reload,下次执行时会直接读取新脚本。
网络任务怎样配置?
备份上传、接口调用等任务可能依赖网络,可以在 Service 中声明:
[Unit] Wants=network-online.target After=network-online.targetAfter=只表示启动顺序,不能保证外部网站一定可访问;Wants=会尝试拉起网络在线目标,但最终仍需要脚本处理 DNS 失败、超时和重试。
Timer 只负责触发,不会因为 Service 返回非零退出码就自动反复重试。失败重试应该通过脚本、自定义 Service 策略或专门的任务队列明确实现。
普通用户也能创建 Timer
不需要 root 权限的个人任务可以使用用户级 Timer。
单元文件目录是:
~/.config/systemd/user/例如:
~/.config/systemd/user/notes-sync.service ~/.config/systemd/user/notes-sync.timer管理命令需要加--user:
systemctl--userdaemon-reload systemctl--userenable--nownotes-sync.timer systemctl--userlist-timers journalctl--user-unotes-sync.service用户退出登录后,用户级 systemd 管理器是否继续运行取决于系统配置。需要退出后继续执行时,可以由管理员开启 linger:
sudologinctl enable-linger username系统级 Timer 的[Service]中也可以使用User=appuser,但这种方式和用户级 Timer 不是同一套管理范围。
常见问题排查
Timer 根本没有启动
systemctl status myapp-backup.timer systemctl is-enabled myapp-backup.timer systemctl list-timers--allenabled表示已经设置开机启动,active才表示当前正在等待触发。只执行enable而没有重启服务器,也没有执行start,Timer 当前可能仍未运行。
修改文件后没有变化
sudosystemctl daemon-reloadsudosystemctl restart myapp-backup.timer再用下面的命令确认 systemd 实际加载了什么:
systemctlcatmyapp-backup.timer时间表达式写错
systemd-analyze calendar--iterations=5'*:0/5'不要仅根据systemctl status猜测,先确认表达式是否能解析、下一次触发是否符合预期。
Service 手动执行也失败
sudosystemctl start myapp-backup.service systemctl status myapp-backup.servicesudojournalctl-umyapp-backup.service-e--no-pager常见原因包括:
ExecStart=使用了相对路径- 脚本没有执行权限
- 脚本依赖登录 Shell 中的
PATH - 运行用户没有文件读写权限
- 安全加固项阻止了目录访问
- 命令返回了非零退出码
- SELinux 或 AppArmor 拒绝访问
定时任务中的命令尽量使用绝对路径。可以这样查询:
command-vtarcommand-vcurlcommand-vmysqldumpTimer 正常,Service 却没有再次执行
先检查 Service 是否仍处于 active 状态:
systemctl status myapp-backup.service相同 Service 还在运行时,后续 Timer 触发不会再启动一个副本。任务卡死时,需要结合日志、进程状态和TimeoutStartSec=排查。
Persistent=true没有补跑
重点检查三项:
- 是否使用了
OnCalendar= - Timer 以前是否启动过并留下触发时间记录
- 错过时间后 Timer 是否真的重新变成 active
Persistent=true对OnBootSec=、OnUnitActiveSec=等单调触发器不起作用。
常用模板
一次性任务的 Service 模板:
[Unit] Description=Run scheduled task [Service] Type=oneshot User=root ExecStart=/usr/local/sbin/task.sh TimeoutStartSec=10min StandardOutput=journal StandardError=journal固定时间执行的 Timer 模板:
[Unit] Description=Run scheduled task every day [Timer] OnCalendar=*-*-* 02:00:00 Persistent=true RandomizedDelaySec=10min AccuracySec=1min [Install] WantedBy=timers.target固定间隔执行的 Timer 模板:
[Unit] Description=Run scheduled task periodically [Timer] OnBootSec=2min OnUnitInactiveSec=30min [Install] WantedBy=timers.target部署命令:
sudosystemd-analyze verify\/etc/systemd/system/task.service\/etc/systemd/system/task.timersudosystemctl daemon-reloadsudosystemctl start task.servicesudosystemctlenable--nowtask.timer总结
systemd Timer 的核心并不复杂:
.service 定义任务内容和运行方式 .timer 定义触发时间日历任务使用OnCalendar=,相对时间任务使用OnBootSec=、OnUnitActiveSec=或OnUnitInactiveSec=。
生产环境中还需要重点关注:
Persistent=true是否需要补跑RandomizedDelaySec=是否需要错峰TimeoutStartSec=是否能防止任务长期卡住- Service 的运行用户和目录权限是否正确
- 脚本是否使用绝对路径并正确返回退出码
- journal 中是否保留了足够的排错信息
从 cron 迁移到 systemd Timer 后,最大的变化不是定时语法,而是每个定时任务都变成了一个可查看状态、可追踪日志、可限制权限的系统服务。
参考资料
- systemd.timer 官方手册
- systemd.time 官方手册
- systemd.service 官方手册
- systemctl 官方手册
- systemd-analyze 官方手册