一台 Rocky 10 云镜像的实例,首次开机竟然要卡两分钟,很多人的第一反应是网络问题。我遇到过类似场景,最后定位下来,问题根本不在网卡、DHCP 或者 DNS,而是启动阶段一堆服务在“排队等待”。
这篇文章想分享一个不一样的排障思路:云镜像首次启动慢,网络只是嫌疑人之一,真正让你等两分钟的,往往是 systemd 服务等待链、cloud-init 超时和镜像初始化顺序。与其反复改网卡配置,不如先把启动时间线拆开,看看到底是哪一步卡住了。
1. 首启慢两分钟,为什么“网络”总是第一个背锅
1.1 网络确实很值得怀疑
云镜像第一次启动时,系统要做很多和网络相关的初始化动作。具体来说,至少包括这几个环节:
- 网卡驱动加载,等待硬件就绪。
- 连接 DHCP 服务器,获取私有 IP。
- 如果是 IPv6 环境,还有 SLAAC 和 Router Advertisement 的等待。
- NetworkManager 或者 network daemon 启动,并等待设备进入 connected 状态。
- cloud-init 尝试访问云平台的 metadata 服务,拉取主机名、SSH 公钥、用户数据等。
- 如果配置了 DNS,某些服务启动时要解析主机名或者软件源域名。
任何一个环节长时间没有响应,都可能导致启动时间被拉长。所以遇到首启慢,先怀疑网络非常正常,这不算误判,只是下一步的排查方向要更精确。
但问题在于,“网络服务最终起来了”和“启动过程中没有被网络拖住”是两码事。你 SSH 进去之后执行ip addr能看到地址,ping也能通,并不代表启动阶段没有发生过等待。systemd 不是所有服务都并行启动的,它会让某些服务等待网络栈达到某个状态,再继续拉起后续任务。
1.2 但“能访问”和“没卡在网络”不是一回事
我在处理这类问题时,经常看到这样一种有意思的现象:用户通过云控制台的 VNC 或者串口登录,界面一直停在启动日志里,最后跳出来的却是一行和 network 相关的超时提示。于是大家开始排查网卡驱动、私有网络、安全组、路由表,折腾半天,把静态 IP 都配了一遍,问题依然存在。
后来发现,系统实际上是在等待 metadata 服务返回。这个服务请求走的是内网地址,并不是传统意义上“你家里能否上网”的网络问题。很多云厂商的 metadata 服务,正常情况下解析很快,但如果你用的是自制镜像,或者 cloud-init 的配置文件指向了不存在的地址,它就会一直重试,直到超时。
这一类“网络等待”很难通过常规网络诊断发现。你pingmetadata 地址可能会通,也可能完全不可达,但真正影响启动时间的,是 cloud-init 内部的超时策略。
所以我的第一步判断是:不要急着进入“网络配置”这一个方向,先把启动时间线拿出来,看看哪些服务耗时最多,再决定下一步。
2. 先不要拍脑袋,用 systemd-analyze 给启动过程做一次时间线拆解
2.1 最小命令集
Rocky 10 使用的 systemd 版本比较新,systemd-analyze的工具链已经很成熟。第一次登录进系统后,如果还能进入 shell,先执行这几个命令:
systemd-analyze time这条命令会给出总体启动时间,包括固件、引导加载器、内核、用户空间的时间。如果你只关心用户空间里 systemd 管理了哪些慢服务,继续执行:
systemd-analyze blame | head -20这条命令会把用户空间的服务按耗时从高到低排列。排在最前面的,基本就是导致首启慢的主要嫌疑对象。
为了看得更清楚,还可以生成一个启动过程的矢量图:
systemd-analyze plot > boot-plot.svg然后把这个 SVG 文件下载到本地用浏览器打开,能够直观看到每个服务在时间轴上的位置。
2.2 三个指标怎么看
systemd-analyze time给出的时间,只是让你有个整体概念。真正要分析的是blame里的服务耗时,以及critical-chain里的依赖链。
systemd-analyze critical-chain <service>例如:
systemd-analyze critical-chain cloud-init.service依赖链会告诉你,这个服务到底等了哪些前置资源。很多时候,一个服务在blame中耗时很长,并不是它自身执行慢,而是它前面的某个依赖迟迟没有满足。
2.3 判定顺序:先看“谁在等”,而不是“谁最慢”
这里有个很容易犯的错误:看到blame里某个服务耗了 90 秒,就急着去禁用或者优化它。但你应该先看一下这条依赖链里还有没有其他服务。
举例来说,NetworkManager-wait-online.service经常上榜。它不是一个“干活”的服务,它只是告诉系统“我刚等完了网络在线”。真正让它等待的,可能是你某个网卡配置里的 DHCP 一直没成功,也可能是你在/etc/sysconfig/network-scripts/ifcfg-*里定义了额外路由或者网关,导致 NetworkManager 判断“上线”的标准一直无法满足。
所以排查顺序应该是:
- 先看
blame中最耗时的几个服务。 - 再对每个服务执行
critical-chain。 - 如果多个慢服务都指向同一个依赖,说明卡点在依赖上。
- 如果某个服务自身执行时间长,再进去翻服务日志。
这样就不会被表面数字带到错误方向。
注意:
systemd-analyze blame显示的时间是服务启动到进入 active 状态所需时间,不包含它在队列里等待的时间。如果你发现多个服务都在 80~90 秒附近,往往是同一个上游阻塞。
3. 被低估的几个常见卡点:cloud-init、密钥生成、udev settle
3.1 cloud-init 的“服务依赖感”
Rocky 云镜像一般都会安装 cloud-init,它的启动流程分好几个阶段:
- cloud-init-local.service
- cloud-init.service
- cloud-config.service
- cloud-final.service
它们在启动早期依次运行。第一个阶段处理本地数据源,第二阶段开始和云平台的 metadata 服务交互。如果 metadata 服务在短时间内无法访问,cloud-init 会进行重试,这个重试间隔和最大等待时间,直接影响首启速度。
检查 cloud-init 日志时,可以执行:
journalctl -b -u cloud-init.service --no-pager或者直接看:
tail -n 100 /var/log/cloud-init.log日志里经常会出现类似 “Timed out waiting for ...”,或者 “unable to read metadata ...” 的记录。如果确认是 metadata 访问超时,要先去核对镜像里配置的 datasource 和云厂商是否匹配。
有些自建镜像会把 datasource 配置成某个固定地址,但当你把它放到另一个云平台后,该地址不可达。系统不会马上失败,而是反复重试一段时间后才放弃,最终表现为“首启慢两分钟”。
3.2 SSH host 密钥生成与系统熵
另一个容易被忽略的卡点是 SSH host 密钥生成。云镜像首次开机时,如果没有预先生成密钥,sshd 相关服务会在启动阶段调用ssh-keygen生成 host key。
生成 RSA、ECDSA、Ed25519 等密钥需要足够的随机数。在刚刚启动的虚拟机里,内核熵池可能还没有积累足够多的噪声,getrandom()或/dev/random读取可能会阻塞。这就会导致 sshd-keygen 长时间挂在那边,看起来也像卡住。
排查方法也比较直接:
systemctl list-units | grep sshd或者:
systemd-analyze blame | grep sshd如果确实是密钥生成拖慢了启动,可以考虑在制作镜像时预生成 host key,并在镜像内部提前执行systemctl preset sshd-keygen相关动作。还有一种做法是安装并启用 haveged 或者 systemd-random-seed,避免启动阶段熵不足。
3.3 udev settle 与硬件探测
有些自定义云镜像里会出现一个叫systemd-udev-settle.service的服务。这个服务的历史作用,是等 udev 处理完所有设备事件。听起来合理,但问题是它没有精确判断“处理完”的标准,在某些驱动加载慢或者虚拟化设备较多的环境中,它很容易等过头。
如果你看到blame结果里有这个服务,先不要急着禁用它,要确认你的存储或网卡驱动是否真的需要依赖 udev 完成设备命名。现代 systemd 已经不太建议使用systemd-udev-settle.service,只保留少量兼容场景。当你确认当前环境不需要之后,可以把它 mask 掉:
systemctl mask systemd-udev-settle.service但我要提醒一句:mask 之后,某些旧式脚本或第三方服务如果仍然依赖它,可能会遇到设备节点未准备好的问题。所以改之前最好先跑一轮你的业务启动脚本。
4. 网络环节仍然要排查,但按“依赖关系”来,不要乱改配置
4.1 先判断网络服务是否真的故障
虽然文章重点说“不一定卡在网络”,但你不能真的完全不排查网络。理想的做法是,先确认网络服务状态。
在 Rocky 10 里,默认一般使用 NetworkManager。检查状态:
systemctl status NetworkManager也可以看它启动阶段有没有明显报错:
journalctl -b -u NetworkManager --no-pager | tail -n 50如果你判断启动慢是和“等待网络在线”相关,可以查看:
systemctl status NetworkManager-wait-online.service如果它里面出现类似 “Timed out waiting for network connectivity” 的记录,说明有设备迟迟没有达到在线状态。
4.2 如果发现网络服务耗时,怎么区分“等待外网”还是“等待内网元数据服务”
这一步很关键。网络服务耗时未必代表“公网不通”,更常见的其实是:
- 某个网络的 DHCP 地址获取失败后,系统还在反复重试。
- 某些网卡配置文件里设置了
DEFROUTE=no或额外网关,导致路由判断异常。 - DNS 解析出现问题,服务在解析外部域名时超时。
- firewall 或者 nftables 规则导致 metadata 请求被拦截。
区分方法很简单:抓一下系统在启动阶段访问的地址。在等待卡住的时候,打开另一个 SSH 会话执行:
journalctl -b | grep -i "metadata\|datasource\|http"或者:
journalctl -b | grep -i "timed out"如果发现超时对象指向的是云平台内网地址,那就不是传统网络故障,而是 cloud-init 和云平台之间的适配问题。如果超时对象是外网域名,那可能和 DNS 配置、镜像源地址、系统代理设置有关。
4.3 不要一上来就禁用 DHCP
很多人看到卡在网络服务,第一反应是把网卡改成静态 IP。但云镜像的 IP 是由云平台灵活分配的,你改成静态 IP,可能当时能起来,换一台主机或者网络分片就会失败。
在公共云场景下,我建议优先保持 DHCP,但可以调整 NetworkManager 的wait-online超时时间,或者直接让一些重要服务不等待网络在线。修改连接配置:
nmcli connection modify eth0 connection.autoconnect yes nmcli connection modify eth0 ipv4.method auto如果希望 systemd 等待网络的时间缩短,可以在/etc/systemd/system/NetworkManager-wait-online.service.d/override.conf里加入:
[Service] ExecStart= ExecStart=/usr/bin/nm-online -s -q --timeout=10这样就把等待时间限制在 10 秒,至少不会因为它卡满两分钟。要注意,如果你有依赖网络才能挂载的远程目录或需要申请证书等业务,缩短等待时间可能会让这些任务失败。
5. 如果这是你正在做的云镜像,启动优化应该在“打包阶段”完成
5.1 镜像内部可以提前完成的事
如果你不是只是开一台云主机,而是在制作一个自定义 Rocky 10 云镜像,那很多首启问题完全可以在打包阶段规避。下面这几件事我建议在镜像构建脚本里处理掉:
- 预生成 SSH host key,并清空
machine-id,让每台实例启动后能生成独立 ID。 - 清空 cloud-init 在旧实例上留下的缓存和 machine-id。
- 安装
systemd-random-seed或haveged,避免熵不足。 - 禁用不需要的等待类服务,比如
NetworkManager-wait-online.service。 - 检查 cloud-init 的 datasource 配置,确保它和目标云平台匹配。
- 清理不需要的内核模块和固件包,减少 udev 探测时间。
- 如果镜像支持,开启串口控制台,方便后续排障。
5.2 制作云镜像时的验证清单
格式可以做成表格,方便在构建时逐项确认。
| 检查项 | 具体命令 | 期望结果 |
|---|---|---|
| 启动时间 | systemd-analyze time | 整体时间符合预期 |
| 慢服务排序 | systemd-analyze blame | head -20 | 没有明显异常超时 |
| cloud-init 日志 | journalctl -b -u cloud-init* | 无 metadata 访问超时 |
| SSH host key | ls -l /etc/ssh/ssh_host_* | 文件已存在 |
| machine-id | cat /etc/machine-id | 构建时为空或占位值 |
| NetworkManager 等待 | systemctl status NetworkManager-wait-online | 不长时间卡住 |
| 串口输出 | dmesg | grep ttyS | ttyS0 等端口可输出日志 |
验证时最好做一个“干净首启测试”:用镜像起一台新实例,观察第一次启动到 SSH 可达、cloud-init 完全退出需要多久。如果这次测试已经达到生产预期,再发布镜像。
5.3 长期维护:版本升级带来的回归
还有一个容易被忽略的点:Rocky 的版本升级或者内核更新后,某些服务的启动时间可能发生变化。比如新版本内核里某个驱动探测变慢,或者 NetworkManager 的连接判断逻辑不一样。镜像如果长期不重建,只在旧实例上原地升级,遇到首启慢的概率会高一些。
我建议镜像发布前都跑一遍上面的验证清单,并且保留一份基线数据。如果升级后启动时间明显变长,就能快速定位是新版本引入的回归。
6. 沉淀一个通用的首启慢排查框架
6.1 五步排查法
把前面所有内容收敛一下,可以形成一套通用的云镜像首启慢排查框架:
- 拿时间线:用
systemd-analyze time和blame看整体分布。 - 看依赖链:对前几名慢服务执行
critical-chain,找共同上游。 - 翻日志:针对疑点服务检查 journal、cloud-init 日志和网络日志。
- 区分类型:判断是“外部服务等待”“本地服务执行慢”还是“资源不足而阻塞”。
- 回到镜像层:如果是自制镜像,直接修复镜像;如果是云厂商公开镜像,先检查实例类型和区域差异。
这套框架的好处是不会让你一开始就陷入网络配置的细节。先知道“哪一步花了多少时间”,再决定修什么。
6.2 适用边界和不适用场景
这套方法最适合的是“系统最终能启动,只是启动慢”的情况。如果系统完全无法启动、内核崩溃、磁盘损坏或者文件系统只读,那就要先走另一条更底层的排障路线,比如查看串口控制台早期的内核日志。
同时,如果你的慢启动发生在云平台网络策略层面,比如安全组把 metadata 服务端口封了,那你在系统内部看到的日志可能只有超时,没有明确报错。这时要结合云厂商平台侧的网络访问日志或者控制台状态来确认。
还有一个边界是:有些时候启动慢是云厂商底层虚拟化调度导致,和镜像内部完全无关。如果你在多个不同规格实例上测试,发现只有某一类宿主机上慢,那就要联系云厂商查宿主机负载或存储热迁移。
注意:不要把镜像内部的一切启动问题都归因于 “cloud-init 或 NetworkManager”。如果只有特定实例类型出现延迟,先做一个交叉验证:换一个 CPU 型号或存储类型试试。
6.3 什么时候才需要“改配置”
只有当系统确认某个服务确实超时,并且这个服务对当前业务没有关键影响时,才建议通过 systemd override 或者 mask 方式去掉等待。比如:
- 你不依赖 DHCP 等待外部存储挂载,可以缩短
NetworkManager-wait-online超时。 - 你的实例不使用 cloud-init 拉取用户数据,可以调整 cloud-init 的超时策略。
- 你的环境不依赖传统 udev settle 设备命名,可以 mask 相应服务。
但不要为了“刷启动时间”而批量禁用服务。很多服务在登录后你感觉不到它的存在,但实际承担着网络、密钥、硬件初始化等重要职责。最稳妥的方式是保留服务,只调整超时时间。
回到这次 Rocky 10 云镜像首启慢两分钟的问题。我最大的体会是,云环境里的“慢”往往比“完全不可用”更难排查。它不会给你一个醒目的红字,而是默默地在某个服务上等待,直到超时后才继续往下走。网络只是显眼的表象,真正的卡点可能藏在 systemd 依赖链、cloud-init 的重试逻辑、SSH 密钥生成甚至 udev 的设备等待里。
下次如果你也遇到类似情况,建议先别急着改网络配置文件。执行一条systemd-analyze blame,把启动时间线拉出来,看到底是谁在拖后腿。多数时候,答案会比你想的更清晰。