我有几个服务(静态网站生成器),我想从同一个 systemd 计时器定期触发它们。我发现这个问题/答案,它完全涵盖了我想要做的事情,并描述了一个设置,即一个.target
文件Wants=
由相应的计时器触发多个服务。这听起来很棒,但我发现当我实际设置它时,它只会触发一次,则自行禁用!
我准备了一个最小的工作示例(这不会触发多个服务,但演示了同样的问题):
test-timer.timer
:
[Unit]
Description=A test timer
[Timer]
OnCalendar=*-*-* *:*:30
Unit=test-timer.target
[Install]
WantedBy=timers.target
test-timer.target
:
[Unit]
Description=Target unit
Wants=test-timer.service
After=test-timer.service
[Install]
WantedBy=timers.target
test-timer.service
:
[Unit]
Description=Run test
[Service]
ExecStart=/usr/bin/bash -c "date --rfc-3339='seconds' >> /tmp/test-timer-output"
[Install]
Also=test-timer.target
启用计时器:
$ sudo cp test-timer.* /etc/systemd/system/
$ sudo systemctl enable --now test-timer.timer
Created symlink /etc/systemd/system/timers.target.wants/test-timer.timer → /etc/systemd/system/test-timer.timer.
systemctl list-timers --all
然后,当我查看第一次运行之前的输出时,我得到(忽略其他计时器):
NEXT LEFT LAST PASSED UNIT ACTIVATES
Fri 2021-10-08 10:38:30 EDT 21s left n/a n/a test-timer.timer test-timer.target
第一次运行后,NEXT
和LEFT
已被替换为n/a
:
NEXT LEFT LAST PASSED UNIT ACTIVATES
n/a n/a Fri 2021-10-08 10:38:32 EDT 1min 5s ago test-timer.timer test-timer.target
我也尝试过添加Persistent=true
并test-timer.target
明确启用test-timer.target
,但这些都不起作用。每次我这样做时systemctl restart test-timer.timer
,它都会重新启动,但只会触发一次运行,然后再也不会运行。
Unit=
如果我通过将行更改为 来删除间接层,test-timer.timer
则Unit=test-timer.service
该服务会按预期每分钟顺利地触发自身。
我是否缺少一些配置或安装步骤?
答案1
在 Twitter 上获得一些帮助后,我设法解决了这个问题。问题是 systemd计时器只会激活非活动服务,并且目标是激活并保持活动状态,除非有什么东西使它关闭(它与它所激活的单位的生命周期无关Wants=
)。如果任何它激活的服务变为非活动状态,在上面的例子中使用BindsTo=
代替。因此,对于这个最小示例:Wants=
test-timer.target
:
[Unit]
Description=Target unit
BindsTo=test-timer.service
After=test-timer.service
[Install]
WantedBy=timers.target
test-timer.service
:
[Unit]
Description=Run test
[Service]
ExecStart=/usr/bin/bash -c "date --rfc-3339='seconds' >> /tmp/test-timer-output"
[Install]
Also=test-timer.target
然后您可以看到,一旦test-timer.service
运行完成,test-timer.target
也将变成inactive
(因此计时器将能够再次激活它):
$ sudo systemctl list-units --all test-timer.target test-timer.service
UNIT LOAD ACTIVE SUB DESCRIPTION
test-timer.service loaded inactive dead Run test
test-timer.target loaded inactive dead Target unit
而在更改之前,目标在服务终止后仍然保持活跃:
$ sudo systemctl list-units --all test-timer.target test-timer.service
UNIT LOAD ACTIVE SUB DESCRIPTION
test-timer.service loaded inactive dead Run test
test-timer.target loaded active active Target unit
答案2
使用BindsTo
有一些缺点:
一个小缺点是,你违背了目标单元的目的:目标单元充当众所周知的同步点,因此不能包含任何自身的逻辑(即,目标单元不能静态地依赖于除其他目标单元之外的任何其他单元)。通过静态地向目标单元添加依赖项(
BindsTo
这是一种非常强的依赖项),你将服务单元的部分逻辑移动到所述目标单元,使得无法仅通过查看服务单元来了解服务单元的作用。这就是为什么服务单元必须使用部分将自己“注册”为目标单元的依赖项[Install]
。但是,由于这种逻辑反转,你不能再使用BindsTo
- 因为它的反转BoundBy
,,不能直接指定。无论如何,一个更大的缺点是
BindsTo
强制并发。即使您只提到一个服务单元,我假设您实际上是试图通过一个计时器启动多个服务单元 - 否则目标单元将毫无用处。如果您尝试将多个服务单元绑定到目标单元,则所有服务单元都需要同样运行才能运行目标单元。这意味着所有服务单元必须同时启动(即强制并发),也意味着如果任何指定的服务单元停止,您的目标单元也会停止。当目标再次启动(手动或由于计时器)而某些服务单元仍在运行时,这可能会导致意外行为。
为了避免这些缺点,我建议如下:
test-timer.timer
创建启动的定时器单元test-timer.target
。不要忘记启用定时器单元:systemctl enable test-timer.timer
test-timer.timer
:[Unit] Description=A test timer [Timer] OnCalendar=*-*-* *:*:30 Unit=test-timer.target [Install] WantedBy=timers.target
创建提到的
test-timer.target
。您无需在此处指定所需的服务单元,服务单元将使用将自身“注册”为依赖项WantedBy
。由于我们不希望目标单元无限期地运行,因此我们添加StopWhenUnneeded=true
。Systemd 将在目标单元完成其使命后立即停止它,即当 Systemd 将服务单元排队启动时。test-timer.target
:[Unit] Description=Target unit StopWhenUnneeded=true
按照您想要的方式创建服务单元。您可以像往常一样添加、、、和所有其他(例如)指令。还可以添加,
Wants
允许服务单元在启用时将自身“注册”为目标单元的要求。启用服务单元后,Systemd 将在目标单元启动后立即启动服务单元,即当计时器触发时。Requires
Before
After
Conflicts
WantedBy=test-timer.target
但是,由于
StopWhenUnneeded=true
,Systemd 会在服务单元排队启动后立即停止目标单元(即立即停止)。这不会妨碍任何人(手动或由于计时器)再次启动目标单元。由于服务单元可能仍在运行,您可能会遇到意外行为,就像 一样BindsTo
。因此我们还添加了Upholds=test-timer.target
和After=test-timer.target
。这些指令使 Systemd 确保在服务单元运行时目标单元也运行。但它不会停止目标单元;那是StopWhenUnneeded=true
的工作。它们一起有效地使 Systemd 保持目标单元运行,直到全部服务单元Upholds=test-timer.target
停止(BindsTo
相反,一旦停止目标单元任何服务单元停止)。一般来说,我建议在计时器触发的服务单元上下文中使用
Type=oneshot
服务而不是默认服务。这是因为 Systemd 会认为服务单元立即启动,并在命令完成时停止运行。使用Systemd 时,会认为服务单元在命令运行时“正在启动”,并在命令完成后直接切换到停止运行状态。这是并发和/指令的主要区别:使用服务单元的命令不会等待已声明运行的另一个服务单元的命令完成。使用Systemd 会等待。Type=simple
Type=simple
Type=oneshot
After
Before
Type=simple
After
Type=oneshot
好的,现在我们假设我们要启动两个服务单元
test-timer-1.service
和test-timer-2.service
。它们可以并行运行,但不是必须的。因此,Systemd 将触发计时器单元,然后启动目标单元,然后并行启动两个服务单元。目标单元不应在两个服务单元停止之前停止,计时器单元也应如此。为此,请创建以下两个服务单元。不要忘记启用服务单元(systemctl enable test-timer-1.service test-timer-2.service
)。test-timer-1.service
:[Unit] Description=Run test 1 After=test-timer.target Upholds=test-timer.target [Service] Type=oneshot ExecStart=/usr/bin/bash -c "sleep 3 ; date --rfc-3339='seconds' >> /tmp/test-timer-output-1" [Install] WantedBy=test-timer.target
test-timer-2.service
:[Unit] Description=Run test 2 After=test-timer.target Upholds=test-timer.target [Service] Type=oneshot ExecStart=/usr/bin/bash -c "sleep 3 ; date --rfc-3339='seconds' >> /tmp/test-timer-output-2" [Install] WantedBy=test-timer.target
如果您希望在完成
test-timer-2.service
之前不启动,只需添加到。然后,Systemd 将触发计时器单元,然后启动目标单元,然后启动第一个服务单元。当第一个服务单元完成后,Systemd 将启动第二个服务单元。一旦所有服务单元完成(此处:仅第二个服务单元),Systemd 就会停止目标单元,同时导致计时器单元停止。这在 中是不可能的。同样,不要忘记启用服务单元。test-timer-1.service
After=test-timer-1.service
test-timer-2.service
BindsTo
test-timer-1.service
:[Unit] Description=Run test 1 After=test-timer.target Upholds=test-timer.target [Service] Type=oneshot ExecStart=/usr/bin/bash -c "sleep 3 ; date --rfc-3339='seconds' >> /tmp/test-timer-output-1" [Install] WantedBy=test-timer.target
test-timer-2.service
:[Unit] Description=Run test 2 After=test-timer.target test-timer-1.service Upholds=test-timer.target [Service] Type=oneshot ExecStart=/usr/bin/bash -c "sleep 3 ; date --rfc-3339='seconds' >> /tmp/test-timer-output-2" [Install] WantedBy=test-timer.target
我正在使用这个精确的设置来协调备份:我首先运行一些服务单元来准备系统备份(例如创建快照),然后运行备份(有些可以并行运行,有些必须在另一个之后运行)并最后清理。