⏰ 第04章 · Type/依赖/WantedBy

Type=simple/After/WantedBy

Type、依赖与 WantedBy

前几章你会看到 Type=simple、After=mysql.service、WantedBy=multi-user.target 这三个短语反复出现。这一章把它们彻底讲透:systemd 怎么判断"启动完成"、怎么排启动顺序、怎么表达依赖关系、以及"开机自启"到底是怎么通过 WantedBy 实现的。

Type:systemd 怎么判断"启动完成"

Type= 告诉 systemd:服务到什么程度算"成功启动",只有它认定启动完成,才会接着启动排在 After= 后面的服务。取值如下:

Type 启动完成判定 适用场景
simple 主命令一 fork 即认为完成 前台常驻进程(node、python、大多数 web 服务)最常见
exec 主命令成功 exec 才算完成 同 simple,但在 exec 失败时能报更准的错误
forking 主进程 fork 出守护子进程后才算完成 传统 do-while 型守护进程(传给 pidfile)
oneshot 命令执行完一次性任务即完成 ExecStart 跑完就结束的脚本,如初始化、备份
notify 程序显式发 READY=1 通知 支持 sd_notify 的服务
dbus 在 DBus 上注册了名字才完成 DBus 服务
idle 无明显 I/O 时启动,避免抢资源 少量用途

易错 一个最常见的问题:用了 Type=forking 却没配 PIDFile=,导致 systemd 找不到子进程 PID,status 里出现 "no main process" 或误判 active 其实已死。对 node server.js、python app.py 这类前台进程,请用 Type=simple,不要用 forking。

[Service]
Type=simple
ExecStart=/usr/local/bin/node server.js

提示 判别口诀:前台直接跑的一律 simple;会自己 fork 变成后台守护进程的老式程序才用 forking;跑完就退的脚本用 oneshot。

After / Before:启动顺序,而非依赖

After=a.service 表示"等 a 启动完成,再启动我"。注意它管的是顺序,不是"必须有 a"。

[Unit]
After=network.target mysql.service

这台机器上 bangdream.service 的 After=network.target mysql.service,意思是:先等网络就绪、先等 MySQL 起来,再启动 API。——因为 API 启动时要连数据库,数据库没好它必然报错。

Before= 则相反:在某个 unit 之前先启动自己。After/Before 常成对使用规划一棵启动树。

Wants / Requires / BindsTo:真正的依赖

顺序之外,还有三类依赖:

字段 强度 含义
Wants= 弱 尽量拉起依赖;依赖失败不阻止自己启动
Requires= 强 依赖没起动,自己也不起;依赖失败自己会停
BindsTo= 最强 依赖停止,自己跟着停(绑定关系)
[Unit]
Requires=mysql.service   ; 强依赖
Wants=network.target     ; 弱依赖

Wants 和 Requires 都隐含着启动顺序,但可被 After= 显式覆盖。通常写法是":用 After 管顺序,用 Wants 管弱依赖"。因为 Requires 太激进——依赖一挂,自己也被拉着停,容易连锁反应。生产上更常见:

[Unit]
After=network.target mysql.service     ; 排顺序
Wants=mysql.service                    ; 尽量带上,但不强绑

易错 别把 After 当成可以省略 Wants 的理由:After=mysql 只排顺序,不保证 MySQL 真的会启动。若 MySQL 没在启动列表里,你的服务会"等着也可能白等"。多数情况下要 After + Wants 配合用。

WantedBy:开机自启的实现原理

[Install] WantedBy=multi-user.target 是"开机自启"的开关。它的机制很朴素:systemctl enable 时会在 /etc/systemd/system/<target>.wants/ 目录里,为你的服务创建一个符号链接。

systemctl enable bangdream.service
ls -l /etc/systemd/system/multi-user.target.wants/bangdream.service
# lrwxrwxrwx ... /etc/systemd/system/multi-user.target.wants/bangdream.service -> /etc/systemd/system/bangdream.service

multi-user.target 这个"目标"本身定义了一堆 Wants= 依赖,当开机进入它时,systemd 就依 .wants 目录里的链接,把每个目标服务拉起来。

易错 为什么有时 enable 报错?因为 unit 文件里没有 [Install] 段,或 WantedBy 写错了 target 名。没有 [Install],systemd 不知道把它挂到哪,enable 就会失败。这是新手写 unit 时最常见的坑——只写了 [Service],忘了 [Install]。

systemctl list-dependencies multi-user.target   # 看这个 target 会拉起哪些服务
systemctl get-default                           # 看默认启动 target(通常就是 multi-user)

xray.service 的额外:权限与资源限制

xray.service 是这台机器上最讲究的一个,除了基础三段,还加了权限收紧设置:

[Service]
User=nobody
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE   ; 只授予两类能力
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
NoNewPrivileges=true                                        ; 禁止提权
ExecStart=/usr/local/bin/xray run -config /usr/local/etc/xray/config.json
Restart=on-failure
RestartPreventExitStatus=23                                 ; 退出码 23 不触发重启
LimitNPROC=10000
LimitNOFILE=1000000
RuntimeDirectory=xray
RuntimeDirectoryMode=0755
  • User=nobody 用最低权限用户跑,即使被攻破也影响有限;
  • CapabilityBoundingSet 只给它两条网络类能力(管理网卡、绑定低端口),其余能力全没收;
  • NoNewPrivileges=true 防止它 setuid 提权;
  • LimitNPROC/LimitNOFILE 放开进程/文件描述符上限,因为反代要扛大量连接;
  • RuntimeDirectory 让它能在 /run/xray 建临时目录。

这背后是同一个思想:程序需要的权限尽量小,崩溃的重启策略尽量智能。on-failure 配合 RestartPreventExitStatus=23,避免"明知不该重启的退出码"也进入死循环。

一个完整的依赖链案例

假设你的服务需要"先 MySQL、再 Redis、最后自己",且都弱依赖:

[Unit]
Description=My stacked service
After=network.target mysql.service redis.service
Wants=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/my_app
Restart=on-failure

[Install]
WantedBy=multi-user.target

启动顺序:network → mysql → redis → my_app。任何一个在 After 里排在前面、又没能起来,mys_app 的启动顺序会被延后(弱依赖会继续,强依赖则等待失败)。

systemctl list-dependencies my_app.service   # 看这条服务的依赖树

易错点

  • After 是顺序不是依赖:想"必须有它",要 Requires/BindsTo,想"尽量带上"用 Wants。
  • 忘写 [Install]:enable 会报错,开机自启失效。一定要有 [Install] WantedBy=multi-user.target。
  • 前台程序用 simple:forking 不给 PIDFile 会误判状态,新手坑。
  • Requires 太激进:依赖一挂自己跟着停,容易连锁;默认用 Wants。
  • enable 后记得 systemctl start:enable 只配自启不立即启动,两步都要做。

相关命令

systemctl list-dependencies(依赖树)、systemctl get-default/set-default(默认 target)、systemctl cat(看单元定义)、business 判断 systemctl show -p After/Requires/WantedBy。