生产环境的服务器突然崩了,最头疼的不是重启本身,而是你不知道它为什么崩。Prometheus 告警说业务接口延迟飙到 15 秒,紧接着 ssh 直接断连——不是卡,是彻底连不上。我打开云控制台,实例状态那一栏明晃晃写着“运行中”,但 VNC 进去之后,屏幕底部一行白字扎眼得很:。脑子很清醒:这不是硬件坏了,是内核在自杀。

重启是条件反射。我点了强制重启,结果系统卡在 GRUB 菜单死活不进内核。倒计时走完,手动选了最新内核,屏幕闪了几下又回到 GRUB——引导失败了。这不是简单的“重启大法好”能解决的问题,内核在加载阶段就崩了,连根文件系统都没挂上。

排查第一步,先把明显能跑的干扰项划掉。安全组没动过,网络 ACL 也正常,VNC 能连但就是卡在登录前——控制台的 CPU 和内存曲线在那段时间甚至看不出任何异常,没有跳崖,没有拉满。既然硬件层面排除了,那方向就窄了。要么是内核文件本身在更新时被截断了(比如断电那一下正好打在 grub 写文件的时间窗里),要么是某个驱动或者模块在 init 阶段直接炸了,触发 panic 后连回退的机会都没有。这已经不是普通故障排查的范畴了,dmesg 和 journalctl 根本看不到那一瞬间发生了什么——因为系统在写完日志之前就已经彻底死透了。

那接下来就得进急救模式,想办法把崩溃的内存快照捞出来,才有机会定位到底是谁捅了马蜂窝。

先让机器学会“死前留遗言”

光看启动失败的表象没用,必须把内核崩掉那一瞬间的内存镜像抓到手。否则就算重装,下次还可能在同样的调用链上翻车。

第一步先看机器有没有预留“抓包内存”。执行 grep -c "crashkernel" /proc/cmdline,返回空空如也。也就是说,kdump 根本没开,出事也不会留下 vmcore。那就自己动手配。

先装工具:(CentOS/Anolis 都类似)。随后编辑 /etc/default/grub,在 行加上 crashkernel=256M。别急着完事,记得重新生成引导:(若是 UEFI 机型,路径换成 /boot/efi/EFI/anolis/grub.cfg),然后 reboot 让新参数生效。

重启回来,再次 grep -c "crashkernel" /proc/cmdline 确认参数已写入。接下来可选调优:默认 vmcore 保存到 /var/crash/,若磁盘紧张可以改 /etc/kdump.conf 指定别的目录;我个人偏好把 vmcore 压缩算法改成 lzo,牺牲一点点还原速度换取更小的 I/O 尖峰。

配置到底行不行?总不能等真正宕机才验证。一条 echo c > /proc/sysrq-trigger 就能让内核自爆。敲下去之前务必确保没人正在跑重要任务——瞬间整机 hang 住,屏幕刷满 Oops,几秒后自动重启。登录查看 /var/crash/,若出现以当前时间戳命名的文件夹,里面躺着 vmcore,那就证明 capture 机制活了。

Server kernel panic on boot GRUB screen

从 vmcore 里撬出凶手

重启回来,/var/crash/ 目录下果然躺着带着时间戳的文件夹——进去了,一个 vmcore 文件,大小将近两个GB。这东西不是文本,别想着用 cat 或者 less 去读,那是内存的原始镜像,必须搭配内核调试符号文件才能撬开。

所以第一步不是装 crash,而是找到跟当前内核版本完全一致的 包。这东西在 CentOS 的 debuginfo 仓库里,命令类似:yum install kernel-debuginfo-$(uname -r)。装完后会在 /usr/lib/debug/lib/modules/$(uname -r)/ 下生成一个 vmlinux 文件,这个就是带完整符号表的调试内核镜像。版本对不上,crash 会直接拒绝加载,告诉你符号不匹配——别问我怎么知道的。

有了 vmlinuxvmcore,就可以进场了:

crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux /var/crash/20260301-020917/vmcore

进去之后先别急着看调用栈——我习惯第一个命令是 log。它会倒出内核环形缓冲区里最后几百条日志,也就是内核在 panic 之前吐出来的临终遗言。从中能看到什么?内存分配失败、段错误、或者某个驱动模块的异常信息。那次排查,我一眼看到一行:Out of memory: Killed process 12345 (mysqld),紧跟着又是一条 page allocation failure: order:4, mode:0x100cca(GFP_HIGHUSER_MOVABLE)。OOM Killer 把 MySQL 干掉了,但系统还在尝试分配高阶内存,说明内存早就见底了。

但光知道 OOM 还不够——是谁在疯狂吃内存?bt 命令回溯当前 CPU 的调用栈,能直接看到 panic 时正在执行的函数链。那次输出的栈顶是 __alloc_pages_slowpath+0x...,再往下翻,看到一个熟悉的名字:my_driver_ioctl+0x...,来自一个第三方存储加速模块 my_accel.ko。这模块在 ioctl 里反复申请大块 DMA 缓冲区,但释放逻辑有个分支没走到,导致内存只借不还。

为了确认不是误判,再用 ps 看进程状态——不只是看活着的进程,ps 在 crash 里能显示所有进程的 task_struct,包括被杀死但还没回收的僵尸。那次确实看到 mysqld 的状态是 DEAD,而 my_accel.ko 对应的用户态守护进程居然还挂着 D 状态(不可中断睡眠),说明它正卡在某个内核路径上,占着内存不放。

接下来要量化泄漏量。用 struct task_struct <pid> 查看具体进程的内存描述符,配合 rd 读取对应物理地址上的内存内容,能确认模块申请的那些缓冲区是否真的没有回收到伙伴系统。那次我用 vm <pid> 列出了模块进程的所有虚拟内存区域,发现好几块标记为 VM_IO | VM_PFNMAP 的区域,大小加起来超过 4GB,而系统总内存才 8GB。这些区域在模块卸载后依然存在——这模块的退出函数根本没调用 dma_free_coherent()

看到这一步,根因已经不用猜了。所有线索都指向那个 my_accel.ko 驱动在特定 I/O 场景下漏了 DMA 缓冲区。剩下的工作就是把这个模块标记为黑名单,先让系统跑起来,再跟厂商要修复固件。

回头想想,如果当时跳过 vmcore 直接重装,就算换成最新内核,下次遇到同样 I/O 压力还是得崩。能拿 crash 把证据一条条从内存快照里撬出来,这种感觉比盲猜踏实多了——证据链闭合的,你甚至能想象出那个模块代码里忘记释放的 kmalloc 在哪个括号后面。

Configure kdump crashkernel memory reservation

从 GRUB 手里把系统抢回来

根因锁定了,模块也黑名单了——但服务器还是起不来。因为 panic 发生在 initramfs 阶段之后、rootfs 挂载之前,那个 my_accel.ko 已经被加载进内存,而它的 DMA 泄漏直接干垮了伙伴系统。重启后卡在 GRUB 菜单不动,或者报 Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0),说明 initramfs 里缺了关键驱动,或者 GRUB 加载的内核参数把 crashkernel 内存又占了一遍。

挂进去,先保命再动手

用同版本镜像拉起一台急救实例(Anolis OS 8.8 / CentOS Stream 9),把故障盘作为数据盘挂载。先 mkdir /mnt/rescue && mount /dev/vdb1 /mnt/rescue,然后手动检查 /mnt/rescue/boot/grub2/grub.cfg 是否存在、/mnt/rescue/lib/modules/$(uname -r) 下有没有 my_accel.ko。那次发现模块文件还在,但 lsinitrd /mnt/rescue/boot/initramfs-$(uname -r).img | grep my_accel 返回空,说明 dracut 构建时跳过了它(因为黑名单生效太晚)。

重建 initramfs:别信默认配置

进去后第一件事不是重装内核,而是删掉旧 initramfs:rm -f /boot/initramfs-$(uname -r).img。然后强制重建:。注意加 --regenerate-all,否则它只更新当前运行中的内核镜像——而你此刻跑的是急救系统的内核,不是故障盘里的那个。重建完立刻验证:lsinitrd /boot/initramfs-5.14.0-284.30.1.el9_2.x86_64.img | grep -E "(ahci|nvme|my_accel)",确保存储控制器和你留下的兼容驱动都在。

GRUB 不是刷个配置就完事

执行完,还得看生成的内容:grep "crashkernel=" /boot/grub2/grub.cfg。那次发现新 cfg 里自动加了 crashkernel=auto,可物理内存只有 8GB,auto 会算成 768M,挤占了本就不宽裕的 lowmem——得手动改回 crashkernel=256M@64M。最后确认默认启动项:,再 看是否生效。重启前,我习惯在 /etc/default/grub 里把 rd.debugrd.shell 加上,真崩了还能敲 shell 抓第一手日志。

Crash tool analyzing vmcore log and backtrace

重启之后,别急着撤

重启前那杯咖啡已经凉透了,但我还是盯着终端输出,等那个熟悉的「Welcome to」出现才敢喘口气。

SSH连上后第一件事不是看业务进程——先查/var/crash/下有没有新目录。那次看到的文件夹躺在那里,心里的石头才算落了地。里面有vmcore文件,大小2.3GB,说明kdump确实在最后一刻抓到了现场。

然后才去检查业务。MySQL连上,Nginx status返回200,PHP-FPM的进程数正常——但别高兴太早,重启能跑不等于问题根因已经解决。

把kdump焊死在启动参数里

很多人修完系统就撤了,下次崩了又从头查,我吃过这种亏。

永久配置其实就两行的事:

# 在 /etc/default/grub 的 GRUB_CMDLINE_LINUX 里追加 crashkernel=256M@64M
# 然后更新GRUB并重启
grub2-mkconfig -o /boot/grub2/grub.cfg
systemctl enable kdump

注意那个crashkernel=256M@64M,别偷懒写成auto。我见过8GB内存的机器,auto自动算了768M,结果低端内存被挤压到OOM——这不是段子,是真事。

后来我每个月会挑一台非核心机器,手动触发一次echo c > /proc/sysrq-trigger,看kdump能不能正确生成vmcore、重启后能不能自动拉回业务。听起来有点自虐,但下次真崩的时候,你会感谢那个提前自虐的自己。

毕竟,内核崩溃这事,遇上一次就够呛了。下次?最好没有下次。