上午在一处内部讨论里看到有人提问:从仓库拉了一份 Rocky 10 云镜像,实例创建成功后进入系统非常慢,控制台已经打印完常规启动日志,但 SSH 要等两分钟左右才能连上。群里很快就滑向“网络问题”这个方向,有人建议查路由,有人建议重新配置静态 IP,还有人准备抓包。
这类“首启慢两分钟”的现象,在云镜像场景里其实经常不是网络链路劣化,而是系统启动流程里某个服务把一切卡在了“等待”状态。本文就按这个思路展开排障:先不碰网络参数,用 systemd 自带工具把启动时间拆开,找到那 120 秒到底消耗在哪个单元上,再判断要不要修改网络配置。文末会给出一套可复用的云镜像优化操作,适用于 Rocky 8/9/10 以及其它使用 systemd 的 RHEL 系发行版。
排障思路很简单:系统不是“传输慢”,而是“等超时”。云镜像首启慢的常见来源,基本都集中在等待网络在线、等待元数据服务、等待 SSH 主机密钥、等待 SELinux 重标记这几类。
1. 核心排查点速览
先给出这张速查表,后面的操作都对应到这里的行项目。遇到 120 秒级别的首启延迟,优先从下面这些对象入手。
| 排查点 | 为什么会导致首启慢 | 常用验证手段 |
|---|---|---|
| NetworkManager-wait-online | 等待所有网络连接进入“已连接”状态,超时普遍按 90 秒或 120 秒设置 | systemctl is-enabled、systemd-analyze critical-chain |
| cloud-init 元数据访问 | 启动时请求云平台元数据服务,连接不上会连续重试 | journalctl -u cloud-init.service |
| SSH 主机密钥生成 | 镜像未预生成主机密钥,首次启动临时生成,日志量少但明显 | ls /etc/ssh/ssh_host_* |
| SELinux 重标记 | 镜像内文件安全上下文缺失,整机文件需要重新标定 | journalctl 中检索 relabel 相关日志 |
| 云盘挂载与 udev 扫描 | 多路径、磁盘热插拔、设备重命名导致内核等待 | systemd-analyze plot |
注意第一行:NetworkManager-wait-online 这类服务虽然名字带网络,但它慢的原因不是带宽不够,而是“等待状态始终没满足”。它背后有非常典型的超时机制,这正是“两分钟”这个数字的常见出处。所以在动手改 DNS、路由、静态 IP 之前,先把系统启动时间轴打出来看。
2. 排障前先把“慢”的边界定清楚
“首启慢两分钟”这句话其实不够精确,因为不同人看到“慢”的起点和终点不一样。有人从实例创建开始计时,到 SSH 能连上;也有人从内核开始启动算起,到控制台出现登录提示。这两种口径会差出不少时间,因为前者还包含云平台调度、磁盘初始化、cloud-init 拉取用户数据等环节。
推荐统一成 systemd 的视角:关注内核接管之后到 multi-user.target 就绪之间的耗时。这个区间是云镜像排障时最值得分析的一段,因为绝大多数“等待型”服务都集中在里。先跑三条基础命令,把这段启动耗时拉出来。
# 列出本次启动的整体耗时 systemd-analyze # 查看启动过程的时间分布 systemd-analyze time # 查看耗时最长的启动单元 systemd-analyze blame | head -30如果 systemd-analyze 不可用,说明当前环境是轻量容器或缺少 systemd 工具链,先切换到完整安装的 Rocky 实例再排。后续验证也应该在同一个规格的测试实例上进行,避免云平台调度差异影响判断。
另外,建议在排障时记录三个时间点:内核启动时间、用户空间初始化完成时间、SSH 端口可连接时间。云平台控制台的 VNC 截图也可以作为旁证,但最终以 journald 的时间戳为准。
3. 用 systemd-analyze 把两分钟拆开
systemd 系列发行版有一个比直觉好用的地方:启动过程不是一团黑盒,而是由带依赖关系的 service 组成。任何一个 service 卡住,都能在启动时间轴上体现出来。
先看systemd-analyze critical-chain,它可以直接指出卡在“链路”上的单元。输出大体是一个倒推结构,从 multi-user.target 往回列。下面是一段典型格式,具体内容以你的环境为准:
multi-user.target @36.7s └─cloud-init.service @35.1s +100ms └─network-online.target @34.2s └─NetworkManager-wait-online.service @2.1s +30.5s如果你看到的输出里,某一行的耗时明显在 30 秒、60 秒甚至 90 秒以上,说明根因已经浮出来了。重点看两类单元:后缀带-wait-online的服务,以及cloud-*开头的 cloud-init 相关服务。
继续用systemd-analyze blame看单点耗时 Top 列表。注意blame只显示单个服务的“已用时间”,不一定能直接区分“服务主动运行很久”和“服务在等待别的资源”。可以把critical-chain的结果当作依赖链参考,两者配合使用。
# 查看指定目标的启动关键链路 systemd-analyze critical-chain systemd-analyze critical-chain sshd.service # 生成启动过程 SVG 图表,方便快速定位长时间间隔 systemd-analyze plot > /tmp/boot.svgSVG 文件可以在浏览器打开,也可以直接用scp拉回本地看。图中每个服务是一条横向时间条,超过 30 秒的“长条”非常显眼。实际排障里,这一步基本能把 90% 的问题定性。
4. 用 journalctl 精确到秒定位延迟
systemd-analyze 给出的是服务级结果,但还需要进一步确认具体卡在哪一次操作上。journald 默认记录了本次启动的完整日志,可以根据时间戳逐行核对。
# 查看本次启动日志,使用单调时间 journalctl -b --no-pager -o short-monotonic | grep -E "systemd\[1\]|cloud-init|NetworkManager|sshd-keygen" | head -100 # 只看本次启动中的错误和告警 journalctl -b --no-pager -p 3观察相邻两条日志的时间差。如果两行日志之间隔了 60 秒到 90 秒,而且附近出现NetworkManager-wait-online或cloud-init的日志,说明系统在等待某个外部条件满足,超时后才继续往下走。
也可用更细分的字段筛选,配合 awk 计算连续日志的时间间隔。不同的 systemd 版本输出格式略有差异,但思路一致:把时间轴拉出来,找“空窗”。
# 按时间先后打印启动日志,便于人工找大间隔 journalctl -b --no-pager -o short-precise --since "2025-01-01 00:00:00"如果不想看日志,也可以在测试机上用systemctl list-jobs临时观察当前挂起任务。首次启动慢的场景里,list-jobs可以显示出仍在等待的 job,比如wait-online或cloud-init目标。这个命令适合在另一个 SSH 会话或 VNC 控制台里执行。
5. 卡住的位置往往不在“网络传输”上
定位到具体服务后,下一步是判断它到底在为什么等待。这一节把最常见的几类原因展开说明,方便对照日志快速归类。
5.1 网络等待型服务:名字带网络,不等于网络不好
NetworkManager-wait-online 的作用是等待所有受管连接进入 connected 状态。云服务器通常只有一张网卡,如果这张网卡在 DHCP 分配 IP 之前被标记为“未连接”,wait-online 就会进入超时等待。
更麻烦的是,有些多网卡实例默认有多个连接配置,其中某个连接在云环境里根本不存在。NetworkManager 会一直尝试激活这个连接,直到超时。这类场景里,哪怕你手动配置静态 IP,也无法避免等待,因为问题不在 IP 获取,而在连接状态管理。
先验证 wait-online 是否被启用:
systemctl is-enabled NetworkManager-wait-online.service systemctl status NetworkManager-wait-online.service --no-pageris-enabled返回enabled时,再结合 critical-chain 里的耗时,基本就能确定是它在拖慢启动。
5.2 cloud-init 元数据等待:日志像网络,本质是云平台对接
cloud-init 在首启时会尝试访问云平台的元数据服务,比如阿里云、AWS、OpenStack 各自的接口。如果镜像里的 datasource 列表配置过宽,或者当前环境没有元数据服务,cloud-init 会逐项尝试,每次超时后继续试下一个,最终表现就是启动过程被拉长。
日志上看起来非常像“在访问网络”:因为确实有网络请求发生。但它和带宽、丢包、DNS 关系不大,属于云平台适配问题。用这条命令可以快速查看 cloud-init 在启动阶段做了什么:
journalctl -u cloud-init.service --no-pager -b journalctl -u cloud-init-network.service --no-pager -b如果日志里出现大量Retry、Timed out、Trying datasource等关键字,说明 cloud-init 正卡在元数据探测上。
5.3 SSH 主机密钥生成:SSH 连不上,但服务已经在跑
云镜像是为了分发而打包的,通常不保留源机器的 SSH 主机密钥。这样做的目的是防止私钥泄露,但也意味着每个新实例第一次启动时要临时生成密钥。
生成密钥本身只要几百毫秒到几秒,但如果系统熵源不足,或者/etc/ssh所在分区 IO 很慢,这一过程会被拉长。更关键的是,sshd.service 会等待密钥生成完成后再开始监听,因此用户看到的现象是“端口没起来”“SSH 超时”。检查方式很直接:
ls -l /etc/ssh/ssh_host_*如果看到多个ssh_host_ed25519_key、ssh_host_rsa_key文件,说明密钥已生成。如果文件为空或只有部分类型,说明首启生成还没完成,或者生成过程中断。
注意:不要把同一份 SSH 主机密钥散落到所有实例。安全敏感环境下,让每个实例保留独立密钥是正确做法;排障时要同时考虑安全和启动速度,不要一删了之。
5.4 SELinux 重标记:镜像文件上下文不完整
RHEL 系发行版默认启用 SELinux,云镜像打包时如果文件没有正确的安全上下文,首次启动就需要重标记整个文件系统。文件数量越多,磁盘 IO 越慢,耗时越长。
日志中会出现relabel、restorecon、selinux相关字样。处理办法是等首启完成重标记后再制作镜像,或者在镜像构建阶段执行一次自动重标记。
5.5 其它“等待型”服务
除了上面几类,还有 systemd-random-seed、systemd-tmpfiles、fstrim 和 udev settle 也可能产生明显延迟。它们的共同特点是:不是持续吞吐数据,而是在等待某个条件变成“就绪”,一旦超时就继续往下走。
排障时记住一个经验:如果某个服务的耗时接近 90 秒或 120 秒,基本都是超时等待,而不是运算密集任务。这个规律对定位非常有用。
6. 几种典型的“冤枉网络”故障模型
很多“首启慢”排到最后都会发现,网络配置没改任何东西。下面这几类模型值得在排查时优先对照。
第一类:wait-online超时,但主机网络完全正常。你可以在实例启动后用nmcli device status查看网卡状态,发现已经 connected。这说明 wait-online 等待的不是当前主网卡,而是某个失效连接或额外网卡。此时静态 IP、路由、DNS 都没有问题,却依然整体卡 120 秒。
第二类:cloud-init 反复重试元数据,从现象看像“访问外网慢”,但用网络测速工具看带宽又正常。这种场景常见于本地虚拟机测试,或镜像在云平台之间迁移后 datasource 列表残留。日志特征是 cloud-init 模块逐个尝试多个 datasource,每个都超时。
第三类:能 Ping 通实例,但 SSH 端口迟迟不监听。第一反应通常是防火墙或安全组拦截,但实际可能只是 ssd 在等待主机密钥生成。安全组规则没问题,系统防火墙也没拦,就是 SSH 服务本身没就绪。此时在 VNC 控制台或另一条会话里执行systemctl status sshd,能看到等待关系。
这三类模型覆盖了绝大多数“首启慢两分钟”的云镜像排障场景。核心判断依据不是网络工具,而是systemd-analyze和journalctl的时间戳。
7. 修复与验证流程
找到根因后,修复方式要区分“临时救急”和“镜像持久优化”。下面按模块给出可复制的操作。
7.1 处理 wait-online 超时
如果是 NetworkManager-wait-online 引发的等待,最常见的处理有两种:一是直接禁用,二是给它设置一个更短的启动超时。对于云服务器,强烈建议保留基本等待能力,但把超时压缩到 15 秒左右,避免无限等待。
# 临时禁用,验证是否为问题根因 sudo systemctl disable --now NetworkManager-wait-online.service sudo reboot# 保留服务但缩短超时 sudo mkdir -p /etc/systemd/system/NetworkManager-wait-online.service.d sudo tee /etc/systemd/system/NetworkManager-wait-online.service.d/timeout.conf > /dev/null <<'EOF' [Service] TimeoutStartSec=15 EOF sudo systemctl daemon-reload如果是网卡存在多余连接,先查看连接列表:
nmcli connection show只保留真正使用的连接,删除无用连接:
sudo nmcli connection delete <冗余连接名>7.2 调整静态 IP 与 DHCP 超时
云服务器上手动配置静态 IP 时,不要把 DHCP 等待时间拉得太长。用 NetworkManager 可单独配置连接的超时时间:
# 查看现有连接的名称 nmcli connection show sudo nmcli connection modify <连接名> ipv4.dhcp-timeout 10 sudo nmcli connection up <连接名>如果公司或云 VPC 要求固定内网 IP,更推荐在连接文件中直接配置,而不是使用 DHCP reservation 之外的方式。配置完毕后用nmcli device show eth0验证 IP 是否在预期网段内。注意这里的<连接名>要替换成实际连接名称,不要照抄。
7.3 SSH 密钥预生成与保留策略
对于测试环境或内部镜像,可以在构建阶段预生成 SSH 主机密钥:
sudo ssh-keygen -A sudo systemctl restart sshd这条命令会补齐所有平台所需的密钥类型。之后重启一次,确认 SSH 能在几秒内就绪。多租户分发场景下,如果要保证私钥不泄露,建议在快照前清掉主机密钥,并接受首启生成的一次性开销。真正的优化目标是把生成耗时控制到秒级,而不是完全避免生成。
7.4 cloud-init 限制 datasource
如果确定实例只运行在特定云平台,应该在/etc/cloud/cloud.cfg.d/下限制 datasource 列表,避免 cloud-init 逐个尝试所有平台。具体配置格式以所用系统版本和云平台文档为准。下面是一个通用示意,不要直接照抄:
# /etc/cloud/cloud.cfg.d/99-datasource.cfg datasource_list: [AliYun, None]改完配置后立即验证语法是否正确:
sudo cloud-init schema --system sudo cloud-init clean --logs sudo reboot注意:cloud-init clean --logs会清理 cloud-init 状态,首启流程会在下次启动时重新执行。用于排障验证没问题,但不要在正式运行中的业务实例上随意操作。
7.5 处理 SELinux 重标记
如果确定首启慢来自 SELinux 重标记,可以主动触发一次完整重标记,并观察后续启动是否恢复正常。
sudo fixfiles -f relabel sudo reboot重标记会持续一段时间,期间不要中断电源。重启后检查启动耗时,并确认系统进入 enforcing 模式后一切正常。
7.6 验证修复结果
修改完任何一项,都要重新验证启动耗时:
systemd-analyze systemd-analyze blame | head -20 systemd-analyze critical-chain journalctl -b --no-pager | grep -E "startup finished|reach target|Timed out"只有重启后确认总耗时明显下降,才说明定位正确。如果耗时依旧,回到第 4 节重新看日志时间轴。
8. 发布云镜像前的优化清单
如果手头在维护 Rocky 9/10 的云镜像,与其等用户报“首启慢”,不如在构建阶段就把这些坑填掉。下面是一份可纳入镜像发布流程的检查清单。
第一,快照前处理一次性初始化。运行一次cloud-init clean --logs,清掉临时状态文件,避免快照包含上一实例的机器 ID、SSH 密钥指纹或日志残留。很多首启慢其实是“残留状态与当前实例冲突”造成的。
第二,重置或预留 machine-id。云镜像通常会把/etc/machine-id清空,让每个实例启动时生成唯一 ID。若镜像保留了旧 machine-id,会导致部分工具等待异常。可以先清空再重新生成。
sudo cloud-init clean --logs sudo rm -f /etc/machine-id sudo systemd-machine-id-setup第三,SSH 主机密钥按安全策略处理。内部测试镜像可以直接预生成,多租户镜像需要在速度和私钥泄露风险之间做取舍。至少要在快照前确认没有把包含未知主机密钥的 SSH 指纹写进镜像说明文档。
第四,NetworkManager 连接配置提前固化。在构建阶段就把主网卡的连接文件写好,明确使用 DHCP 或静态 IP,并设置连接超时参数。避免实例首启时重新执行交互式网络配置。
第五,配置内核参数时不要盲目禁用网卡命名。Rocky 10 默认使用可预测的网络接口命名,云平台驱动和 systemd 依赖接口名变化来识别设备。强行关闭可能反而导致启动等待设备匹配超时。
第六,做一次“冷启动回归测试”。每次发版前,用最终镜像新建一个实例,记录systemd-analyze的耗时和 SSH 就绪时间。把这项测试加入发布流水线,能拦截绝大多数回归问题。
9. 常见问题与排查清单
下面这张表适用于 Rocky 8/9/10 云镜像首启慢的现场快速排查,可以直接复制到自己的排障手册里。
| 问题现象 | 可能原因 | 先行检查 | 解决方案 |
|---|---|---|---|
| 首启刚好卡 120 秒 | wait-online 等待超时 | systemctl is-enabled NetworkManager-wait-online.service | 禁用服务或设置 TimeoutStartSec=15 |
| cloud-init 日志大量 Retry | 元数据服务无法访问 | journalctl -u cloud-init.service | 限制 datasource 列表 |
| 能 Ping 通但 SSH 连不上 | sshd 等待主机密钥生成 | ls /etc/ssh/ssh_host_* | 预生成密钥或检查熵不足 |
| 日志出现大量 relabel 字眼 | SELinux 重标记文件系统 | journalctl -b | fixfiles -f relabel |
| 内核到 systemd 阶段耗时很长 | 云盘挂载或驱动加载慢 | systemd-analyze plot | 调整存储类型或驱动参数 |
| 修改后重启又回到 120 秒 | 修改没有持久化或连接配置冲突 | 对比两次 journalctl | 检查 drop-in 文件和 nmcli 连接 |
| 只有首次启动慢,后续正常 | 一次性初始化任务 | 再次 reboot 验证 | 在镜像构建阶段执行 clean |
| 首启阶段 DHCP 反复尝试 | DHCP 服务超时 | journalctl -u NetworkManager | 设置 ipv4.dhcp-timeout |
如果按下表定位后仍然找不到根因,就把完整的journalctl -b日志导出,重点找两条日志之间的时间空洞。只要时间空洞找到了,问题基本就水落石出。
10. 总结与下一步
云镜像首启慢两分钟,最值得先跑的命令就是systemd-analyze critical-chain和journalctl -b。这两个命令可以把模糊的“两分钟”还原为具体的服务等待时间。最容易踩的坑则是一上来就调网络参数:静态 IP、DNS、路由都改一遍,最后发现 wait-online 超时和这些配置毫无关系。
建议把这套时间轴分析方法固化成自己的排障模板:先看总时长、再看单服务耗时、再看日志空窗、最后判断类型。只要遵循这个顺序,Rocky 10 云镜像首启慢这类问题就不会再反复折腾网卡和防火墙了。
下一步,可以进一步测试不同云平台下 cloud-init 的表现差异,也可以把发布前冷启动测试接入 CI 流程,让“首启慢”这类问题在镜像发布前就被自动拦截。