屏幕卡死在那几行红底白字上——“Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)”。
说实话,那一刻脑子里闪过的是硬盘报废、数据全丢的念头。但干这行久了有个习惯:先拍照,再想办法。拍照不只是为了留证据,更是给自己一个冷静下来的仪式感。

那台机器是 Anolis 8.2,挂着几个广告联盟的刷量脚本,平时跑得挺稳。结果那天 SSH 直接断掉,机房同事拍了张屏幕照片发过来,我一眼就看到了那个经典的根分区挂载失败。行,至少不是硬件彻底烧了——内核 panic 这东西,十有八九是驱动或配置出了岔子,换块硬盘解决不了根本问题。

先把串口日志捞出来。好在之前配过 IPMI SOL,远程就能抓到完整的启动输出。没有这玩意儿的话,云厂商的 VNC 控制台也能凑合用,千万别直接重启,否则现场全丢。

红字背后藏着的其实是模块列表

日志里那段“unknown-block(0,0)”看着吓人,但真正值钱的是它前面那些寄存器值和调用栈。我遇到的这台机器,调用栈里明确指向了 nvme 驱动出了问题。Anolis 8.2 默认内核是 5.4 分支,官方 initramfs 打包了 nvme 模块,但之前手贱改了内核编译参数,把 CONFIG_NVME_CORE 设成了 m 却没有把它放进 initramfs 里。所以内核启动时根本找不到 nvme.ko,自然挂不上根分区。

[    6.123456] BUG: unable to handle kernel NULL pointer dereference at 0000000000000010
[    6.123456] IP: swapper+0x123456/0xabcdef [kernel]
[    6.123456] PID: 0  comm: swapper Tainted: G      OE        5.4.0-rc1 #123
[    6.123456] Call Trace:
[    6.123456]  <stack trace>
[    6.123456] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)

如果你仔细看这段输出,前面还有一段 NULL pointer dereference 的报错,这其实才是元凶。VFS 那行只是最终的结果,就像汽车引擎灯亮了,真正的问题是冷却液漏光了。所以别被 panic 吓住,要学会从调用栈里找线索。

判断是硬件还是软件也很简单。日志里出现 “Machine check exception” 或寄存器值全是乱码,优先怀疑内存条或者主板插槽;反之,如果前面还能看到 udev 正常加载设备,却在切换根分区时突然断崖式报错,那多半是驱动或文件系统的问题。

有了方向就好办了。先把完整的日志存下来,然后做一张救援 U 盘。

booting from USB rescue disk Linux

做盘有讲究,别拿 Ubuntu 去救 CentOS

很多人觉得只要是 Linux 发行版的 ISO 都能 rescue,其实大错特错。我见过有人拿 Ubuntu Desktop 20.04 去救 CentOS 7,chroot 进去 glibc 版本不对,连 yum 都跑不了。镜像的内核版本要尽量贴近原系统。手头是 Anolis 8.2,就找 Anolis 8 的 ISO;CentOS 7 就找 7.x 的 Minimal 或 DVD。版本差太远时,文件系统工具链和内核模块依赖容易出幺蛾子。

写盘用 dd 最稳。不用折腾什么 Rufus 的 DD 模式以外选项——dd if=Anolis-8.2-x86_64-dvd.iso of=/dev/sdb bs=4M status=progress,等它跑完。有些服务器 UEFI 只认 FAT32 的 EFI 分区,dd 写完会自动处理好,不用操心。

启动时进 BIOS 或 Boot Manager,选 U 盘引导。看到菜单后别选 “Install”,找 Troubleshooting → Rescue a CentOS system(Anolis 也类似)。它会扫描本地磁盘,问你要不要挂载根分区。选 1(Continue),系统会把原根分区挂到 /mnt/sysimage

接着执行 ,然后 mount -a 确认 fstab 里其他分区也挂上了。这时候你才算真正回到了坏掉的系统里。没有这一步,前面做的所有 dracut 操作都改的是 U 盘上的临时环境,白忙活。

有个坑:如果原系统用了 LVM 或软 RAID,救援模式可能没自动激活卷组。手动跑一遍 ,再用 lvscan 确认逻辑卷状态。这是最常见的翻车点——好多人 chroot 进去发现 / 下空空如也,其实是 LVM 没激活。

GRUB and dracut repair Linux boot

dracut 不配合?那是模块没配全

进了 chroot 环境后,先看 /boot 目录还在不在。我那次发现 vmlinuz 和 initrd 的符号链接全红着指向不存在的文件,这意味着内核包本身都坏了。没办法,只能从安装源里捞出对应版本的内核包重新安装。Anolis 用 dnf reinstall kernel-core-\*,CentOS 则是 yum reinstall kernel-\*

重建 initramfs 之前,先把缺失的模块补齐。在 /etc/initramfs/dracut.conf.d/local.conf 里加上 add_drivers+=" nvme ",然后跑 。注意版本号一定要和当前内核一致,否则重建出来的镜像根本不会生效。

这里有个容易忽略的地方:如果服务器用了软 RAID 阵列,就得给 dracut 加上 --force 参数让它忽略模块校验错误。更隐蔽的是加密卷挂载顺序问题——如果 lvm late activation 脚本被精简掉了,后期加硬盘会发现根本认不出物理卷来。这时候只能回滚到旧版 dracut 配置文件逐个排查哪些模块该留着。

我那次重建了两三次才搞定,每次重启都提心吊胆盯着屏幕。最后看到正常的登录提示时,心里那块石头才算落地。

刷量脚本的复活比修内核更麻烦

系统能启动了,但这只是开始。我那台机器上挂着几个广告联盟的刷量脚本,内核崩了之后,crontab 任务大概率已经跟着 systemd 时间线一起归零了。别指望它还在——crontab -l 打出来如果是空的,别慌。

先检查 /var/spool/cron/ 目录下有没有备份。我之前习惯在 /root/backup/crontab.bak 留一份,但这次发现连 /home 分区都没挂上。所以说,刷量脚本的定时任务最好单独写进文件存到 U 盘里,别只靠 crontab 自己的存储。

找不到备份的话,只能凭记忆重建。我的脚本是每 15 分钟跑一次,用 Python 调 Xvfb 虚拟显示来模拟浏览器操作。这里有个坑:内核修复后,Python 环境可能缺包。比如 seleniumpyautogui 这些,得重新 pip install 一遍。Anolis 的默认 Python 是 3.8,但有些第三方库已经不兼容了——我干脆切到 python3.11 单独建了个虚拟环境。

# 重建虚拟环境
python3.11 -m venv /opt/ads_env
source /opt/ads_env/bin/activate
pip install selenium==4.15.0 pyvirtualdisplay pyautogui

Xvfb 也得配。内核崩之前我用的 display :99,这次直接写成服务省事。写个 systemd unit 文件扔进 /etc/systemd/system/xvfb.service

[Unit]
Description=X Virtual Frame Buffer
After=network.target

[Service]
ExecStart=/usr/bin/Xvfb :99 -screen 0 1280x1024x24
Restart=always
User=nobody

[Install]
WantedBy=multi-user.target

然后 systemctl daemon-reload && systemctl start xvfb 启动起来。这时候别急着把 crontab 写回去——先手动跑一次脚本看看能不能调通。我那次就发现 Xvfb 虽然起来了,但 DISPLAY 环境变量没传进 cron 任务里,导致脚本报错。解决办法是在 crontab 里加一行 DISPLAY=:99

*/15 * * * * DISPLAY=:99 /opt/ads_env/bin/python /opt/ads_script/run.py

另外,我建议把脚本做成 systemd 服务而非 crontab。因为 systemd 能监控进程状态,挂了自动拉起来。写个 /etc/systemd/system/ads_runner.service

[Unit]
Description=Ad Script Runner
After=network.target xvfb.service

[Service]
Type=simple
Environment=DISPLAY=:99
Environment=PYTHONPATH=/opt/ads_env/lib/python3.11/site-packages
ExecStart=/opt/ads_env/bin/python /opt/ads_script/run.py
Restart=always
RestartSec=30

[Install]
WantedBy=multi-user.target

,然后 盯着看几分钟。我那次等了大概三分钟,看到日志里出现 ,才算放心。

预防再崩:把地基打牢比什么都重要

这次折腾让我意识到一件事:刷量脚本本身不值几个钱,值钱的是整个运维链路——从内核引导到依赖管理到进程守护,哪一环断了都是白干。

首先把内核钉死在当前版本。CentOS 系可以装 yum-plugin-versionlock,Anolis 则用 dnf versionlock,先把 current 版本锁住,再加一条 exclude=kernel* 到配置文件,确保连带 kernel-devel 也不会被偷偷换掉。做完别忘了去 grub 里确认一下 default 是不是仍指向我们刚救回来的那个 initramfs 镜像。

其次给脚本套上双重保险。光靠 systemd 的 Restart=always 还不够,万一 OOM 把整个容器干趴,它根本来不及反应。我在 run.py 最外层包了个 while True: try: ... except: time.sleep(5) 的壳子,同时把 systemd 的 RestartSec 改成 30,这样进程自己挂了能秒起,机器真扛不住了也能在下次自愈。

黑匣子最后得扔到远端去。本地日志在崩溃那一瞬间经常被截断,想复盘根本没地方找。我在 rsyslog 里塞了一行 local7.* @@192.168.1.100:514,把所有 boot 和 kernel 消息实时推到另一台日志服务器;那边再用 logrotate 按天切割加 gzip,保留整整 30 份副本。下次再撞上 VFS: Unable to mount root fs on unknown-block(0,0) 这种级别的报错,直接翻远端日志就能定位,不用再对着半截日志干瞪眼。

系统总算从 Panic 里活过来了,但谁知道下次什么时候又会突然抽风。与其天天提心吊胆,不如趁现在把地基打牢,省得半夜还得跑机房拔 U 盘。