uptime 这个数字几乎所有站长都见过,但从它身上能读出的东西,比大多数人以为的多。一个跑了 400 天没重启的服务器,听起来像勋章,其实更可能意味着内核已经十几个月没打过补丁;反过来,一台三天两头重启的机器,也不一定就是不稳定,可能只是有人在频繁改配置。这篇写的是怎么把 uptime 和开机耗时这两组数字,变成真正能用的诊断信息。
一、uptime 的三种读法
直接跑 uptime,输出大概是这个样子:
14:32:07 up 212 days, 4:11, 2 users, load average: 0.84, 0.62, 0.55第一个读法是「上次重启是什么时候」,用来判断故障窗口。当某个服务在日志里报了一堆错误,先看一眼 uptime,如果重启时间正好落在报错前几分钟,那么排查方向立刻收窄到「重启导致的状态丢失」——PHP-FPM 重启会丢 OPcache,MySQL 重启会清掉 buffer pool,Docker 重启会让所有容器的临时文件消失。很多「莫名其妙变慢」的案例,根因就是一次没人记得的自动重启。
第二个读法是「内核是否该更新了」。Debian/Ubuntu 的内核安全补丁需要重启才生效,unattended-upgrades 会把新内核装上,但不会自动重启(除非你开了那个选项,而那个选项通常会被建议关掉)。于是出现一种情况:补丁列表干干净净,实际运行的还是三个月前那个有漏洞的内核。判断方式很直接:
# 有没有待生效的内核更新
test -f /var/run/reboot-required && cat /var/run/reboot-required
# 当前运行的内核 vs 已安装的最新内核
uname -r
dpkg -l 'linux-image-[0-9]*' | awk '/^ii/{print $2}' | sort -V | tail -1如果 uname -r 明显落后于最后安装的版本,就说明「装着新内核,跑着旧内核」。这个状态不是错误,但它需要一个明确的重启计划,而不是无限期挂着。
第三个读法是 load average 要结合核数看。三个数字分别是 1、5、15 分钟的平均值。单核机器上 load 2 就是满载,8 核机器上 load 8 才是满载。很多人看到 load 3 就紧张,其实是虚惊。真正需要关注的是「5 分钟和 15 分钟都在持续上升」——那说明压力不是瞬时尖峰,而是在累积。
还有一个反直觉的现象:load 高但 CPU 空闲,通常是 IO 等待或大量 D 状态进程。这时候 top 里 CPU 使用率可能只有 10%,但 load 到了 20,原因是有几十个进程卡在磁盘或网络文件系统上。这种场景用 vmstat 1 看 b 列(阻塞进程数)比看 load 更直接。
二、开机耗时分析:systemd-analyze 三板斧
systemd-analyze 是 systemd 自带的启动分析工具,Debian 9 之后默认就有。最基础的用法:
$ systemd-analyze
Startup finished in 3.102s (firmware) + 4.211s (loader) + 2.994s (kernel) + 41.882s (userspace) = 52.190s四个数字对应四个阶段:固件(BIOS/UEFI 自检)、引导器(GRUB 等待与加载)、内核(内核初始化加 initramfs)、用户空间(systemd 拉起所有服务,直到 default target 达成)。这个分解特别有用,因为它一眼就能看出该往哪查:
如果 firmware 或 loader 占了大头(比如 loader 20 秒),那不是系统的问题,是 GRUB 超时设置——检查 /etc/default/grub 里的 GRUB_TIMEOUT,云主机上默认 5 秒很正常,物理机上有时被设成 30 秒。
如果 kernel 超过 10 秒,多半是 initramfs 太大或者硬件探测慢,云主机上少见,但如果服务器上装了大量 DKMS 模块(ZFS、某些网卡驱动),initramfs 会显著变大。
如果 userspace 占了 30 秒以上,那才是真正值得优化的部分,用下面这条看细节:
systemd-analyze blame | head -20它会按耗时排序列出各单元。常见的「拖后腿」单元有这么几类:
一是 network-online.target 相关的等待。配了静态 IP 或者云厂商的 cloud-init 时,systemd-networkd-wait-online 可能等满超时。解决不是删掉它,而是检查它等的是哪个接口——有时它等的是一块根本不会 up 的网卡。
二是 cloud-init 的多个 stage,在已经不需要初始化配置的机器上仍然每次开机跑一遍,每个 stage 一两秒,加起来十几秒。如果确认不再需要它重新配置网络和 SSH 密钥,可以 systemctl disable cloud-init(但要先确认自己记得主机名、SSH key 是怎么配的)。
三是 apt-daily.service 和 apt-daily-upgrade.service。这两个是定时任务单元,开机时可能被触发一次,如果 apt update 恰好卡在网络慢的时候,会拖很久。正确的做法不是禁用,而是给它们加 RandomizedDelaySec 并确认它们不在关键路径上(apt-daily 默认是 After=network-online.target,但不会阻塞 multi-user.target,一般不会影响登录)。
第四类是自定义服务的 ExecStartPre 里有 sleep 或者网络探测。这是自己埋的坑,值得翻一遍所有自定义 unit。
三、critical-chain:找串行瓶颈
blame 的局限在于它只统计单元自身耗时,不显示依赖关系。一个服务自己只花 0.1 秒,但它等待的前置服务花了 20 秒,blame 上看不出来这是它的责任。critical-chain 展示的是「关键路径」——从最早的单元一路到 default target,耗时最长的那条链:
$ systemd-analyze critical-chain
The time when unit became active or started is printed after the "@" character.
The time the unit took to start is printed after the "+" character.
multi-user.target @41.882s
└─nginx.service @40.112s +1.7s
└─network-online.target @40.005s
└─systemd-networkd-wait-online.service @28.441s +11.5s
└─systemd-networkd.service @26.902s +1.4s
└─systemd-udevd.service @2.211s +24.6s从这张图能直接读出来:udevd 花了 24 秒(硬件探测慢),networkd 等了 11 秒,最后 nginx 才起。优化目标应该是 udev 那 24 秒,而不是盯着 nginx 的 1.7 秒。这就是 critical-chain 比 blame 有价值的地方——它告诉你优化哪个能真正缩短总时长。
有一点要注意:systemd-analyze 的数据来自本次启动记录的 journal,如果 /var/log/journal 没开持久化(默认 Ubuntu 的 journal 是 volatile,重启就清),那么重启后 journal 是空的,但 systemd 的启动数据在 /run/systemd 里还是有的,一般仍能读到。如果读不到,需要先确保 Storage=persistent 已经配好,下次开机才能分析。
四、一个容易被忽略的指标:重启次数
uptime 只告诉你「这次开了多久」,不告诉你「有多频繁重启」。后者的信息量更大。用这两条看:
# 最近 30 天的开机记录
journalctl --list-boots | tail -30
# 统计最近 30 天的引导次数
journalctl --list-boots | wc -l如果 30 天里有十几次 boot,而你自己只重启过两次,那就是有人(或某个自动化)在替你重启。云厂商的维护、OOM 之后被宿主重置、内核 panic 后的自动重启,都会产生 boot 记录。结合 journalctl -b -1 -p err 看上一次引导的错误日志,往往能查到原因:OOM killer 干掉关键进程、或者某个驱动崩溃触发 panic。
对个人站来说,「机器偶尔重启」本身不一定需要处理,但如果每次重启后站点都出现异常(FPM 启动慢、容器没自动起来、定时任务漏跑),那就需要把「重启后自愈能力」当成一个正式指标来管:所有关键服务 enabled,依赖写进 unit,重启后 5 分钟内的健康检查自动跑一遍。
五、开机耗时里最不该优化的地方
分析了半天启动链之后,很容易产生一种冲动:把所有看起来慢的服务都改成并行启动。但 systemd 的默认策略已经是尽可能并行了,你能看到串行的地方,基本都是「确实有依赖」的。强行去掉依赖(比如给 nginx 加 After= 之外再把它从 network-online.target 里摘出来)会让服务在网卡还没配好 IP 时就 bind,结果是启动成功但监听在错误的地址上,或者索性 bind 失败进入重启循环。
同样不值得优化的还有 sshd 的启动时间。它通常只要零点几秒,但它在关键路径上,于是有人会想去掉 PAM 或者关掉 DNS 反查。关掉 UseDNS no 确实能减少登录延迟(这一项是真的有用,尤其是云主机反查超时的时候),但它影响的是「登录慢」,不是「开机慢」,两件事不要混为一谈。
真正值得花时间的优化只有一类:把你自己的服务启动流程做得可预测。具体说就是,每个自定义 unit 都写清楚 After、Requires、Restart=on-failure 和 RestartSec,并且保证它在依赖没就绪时是「等待」而不是「失败后放弃」。这比把总启动时间从 40 秒压到 30 秒有价值得多——因为 40 秒的重启在维护窗口里完全无感,而一次启动顺序错乱导致的 502 可能要查一晚上。
顺带一提,重启频率还和「配置变更的方式」有关。如果改一次 Nginx 配置就要 reboot 一次,那说明流程有问题:Nginx 支持 nginx -t 校验后平滑 reload,MySQL 的大部分参数支持在线改,PHP-FPM 支持 reload 而不丢连接。把「必须重启」的清单缩到最短(内核、initramfs、少数需要重启才生效的模块参数),重启次数自然会降下来,uptime 这个数字也才有意义。
六、把 uptime 变成日常运维的一行数据
在日常巡检里,uptime 和开机耗时适合做成一条日报中的两个字段,成本几乎为零:
printf '%s | uptime_days=%s | boots_30d=%s | kernel=%s | reboot_required=%s\n' \
"$(date +%F)" \
"$(( $(awk '{print int($1/86400)}' /proc/uptime) ))" \
"$(journalctl --list-boots 2>/dev/null | wc -l)" \
"$(uname -r)" \
"$(test -f /var/run/reboot-required && echo yes || echo no)"这四组数放在一起就有了对照价值:uptime 天数一直在涨而 reboot-required 变成 yes,说明该安排维护窗口了;boots_30d 突然变多,说明有异常重启;kernel 版本和安装的最新内核不一致,说明补丁生效被推迟了。
最后一个实践建议:不要为「uptime 长」自豪。服务器不是宠物,它的价值在于稳定提供服务,而不是永不重启。真正健康的指标是「重启一次,十分钟内一切正常」,而不是「三年没重启,也没人敢重启」。把重启做成一件可预期、可回滚、有检查单的事,比把 uptime 刷到四位数更有意义。