服务器入坑指北:单元依赖与启动顺序编排
服务起来了,可它依赖的东西还没就绪,只能靠重启碰运气;把"想要"写成了"必须"之后,一个可选组件挂了,整套都起不来;两个本来互不相干的服务明明没有依赖,却串行启动,开机时间白长一段。
顺序是先分清两件事:“想要"还是"必须”,以及**“先后”。它们经常被写混——After=这类顺序类指令只表达先后**,Wants=这类需求类指令只表达必要性;两者正交。
这篇给三段可直接粘贴/填空的内容:读依赖图 → 两类依赖的确切写法 → 顺序与必要性都写全的两种完整写法。单元名与依赖输出用占位符,指令名与命令名照写。
先分清"想要"与"必须"
| 指令 | 含义 | 失败的后果 | 什么时候用 |
|---|---|---|---|
Wants= |
列出的单元随本单元一起启动 | 对方启动失败或无法加入事务,对本事务整体的有效性没有影响,本单元仍会启动 | 官方推荐用它建立自定义依赖 |
Requires= |
声明更强的需求 | 任一必需单元启动失败,本单元就不会被激活 | 只有真正缺了它就不能工作时 |
取舍写清楚:Requires= 给的是"确定性",代价是把可选组件的故障升级成整体故障。所以官方那句话值得记住——通常更好的选择是用 Wants= 而不是 Requires=,以获得一个在服务失败时更健壮的系统。 |
"顺序"与"必要性"正交:先记住这条再动手
在你去配依赖之前,先把这条前提看清:
After=这类顺序类指令只表达先后:本单元只在所列单元已激活之后才启动;而且它不会显式激活所列单元(这与Requires=不同);Wants=与Requires=这类需求类指令不影响启动或停止的顺序——文档明说:需求依赖不影响顺序,必须用顺序指令独立配置;- 两者是正交的:只配了
Wants=而没有配After=时,两个单元会同时启动、彼此之间没有任何延迟。
结论:想要"先起 A 再起 B",必须显式写After=;想要"B 失败则整体失败",才写Requires=;两者都要,就都写。另外Before=是反向排序(本单元在所列单元之前启动),它同样不激活对方。
读依赖图块:三种看法
systemctl list-dependencies <单元> # 正向:递归列出所需与所想要的单元
systemctl list-dependencies --reverse <单元> # 反向:谁依赖它
systemctl list-dependencies --after <单元> # 只看顺序关系:谁排在它之后
systemctl list-dependencies --before <单元> # 反向的顺序关系:谁排在它之前
systemctl show -p Wants -p Requires -p After -p Before <单元> # 现读四个关键属性的实际取值
判据:
- 正向回答"它需要谁";反向回答"谁需要它";
--after/--before回答"谁先谁后"——三者合起来就是依赖图; systemctl show -p Wants -p Requires -p After -p Before <单元>给的是现读值:你写下的与它对不上,说明写在了别处或被别的片段改了。
两类依赖写法块
[Unit]
Wants=<依赖单元> # 弱依赖:对方起不来,本单元仍会启动
Requires=<依赖单元> # 强依赖:对方启动失败,本单元不会被激活(按需二选一)
systemctl show -p Wants -p Requires <单元> # 写完之后现读:确认落到了这一个单元上
判据:这两条都不表达顺序——写完它们,两个单元之间仍然没有先后。这是最容易想当然的一处。
写法建议落在补充片段里(用 systemctl edit <单元> 生成一个 drop-in),不要直接改上游的单元文件——上游文件会被升级替换。
顺序与必要性都写全的两种写法
写法一:先起 A 再起 B,且 B 失败不影响整体
[Unit]
Wants=<A 单元>
After=<A 单元> # 只表达先后;不会去激活 A
写法二:先起 A 再起 B,且 B 失败则整体失败
[Unit]
Requires=<A 单元> # 失败则本单元不被激活
After=<A 单元> # 顺序仍然要单独写:两者正交
systemd-analyze verify <单元> # 写法校验:先过语法这一关
systemctl daemon-reload # 让片段生效(高风险写操作,需回滚点)
systemctl list-dependencies <单元> # 读回依赖图:顺序边与需求边都在了
systemctl list-units --failed # 判连带失败:强依赖写错时这里会出现连带项
另外还有个常被忽略的 PartOf=:它让本单元随所列目标一起重启或停止(停止/重启的传播方向),与"启动先后"是两回事,别拿它当 After= 用。
判据:改完必须只读复核——依赖图里出现期望的关系,且 systemctl show -p Wants -p Requires -p After -p Before <单元> 的现读值与你的预期一致。
真正的并行来自依赖图
启动是不是并行,取决于依赖图的形状,而不是"你在文件里把两段配置写得一前一后"。先画依赖图,再谈并行;把本来无依赖关系的单元写成串行,等于白等。
想看"串行到底串在哪",用 systemd-analyze critical-chain 看关键链;启动耗时与关键链的完整分析属于《开机为什么这么慢》那篇,本篇只看依赖图的形状。
判据:说出"这两个单元之间有没有先后边"——没有边就是并行,哪怕它们的配置写在同一个文件的上下两段里。
图里的依赖不一定是你写的
系统会隐式创建并维护大部分依赖(例如由目标单元、路径需求等自动产生的顺序关系),而且显式与隐式引入的依赖都会出现在列表里。
判据:读图时要知道"看到的可能不是你写的"。看到陌生的关系,先判断它是隐式生成的还是自己写的,再决定要不要动。想看得更直观,可以用 systemd-analyze dot 导出依赖图;隐式依赖多来自 DefaultDependencies= 与目标单元的编排。
还有一类要特别对待:如果某个依赖由平台注入(初始化单元一类),把它当外部给定,不要试图改写——改了也可能在下一轮初始化时被顶回去。
现象到动作的判定表
| 现象 | 结论 | 动作 |
|---|---|---|
| 服务起来了,但依赖没就绪 | 只写了 Wants=,没写 After= |
补 After=(两者都要就都写) |
| 可选组件挂了,整套起不来 | 把"想要"写成了"必须" | 把 Requires= 改成 Wants= |
| 无依赖却串行启动 | 依赖图里有不必要的先后边 | 用 systemctl list-dependencies --after <单元> 找出多余的顺序关系并去掉 |
| 写了顺序但还是并行 | 顺序只表达先后,不会激活对方;或 After= 没落在这个单元上 |
用 systemctl show -p After <单元> 现读确认 |
| 看到自己没写过的依赖 | 隐式创建或平台注入 | 先判断来源;平台注入的当外部给定 |
改完怎么验证:每项一条判据
- 依赖图里方向正确:
systemctl list-dependencies <单元>与--reverse各看一次; - 可选组件故障不再拖垮整体:把弱依赖那一条临时指向一个不存在的单元,本单元仍应启动;
- 顺序确实生效:用
--after/--before复读,或看关键链里两者的先后; - 写法校验通过:
systemd-analyze verify <单元>不报错; - 重启后仍成立:改动写进了单元片段,而不是只在运行时改过——
[Install]段里的WantedBy=决定它被哪个目标拉起。
这篇不解决什么
- 讲单元类型与重启策略必须配套的那篇管就绪判定与重启;本篇管依赖与顺序——不重复就绪判定。
- 讲开机为什么这么慢的那篇看启动耗时与关键链;本篇只看依赖图的形状,需要时互相指路。
- 讲动手前先留好救援通道的那篇是失联后的入口与回滚;本篇是正常启动时的依赖编排。
踩坑清单
- 把"想要"写成"必须":可选故障被升级成整体故障。
- 只写
Wants=不写After=:两者并行启动,依赖还没就绪。 - 只写
After=以为会激活对方:After=不激活所列单元。 - 以为写了需求就有顺序:需求与顺序正交,必须单独写。
- 用
Requires=图省事:把整个系统绑成一条链,一处失败处处不起。 - 把无依赖的单元写成串行:开机时间白长。
- 看见平台注入的依赖就想去改:它会被下一轮初始化顶回去。
- 不看依赖图就谈并行:只能靠猜。
- 改完不做只读复核:
systemctl show -p ...一读就知道对不对,却没读。 - 把"服务起来了"当成"依赖就绪了":拉起不等于可用。
- 用
PartOf=当顺序指令:它管的是停止与重启的传播,不是启动先后。 - 忘了
systemctl daemon-reload:片段写了但没被读取,show里还是旧值。
- 点赞
- 收藏
- 关注作者
评论(0)