Linux | systemd - 基本使用/服务管理

2020 08 11, Tue

我们前面在FRP、Aria2和docker中都提到了systemd。我们一直把它当作系统服务管理工具,实际上不只是管理了服务,systemd还可以管理非常多的资源。这也是关于systemd争议的来源:systemd太大了。

不管怎么说,我还是蛮喜欢systemd的。稍微总结一下systemd的用法。

组成

systemd由一个很大的生态组成:

  • systemd: PID=1的daemon,替代之前的init
    • systemctl: 命令行管理工具
  • initrd module: 负责PID=1之前、初始化的工作
    • systemd
    • sd-vconsole
    • sd-encrypt
    • sd-lvm2
  • journald: 日志管理工具
    • journalctl
  • dbus: 进程通信
  • polkit: 策略管理
  • systemd-login: 管理session
  • 其他服务:
    • udev: 替代原来的udev
    • tmpfile: 替代原来的tmpfiles
    • resolved: 可选替代之前的dns cache
  • 衍生工具
    • coredump
    • machines
    • nspawn
    • cgtop
  • 周边工具
    • cockpit: …

这个列表还在无限延长中,同时本身还可以替代cron这些工具。不需要全都会,知道有啥,出问题或者需要的时候能想起来就行。

概念

Unit类型

systemd管理各种资源以unit的形式组成,不同的资源对应不同的unit类型,比如说

  • 服务集合和启动同步: target
  • 服务: service
  • 定时任务: timer
  • socket
  • 资源限制组: slice
  • 路径监视: path
  • 挂载: mount

这个概念也很多、很杂,上面这些一般足够我们做大部分的工作了。

Unit放置的路径

每个unit放在对应的路径下就可以被systemd找到,做修改的话需要systemctl daemon-reload才行。systemd的搜索目录包括

  • 系统范围,直接用systemctl会管理到的服务
    • /etc/systemd/system/
    • /usr/lib/systemd/system/
  • 用户范围,用systemctl –user管理到的服务,在用户登录时启动
    • $HOME/.config/systemd/user/

除此之外,/etc/systemd/system/$TARGET.target.wants/目录下的文件会决定systemd启动某个target时,会启动哪些资源。

Unit管理

先从服务开始提一些最简单的

最简单的定义

一个systemd unit可以只定义核心的部分,不要任何其他部分,比如说service,可以只要Service段,不要Unit和Install段。有了这个定义,systemctl start/stop就可以启停资源了。

当然这是不被倡导的。

描述

每个Unit都是一个ini文件,都可以有一个Unit段,Unit段中可以写资源的描述。

[Unit]
Description=Some unit

资源依赖

Unit段还可以包含顺序和依赖关系。

如果有依赖的需要,比如说我的nginx依赖了一个挂载点,/mnt/data,那么我就可以把这个unit的Unit段改成这样

[Unit]
Description=Some unit
Requires=mnt-data.mount

这样这个unit启动时一定会等待/mnt/data挂载,挂载后才会开始启动

Install: 自动启动

如果希望它的自启与否可以被systemctl管理,那么还要加上Install段:

[Unit]
Description=Some unit
# 一个资源可以有依赖或者规定顺序

[Install]
WantedBy=some-target.target

然后systemctl enable/disable就知道他是要在/etc/systemd/system/some-target.target.wants/目录下建链接,systemd启动时就会自动启动这些资源。

Dropin: 修改已有的Unit

有的服务大体上不会变,但是会需要在不同环境下有一些细项不一样。systemd提供了一种机制叫Dropin,可以方便的修改部分unit定义

举个例子,我们可以手动在/etc/systemd/system/unit.type.d/any.conf建一个文件,写上段和配置项就行。比如说上面依赖/mnt/data的例子,可能不是所有节点都需要这个挂载点,那么我们只要在需要的节点上建立文件/etc/systemd/system/nginx.service.d/require-mount-mnt-data.conf写上

[Unit]
Requires=mnt-data.mount

这个配置就会被merge进之前的nginx.service中。

一般来说merge的过程是新增项的过程,如果需要替换,那么需要先定义一个空项,然后定义新的,比如说

[Unit]
Requires=
Requires=mnt-data.mount

第一个空项就会把之前的配置清空,第二个就可以生效了。之后我们有些配置只能定义一次的,可能就需要这样修改,比如说service的ExecStart。

彻底替换Unit

当然如果要修改的项繁多,我们不如直接覆盖之前的定义,那么我们也可以直接覆盖原来的定义文件。

systemd搜索路径总会优先/etc/systemd/system/,而大部分时候包管理都会把包里的unit放到/usr/lib/systemd/system/,我们只要把同名的unit放到前者之中就行。

服务管理:替代init/upstart/….

systemd中,服务定义通过service unit完成,启动级别的概念完全被target替换,既然是unit,那么上面的unit中的东西,自然都是可以用的。

除此之外service有一些自己的配置。

服务定义配置项

service最基本要知道的就是要怎么写一个启动一个服务的service定义。大部分都会在[Service]段中。

指定要启动的程序路径

举个例子,我们有一个web服务,需要启动/opt/some/app。我们只要新建一个/etc/systemd/system/my-app.service

# 可以自己加上Unit和Install段

[Service]
ExecStart=/opt/some/app

ExecStart只能定义一次。

如果需要启动前做准备,启动后检查一次健康状态,还可以有ExecStartPre和ExecStartPost,这两个都是可以定义多次的。

[Service]
ExecStartPre=/opt/some/app prep
ExecStartPre=/opt/some/app bootstrap
ExecStart=/opt/some/app
ExecStartPost=/opt/some/app ping

服务启动类型

systemd默认会把程序当成一个不会fork的简单服务,程序退出时服务变成停止状态,如果返回值不会0则是failed。

但是有很多老的服务,会用double fork的技巧来产生一个daemon进程。如果还用进程退不退出的方法判断就会出问题,systemd给出的方案是,新加一种服务的类型: Type=forking。

假如是上面的例子,那么我们的服务定义就变成了:

[Service]
ExecStart=/opt/some/app
Type=forking

systemd会在回收退出的进程时一级级找到子进程,也有可能是通过PID namespace或者cgroup来找,只要子进程还在,那服务就没事儿。

大致来说systemd的服务类型有

  • simple: 自己不退出,一直运行。systemd只要没看到这个进程退出,就认为正常。
  • forking: 自己fork后会退出。systemd认为fork即完成启动。
  • notify: 启动准备好了会通过notify告诉systemd:我好了。通常是通过sd_notify函数完成。
  • dbus: 等待dbus object name准备就绪。

除此之外还有一些其他手段。比如说可以Type=forking的服务还可以定义PID File:

[Service]
ExecStart=/opt/some/app
Type=forking
PIDFile=/run/app.pid

自动重启

有的时候服务会出错、退出。这个时候我们可能需要服务自动重启。

比较基本的就是告诉systemd什么情况下重启。属性名字叫Restart,对应的值有(我就简单翻译一下文档

重启情况\Restart值 no always on-success on-failure on-abnormal on-abort on-watchdog
正常退出或者信号 X X
不正常退出 X X
不正常信号 X X X X
启动超时 X X X
看门狗 X X X X

先不考虑后面俩启动超时和看门狗。我们一般用on-failure就可以了。

除此之外还有个问题,万一程序启动之初就失败了,退出然后被重启,这个过程如果时间很短,机器就会不停的重启一个程序,就可能会带来大量负载。

针对这个问题systemd会在短时间内快速重启几次后让服务进入start-limit状态,这个时候无论是restart还是start都会被阻止。牵扯到两个配置项:多久内StartLimitIntervalSec、重启多少次StartLimitBurst。有时候我们也需要调整重启间隔时间RestartSec。

举个例子

[Service]
ExecStart=/opt/some/app
Restart=on-failure
RestartSec=3s

StartLimitBurst=3
StartLimitIntervalSec=30s

这样就会在失败时等待3s然后重启,如果30s内重复失败3次,就会进入start-limit状态。

看门狗

有一些非常极限的情况,你的服务会失去响应,没有日志,不处理请求,没有负载。简单说它什么都不干,没啥压力,没有任务在排队,不是忙到不可开交,但是就是没反应。

说一句题外话,这个情况在嵌入式上也有,好像一般叫程序跑飞了,你的芯片对外的信号突然就不对了,更惨的是嵌入式裸片没有PC这样的桌面可以直接上手确认,出了问题就什么都没法知道。所以嵌入式用了一种叫看门狗的外设,程序需要定时check in,否则就会被外设重设回一开始的地方,让你的芯片从头开始运行。

systemd为了应对类似的问题,实现了一个同名的机制,程序需要在规定时间内打卡,否则超出一定时间就会被杀掉重启。相关的配置项是WatchdogSec,描述多久应该打一次卡。比如说

[Service]
ExecStart=/opt/some/app
WatchdogSec=10s
Restart=on-failure
RestartSec=3s

StartLimitBurst=3
StartLimitIntervalSec=30s

我们的程序就需要用sd_notify每隔几秒打一次卡,超出10s就重启。

继承自systemd exec类型的配置项

exec通常指启动一个进程,有时候也包括socket或者别的资源。

环境有关的

systemd可以在启动时配置环境,比如说环境变量:

[Service]
Environment=SOME_VAR=val
EnvironmentFile=/etc/sysconfig/some-app
ExecStart=/opt/some/app

或者启动时的工作路径:

[Service]
WorkingDirectory=/opt/some
ExecStart=/opt/some/app

和用户有关的

设置服务启动时使用的用户:

[Service]
User=nobody
Group=nobody
ExecStart=/opt/some/app

涉及安全的

Linux可能会对进程作出很多除了用户、文件权限以外的限制,比如说capabilities啊、selinux啊什么的。

[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE
SELinuxContext=system_u:system_r:httpd_t:s0
ExecStart=/opt/some/app

资源限制

有的时候进程可能会占用大量资源,比如说CPU啊、内存啊这些。systemd可以利用ulimit直接为服务设置合适的限制

还可以设置进程优先级Nice,还有CPU亲和性CPUAffinity。

沙箱

除了这些以外,systemd还可以保护特定的资源不被乱改。

比如说保护

  • 系统目录:ProtectSystem
  • 加目录:ProtectHome
  • 临时目录:PrivateTmp
  • 设备:PrivateDevices
  • 网络:PrivateNetwork
  • Hostname: ProtectHostname
  • 时钟:ProtectClock
  • 内核配置:ProtectKernelTunables
  • 内核模块:ProtectKernelModules
  • 内核日志:ProtectKernelLogs
  • ….

甚至还可以放行或者阻止一些syscall。