Systemd 技巧
Systemd 技巧
MeteorCat目前 Linux 发行版都集成 SystemCtl 做系统管理, 现在如果要把二进制应用写入成系统应用基本上绕不开.
Systemctl 的服务基本可以分为以下类别:
service: 常规服务timer: 定时器mount: 挂载点device: 设备socket: 套接字
这里先从基础的自定义 service 编写服务开始:
1 | # ============================================================================ |
这就是编写系统单元所需要配置, 实际上排除注释也就几个配置, 以部署 Jenkins 为例子:
1 | [Unit] |
这几行就直接编写好系统服务单元了, 之后更新下系统服务并且启动即可:
1 | sudo systemctl daemon-reload # 更新系统服务 |
上面那个例子修改下基本上就可以在其他服务上面使用, 其他的就是 timer|mount|device 相关的服务.
另外需要说明下的是 Type=oneshot 这种类型的服务单元, 这类服务单元可以实现很有用的小技巧;
比如可以在内存足够大的情况, 可以生成本地缓存空间:
1 | [Unit] |
这样就生成内存本地内存目录, 适用给 make|npm 等工具配置缓存目录,
还有 Nginx/Apache 的 proxy_cache_path 设定缓存目录提升静态资源响应速度.
还有比较激进的方法就是利用 Unixdomain 的文件防止在其中, 然后通过 nginx 代理转发进来提高 http 的网络转发效率.
这种方法利用 Unix 域套接字(Unix domain socket) 减少网络协议栈开销能显著降低延迟并提高吞吐量,
但这种方法需要进行严格测试, 大部分功能场景的瓶颈其实不是内部的网络转发, 所以很多时候并不是那么迫切用到这么激进的策略.
本地编译直接创建个内存盘提高编译时候的读写效率, 这里也附带下参考(编译生成的产物可能很大, 需要确定内存盘容量能够承载):
1 | 这里提供下 skynet 手动编译的流程 |
之后就是要说 timer(定时器) 方面的问题, 日常如果用 crontab 其实没有问题, 当时两者都有其优劣的地方:
crontab: 几乎所有Linux/Unix都支持, 简单直接调用命令脚本或者命令即可, 但是仅支持分钟级精度(最小单位 1 分钟)systemctl-timer: 强依赖systemd, 学习成本较高且命令都要编写成systemd单元让其调用, 但是支持复杂精细时间调度
像是数据库备份到本地的任务, 其实不在于执行时间复杂度, 直接对于授权维护用户启动 crontab -e 编辑追加一行即可:
1 | 每天凌晨2点执行导出任务 |
这种情况下对时间的精度就没有这么多要求, 但是如果是高精度时间调度情况, 比如 支付回调返回 的情况, 每分钟/次的唤醒调度就有大问题.
海外有的支付通道默认只返回单次返回结果, 所以如果作为支付服务提供方接收到直接返回成功然后另外开后台定时任务
这种独立调度的任务大部分需要秒级的调度, crontab 能够 sleep 延迟调度实现秒级别调第, 但是会创建大量子进程严重拖慢系统效率.
所以基于这种情况下 systemctl-timer 就可以实现, 首先创建被定时器调度的单元(比如 payment-callback.service ):
1 | [Unit] |
这就是简单被定时器调度的单元, 之后就是调度定时器配置(比如 payment-callback.timer, 定时器常规命名需要为 .timer 后缀):
1 | [Unit] |
更新下系统服务就可以启动, 并且可以看到所有定时调度任务:
1 | sudo systemctl start payment-callback.timer # 启动服务 |
这样就实现系统级别优雅的秒级调度任务, 但是也将任务的稳定性全盘移交给 systemd 托管, 如果想临时调整属性可以按照以下处理:
1 | 临时调整为10秒间隔 |
systemctl set-property 可以修改大部分的执行当中服务单元, 而不需要重新加载并重启系统服务.
相关技巧
这里提供些比较少用到的好用技巧, 以之前部署的 jenkins 服务为例子, 可以通过以下命令查看到系统服务涵盖的属性:
1 | 这里面提供大量的属性配置, 这里取几个比较好用的配置: |
以上配置都是正式环境推荐设置的, 能够有效安全管理 systemd 服务.
另外有个特殊配置 [Socket], 如果你按照 ssh 服务可以在 /etc/systemd/system/ssh.service.requires/ssh.socket 看到:
1 | [Socket] |
这部分的服务都是以 .socket 后缀做结尾, 用于 systemd 直接创建并监听套接字(而非服务自身启动后监听), 确保套接字始终可用.
实际上就是起到套接字抢占避免服务端口等资源被其他服务抢占, 也就是访问 ListenStream 地址会自动创建进程激活 Service:
1 | # 以 /etc/systemd/system/php-fpm.socket 为例子 |
Socket 系统服务单元激活后的服务按照 Type 来做区分:
Type=oneshot: 远程客户端关闭时, 绑定的服务单元状态变为inactive(正常结束)并等待下次唤起重新启动serviceType=simple(默认): 远程客户端关闭时, 按照fork创建子进程或者初始化单独线程, 这类会将连接转移之后主进程不会做退出- 其他基本上很少用到, 需要的话可以参考其他资料
如果要做进程池或者线程池模型单独处理, 可以考虑 Type=simple, 不过一般也很少回去用到 [Socket] 类的功能.
对于非高频服务通过
.socket实现 ‘有连接才启动进程’, 平时不占用内存(对比传统 systemctl start 启动后常驻内存的方式)
最后就是关于 systemctl 命令行的参数说明, 除了传统的 start|stop|reload|enable|disable 之外还提供以下功能:
1 | 重启并立即生效(自动重载配置), 而不需要 systemctl daemon-reload |
安全实践
这里需要说明创建 systemd 系统服务主要一定要明确 不能给运行的单元赋予太高权限, 所以考虑单元的权限等级:
是否需要HOME目录: 有的第三方程序会用到本地ssh机制(jenkins|gitea), 所以需要针对HOME目录操作是否需要LOGIN账号: 大部分作为运行宿主的系统用户不需要提供ssh|desktop登陆功能, 但是可能有少部分需要登陆维护操作是否需要SUDO操作: 有些服务可能需要设计配置变动之后重启系统服务(比如重启nginx), 需要特殊分配提供免密码sudo操作
按照这种方式分配创建的安全专属系统账户:
1 | 创建专属的用户和用户组 jenkins:jenkins |
对于大部分自己编写的功能业务情况, 推荐采用 useradd -r -s -g 65534 -G www-data,{其他组} {创建特权用户} 即可,
挂载在 nobody 运行且又属于 www-data 组能够将权限收缩到比较小范围.
如果需要分配额外 sudo 特权, 比如要赋予允许登陆的账号能够执行重启 nginx 的功能, 需要自己定制 sudoers.d 特权:
1 | 比如赋予能够登陆的 devops 特权账户重启 nginx 功能, 如果是要按照组规则首用户写 '%devops', 否则默认按照专门用户 |
系统预定义变量
之前能看到 $MAINPID 这种主进程ID声明的变量, 这部分就是 systemd 内部启动的时候捕获设定的, 这里提供系统定义变量表:
| 变量名称 | 核心作用 | 依赖条件/生效前提 | 适用单元类型 |
|---|---|---|---|
$MAINPID |
服务主进程的 PID | 服务启动后自动生成 | .service |
$PIDFile |
服务 PID 文件的完整路径 | 单元文件中显式配置 PIDFile= |
.service |
$SERVICE |
当前服务单元的名称(不含 .service 后缀,如 sshd) |
无(服务单元默认生效) | .service |
$UNIT |
当前单元的完整名称(如 sshd.service、nginx.path) |
无(所有单元类型默认生效) | 所有单元类型 |
$USER |
服务运行的用户账号 | 单元文件中配置 User= |
.service |
$GROUP |
服务运行的用户组 | 单元文件中配置 Group= |
.service |
$HOME |
运行用户的主目录路径 | 已配置 User=(自动关联用户主目录) |
.service |
$RUNTIME_DIRECTORY |
服务专属运行时目录(通常位于 /run/ 下) |
单元文件中配置 RuntimeDirectory= |
.service |
$STATE_DIRECTORY |
服务状态数据存储目录(通常位于 /var/lib/ 下) |
单元文件中配置 StateDirectory= |
.service |
$CACHE_DIRECTORY |
服务缓存文件存储目录(通常位于 /var/cache/ 下) |
单元文件中配置 CacheDirectory= |
.service |
$LOGS_DIRECTORY |
服务日志文件存储目录(通常位于 /var/log/ 下) |
单元文件中配置 LogsDirectory= |
.service |
$CONFIG_DIRECTORY |
服务配置文件存储目录(通常位于 /etc/ 下) |
单元文件中配置 ConfigDirectory= |
.service |
$PATH |
进程执行命令的环境变量 PATH |
可通过 Environment=/EnvironmentFile= 自定义 |
所有单元类型 |
$SYSTEMD_EXEC_PID |
执行当前单元的 systemd 子进程 PID | 单元启动时自动生成(较少直接使用) | 所有单元类型 |
$NOTIFY_SOCKET |
服务状态通知的 Unix 套接字路径 | 单元文件中配置 Type=notify |
.service |
$WATCHDOG_PID |
看门狗进程的 PID | 单元文件中配置 WatchdogSec= |
.service |
$WATCHDOG_USEC |
看门狗超时时间(单位:微秒) | 单元文件中配置 WatchdogSec= |
.service |
$TRIGGER |
触发路径单元的具体文件/目录路径 | 路径单元监测到文件/目录事件时生成 | .path |
$DEVICE |
设备单元对应的设备路径(如 /dev/sda1) |
无(设备单元默认生效) | .device |
$INTERFACE |
网络接口单元对应的接口名称(如 eth0、wlan0) |
无(网络接口相关单元默认生效) | .netdev、.network |
$LISTEN_FDS |
通过文件描述符传递的监听套接字数量 | 单元文件中配置 ListenStream= 等 socket 选项 |
.service、.socket |
$LISTEN_PID |
传递监听套接字的进程 PID | 与 $LISTEN_FDS 配合使用(socket 激活场景) |
.service、.socket |
注意:
*_DIRECTORY相关变量不允许采用绝对路径而是默认采用附加形式; 如$CONFIG_DIRECTORY=aaa是设定成/etc/aaa
我这边提供一个脚本命令用于打印这部分变量:
1 | [Unit] |
创建和启动服务:
1 | 写入单元内容 |
对于全局变量必须明确生效的场景, 如果没有生效默认都会是空值, 涉及移动|删除敏感操作不要用
root权限, 非常危险的操作!!!
模板单元
比如部署个游戏服务 game.service 目标需要启动 /usr/bin/app 启动二进制ExecStart=/usr/bin/app -f server1.conf;
这里默认启动的是 QQ-1 服, 那么如果要多进程部署的话就要重新设定 game-2.service, 而内部仅仅是 -f server2.conf 变化而已.
systemd 内部提供这种单元复用方案, {单元服务}@.service 这种声明(服务名 + @.service), 这就是 模板单元(Template Units)
这里采用这种以下方式声明, 文件名如下:
1 | 声明模板单元文件 |
模板单元的内容如下, 注意内部特殊变量 %I :
1 | [Unit] |
- 核心变量
%I: 表示模板实例的参数(即@后面的部分, 如game@server1中%I为server1) - 其他可用变量:
%i(小写i,会将@后的参数中的/替换为 -,适合路径场景) - 其他比如
%H(主机名)等, 可以参考man systemd.unit
那么命令行启动的时候就需要另外变动:
1 | 默认会去加载 /etc/game/server1.conf 配置并启动 |
这里面涉及到 -i/-I 之类特殊模板变量, 还有涉及其他模板会用到的变量, 可以按照需要来使用:
| 变量 | 含义与用途 | 示例(假设实例为 game@server-1/data) |
|---|---|---|
%I |
保留原始实例名称(包括 / 等特殊字符),不做任何替换 |
实例 game@server-1/data 中,%I 为 server-1/data |
%i |
与 %I 类似,但会将名称中的 / 替换为 -(避免路径冲突,适合作为文件名) |
实例 game@server-1/data 中,%i 为 server-1-data |
%H |
当前主机的 hostname(主机名) | 若主机名为 game-server,则 %H 为 game-server |
%h |
运行服务的用户的主目录(依赖 User= 配置,类似环境变量 $HOME) |
若 User=gameuser,其主目录为 /home/gameuser,则 %h 为 /home/gameuser |
%t |
系统运行时目录(固定为 /run,等价于 /var/run 的软链接) |
%t 始终为 /run |
%T |
服务的临时目录(通常为 /tmp 或 /var/tmp,由 systemd 管理) |
%T 通常为 /tmp |
%v |
系统的版本号(来自 /proc/sys/kernel/osrelease) |
例如 6.2.0-31-generic(Ubuntu 系统) |
%b |
系统启动目录(通常为 /boot) |
%b 为 /boot |
%m |
机器 ID(来自 /etc/machine-id,唯一标识当前系统) |
例如 a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 |
%M |
机器 ID 的短格式(前 12 个字符) | 基于上面的示例,%M 为 a1b2c3d4e5f6 |
%p |
单元文件的前缀名称(即 @ 前面的部分) |
对于 [email protected],%p 为 game |
%N |
单元文件的完整名称(包含 @ 和实例部分) |
对于 [email protected],%N 为 [email protected] |
%n |
单元文件的名称(不含 .service 等后缀) |
对于 [email protected],%n 为 game@server1 |
%C |
服务的配置目录(通常为 /etc/ 下的服务专属目录,需配合 ConfigDirectory=) |
若 ConfigDirectory=game,则 %C 为 /etc/game |
%R |
服务的运行时目录(配合 RuntimeDirectory=,等价于 $RUNTIME_DIRECTORY) |
若 RuntimeDirectory=game,则 %R 为 /run/game |
大部分常用的其实是
-i, 大写的-I不对/等特殊字符不做替换, 这种情况容易出现问题
支持了解以上知识基本上能够手动编写好自己的 systemd 服务, 其他额外扩展就需要查阅官方手册去自行处理.