为什么当单位是目标时我的 systemd 计时器仅触发一次?

为什么当单位是目标时我的 systemd 计时器仅触发一次?

我有几个服务(静态网站生成器),我想从同一个 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

第一次运行后,NEXTLEFT已被替换为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=truetest-timer.target明确启用test-timer.target,但这些都不起作用。每次我这样做时systemctl restart test-timer.timer,它都会重新启动,但只会触发一次运行,然后再也不会运行。

Unit=如果我通过将行更改为 来删除间接层,test-timer.timerUnit=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强制并发。即使您只提到一个服务单元,我假设您实际上是试图通过一个计时器启动多个服务单元 - 否则目标单元将毫无用处。如果您尝试将多个服务单元绑定到目标单元,则所有服务单元都需要同样运行才能运行目标单元。这意味着所有服务单元必须同时启动(即强制并发),也意味着如果任何指定的服务单元停止,您的目标单元也会停止。当目标再次启动(手动或由于计时器)而某些服务单元仍在运行时,这可能会导致意外行为。

为了避免这些缺点,我建议如下:

  1. 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
    
  2. 创建提到的test-timer.target。您无需在此处指定所需的服务单元,服务单元将使用将自身“注册”为依赖项WantedBy。由于我们不希望目标单元无限期地运行,因此我们添加StopWhenUnneeded=true。Systemd 将在目标单元完成其使命后立即停止它,即当 Systemd 将服务单元排队启动时。

    test-timer.target

    [Unit]
    Description=Target unit
    StopWhenUnneeded=true
    
  3. 按照您想要的方式创建服务单元。您可以像往常一样添加、、、和所有其他(例如)指令。还可以添加,Wants允许服务单元在启用时将自身“注册”为目标单元的要求。启用服务单元后,Systemd 将在目标单元启动后立即启动服务单元,即当计时器触发时。RequiresBeforeAfterConflictsWantedBy=test-timer.target

    但是,由于StopWhenUnneeded=true,Systemd 会在服务单元排队启动后立即停止目标单元(即立即停止)。这不会妨碍任何人(手动或由于计时器)再次启动目标单元。由于服务单元可能仍在运行,您可能会遇到意外行为,就像 一样BindsTo。因此我们还添加了Upholds=test-timer.targetAfter=test-timer.target。这些指令使 Systemd 确保在服务单元运行时目标单元也运行。但它不会停止目标单元;那是StopWhenUnneeded=true的工作。它们一起有效地使 Systemd 保持目标单元运行,直到全部服务单元Upholds=test-timer.target停止(BindsTo相反,一旦停止目标单元任何服务单元停止)。

    一般来说,我建议在计时器触发的服务单元上下文中使用Type=oneshot服务而不是默认服务。这是因为 Systemd 会认为服务单元立即启动,并在命令完成时停止运行。使用Systemd 时,会认为服务单元在命令运行时“正在启动”,并在命令完成后直接切换到停止运行状态。这是并发和/指令的主要区别:使用服务单元的命令不会等待已声明运行的另一个服务单元的命令完成。使用Systemd 会等待。Type=simpleType=simpleType=oneshotAfterBeforeType=simpleAfterType=oneshot

    • 好的,现在我们假设我们要启动两个服务单元test-timer-1.servicetest-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.serviceAfter=test-timer-1.servicetest-timer-2.serviceBindsTo

      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
      

我正在使用这个精确的设置来协调备份:我首先运行一些服务单元来准备系统备份(例如创建快照),然后运行备份(有些可以并行运行,有些必须在另一个之后运行)并最后清理。

相关内容