背景说明
当服务器遇到 Linux 内核崩溃时,能否快速定位原因至关重要。目前,主流的 Linux 发行版默认都会开启 kdump.service 服务。它的工作原理是,在内核崩溃的瞬间,利用 kexec 机制快速拉起一个保留在内存中的第二个内核。这个“救援内核”会负责收集并转储崩溃时的日志信息,也就是我们常说的 vmcore 等文件。
这套机制需要服务器硬件的支持,好在如今常见的服务器基本都已具备此能力。在默认配置下,崩溃日志会以本地磁盘(disk)方式保存。对于 CentOS/Red Hat 系列的系统,默认保存路径是 /var/crash/。
需要了解的是,默认生成的 vmcore 文件并非完整的内存镜像,为了节省磁盘空间和传输时间,makedumpfile 工具默认使用级别 31(-d 31),只导出内核态的数据。因此,vmcore 文件的大小主要取决于崩溃时内核占用的内存量,通常不会特别巨大。下面,我们就来详细介绍如何收集并初步分析这些 vmcore 文件。
获取 vmcore
当我们需要在主机上分析 vmcore 文件时,一个关键前提是安装对应内核版本的 debuginfo 包,以获取内核函数的符号信息。举例来说:
kernel-3.10.0-957.27.2.el7.x86_64 # 系统运行的内核版本
kernel-debuginfo-3.10.0-957.27.2.el7.x86_64 # 需要安装的对应 debuginfo 包
通常,我们有两种方式来快速分析 vmcore:
- 在出问题的机器上直接安装对应版本的
debuginfo 包。
- 将
vmcore 文件传输到另一台专门用于分析的主机。
无论哪种方式,分析主机都需要安装 crash 工具。第一种方式虽然快捷,但会改动线上服务器的环境,且不适合大规模部署。第二种方式传输需要时间,但非常适合对线上故障进行统一的汇总和深入分析,因此也是更常见的做法。
我们通常采用第二种方式。你可以修改 kdump 配置,使其通过 NFS 或 SSH 将崩溃日志直接传送到指定机器,也可以在问题主机重启后手动传送。这里要注意,传输速度会直接影响服务器恢复正常的时间,因为日志传送完成后,系统才会开始重启。
分析 vmcore
安装 debuginfo 包的核心目的是获取对应内核的符号文件(vmlinux)。因此,我们可以在专门的分析机上预先收集好业务所用各版本内核的 debuginfo 包和内核源码。建议的目录结构如下:
/data/
├── kernel-crash # 存放从各主机收集来的 vmcore 文件
├── kernel-debuginfo # 解压后的各版本 debuginfo 文件(包含 vmlinux)
├── kernel-package # 存放下载的 kernel 及 debuginfo 的 rpm 包
└── kernel-source # 对应线上内核版本的源码文件,便于查阅
你可以使用 rpm2cpio 命令来解压不同版本的 rpm 包,获取所需文件。操作示例如下:
mkdir /data/kernel-debuginfo-3.10.0-957.21.3
pushd /data/kernel-debuginfo-3.10.0-957.21.3
rpm2cpio /data/kernel-package/kernel-debuginfo-3.10.0-957.21.3.el7.src.rpm | cpio -div
popd
最终,kernel-debuginfo 和 kernel-source 目录下的结构可能如下:
/data/kernel-debuginfo/
├── kernel-debuginfo-2.6.32-642.13.1.el6.x86_64
├── kernel-debuginfo-3.10.0-862.14.4.el7.x86_64
├── kernel-debuginfo-3.10.0-957.21.3.el7.x86_64
└── kernel-debuginfo-3.10.0-957.27.2.el7.x86_64
/data/kernel-source/
├── linux-2.6.32-642.13.1.el6
├── linux-3.10.0-862.14.4.el7
├── linux-3.10.0-957.21.3.el7
├── linux-3.10.0-957.27.2.el7
准备工作就绪后,使用 crash 命令,并指定对应内核版本的 vmlinux 文件和收集到的 vmcore 文件,就可以开始分析了:
# crash /data/kernel-debuginfo/kernel-debuginfo-3.10.0-957.21.3.el7.x86_64/usr/lib/debug/lib/modules/3.10.0-957.21.3.el7.x86_64/vmlinux vmcore
crash 7.2.3-10.el7
......
KERNEL: /export/kernel-debuginfo/kernel-debuginfo-3.10.0-957.21.3.el7.x86_64/usr/lib/debug/lib/modules/3.10.0-957.21.3.el7.x86_64/vmlinux
DUMPFILE: vmcore [PARTIAL DUMP] # 注意此处提示为部分转储
CPUS: 40
...
RELEASE: 3.10.0-957.21.3.el7.x86_64
...
PANIC: "BUG: unable to handle kernel NULL pointer dereference at (null)"
PID: 167966
COMMAND: "java"
TASK: ffff880103d74500 [THREAD_INFO: ffff880013c68000]
CPU: 11
STATE: TASK_RUNNING (PANIC)
crash> bt
PID: 167966 TASK: ffff880103d74500 CPU: 11 COMMAND: "java"
#0 [ffff880013c6ba38] machine_kexec at ffffffff81051beb
#1 [ffff880013c6ba98] crash_kexec at ffffffff810f2782
#2 [ffff880013c6bb68] oops_end at ffffffff8163ea48
#3 [ffff880013c6bb90] no_context at ffffffff8162eb28
#4 [ffff880013c6bbe0] __bad_area_nosemaphore at ffffffff8162ebbe
#5 [ffff880013c6bc28] bad_area_nosemaphore at ffffffff8162ed28
#6 [ffff880013c6bc38] __do_page_fault at ffffffff8164184e
#7 [ffff880013c6bc98] do_page_fault at ffffffff816419e3
#8 [ffff880013c6bcc0] page_fault at ffffffff8163dc48
[exception RIP: unknown or invalid address]
RIP: 0000000000000000 RSP: ffff880013c6bd78 RFLAGS: 00010282
RAX: ffff880103d74500 RBX: ffff880013c6be10 RCX: ffff880013c6bfd8
RDX: 0000000000000000 RSI: 0000000000000000 RDI: ffff880103d74500
RBP: 0000000000000000 R8: ffff880013c68000 R9: 0000000000000018
R10: 0000000000000000 R11: 0000000000000001 R12: 0000000000000001
R13: 00007f7e88012454 R14: ffffc9001ce8efc0 R15: ffff880013c6bd60
ORIG_RAX: ffffffffffffffff CS: 0010 SS: 0018
......
crash>
通过 bt(backtrace)命令,我们可以看到内核崩溃时的调用栈。上例清晰地显示了一个由 Java 进程引发的空指针解引用(NULL pointer dereference)错误。要掌握更多的调试命令和技巧,可以参考 crash 官方文档。
分析 vmcore 的目的
内核崩溃时,会将内核缓冲区(kernel buffer)的信息写入 vmcore-dmesg.txt 文件,但缺乏符号信息通常让我们难以深究。而 vmcore 文件虽然提供了汇编和代码级别的信息,但它也只是部分转储,分析结果不一定能揭示全貌。通常需要结合这两个文件一起查看。
然而在实际运维中,分析 vmcore 存在一定局限性。很多时候我们难以 pinpoint 问题的根本原因,即便找到了,也可能因为稳定性、兼容性等原因无法立即升级内核。因此,对于每一次内核崩溃,我们分析 vmcore 的主要目的可以归纳为以下几点:
- 初步定因:明确内核崩溃的大致原因和类型(如空指针、内存越界等)。
- 深入挖掘:在时间允许的情况下,对崩溃原因进行更细致的代码级分析。
- 事件归类:将故障事件按照代码调用栈或错误类型进行分类和归档,积累知识库。
- 辅助外援:如果购买了原厂或第三方技术支持服务,提供详尽的
vmcore 分析结果能帮助他们更快定位问题。
线上处理注意事项
kdump 配置问题
如果内核崩溃的频率不高,可以保持 kdump 的默认配置,将日志存储在本地 /var/crash 目录:
# grep ^path /etc/kdump.conf
path /var/crash
如果崩溃发生频率较高,可以考虑配置 ssh 选项,将崩溃日志直接发送到内网指定的收集机器。这里要特别注意内网传输速度,速度越慢,文件传送耗时越长,机器恢复正常服务的时间也就越长。配置 ssh 时需注意以下几点:
# /etc/kdump.conf
path /var/crash
ssh kerenl-crash@collect-host
default dump_to_rootfs # 如果 ssh 失败,则降级转储到本地磁盘
core_collector makedumpfile -l --message-level 1 -d 31 -F # 启用 ssh 时需增加 -F 选项,使导出到远程的数据为 flattened 格式
启用 ssh 后,必须在 makedumpfile 命令中增加 -F 选项,数据会以 flattened 格式存储。如果 ssh 传输失败,则会以降级方式在本地存储为标准格式。远程收集主机收到的是以 .flat 为后缀的文件,可以通过 -R 选项将其转换回标准格式:
makedumpfile -R vmcore < vmcore.flat
快速查看原因
当需要快速了解崩溃原因以做出决策时,可以优先查看崩溃主机(如果已重启成功)生成的 vmcore-dmesg.txt 文件。这个文件包含了内核崩溃时的堆栈信息,能帮助我们快速把握问题轮廓。该文件通常位于如下路径:
/var/crash/127.0.0.1-2019-11-11-08:40:08/vmcore-dmesg.txt
加快 vmcore 分析
vmcore 文件的大小由 kdump 的转储级别(默认为 31,仅内核态数据)决定,通常小于系统 Slab 内存占用量(可通过 /proc/meminfo 查看)。为了加快分析速度,如果传输较慢,可以考虑将 vmcore 文件拉到内网预置好的分析机上。这台分析机需要提前准备好各版本内核的 debuginfo 文件,无需通过 rpm 安装,用 rpm2cpio 解压存放到指定目录即可。
备注:使用 crash 分析 vmcore 时,常会看到 [PARTIAL DUMP] 提示,这正表明了当前分析的是部分转储的数据,是正常现象。
处理反馈及建议
在分析内核崩溃时,建议遵循以下流程,以便快速向业务方反馈:
- 快速响应:首先查看
vmcore-dmesg.txt,了解故障大致原因。
- 经验复用:如果曾处理过类似问题,直接参考既有知识库方案。
- 深入分析:如果是新问题,则需分析
vmcore,并在约定时间内给出初步处理建议。
- 知识沉淀:反馈后,再对根本原因和处理方式进行深入分析,并汇总到知识库中。
处理复杂的系统级故障是提升运维团队能力的绝佳途径。如果你在分析 Linux 内核崩溃或其它运维难题时有独到心得,欢迎到 云栈社区 分享你的经验,与更多同行交流探讨。