服务器入坑指北:开机为什么这么慢
重启一次要等好几分钟,登上去看"耗时排行",把排在前面的几个服务改成延迟启动、或者干脆禁用——再重启,时间几乎没变。还有更让人费解的:某个服务单看只花了一瞬,整机却慢。
原因在启动的方式本身:启动是高度并行的,整机时长取决于最长的那条依赖链,而不是"谁自己花得久"。所以"谁自己慢"和"谁在等谁"是两个不同的问题——耗时排行只回答前一个,而拖住整机的往往是后一个。
这篇的顺序是:先分段看时间花在哪一段 → 再看关键链找堵点 → 最后才用排行找可延迟的支线;两个工具各自的盲区也一并讲清。全文不给"多少秒算正常"这类阈值。
先分段:时间花在哪一段
启动过程是有阶段的:固件 → 引导加载器 → 内核(可选挂载早期启动镜像)→ 早期启动镜像接管 → 交给根文件系统上的系统管理器。对应的测量把时间分成三段:内核段、早期启动镜像段、应用侧段(工具输出里写作 userspace)。
systemd-analyze time # 分段概览:三段各占多少
判据很直接:哪一段占大头,决定下一步查什么。
- 内核段或早期启动镜像段占大头 → 问题不在服务与单元上,往引导配置、镜像内容、硬件等待这些方向查;
- 应用侧段占大头 → 才轮到单元与它们之间的依赖关系。
这里必须写清测量口径的限定:这些数字只测到"所有服务被拉起"为止。它不等于服务已经初始化完成——很多服务被"拉起"之后还要花时间才能对外可用;它也不代表磁盘已经空闲,启动末尾仍在补写的动作不会体现在这个数字里。
关键链:为什么总时长取决于最长的那条链
机制层面:启动过程高度并行,各个目标到达的先后顺序并不确定,但仍然遵守有限的先后结构。并行的含义就是——总时长由最长的那条依赖链决定。一条链上只要有一处在等,后面的都会跟着往后排,这些等待会串起来;而链外的单元就算自己慢,只要不在链上,就不影响总时长。
systemd-analyze critical-chain # 整机的关键链(时间关键链的树)
systemd-analyze critical-chain <单元> # 也可以只看某个单元自己的关键链
输出里两个符号的含义必须记住:
@= 该单元到达活动(启动)的"时刻";+= 该单元"自己"启动花的时间。
于是读法就一句话:"谁在等谁"看@,"谁自己慢"看+。
这条读法能解释一个常见困惑:链上某个单元自己只花了一点点,却排在很后面——它往往才是真正的堵点前驱,因为它前面有什么东西把它卡住了,而它又卡住了后面一串。顺着@往回找"先到的那一个",比对着+的大小排名有用得多。
同样要记住这个工具的限定:它不包含从未进入"启动中"状态的单元(例如设备类单元,直接从"未激活"变成"活动"),也不显示作业信息(尤其是超时的作业)。
耗时排行只能当辅助:它的三类盲区
systemd-analyze blame # 按单元初始化耗时排序
systemd-analyze plot > boot.svg # 启动图:时间线可视化;输出路径自选,但目录要可写(只读目录或权限不足会直接失败);产物是 SVG,用浏览器打开
systemd-analyze dot --order # 依赖图:只看"先后依赖"
systemd-analyze dot --require # 依赖图:只看"需要关系"
耗时排行按"单元初始化耗时"排序,看起来最像"排行表",但官方文档自己就警告它会误导:某个服务的初始化看起来慢,可能只是因为它在等另一个服务完成初始化。
三类盲区必须一起记住,否则一定会误判:
- 不显示简单型(
Type=simple)服务——系统认为这类服务"立即启动",所以它们根本不会出现在排行里,哪怕它们在背后做了很多事; - 只统计处于"启动中"状态的时间,不反映作业在队列里的等待;
- 不能准确反映"等待硬件"造成的延迟。
这两类限定其实是同源的:关键链与耗时排行都依赖"启动中"状态,也都不反映作业排队——它们是同一件事的两个侧面,不必当成两条不同的坑。
所以它的正确用法是:找可延迟、可按需化的支线单元,而不是"判定瓶颈"。判定瓶颈要回到关键链。
顺带一个使用细节:启动图基于最近一次每单元的计时数据,启动后尽早看更准确。
现象到动作的判定表
| 你看到的 | 结论 | 动作 |
|---|---|---|
| 内核段或早期启动镜像段占大头 | 问题不在服务与单元 | 往引导配置、镜像、硬件等待方向查 |
应用侧段占大头,且关键链上有很长的 + |
这个单元自己慢 | 查它自己的初始化动作与外部依赖 |
关键链上 + 都很短,但 @ 一个比一个晚 |
有人在等别人,堵点在链条上游 | 顺着 @ 往前找"先到的那个" |
| 排行里有个大项,但它不在关键链上 | 它在支线 | 可以延迟或按需化,但别指望缩短总时长 |
| 总分没变,关键链却变了 | 改的是支线,或基线不可比 | 回到基线纪律,用同类启动重测 |
两条优化路线的取舍
路线一:改非关键单元——延迟启动、按需触发、调整依赖顺序。它改善的是观感与资源竞争(启动过程更清爽、争抢更少),但不缩短关键链,也就不改变总时长。代价小、风险低,但它不解决"开机慢"这件事本身。
路线二:真正缩短关键链——把依赖改成并行(在"需要关系"允许的前提下)、把"等外部事件"的单元改成事件驱动、把不必要的等待目标从关键链上摘掉。只有这条会改变总时长,代价是它动的是依赖结构:改动前必须用依赖图确认安全,改完必须复核功能。
这两条不是二选一,而是有先后:先把关键链缩短,再考虑把支线延迟。顺序反了就会出现开头那种情况——关了排行上最大的几项,开机时间没变。
依赖图有两种限定,别混着看:**“先后依赖"回答"谁排在谁前面",“需要关系”**回答"谁真的需要谁”。判断"这两个单元能不能并行"要用后者;把两者混为一谈,就会把一条只是"顺序凑巧"的关系当成硬依赖。
基线纪律与验证
基线纪律:用一次冷启动的测量做基线,任何变更之后再测一次;而且只在同一类启动之间比较(冷启动比冷启动),否则你会把正常波动当成优化效果。
验证清单,每项一条判据:
- 同一类启动下总分是否变短:与基线同口径对比。
- 关键链是否变短:如果总分没变而关键链变了,要能解释清楚——是改动了支线,还是两次测量的口径不同。
- 被延迟或按需化的单元功能是否仍正常:延迟启动最容易踩的坑是"需要它的时候它还没起来"。
- 重启后服务可用性复核(含自启):确认该起来的服务都起来了,而且是在预期的时间点可用。
- 回滚路径:改动依赖或延迟启动之前,记下原配置;验证不通过就还原并再测一次。
- 验证方式固定成一句话:用同一类启动复测一次,总分与关键链的变化能与改动对得上,才算通过。
这篇不解决什么
- 不展开单元语义:单元的类型与重启策略、就绪判定这些内容属于单元语义层;本篇在启动路径与关键链层。
- 不决定"该不该重启":那是另一个问题(例如内核更新之后的重启决策);本篇解决的是"重启为什么慢、慢在哪一段"。
- 不处理运行期等待:运行期的"进程在等谁"属于另一条线;本篇是启动期的关键链——同一个"谁在等谁"的思路,用在两个不同阶段。
踩坑清单
- 只看耗时排行就动手:排行会误导,慢可能只是"在等别人"。
- 关掉排行里最大的项却发现没变快:它在支线上,本来就不影响总时长。
- 把"服务被拉起"当成"服务已就绪":分段数字只测到拉起为止。
- 忽略简单型服务不出现在排行里:它们可能做了很多事,却完全不计入。
- 拿一次测量当基线,或在不同类启动之间比较:会把波动当成效果。
- 改了依赖却没验证功能仍正常:延迟启动最容易造成"需要时还没起来"。
- 把硬件与早期启动的等待算到服务头上:那一段的问题不在单元里。
- 只延迟启动、不缩短关键链,却宣布优化完成:总时长没变。
- 看依赖图不加限定:把"先后"和"需要"混为一谈,会误判能不能并行。
- 忽略关键链的限定:它不包含从未进入"启动中"的单元,也不显示超时作业。
- 关键链上只看
+的大小:@才回答"谁在等谁"。 - 改完不复测、也不留回滚:没法判断改动是否真的有效。
- 点赞
- 收藏
- 关注作者
评论(0)