重启一次别出事:从检查到验收的完整流程

一、先说事故:重启跑了一晚上,网站白了五分钟

去年冬天我给一台跑了三年 WordPress 的 VPS 做例行重启,装完内核补丁敲了 reboot,躺下睡觉。第二天早上看告警才发现:重启后 11 分钟网站才恢复,期间 502 一直刷。查日志才知道,MySQL 起来时 InnoDB 要过一遍 redo 日志恢复(上次是 kill -9 掉的,脏页一大堆),而 PHP-FPM 在 MySQL 没准备好之前就已经接客,连不上库直接 502;等 MySQL 起来,又被几十个并发连接瞬间打满,连上之后还要预热缓存,用户感知就是「网站挂了」。

这件事之后我给自己定了个规矩:重启不是一条命令,而是一套有顺序、有校验、有回滚预案的流程。下面这套东西我用了两年,覆盖内核更新、配置变更、扩容迁移三种场景,单机博客、VPS 都适用。

二、先分清三种「重启」,代价差十倍

很多人把「重启一下」当成一个动作,其实它是三件完全不同的事:

  • 进程重启(systemctl restart php-fpm):秒级,内存清空,OPcache/APCu 全部失效,冷启动。代价最小。
  • 平滑重载(systemctl reload nginx / nginx -s reload / kill -USR2 php-fpm):不断连接,旧 worker 优雅退出。代价更低,但对某些改动无效(改监听地址、改 FPM 的 pm 参数部分项、升级二进制)。
  • 系统重启(reboot):分钟级,涉及 initramfs、磁盘检查、服务依赖、数据库恢复。代价最大,且失败面最广。

结论:能 reload 就别 restart,能 restart 就别 reboot。判断标准很简单——改动是否需要重新加载二进制或被内核接管。改 nginx.conf 的 upstream 权重 → reload;升级 nginx 版本 → restart(master 进程要换);改内核参数 net.core.somaxconn → 不用重启,sysctl -w 即时生效;装内核包 → 必须 reboot 才能跑新内核。

三、重启前的六项检查(三分钟,能省一晚上)

这套清单写成了一个脚本 /root/pre-reboot.sh,每次重启前跑一遍,五分钟搞定。核心是「先把不确定的东西变成确定的」。

3.1 检查有没有正在跑长任务

备份、采集、数据导入这类任务一跑就是半小时,重启会把它拦腰砍断。查询方式:

# 看有没有正在跑的大事务
mysql -e "SELECT trx_id, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS age,
  trx_rows_locked, LEFT(trx_query,60) FROM information_schema.innodb_trx ORDER BY age DESC;"
# 看有没有正在跑的备份/同步进程
ps -eo pid,etime,cmd | grep -E 'mysqldump|rsync|restic|wp-cli|curl' | grep -v grep
# 看有没有正在处理的队列任务
systemctl list-units --state=running | grep -E 'worker|queue|timer'

注意 innodb_trx 的 age:如果有个事务已经跑了 40 分钟还没提交,重启后 InnoDB 恢复阶段要回滚它,恢复时间可能比正常运行还长。这种情况宁愿等它跑完再重启,或者先 KILL 掉让它慢慢回滚(回滚可能更慢,别乱来)。

3.2 检查磁盘空间和 inode

重启后 journald、MySQL 的 redo、临时文件都会重新写盘。空间不够的情况我在论坛见过两次:一重启 MySQL 直接起不来,报 No space left on device,而服务商后台只能 VNC 登录。

df -hP | awk 'NR==1 || $5+0>=80'
df -iP | awk 'NR==1 || $5+0>=80'
du -xh --max-depth=1 /var 2>/dev/null | sort -rh | head -5

-P 是为了防止长设备名把 df 输出折行,导致 $5 取错列——这是老问题,很多从博客抄来的 df 检查脚本都有这个 bug。

3.3 检查关键配置的语法

改过 nginx、php、mysql 的配置还没验证就要重启,是最常见的自找麻烦。三项检查:

nginx -t
php-fpm8.2 -t   # 或 php-fpm -t,看实际路径
mysqld --validate-config 2>&1; echo "exit=$?"

nginx -t 通过不等于线上能跑——比如 upstream 里的域名解析失败它不报错,但 reload 后整个 upstream 变空。所以配完一定要 curl -sI 探一下。PHP 那边 -t 只检查 php.ini 和 pool 配置的语法,不检查扩展是否加载成功。

3.4 检查开机自启项

很多人踩过这个坑:服务本来手动 systemctl start 起来的,根本没 enable,一重启就永久消失,事后还要怀疑是不是被黑了。列一遍:

systemctl list-unit-files --state=enabled --type=service
# 反查:哪些正在跑但没 enable
comm -23 <(systemctl list-units --type=service --state=running --no-legend | awk '{print $1}' | sort) \
         <(systemctl list-unit-files --state=enabled --type=service --no-legend | awk '{print $1}' | sort)

第二条命令的 comm -23 输出「正在运行但未设自启」的服务。我在这台机器上就是这么发现 nginx 没 enable 的——因为迁移的时候是手工起的,忘了。

3.5 记下当前状态快照

重启后要对比「哪里不一样」,就得先有基线。三份快照,成本不到一秒:

ss -lntup > /root/reboot-before.ss   # 监听端口
systemctl list-units --state=running --no-pager > /root/reboot-before.units
free -m > /root/reboot-before.mem; uptime > /root/reboot-before.uptime

重启后 diff 一下,少了哪个端口、少了哪个服务一目了然。这比凭记忆猜效率高太多,尤其是机器上跑了十来个自建服务的时候。

3.6 留一条后路

云主机重启失败的概率不高,但 Docker 起来卡在 overlay2 挂载、fstab 挂错 NFS 卡住、内核 panic 这些事我都真见过。三条后路:

  • 服务商面板上的 VNC/控制台 确认可用(不是「有按钮」就行,点进去看看能不能出登录提示);
  • 如果有快照功能,重启前打个快照,成本几毛钱;
  • 保留一个已经登录的 SSH 会话不要关——如果 sshd 起不来,老会话还能救。

四、重启时的顺序:从下层到上层

系统重启的顺序由 systemd 管,我们能控制的是应用层启动的先后与依赖。正确顺序是:网络 → 磁盘/挂载 → 数据库 → 缓存 → 应用(FPM/PHP)→ Web 服务器。但系统默认并行启动,FPM 可能在 MySQL 之前就位。

4.1 用 Requires/After 约束依赖

最干净的做法是让 FPM 依赖 mysqld。只加 After= 不够(只排顺序,不保证起没起成功),要加 Requires=:

# /etc/systemd/system/php8.2-fpm.service.d/after-db.conf
[Unit]
After=network-online.target mysql.service
Wants=network-online.target
Requires=mysql.service

然后 systemctl daemon-reload。注意副作用:加了 Requires= 后,如果你手动 systemctl stop mysql,FPM 也会被一起停掉。这就是你想要的效果(网站本来就该挂),但要知道有这回事。

不过我必须提醒一句:不要为了这个小收益去搞一堆 systemd 依赖。我见过有人给每个服务都加 Requires=,结果一次 docker 起不来直接连带把 nginx 也拖住,整机服务全挂。依赖只加在真正必要的链路上。

4.2 开机脚本里的等待逻辑

如果不想动 systemd,就在 FPM 前面加一个「等数据库可用」的探针。最稳的写法是直接用 mysqladmin ping 循环:

#!/bin/bash
# /usr/local/bin/wait-for-mysql.sh
for i in $(seq 1 30); do
  if mysqladmin ping --silent 2>/dev/null; then
    sleep 2   # ping 通了还要等 InnoDB 恢复完
    exit 0
  fi
  sleep 2
done
echo "MySQL 60 秒内未就绪" >&2
exit 1

注意 sleep 2:mysqladmin ping 返回 OK 只代表 socket 能连上,此时 InnoDB 可能还在 recovery,一上来打满并发照样卡。等两秒再做压测级的验证更保险。

五、重启后必做的五项验收

重启命令敲完就关终端,等于没做验收。这五项我每次都跑,用一个脚本串起来:

#!/bin/bash
# /root/post-reboot-check.sh  — 不要 set -e(grep -c 返 1 会中断)
echo "== 1 启动耗时"; systemd-analyze | head -1
echo "== 2 失败单元"; systemctl --failed --no-legend | wc -l
echo "== 3 端口差异"; diff <(sort /root/reboot-before.ss) <(ss -lntup | sort) | head -20
echo "== 4 首页"; curl -s -o /dev/null -w '%{http_code} %{time_total}\n' --max-time 10 http://127.0.0.1/
echo "== 5 错误日志"; journalctl -p err -b --no-pager | wc -l

5.1 启动耗时看 systemd-analyze

systemd-analyze
# Startup finished in 8.212s (firmware) + 1.340s (loader) + 4.102s (kernel) + 26.771s (userspace) = 40.427s

四段里,firmware 大是 BIOS/UEFI 的锅(云主机改不了),loader 大是 GRUB_TIMEOUT 的锅(改 /etc/default/grub 的 GRUB_TIMEOUT=1),kernel 大是 initramfs 里有 DKMS 模块,只有 userspace 大才是我们能优化的。想看是谁拖的,用 systemd-analyze blame;但 blame 只显示「自身耗时」,看不到等待依赖的时间,真正的瓶颈要用 systemd-analyze critical-chain。我遇到过 blame 里 udevd 显示 24 秒,实际它只是在等磁盘,优化 udevd 完全无效的例子。

5.2 失败单元

systemctl --failed 一定要看。重启后出现 failed 单元是最常见的「重启后遗症」,比如某个 timer 依赖的路径没挂载、某个 socket 被占。一条命令看全部:

systemctl --failed
journalctl -u 那个服务名 -b -n 50 --no-pager

5.3 先本地后公网

验证首页一定要先 curl 127.0.0.1,再 curl 公网域名。因为 CDN 可能一直在缓存旧页面,公网看起来 200 其实是缓存,源站可能根本没起来。绕开 CDN 的方式:

curl -s -o /dev/null -w '%{http_code}\n' --resolve www.example.com:443:127.0.0.1 https://www.example.com/

我这次事故里就是因为「公网打开正常」而误判已经好了——那五分钟里其实是 CDN 在扛着缓存,源站全 502。

5.4 数据库可写性验证

首页 200 不代表数据库正常。数据库只读可能是重启后的隐藏状态(InnoDB 崩溃恢复失败会进入 read-only,此时 SELECT 全对、INSERT 全挂,前台发文章才发现)。一行验证:

mysql -e "CREATE DATABASE IF NOT EXISTS _probe; DROP DATABASE _probe; SELECT @@read_only, @@super_read_only;"

或者更轻量:mysql -e "select @@read_only, @@super_read_only, @@innodb_force_recovery",两个都是 0、force_recovery 是 0 才算正常。

5.5 看错误日志的「新增部分」

重启后的错误日志只有本次启动的,所以 journalctl -p err -b 天然就是「这次启动以来」的。这个数字要和历史对比——如果你平时一次启动只有 2 条 err,这次出来 40 条,就是有问题。

六、三个反直觉的坑

6.1 uptime 归零不等于重启成功

有时候重启后发现 uptime 很短,但服务都正常,就以为成功了。真正要确认的是新内核有没有生效:

uname -r
ls -t /boot/vmlinuz-* | head -1     # 最新的内核文件
cat /var/run/reboot-required 2>/dev/null && echo "还需要重启"

Debian/Ubuntu 装完内核包会在 /var/run/reboot-required 留个标记。如果这个文件还在,说明还有待生效的更新(比如 libc、内核),重启一次不够。

6.2 重启次数比 uptime 天数更有信息量

一台显示 uptime 400 天的机器,不代表稳定,可能代表「从来没人维护过、内核补丁一年没打」。反过来,一台三天重启五次的机器要立刻查原因:

journalctl --list-boots | tail -10
last -x reboot | head -10

journalctl --list-boots 列出所有开机记录,如果发现某天有 5 条,说明有非计划重启(OOM、云厂商维护、硬件问题)。这种排查比看监控曲线有用得多。

6.3 重启后 OPcache 为空,首屏会慢一截

PHP 的 OPcache 在内存里,进程一重启就清空。做法有两个:一是部署后主动预热热门 URL(curl 打一圈,串行不要并发,否则反而打满 FPM);二是在 FPM 的 pool 里配 pm.max_requests 别设太小,避免频繁回收。OPcache 的 opcache.file_cache 可以让重启后从磁盘读字节码,比完全冷启动快很多,配置一行:

opcache.file_cache=/var/cache/php/opcache
opcache.file_cache_only=0       ; 0 = 内存优先,磁盘兜底
opcache.validate_timestamps=0   ; 部署时 reload,平时不检查

加上 file_cache 后,我这台机器的首屏 TTFB 从重启后冷启动的 780ms 降到 190ms 左右。

七、一次完整流程的实录

拿昨天这台机器的内核升级举例,完整流程与耗时:

  1. /root/pre-reboot.sh:检查长事务(无)、磁盘 61% 空间充足、nginx -t OK、发现 nginx 未 enable(顺手修了)、snapshot 已记——共约 2 分钟。
  2. 服务商面板打快照:40 秒(后台异步)。
  3. 保留一个 SSH 会话,另开窗口执行 systemctl reboot。
  4. 约 45 秒后 SSH 恢复;systemd-analyze 显示 userspace 31 秒(比之前多 5 秒,原因是 MySQL 恢复)。
  5. systemctl --failed 为空;端口对比少了 8080(PHP 调试端口,重启后没起,符合预期)。
  6. curl 127.0.0.1 200 / 78ms;--resolve 绕 CDN 200;mysql -e "select @@read_only" 为 0。
  7. journalctl -p err -b 3 条,全是 Docker 的无关警告,历史基线也是 2~3 条。
  8. 最后 uname -r 确认是新内核,reboot-required 文件消失。

整套 6 分钟。相比之前那次「重启后白屏 11 分钟」,这 6 分钟花得太值了。

八、写在最后

运维这件事的核心不是「会不会敲命令」,而是把不确定变成确定。重启前多花三分钟做检查,重启后多花三分钟做验收,看起来啰嗦,但它把一次「可能的故障」变成了「确定的正常」。

最后一句经验:重启的验收标准,是「你敢不敢再重启一次」。如果你对这台机器重启后的表现心里没底,说明你的检查清单还没建完。把它补上,下次就不会再半夜被 502 叫醒了。

Last modification:October 23rd, 2026 at 09:25 pm

Leave a Comment