- System and Service Manager
- systemd.index — List all manpages from the systemd project
- systemd - wiki.archlinuxcn.org
- systemd.index 中文手册
- When should the option RemainAfterExit needs to be set true when creating new systemd services?
- systemd/定时器
- journalctl:查詢 systemd 日誌
本文主要关注用于查看系统状态以及管理系统和服务的 systemctl,参考了 systemd.index — List all manpages from the systemd project 和 systemd - wiki.archlinuxcn.org 以及前者的中文翻译 systemd.index 中文手册。限于篇幅、笔者的知识深度和翻译能力,读者还是要以查阅文档为主。
systemd 是一个专用于 Linux 操作系统的系统与服务管理器。作为启动进程(PID=1)运行时负责启动并维护各种用户空间的服务。
可以通过 systemd.index — List all manpages from the systemd project 查阅 systemd 工具箱的全部功能,例如:
- 管理主机名的
hostnamectl - 管理用户目录的
hostctl - 管理网络配置的
networkctl - 管理本地化设置的
localectl - 管理日期时间的
timedatectl - 管理用户的
loginctl - 记录日志的
journalctl - ……
系统资源在 systemd 中被称为单元(Unit),单元有多种类型,应用开发者日常一般使用服务单元(Service Unit)来监控和管理进程,也可能接触一些目标单元(Target Unit)和定时器单元(Timer Unit)。
-H <用户名>@<主机名> 参数可以远程控制其他机器,该功能使用 SSH 连接远程 systemd 实例。
怎样管理单元
如果不使用扩展名,systemctl 会假定扩展名为 .service,因此在查询 NGINX 服务的状态时, nginx 和 nginx.service 是等价的。
脚本需要更加简洁的输出,可以使用:
其他一些命令有:
--now 选项可与 enable、disable、mask 同时使用,使这些动作立即生效。
systemctl list-dependencies 可以列出单元的依赖:
systemctl show 可以查看单元配置:
系统单元与用户单元
systemctl 命令默认携带 --system 参数,表示对系统单元进行操作。
非 root 用户登录系统时,systemd 会启动一个 systemd --user 实例,用于管理用户单元。操作非 root 用户的用户单元需要在执行命令时添加 --user 参数。用户全部会话退出后,用户 systemd 的进程将被销毁,用户单元会停止。
如果需要在用户首次登陆时运行单元,可以执行 systemctl --user enable <unit>。如果服务不依赖用户会话(例如需要以非 root 用户部署有守护进程的应用程序),可以使用以下命令启用驻留指定用户,这样用户单元会在系统启动时加载。
systemd --user 和 systemd --system 运行在不同的进程中,所以用户单元不能引用或依赖系统单元,也不能引用或依赖其他用户的用户单元。
单元文件
单元文件告诉 systemd 怎样启动单元。单元文件可以从多个地方加载,systemctl show --property=UnitPath 可以显示加载目录。
主要的加载目录按优先级从低到高排列为:
/usr/lib/systemd/system/:软件包安装的单元/etc/systemd/system/:系统管理员安装的单元
用户单元则单独使用 $HOME/.config/systemd/user/。
运行 systemctl enable nginx 可以看到 systemctl 在 /etc/systemd/system/multi-user.target.wants/ 的指定目录中创建了一个指向 /usr/lib/systemd/system/nginx.service 的符号链接:
该行为是由单元文件中的 [Install]WantedBy=multi-user.target 字段决定的。
该操作相当于为 multi-user.target 添加了 Wants=nginx 配置,因此当系统启动时,nginx.service 单元也会被启动。
单元文件的组成部分
服务单元配置文件是 .ini(或者说 .toml 🤔)格式的文本文件,也可以通过 systemctl cat nginx 查看,这样能打印单元文件的绝对路径。
[Unit] 区块
[Unit] 区块定义了 Unit 的元数据,以及与其他 Unit 的关系。
[Unit] 配置中的 Requires= 和 Wants= 可以声明服务的依赖关系,有多个依赖服务时使用空格分隔。Wants= 表示可选依赖,Requires= 依赖不能成功启动时,本单元也必然会启动失败。Requires= 和 Wants= 依赖是并行启动的,要表达先后关系,可以使用 After=。
如果服务依赖有强关联关系,则可以使用 BindsTo 表示依赖单元退出会导致本单元的退出。
服务单元将会自动添加 Requires=sysinit.target,After=sysinit.target basic.target, Conflicts=shutdown.target 和 Before=shutdown.target 以确保(1)可以在系统完成基本的初始化后拉起单元;(2)在系统关闭前优雅地终止单元。那些需要在系统刚启动时就运行的服务,或是在系统关闭流程结尾才能停止的服务需要显式地设置 DefaultDependencies=no。
[Service] 区块
只有服务单元才有 [Service] 区块,[Service] 区块描述如何启动 / 关闭 / 重载服务单元。
服务进程的启动类型
Type=simple 时 systemd 认为 ExecStart= 启动的进程就是该服务的主进程,且在创建主服务进程之后该服务就已经启动完成。这意味着 simple 类型的服务即使不能成功调用主服务进程,例如由于没有权限、可执行文件不存在等问题导致启动失败,systemctl start 也是成功的。
如果只需确保服务二进制文件被调用,而服务本身不太可能初始化失败,也不存在服务间的依赖关系,建议使用 Type=simple,它是最简单,最快的选项,并且不会阻塞启动过程。
Type=oneshot 与 Type=simple 类似,但是 systemd 在主服务进程退出之后认为服务启动完成,然后接着启动后继单元。
oneshot 类型的服务单元通常用于执行某些任务。如果要执行没有状态的操作(例如清空 /tmp 或是覆写某些文件),因为没有配置持续运行的进程,服务将永远不会进入「active」状态,而是直接从「activating」过渡到「deactivating」或「dead」。
如果要执行有状态的操作,例如需要创建一个标志文件,需要搭配 RemainAfterExit=yes 配置,这样主服务进程虽然退出了,systemd 仍会将该服务标记为「active」,可以设置在 systemctl stop 时删除这个标志文件。
Type=forking 类似传统的 UNIX 守护进程行为,父进程将在启动完成后退出,子进程作为主服务进程继续运行。systemd 会认为在父进程退出时,该单元启动完成,继续启动后续单元。建议同时使用 PIDFile= 选项以便 systemd 能够可靠地识别服务的主进程。
Type=notify 的行为类似 exec,不过 systemd 认为服务会在完成启动后通过 sd_notify(3) 或者类似的调用发送 READY=1 通知,systemd 会在发送消息后继续启动后继服务单元。要使用该类型,需要同时设置 NotifyAccess=。
服务管理配置
ExecStart= 描述服务启动时执行的命令,命令的语法与 shell 类似但不完全相同,例如重定向(<,<<,>,>>)、管道 |、后台运行 & 都无法使用。
ExecStartPre= 和 ExecStartPost= 分别描述在 ExecStart= 命令之前和之后执行的命令,这些配置中的任何命令失败都会导致其余命令不执行且单元被视为启动失败。注意 ExecStartPre= 不能用于启动长期运行的进程 —— 在运行下一个服务进程之前,通过 ExecStartPre= 调用派生出的所有进程都将被杀死。
ExecCondition= 起到条件执行的作用,在 ExecStartPre= 命令之前执行。当 ExecCondition= 命令以 1 至 254 退出时,跳过其余命令,单元不会被标记为失败。当退出代码为 255 或异常退出(例如超时、被信号杀死等)时,单元将被视为失败。退出代码为 0 或符合 SuccessExitStatus= 时单元将继续执行。
ExecReload= 描述重载服务的命令,有一个特殊的环境变量 $MAINPID 用于表示主进程的 PID,因此可以实现 /bin/kill -HUP $MAINPID 这样的命令。
ExecStop= 用于停止服务,ExecStopPost= 描述服务停止后的清理命令。只有成功启动的服务才会执行 ExecStop= 中指定的命令。如果服务启动失败,例如 ExecCondition= 失败退出,或是 ExecStartPre=、ExecStart=、ExecStartPost= 中的任何命令失败,或者在服务完全启动前超时,将直接执行 ExecStopPost=。
服务重启请求是在执行停止操作后再执行启动操作。这意味着在服务重启操作期间会执行 ExecStop= 和 ExecStopPost=。
Restart= 配置服务进程退出、被杀死或超时时是否重新启动服务。对于长期运行的服务,建议将其设置为 on-failure,以便通过尝试从错误中自动恢复来提高可靠性。对于需要自行终止(避免立即重启)的服务,on-abnormal是另一种选择。
环境变量
有两种方法设置环境变量,第一种是通过 Environment= 指定键值对。虽然一个 Environment= 可以配置多个键值对,出于方便阅读的考虑,还是建议写成多个配置项。
如果环境变量的值包含等号或空格,需要使用双引号标记,如 Environment="VAR12=foo =bar"。单元文件中广泛使用的替换符(% 连接一个字符,例如 %H 表示系统的 hostname)会被解析,因此如果传递的值含有 %,需要转换为 %%。
还可以通过 EnvironmentFile= 指定一个文件。文件使用 ; 和 # 作为行注释符号,符合 VAR=VALUE 的 shell 变量赋值语法,行尾的 \ 视为续行符,同样可以使用双引号处理空格等字符。
EnvironmentFile= 接受绝对路径,在路径前加上 - 前缀表示忽略文件不存在的情况,读取的环境变量会覆盖 Environment= 以及之前的 EnvironmentFile= 配置中 中设置的同名变量。
原则上不应使用环境变量向单元中的进程传递机密信息。一方面,环境变量会通过 D-Bus IPC 暴露给其他非特权客户端;另一方面,环境变量在概念上也不属于需要保护的机密数据。
[Install] 区块
[Install]通常是配置文件的最后一个区块,只有 systemctl enable|disable 命令在启用 / 停用单元时才会使用该区块的配置。
WantedBy=,RequiredBy= 表示在 enable 此单元时将会在每个列表单元的 .wants/ 或 .requires/ 目录中创建一个指向该单元文件的软连接。相当于为每个列表中的单元文件添加了 Wants=this 或 Requires=this 选项。这样当列表中的任意单元启动时,该单元都会被启动。
要使单元在系统启动或用户登录时启动,系统单元应当依赖 multi-user.target,用户单元应当依赖 default.target。
服务模版(Service Templates)
systemd 可以通过 <service>@<argument>.service 模式接收参数,这样的服务被称为「实例化」的服务,而 <service>@.service 定义被称为「模版」。创建服务实例的时候,systemd 会查找单元文件中的 %i 并直接替换为 <argument>。
例如 hello@service 中设置了 ExecStart=/usr/bin/java -jar -Dserver.port=%i app.jar,则 systemctl --user start hello@3000 和 systemctl --user start hello@3001 将启动两个 service 分别监听 3000 和 3001 端口。
可以通过 systemctl --user list-units 'hello@*' --all --no-legend 列出模版派生的服务。
新增与修改单元文件
新增单元文件后需要通过 systemctl daemon-reload 重新加载单元(用户单元需要添加 --user 参数)。
使用 systemctl edit <unit> 编辑单元文件会在保存后会自动重载单元文件,回退修改则可以使用 systemctl revert <unit>。
systemd-delta 可用于查看哪些单元文件被覆盖、扩增,哪些被修改。
可以用 systemd-analyze verify [unit] 校验单元文件。
创建(服务)单元
执行开机启动的一次性任务
政务云的服务商可能做了一种防呆设计,在重启机器时会重置网卡设置,在这个过程中 NetWork Manager 把 DNS 配制成了假想的网关地址,导致域名解析失败。可以编写一个脚本来覆写 /etc/resolv.conf 文件,脚本本身交给 systemd 执行。
systemd 使用目标单元(Target Unit)将多个单元组织起来,同时目标单元也能够确保系统处于特定状态。为了确保文件覆盖成功,我们可以依赖与网络相关的目标。
网络设置初始化完成后(network-online.target)再启动任务可以确保覆写成功。
要使
network-online.target正确反映网络状态,必须启用所使用的网络管理器的网络等待服务……如果需要更为详细的解释,请查看网络配置同步点中的讨论。若某服务需要执行 DNS 查询,其应该被排在nss-lookup.target后……
系统单元可以直接写入 /etc/systemd/system/dns-setup.service。执行 systemctl daemon-reload && systemctl enable dns-setup.service,可以 reboot 看看效果。
托管简单应用
需要 Java 编写的后端服务实现开机自启,如有异常退出也能够自行启动。项目可通过单条命令(java -jar ……)执行。
使用 Spring Initializr 初始化一个简单的网站,为了方便跟踪应用程序的状态配置了 Spring Boot Actuator。在应用自己的工作目录(/home/yufan/WorkSpace/demo-website)下创建服务单元文件,实际工程中可以把配置文件纳入版本控制方便管理。
创建到 $HOME/.config/systemd/user/ 的符号链接。
重新加载单元文件,设置开机自启动并立即启动服务:
使用 curl -X POST http://alma:8001/actuator/startup 可以观察服务存活情况。
使用 kill $PID 或 curl -X POST http://alma:8001/actuator/shutdown 关闭网站,可以看到 systemd 又启动了服务。重启系统,可以观察到应用程序自己启动了。
你也可以尝试下 systemctl --user restart simple-demo-website.service。
托管容器化应用
Podman 4.4 引入了 Quadlet,允许用户更简单地管理容器服务,相关内容可参考本站的 Quadlet 让 systemd 管理容器更容易。
podman-generate-systemd命令已经标记为弃用,但不会被移除。
笔者目前主要使用的 Fedora 和 AlmaLinux 都将 Podman 作为默认的容器管理工具,podman-generate-systemd(1) 可以直接为已存在的容器创建 unit 文件。
这种方法的缺点是只能管理当前容器的启停,如果使用新镜像创建了容器应用,就需要重新生成单元文件。
容器化应用的一大优势就是创建和销毁特别方便,因此我们可以让我们的服务单元在启动时创建容器,在停止时销毁容器。
以其他用户名义运行服务并把 STDOUT 和 STDERR 输出到指定文件
使用 systemd 管理代码托管工具 Gogs,改写原先的启动命令 su - git -c "nohup /opt/gogs/gogs web>/opt/gogs/app.log 2>&1 &"。
原启动命令表示以用户 git 的名义运行 Gogs 并把标准输出和标准错误输出写入 /opt/gogs/app.log。Gogs 在 $GOGS_PATH/scripts/systemd/ 下已经提供了单元文件 gogs.service,也可以在 GitHub 上查看。
需要根据实际情况修改配置文件,例如使用远端数据库就删除相关的 After= 依赖项,之后创建软链接指向这个文件。
由于这个 Gogs 配置指定日志通过文件输出到其他目录,因此不需要关注标准输出和标准错误输出。如果还是想捕获这两个输出内容,可以指定一个标识符以便后续日志程序(如 RSYSLOG)过滤。
在 RSYSLOG 中新增配置,过滤出相关的信息并写入 /var/log/gogs.log,最后重载 rsyslogd 服务。
可以使用 logger -p user.warn 'gogs_service logger test' 测试是否生效。
通过 @ 参数创建模版任务
如果需要在启动多个实例做负载均衡,则可以在单元文件中使用 @ 符号,@ 符号后填充的内容将会替换单元文件中的 %i。
如上展示的单元文件将在启动时把 @ 后面的参数传递给 -Dserver.port=,使这个 Java 应用监听指定端口,如:
利用定时器单元创建定时任务
尽管 cron 是最有名的计划任务管理器,systemd 定时器单元仍是一个不错的选择。使用定时器的最主要的优势在于每个任务都有它自己的 systemd 服务,使得:
- 任务可以简单地独立于他们的定时器启动,简化调试
- 每个任务可配置运行于特定的环境中
- 任务可以使用 cgroups 特性
- 任务可以配置依赖于其他 systemd 单元
- 任务会被记录于 systemd 日志,便于调试
定时器单元使用两种方法描述何时与如何激活事件。一种是单调定时器,描述在某个时间点的一段时间后激活定时任务。例如 OnBootSec 和 OnActiveSec 分别描述了系统启动和定时器单元激活后过一段时间触发事件。
另一种是实时定时器(或称挂钟定时器),类似 cronjobs,通过一个表达式定义任务的激活规律。你可以使用 systemd-analyze calendar 命令验证这个表达式:
像其他单元一样,定时器单元也使用 start 和 enable 命令启动。要查看定时器任务,使用 systemctl [--user] list-timers。
要执行具体任务还需要关联服务单元。一种方法是在相同目录下编写同名的服务单元,但是不为服务单元编写 [Install] 部分;另一种方法则是在定时器的 [Timer] 部分通过 Unit= 选项指定服务单元。
为了这个例子的完整性,附上对应的服务单元定义文件。
日志
生产应用应当符合日志规范中的日志加工和存储要求,此外本章节内容可用于排查问题时搜集额外的信息。
systemd 提供了自己的日志系统 journal。默认情况下服务的 STDOUT 和 STDERR 都会指向 journal,相当于在单元文件中编写了如下配置:
journalctl可以根据特定字段过滤输出,多个过滤字段使用空格分隔。如果过滤的字段比较多,需要较长时间才能显示出来。
普通用户默认仅能访问自己的日志,要让普通用户访问系统日志,可以将用户加入 systemd-journal 用户组。