别再只会用 cron:Linux systemd Timer 定时任务实战详解
简介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] OnCalendardaily Unitgenerate-report.service所以“Timer 和 Service 必须同名”并不准确。准确说法是没有配置 Unit 时foo.timer 默认触发 foo.service。同名是最省事、也最容易维护的做法但不是硬性要求。Timer 和 cron 有什么区别对比项cronsystemd Timer配置方式一行 crontab.timer和.service单元日历定时支持支持语法更丰富开机后延时通常需要额外处理OnBootSec按间隔循环支持有限多种单调时间触发器关机期间错过后补跑默认不支持Persistenttrue随机延迟通常需要sleepRandomizedDelaySec日志邮件、文件重定向或 syslog直接进入 journal超时和权限控制需要脚本处理交给.service服务依赖较弱使用 systemd 依赖关系查看下次执行时间不够直观systemctl list-timerssystemd Timer 也不是任何场景都比 cron 合适。临时写一个简单的个人任务cron 配置更短服务器已经由 systemd 管理并且任务需要日志、补跑、超时或依赖控制时Timer 更容易维护。最小 Demo每分钟输出一次当前时间先用一个最小示例跑通完整流程。这个示例不写脚本也不使用额外配置只做一件事每分钟执行一次 date 命令并把输出写入 systemd 日志。创建 Service创建/etc/systemd/system/show-time.servicesudovim/etc/systemd/system/show-time.service内容如下[Unit] DescriptionShow current time [Service] Typeoneshot ExecStart/usr/bin/date这里只需要关注两个配置Typeoneshot命令执行完成后服务结束ExecStart/usr/bin/date指定需要执行的命令创建 Timer创建/etc/systemd/system/show-time.timersudovim/etc/systemd/system/show-time.timer内容如下[Unit] DescriptionRun show-time every minute [Timer] OnCalendarminutely [Install] WantedBytimers.targetOnCalendarminutely表示每分钟触发一次。Timer 和 Service 都叫show-time所以show-time.timer会自动触发show-time.service。启动 Timersudosystemctl daemon-reloadsudosystemctlenable--nowshow-time.timer查看下一次执行时间systemctl list-timers show-time.timer不想等待下一分钟可以手动执行一次 Servicesudosystemctl 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] OnBootSec5min OnUnitInactiveSec1h单调时钟不会受到手动修改系统时间的影响更适合固定间隔任务。常用配置.service文件负责什么定时任务真正执行的命令写在.service中。一个最小的 Service 如下[Unit] DescriptionGenerate application report [Service] Typeoneshot ExecStart/usr/local/bin/generate-report.shTypeoneshot表示启动命令执行完成后服务就结束。备份、清理、同步和报表生成等一次性任务通常使用这个类型。常用配置如下配置作用User指定任务使用哪个用户运行Group指定运行组WorkingDirectory指定工作目录Environment设置环境变量EnvironmentFile从文件读取环境变量ExecStart指定执行命令TimeoutStartSec限制任务最长执行时间StandardOutputjournal把标准输出写入 journalStandardErrorjournal把标准错误写入 journalExecStart不是交互式 Shell下面这些内容不能想当然地直接使用~ $HOME *.log 命令1 | 命令2 命令 文件管道、重定向、变量展开等 Shell 语法需要明确调用 ShellExecStart/bin/bash -c date /var/log/task.log复杂逻辑更适合放进独立脚本。单元文件只保留启动参数排错和复用都会更简单。常用配置.timer文件负责什么一个常见的 Timer 如下[Unit] DescriptionRun application report every day [Timer] OnCalendar*-*-* 02:30:00 [Install] WantedBytimers.target各部分作用如下[Unit]保存描述、依赖等通用配置[Timer]保存触发时间和 Timer 行为[Install]决定执行systemctl enable时建立什么启动关系WantedBytimers.target表示启用后系统进入正常运行状态时会拉起这个 Timer。注意enable 负责设置开机自动启动 start 负责当前立即启动 Timer因此最常见的命令是sudosystemctlenable--nowreport.timer--now会立即启动 Timer但不会无条件立即执行 Service。Service 是否马上执行仍由 Timer 的时间规则决定。OnCalendar 日历语法OnCalendar的完整形式可以写成星期 年-月-日 时:分:秒没有用到的部分可以省略常见关键字也可以直接使用。配置含义OnCalendarminutely每分钟一次OnCalendarhourly每小时一次OnCalendardaily每天 00:00OnCalendarweekly每周一 00:00OnCalendarmonthly每月 1 日 00:00OnCalendaryearly每年 1 月 1 日 00:00OnCalendar*-*-* 02:30:00每天 02:30OnCalendarMon *-*-* 09:00:00每周一 09:00OnCalendarMon..Fri *-*-* 09:00:00周一到周五 09:00OnCalendar*-*-01 00:00:00每月 1 日 00:00OnCalendar*-01-01 00:00:00每年 1 月 1 日 00:00OnCalendar*-*-* 09,15,21:00:00每天 9、15、21 点OnCalendar*:0/5每 5 分钟一次Timer 还支持在表达式末尾指定时区OnCalendarMon..Fri *-*-* 09:00:00 Asia/Shanghai日历表达式容易在通配符、步进值上写错。不要靠肉眼猜直接让 systemd 解析systemd-analyze calendar*:0/5输出中会包含规范化后的表达式、下一次触发时间以及距离触发还有多久。查看连续多次触发时间systemd-analyze calendar--iterations5Mon..Fri *-*-* 09:00:00这一步特别适合检查跨天、跨月和工作日规则。单调时间触发器常见的单调时间配置如下配置起算点OnActiveSec30sTimer 被激活后 30 秒OnBootSec5min系统启动后 5 分钟OnStartupSec5minsystemd 服务管理器启动后 5 分钟OnUnitActiveSec1h目标单元上次被激活后 1 小时OnUnitInactiveSec10min目标单元上次进入 inactive 状态后 10 分钟OnUnitActiveSec和OnUnitInactiveSec经常被混淆。假设任务每次运行需要 8 分钟OnUnitActiveSec1h间隔从服务开始运行时计算理论上的两次启动时间相隔 1 小时。OnUnitInactiveSec1h间隔从服务运行结束、进入 inactive 状态时计算上一轮结束 1 小时后才会开始下一轮。对于“每轮完成后休息一段时间”的采集或同步任务OnUnitInactiveSec更符合直觉。开机后先执行一次之后每轮结束 30 分钟再执行可以这样写[Timer] OnBootSec2min OnUnitInactiveSec30min同一个 Timer 中可以配置多个触发器。多个触发器采用“任意一个到期就触发”的关系不需要全部满足。进阶配置Persistent 错过任务后补跑服务器每天 02:30 备份但 02:00 到 04:00 处于关机状态普通日历任务会错过这次执行。加入下面的配置后Timer 再次启动时会检查上次触发记录[Timer] OnCalendar*-*-* 02:30:00 Persistenttrue如果发现关机期间至少错过了一次会尽快补执行一次。这里有两个限制Persistenttrue只对OnCalendar这类日历触发器生效错过多次不会按次数全部补齐只会触发一次备份、账单生成和证书检查等任务通常适合开启高频监控和临时状态采集未必需要补跑。进阶配置AccuracySec 精度不是越高越好Timer 默认允许 systemd 在一定精度窗口内合并唤醒以减少不必要的 CPU 唤醒和能耗。AccuracySec1min这并不表示任务每分钟执行一次而是表示触发时间允许在 1 分钟精度窗口内被安排。确实需要接近指定秒执行时可以缩小精度AccuracySec1s普通备份、清理任务没有必要追求毫秒或微秒精度。系统繁忙、服务依赖未满足或目标服务仍在运行时即使精度设置很高也不能保证程序准点开始执行。进阶配置RandomizedDelaySec 给任务加一点随机延迟多台服务器都在整点备份可能同时请求数据库、对象存储或监控接口瞬间形成流量尖峰。下面的配置会在原定时间后增加一个 0 到 15 分钟的随机延迟RandomizedDelaySec15min这个参数适合集群批量备份软件更新检查日志上传监控数据上报定时调用外部接口随机延迟不是执行间隔。每天 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/dataechosystemd timer demo|sudotee/srv/myapp/data/demo.txtsudoinstall-d-m0700 /var/backups/myapp编写备份脚本创建/usr/local/sbin/myapp-backup.shsudovim/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]];thenechosource directory does not exist:$SOURCE_DIR2exit1fiinstall-d-m0700$BACKUP_DIRtimestamp$(date%Y%m%d-%H%M%S)archive$BACKUP_DIR/myapp-$timestamp.tar.gztemp_file$(mktemp$BACKUP_DIR/.myapp-XXXXXX.tar.gz)cleanup(){rm-f$temp_file}trapcleanup EXITtar-C$SOURCE_DIR-czf$temp_file.mv$temp_file$archivetrap- EXITfind$BACKUP_DIR\-maxdepth1\-typef\-namemyapp-*.tar.gz\-mtime$KEEP_DAYS\-deleteechobackup completed:$archive增加执行权限sudochmod0755 /usr/local/sbin/myapp-backup.sh脚本先写临时文件压缩成功后再通过mv改成正式文件名。压缩中途失败时不会留下一个看起来正常、实际已损坏的正式备份。创建 Service创建/etc/systemd/system/myapp-backup.servicesudovim/etc/systemd/system/myapp-backup.service内容如下[Unit] DescriptionBack up MyApp data Documentationman:systemd.service(5) ConditionPathIsDirectory/srv/myapp/data [Service] Typeoneshot EnvironmentSOURCE_DIR/srv/myapp/data EnvironmentBACKUP_DIR/var/backups/myapp EnvironmentKEEP_DAYS7 ExecStart/usr/local/sbin/myapp-backup.sh TimeoutStartSec30min Userroot Grouproot UMask0077 Nice10 IOSchedulingClassbest-effort IOSchedulingPriority7 NoNewPrivilegestrue PrivateTmptrue ProtectSystemstrict ProtectHometrue ReadWritePaths/var/backups/myapp StandardOutputjournal StandardErrorjournal这里的安全配置把系统目录设为只读只允许任务写入/var/backups/myapp。源目录仍然可以读取。如果实际数据位于/home下ProtectHometrue会阻止访问需要删除这一项或按实际目录调整。创建 Timer创建/etc/systemd/system/myapp-backup.timersudovim/etc/systemd/system/myapp-backup.timer内容如下[Unit] DescriptionRun MyApp backup every day Documentationman:systemd.timer(5) [Timer] OnCalendar*-*-* 02:30:00 Persistenttrue RandomizedDelaySec15min AccuracySec1min [Install] WantedBytimers.target文件名都是myapp-backup因此不需要额外配置Unitmyapp-backup.service校验配置先校验单元文件sudosystemd-analyze verify\/etc/systemd/system/myapp-backup.service\/etc/systemd/system/myapp-backup.timer再校验日历表达式systemd-analyze calendar--iterations3*-*-* 02:30:00没有错误后让 systemd 重新读取单元文件sudosystemctl daemon-reload先手动测试 Service不要等到凌晨才判断脚本能不能运行。直接手动启动一次 Servicesudosystemctl start myapp-backup.service查看执行结果systemctl status myapp-backup.servicesudojournalctl-umyapp-backup.service-n50--no-pagersudols-lh/var/backups/myapponeshot 任务成功执行后变成inactive (dead)属于正常现象不表示任务失败。重点查看Resultsuccess、进程退出码和 journal 日志。启用并启动 Timersudosystemctlenable--nowmyapp-backup.timer查看 Timersystemctl status myapp-backup.timer systemctl list-timers myapp-backup.timerlist-timers常见列含义如下列含义NEXT预计下次触发时间LEFT距离下次触发还有多久LAST上次触发时间PASSED距离上次触发过去了多久UNITTimer 单元ACTIVATES被触发的目标单元常用管理命令查看所有 Timersystemctl 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 时间规则后再重启 Timersudosystemctl restart myapp-backup.timer如果只修改了外部脚本内容通常不需要daemon-reload下次执行时会直接读取新脚本。网络任务怎样配置备份上传、接口调用等任务可能依赖网络可以在 Service 中声明[Unit] Wantsnetwork-online.target Afternetwork-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管理命令需要加--usersystemctl--userdaemon-reload systemctl--userenable--nownotes-sync.timer systemctl--userlist-timers journalctl--user-unotes-sync.service用户退出登录后用户级 systemd 管理器是否继续运行取决于系统配置。需要退出后继续执行时可以由管理员开启 lingersudologinctl enable-linger username系统级 Timer 的[Service]中也可以使用Userappuser但这种方式和用户级 Timer 不是同一套管理范围。常见问题排查Timer 根本没有启动systemctl status myapp-backup.timer systemctl is-enabled myapp-backup.timer systemctl list-timers--allenabled表示已经设置开机启动active才表示当前正在等待触发。只执行enable而没有重启服务器也没有执行startTimer 当前可能仍未运行。修改文件后没有变化sudosystemctl daemon-reloadsudosystemctl restart myapp-backup.timer再用下面的命令确认 systemd 实际加载了什么systemctlcatmyapp-backup.timer时间表达式写错systemd-analyze calendar--iterations5*: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排查。Persistenttrue没有补跑重点检查三项是否使用了OnCalendarTimer 以前是否启动过并留下触发时间记录错过时间后 Timer 是否真的重新变成 activePersistenttrue对OnBootSec、OnUnitActiveSec等单调触发器不起作用。常用模板一次性任务的 Service 模板[Unit] DescriptionRun scheduled task [Service] Typeoneshot Userroot ExecStart/usr/local/sbin/task.sh TimeoutStartSec10min StandardOutputjournal StandardErrorjournal固定时间执行的 Timer 模板[Unit] DescriptionRun scheduled task every day [Timer] OnCalendar*-*-* 02:00:00 Persistenttrue RandomizedDelaySec10min AccuracySec1min [Install] WantedBytimers.target固定间隔执行的 Timer 模板[Unit] DescriptionRun scheduled task periodically [Timer] OnBootSec2min OnUnitInactiveSec30min [Install] WantedBytimers.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。生产环境中还需要重点关注Persistenttrue是否需要补跑RandomizedDelaySec是否需要错峰TimeoutStartSec是否能防止任务长期卡住Service 的运行用户和目录权限是否正确脚本是否使用绝对路径并正确返回退出码journal 中是否保留了足够的排错信息从 cron 迁移到 systemd Timer 后最大的变化不是定时语法而是每个定时任务都变成了一个可查看状态、可追踪日志、可限制权限的系统服务。参考资料systemd.timer 官方手册systemd.time 官方手册systemd.service 官方手册systemctl 官方手册systemd-analyze 官方手册