Systemd 定时器指南:用可靠调度替代 Cron 任务
当需要日志记录、错过运行处理、依赖关系和资源控制时,使用 systemd 定时器替代 cron。
Systemd 定时器指南:用可靠调度替代 Cron 任务
对于许多任务来说,cron 仍然很好用。如果你需要每晚运行一个 shell 脚本,并且已经配置好了日志重定向,那么替换它并没有额外的好处。许多团队将 Linux 上的计划任务迁移到 systemd 定时器,并非出于时尚,而是因为 systemd 定时器为任务提供了真正的服务单元、可预测的日志、依赖处理、错过运行行为以及资源限制。
当任务不仅仅是“运行一个命令”时,这一点就很重要。备份任务可能需要挂载磁盘;缓存预热可能需要网络可用;报告导出可能需要以受限服务账户运行,并为后续值班人员留下可读的日志。systemd 定时器让你能够在管理其他服务生命周期的同一位置描述这些需求。
理解 Systemd 定时器
systemd 定时器是 systemd 单元文件,用于控制其他 systemd 单元(通常是 service 单元)何时被激活。与独立的守护进程 cron 不同,systemd 定时器是 systemd 初始化系统的一个组成部分。这种深度集成带来了几个显著的好处,特别是在可靠性、日志记录和资源管理方面。
systemd 定时器始终与另一个单元配合工作,最常见的是 service 单元。.timer 文件定义了事件何时发生,而对应的 .service 文件定义了事件触发时执行什么操作。这种清晰的关注点分离使得 systemd 定时器高度模块化和灵活。
Systemd 定时器相对于 Cron 的关键优势
虽然 cron 功能齐全,但 systemd 定时器解决了它的许多局限性,提供了更强大、功能更丰富的调度解决方案:
- 可靠性和持久性:如果日历定时器设置了
Persistent=true,并且系统在计划运行期间关机,systemd 会记录运行被错过,并在下次启动后立即启动关联的服务。普通的 cron 通常不会补上,除非使用像 anacron 这样的独立工具。 - 与
systemd集成:定时器受益于systemd强大的日志记录(通过journalctl)、依赖管理和资源控制(cgroups)。这意味着更好的监控、更清晰的错误报告,以及为计划任务定义复杂的启动序列或资源限制的能力。 - 可重现性和版本控制:
systemd单元文件是纯文本文件,可以轻松存储在版本控制系统中。这允许可重现的部署,并更容易跨多个系统跟踪计划任务的变更。 - 基于事件的调度:除了简单的时间调度,
systemd定时器可以相对于系统启动(OnBootSec)或单元上次激活后(OnUnitActiveSec)触发,提供更动态的调度选项。 - 灵活的时间表达式:
systemd提供丰富的日历事件表达式,通常比cron的语法更易读且更通用,包括每小时、每天、每周以及特定的日期/时间。 - 资源管理和依赖:由定时器启动的
systemd服务继承systemd环境,包括 cgroup 设置,并且可以声明对其他systemd单元的依赖(例如,在运行前等待网络或数据库可用)。 - 标准输出/错误处理:
systemd自动捕获由定时器启动的服务的stdout和stderr,并将其定向到系统日志,这使得调试和审计比cron基于邮件的输出或手动重定向简单得多。
配置 Systemd 定时器
配置 systemd 定时器涉及创建两个单元文件:一个服务单元(.service)和一个定时器单元(.timer)。这些文件通常放在 /etc/systemd/system/ 用于系统范围的定时器,或 ~/.config/systemd/user/ 用于用户特定的定时器。
1. 服务单元(.service 文件)
服务单元定义了要执行的实际命令或脚本。它是一个标准的 systemd 服务文件,但通常设计为以非交互方式运行并执行特定任务。
示例:/etc/systemd/system/mytask.service
[Unit]
Description=我的计划任务服务
[Service]
Type=oneshot
ExecStart=/usr/local/bin/mytask.sh
User=myuser
Group=mygroup
# 可选:在较新的 systemd 版本上限制资源
# CPUWeight=50
# MemoryMax=1G
[Install]
WantedBy=multi-user.target
说明:
[Unit]:包含单元的通用信息。Description:人类可读的描述。
[Service]:定义服务特定的配置。Type=oneshot:表示服务运行单个命令然后退出。这对于计划任务很常见。ExecStart:要执行的命令或脚本。提供完整路径。User、Group:定义命令运行的用户和组。始终以最小必要权限运行任务。CPUWeight、MemoryMax:可选的 cgroup 控制。当计划任务不应耗尽主机其他部分资源时很有用。
[Install]:定义如何启用单元。WantedBy=multi-user.target:虽然存在,但此部分对于定时器触发的服务通常不那么关键,因为定时器单元本身通常决定激活。但是,如果你也希望服务可以手动激活或集成到其他systemd目标中,它可能很有用。
2. 定时器单元(.timer 文件)
定时器单元定义了对应的服务单元何时被激活。它必须与其服务对应部分同名(例如,mytask.service 对应 mytask.timer)。
示例:/etc/systemd/system/mytask.timer
[Unit]
Description=每天运行 mytask.service
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=600
AccuracySec=1min
[Install]
WantedBy=timers.target
说明:
[Unit]:通用信息。Description:定时器的描述。
[Timer]:定义定时器特定的配置。OnCalendar:最常见的设置,定义日历事件。它使用如下表达式:daily:每天午夜。weekly:每周一午夜。monthly:每月第一天午夜。hourly:每小时整点。*-*-* 03:00:00:每天凌晨 3:00。Mon..Fri 08:00..17:00:工作日上午 8 点到下午 5 点。Mon *-*-* 03:00:00:每周一凌晨 3 点。
OnBootSec:在系统启动后指定时间激活服务。例如,OnBootSec=10min。OnUnitActiveSec:在服务上次激活后指定时间激活服务。例如,OnUnitActiveSec=1h在上次运行完成后每小时运行一次。Persistent=true:对可靠性至关重要。如果系统在计划运行期间关闭,服务将在下次启动后不久触发。RandomizedDelaySec=600:添加最多 600 秒的随机延迟。当许多机器共享同一个定时器,并且你不希望所有主机在同一秒内访问数据库、API 或备份服务器时,这很有用。
一个真实的 Cron 到定时器迁移
假设你当前有以下 root cron 条目:
15 2 * * * /usr/local/sbin/backup-app.sh >> /var/log/backup-app.log 2>&1
它在安静的机器上工作,但存在常见的弱点。如果备份磁盘未挂载,脚本可能中途失败。如果服务器在凌晨 2:15 关机,运行会被跳过。如果脚本输出有用的错误,有人必须记住检查哪个自定义日志文件。如果脚本开始使用过多内存,cron 不会帮助你限制它。
systemd 版本将命令与调度分开:
# /etc/systemd/system/backup-app.service
[Unit]
Description=备份应用程序数据
RequiresMountsFor=/mnt/backups
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=backup
Group=backup
WorkingDirectory=/srv/app
ExecStart=/usr/local/sbin/backup-app.sh
MemoryMax=1G
CPUWeight=40
Nice=10
# /etc/systemd/system/backup-app.timer
[Unit]
Description=每晚运行应用程序备份
[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
RandomizedDelaySec=15min
AccuracySec=1min
Unit=backup-app.service
[Install]
WantedBy=timers.target
有几个细节值得注意。RequiresMountsFor=/mnt/backups 告诉 systemd 在服务启动前必须挂载该路径。After=network-online.target 和 Wants=network-online.target 仅在你的网络管理器实际提供等待在线服务时有用;在许多发行版上,该服务默认是禁用的。如果备份只写入本地磁盘,请去掉网络依赖。
Type=oneshot 适用于完成工作后退出的脚本。不要将其用于持续运行的守护进程。WorkingDirectory= 可以避免那些意外依赖于从特定目录的 shell 启动的脚本。User=backup 通常比以 root 身份运行任务并希望脚本中的每个命令都小心谨慎更好。
保存文件后:
sudo systemctl daemon-reload
sudo systemctl enable --now backup-app.timer
systemctl list-timers backup-app.timer
要立即测试任务,启动服务,而不是定时器:
sudo systemctl start backup-app.service
journalctl -u backup-app.service -n 100 --no-pager
这一区别可以避免很多混淆。启动 backup-app.timer 会激活调度。启动 backup-app.service 会运行实际的备份。
选择合适的定时器表达式
OnCalendar= 是最接近 cron 语法的替代品,但它的读法不同。你可以在部署之前检查 systemd 对表达式的理解:
systemd-analyze calendar 'Mon..Fri 03:30'
systemd-analyze calendar '*-*-01 04:00:00'
systemd-analyze calendar 'Sun *-*-* 23:00:00'
对于基于挂钟时间的任务使用日历定时器:夜间备份、每周报告、每月清理、证书检查以及其他人类日历重要的任务。对于“在事件发生后运行”的行为,使用单调定时器:
[Timer]
OnBootSec=10min
OnUnitActiveSec=1h
这种模式在启动后十分钟启动服务,然后在最后一次激活后一小时再次启动。它非常适合轮询、本地清理和轻量级维护循环。但它与“每小时整点”不同。如果任务需要十二分钟,下一次运行是从激活时间开始计算,而不是从你的挂钟时间预期。
还要考虑重叠。对于普通服务单元,systemd 不会仅仅因为下一个定时器事件到达而启动同一活动单元的第二个副本。如果你的任务运行时间可能超过其间隔,请决定这是否可接受。有时正确的答案是在脚本中使用锁,例如 flock,因为它可以产生清晰的“上次运行仍在进行中”消息。有时正确的答案是延长间隔。
节省时间的操作习惯
定时器视图是你的第一个仪表板:
systemctl list-timers --all
它显示上次运行、下次运行以及每个定时器激活的单元。如果列出了定时器但服务从未运行,请检查日历表达式以及定时器是否已启用。如果服务运行但失败,暂时忽略定时器并检查服务:
systemctl status backup-app.service
journalctl -u backup-app.service --since today
当你编辑任一单元文件时,运行:
sudo systemctl daemon-reload
sudo systemctl restart backup-app.timer
在调度更改后重启定时器是一个好习惯,因为它会立即刷新下次激活时间。如果你只更改了脚本本身,通常不需要 daemon-reload。
对于用户定时器,使用 systemctl --user 并将单元放在 ~/.config/systemd/user/ 下。它们对于开发者工作站和每个用户的自动化很有用,但有一个重要的注意事项:默认情况下,用户服务与用户的登录会话绑定。如果你需要用户定时器在注销后继续运行,使用 loginctl enable-linger username 启用持久化。这是一个有意的管理选择,而不是隐藏在文章中作为神奇修复的东西。
何时 Cron 仍然是更好的工具
不要盲目迁移所有内容。对于很小的用户本地任务,尤其是在 systemd 不是 PID 1 的旧服务器或最小容器中,cron 更容易阅读。如果你的唯一要求是“每五分钟运行这个无害的命令”,cron 可能是最清晰的答案。
当任务具有类似服务的需求时,systemd 定时器会带来回报:受控的身份、日志中的记录、资源限制、依赖、追赶行为或通过单元文件的标准部署。在实践中,当计划任务失败会吵醒某人时,我会使用定时器。额外的单元文件是值得的,因为它为下一个操作员提供了从“运行了什么?”到“什么失败了?”再到“什么改变了?”的直接路径。
在迁移过程中,还有一个值得采纳的习惯:仅在定时器成功运行几次后,才保留旧的 cron 条目注释在旁边,然后将其删除。重复的调度是安静的损害来源。两个备份任务可能竞争同一个锁,两个清理任务可能比预期更早删除文件,两个报告任务可能发送重复的电子邮件。启用定时器后,检查 systemctl list-timers --all,确认服务日志,并确保旧的 cron 路径不再活动。