Use Systemd Timers Instead of Cron Jobs

某次逛论坛,发现使用Systemd管理的Linux发行版,应该使用Systemd Timers管理定时任务,并替代原来的Cron Jobs。

1. Systemd Units

Systemd Units(单元),是System的最小功能单位。其按功能划分为12种类型,例如target、service、timer等。“单元”之间互相依赖和调用,组成Systemd这个任务管理系统。

System Units存放路径:

  • /usr/lib/systemd/system,系统默认的单元文件
  • /etc/systemd/system,用户安装的软件的单元文件

注意:

  • Debian 12中,/usr/lib映射到/lib,所以/lib/systemd/system/usr/lib/systemd/system的文件相同。
  • Systemd读取Unit文件时,先读/usr/lib/systemd/system,再读/etc/systemd/system。所以修改原来的Unit文件,或者自定义的Unit文件,建议放在/etc/systemd/system
  • 编辑Unit文件,建议使用命令systemctl edit --full xxx.service,保存后会自动reload。

配置Systemd定时器时,需要创建两个Unit:

  • xxx.service,service类型,用于配置需要执行的任务。如果该任务已配置,则不用创建此文件。
  • xxx.timer,timer类型,对xxx.service执行定时触发。

2. 创建Systemd Service

对于Cron Job,会把定时执行的脚本或命令,与定时配置写在一起。配置是简单直接,但是不好管理。Systemd需要把定时执行的任务,配置为oneshot的Service。

例如创建文件/etc/systemd/system/sample-job.service

[Unit]
Description=sample job

[Service]
Type=oneshot
ExecStart=/bin/bash /path/to/sample-job.sh

[Install]
WantedBy=multi-user.target

注意:

  • Type=oneshot,表示只运行一次,不用长期运行。
  • WantedBy,配置了多用户命令状态时,才能执行。

3. 创建Systemd Timer

一般可分为指定“时间点”和“时间间隔”两种触发方式。详见:systemd.timer

  • 指定时间点,只需配置触发的一个或多个时间点。
    • OnCalendar=,设置日期时间触发。
  • 指定时间间隔,需要配置第一次触发和往后的周期触发。
    • OnBootSec=,机器启动后多久触发。在容器中,对于系统管理器实例,这被映射到OnStartupSec=,使两者等效。
    • OnStartupSec=,服务管理器首次启动后多久触发。类似于OnBootSec=。它主要适用于在每个用户服务管理器中运行的单元,因为用户服务管理器通常只在首次登录时启动,而不是在启动过程中启动。
    • OnActiveSec=,定时器单元本身被激活后多久触发。
    • OnUnitActiveSec=,在Unit=的上次激活后多久触发。可实现服务多久执行一次。
    • OnUnitInactiveSec=,在Unit=的上次停用后多久触发。可实现服务完成后隔多久才会再次执行。

指定“时间点”的简单示例,例如创建文件/etc/systemd/system/sample-job.timer

[Unit]
Description=Auto run sample job

[Timer]
OnCalendar=Mon,Thu *-*-* 01:23:49
Unit=sample-job.service

[Install]
WantedBy=multi-user.target

注意:

  • Unit,配置对应的Service。
  • OnCalendar,按配置的时间点(例如指定日期时间)运行。格式是DOW yyyy-MM-dd HH:mm:ss,其中DOW可以是Mon,Fri(周一和周五这2天)或者Mon..Fri(周一到周五这5天)
    • 详细参考:https://www.freedesktop.org/software/systemd/man/latest/systemd.time.html#Calendar%20Events

4. 管理与维护

4.1. 管理命令

# 启动定时器
sudo systemctl start sample-job.timer
# 停止定时器
sudo systemctl stop sample-job.timer
# 查看定时器状态
systemctl status sample-job.timer
# 列出正在运行的定时器
systemctl list-timers
# 设置开机启动
sudo systemctl enable sample-job.timer
# 取消开启启动
sudo systemctl disable sample-job.timer

4.2. 触发问题

对于指点“时间间隔”,记得配置第一次触发,比如开机后多久触发,然后才能周期性触发。

对于指定“时间点”,当前时间满足配置的时间条件则触发,但是要注意错过触发后的“catch up”处理。例如原来设置“10:00:00”触发,在“09:55:00”停止并改为“11:00:00”触发,在“10:10:00”启用。此时检测到上次错过触发,就会在启动后立刻触发。

关于OnCalendar=的“catch up”的解决方案:

  • [Timer]下添加配置Persistent=true,把上次的触发时间持久化。
    • 该设置生效后,会在/var/lib/systemd/timers下创建定时器对应的上次触发时间文件。例如定时器sample-job.timer对应stamp-sample-job.timer
    • 定时器触发后,会把触发时间记录为该文件的修改时间。
  • 配置了OnCalendar=的定时器,修改了“时间点”,在启用前,先更新对应的上次触发时间文件的修改时间。
    • 例如sudo touch /var/lib/systemd/timers/stamp-sample-job.timer
  • 启用修改过的定时器后,该定时器会检测到最近已触发过,不会执行“catch up”。

注意,如果需要限制指定服务的触发次数,比如一天只能触发一次,最好不要依赖定时器,而是业务代码自己处理。例如触发后自己记录一下运行时间,再次触发后检查上次运行时间,并决定是否运行。

4.3. 手工触发

与其它业务集成时,需要根据不同的事件进行触发,但这不是定时器的作用。手工触发时,应该是直接运行对应的service。

Systemd运行service需要root权限。需要避开root权限,可以使用path触发。创建与service同名的path文件,配置其监控指定文件的变化(包括修改时间),触发对应service。

/etc/systemd/system创建对应的path文件,例如/etc/systemd/system/sample-job.path,内容如下:

[Unit]
Description=Watch to run sample job

[Path]
# 监控指定文件的变化
PathChanged=/run/watch-sample-job
# 监控指定路径的变化,例如该路径下新增了文件
#PathChanged=/path/to/watch/%f

[Install]
WantedBy=multi-user.target

/run/watch-sample-job配置任何用户可修改。需要触发时,执行touch /run/watch-sample-job即可。

参考

使用 Hugo 构建
主题 StackJimmy 设计