Linux | systemd - 基本使用/服务管理
我们前面在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。