TT Lab
开始
学习 学习路径 课程

RHEL 系管理

编写 systemd unit 文件

在 TT Lab 中继续学习

目标

从零开始编写 systemd 单元文件,创建一对定时器,并亲手制作单元验证脚本。

为什么重要

单元文件中最重要的一行是 Type=。如果是 Type=simple,而进程又守护进程化了,systemd 就会把父进程退出视为服务结束,并判定为失败。反过来,如果是 Type=forking,而程序却在前台运行,systemd 就会一直等待,最后因超时而把它杀掉——这是“启动卡住 30 秒后失败”的典型原因。

此外,如果没有 [Install] 节,systemctl enable 就什么也不会做,因此开机时无法自动启动。这一点在重启之前没有人会发现。

本实验在 AlmaLinux 9 虚拟机中运行。由于 systemd 真的以 PID 1 运行,写好单元文件之后,可以立刻用 systemctl enable --now 把它启动起来。你将通过亲眼确认文件中的一行会引发什么动作来学习。\n\n以前是在 Pod 中运行的,根本没有 systemctl,只做到编写和验证为止。首次启动大约需要 30 秒。

步骤

  1. 创建 /etc/systemd/system/labhub-api.service,并加入 [Unit]、[Service]、[Install] 三个节。同时创建 /root/unit 工作目录。
  2. 在 [Unit] 中加入 Description、Documentation、After=network-online.target、Wants=network-online.target 这四行。
  3. 在 [Service] 中加入 Type=notify、ExecStart=/usr/local/bin/labhub-api --config /etc/labhub/api.yaml、User=labhub、Group=labhub、WorkingDirectory=/opt/labhub。ExecStart 必须是绝对路径。\n\n 由于实验中的程序是 shell 脚本,所以还要一并加入 NotifyAccess=all。默认值 main 只接收 MainPID 发送的通知,而 systemd-notify 是脚本的子进程,PID 不同。这样一来,systemd 收不到就绪信号,会一直等待,最后以 Job for labhub-api.service failed because a timeout was exceeded 收场——单元文件看起来没有任何问题。
  4. 添加重启策略。在 [Service] 中加入 Restart=on-failure 和 RestartSec=5,在 [Unit] 中加入 StartLimitIntervalSec=300 和 StartLimitBurst=5。这两项在 systemd 230 中从 [Service] 移到了 [Unit]——如果写在 [Service] 中,只会留下 Unknown key name,并且被悄悄忽略,导致速率限制不起作用。
  5. 添加安全指令。NoNewPrivileges=true、ProtectSystem=strict、ProtectHome=true、PrivateTmp=true、ReadWritePaths=/var/lib/labhub /var/log/labhub
  6. 在 [Install] 中加入 WantedBy=multi-user.target,并亲手创建 systemctl enable 会生成的符号链接。路径是 /etc/systemd/system/multi-user.target.wants/labhub-api.service,并且必须指向原始单元。
  7. 创建一对定时器。/etc/systemd/system/labhub-backup.service(Type=oneshot,ExecStart 为绝对路径)和 /etc/systemd/system/labhub-backup.timer(在 [Timer] 中写入 OnCalendar=*-*-* 02:30:00、Persistent=true、Unit=labhub-backup.service,在 [Install] 中写入 WantedBy=timers.target)。
  8. 编写 /root/unit/lint.sh。它要以第一个参数接收单元文件路径,检查下面三条规则,没有违规时以退出码 0 结束,有违规时以非 0 值结束。
    • 规则 1:[Unit]、[Service]、[Install] 三个节必须都有
    • 规则 2:ExecStart 的值必须是绝对路径(以 / 开头)
    • 规则 3:如果是 Type=forking,就必须有 PIDFile= 评分器会分别对你的 labhub-api.service(必须通过)和 /opt/fixtures/unit/bad-sample.service(必须失败)运行它。

参考

三个节的骨架

创建 /etc/systemd/system/labhub-api.service,并加入 [Unit]、[Service]、[Install] 三个节。同时创建 /root/unit 工作目录。

需要三个用方括号括起来的节名。顺序也有惯例。

填写 [Unit] 节

在 [Unit] 中加入 Description、Documentation、After=network-online.target、Wants=network-online.target 这四行。

要写四样:说明、文档、顺序、依赖。顺序和依赖是不同的指令。

[Service] 执行设置

在 [Service] 中加入 Type=notify、ExecStart=/usr/local/bin/labhub-api --config /etc/labhub/api.yaml、User=labhub、Group=labhub、WorkingDirectory=/opt/labhub。ExecStart 必须是绝对路径。\n\n 由于实验中的程序是 shell 脚本,所以还要一并加入 NotifyAccess=all。默认值 main 只接收 MainPID 发送的通知,而 systemd-notify 是脚本的子进程,PID 不同。这样一来,systemd 收不到就绪信号,会一直等待,最后以 Job for labhub-api.service failed because a timeout was exceeded 收场——单元文件看起来没有任何问题。

类型和执行命令是关键。执行路径必须是绝对路径。

重启策略

添加重启策略。在 [Service] 中加入 Restart=on-failure 和 RestartSec=5,在 [Unit] 中加入 StartLimitIntervalSec=300 和 StartLimitBurst=5。这两项在 systemd 230 中从 [Service] 移到了 [Unit]——如果写在 [Service] 中,只会留下 Unknown key name,并且被悄悄忽略,导致速率限制不起作用。

有两个用来防止无限循环的上限指令。间隔也不能为 0。

安全加固

添加安全指令。NoNewPrivileges=true、ProtectSystem=strict、ProtectHome=true、PrivateTmp=true、ReadWritePaths=/var/lib/labhub /var/log/labhub

要同时使用设为只读的指令和指定例外路径的指令。

[Install] 与 enable 的结果

在 [Install] 中加入 WantedBy=multi-user.target,并亲手创建 systemctl enable 会生成的符号链接。路径是 /etc/systemd/system/multi-user.target.wants/labhub-api.service,并且必须指向原始单元。

亲手创建 enable 所生成的符号链接路径,就能理解其结构。

编写定时器对

创建一对定时器。/etc/systemd/system/labhub-backup.service(Type=oneshot,ExecStart 为绝对路径)和 /etc/systemd/system/labhub-backup.timer(在 [Timer] 中写入 OnCalendar=*-*-* 02:30:00、Persistent=true、Unit=labhub-backup.service,在 [Install] 中写入 WantedBy=timers.target)。

.timer 和 .service 是一对。还要加入补上错过的执行的指令。

单元验证脚本

编写 /root/unit/lint.sh。它要以第一个参数接收单元文件路径,检查下面三条规则,没有违规时以退出码 0 结束,有违规时以非 0 值结束。

评分器会分别对正常单元和夹具中的不合格单元运行它。请检查这三条规则。