1. 前言
我们希望在 guest panic 时自动打 coredump,但是现有 coredump 文件太大,与当前 VM 的规格相同,希望通过 minidump 技术减小 vmcore 文件的大小。
2. 前期分析
2.1 现有工具的 minidump 调研
kdump
kdump 是提供崩溃转储机制的服务。该服务可以保存系统内存内容进行分析。
kdump 使用 kexec 系统调用在不重启的情况下引导至第二个内核,然后捕获崩溃内核的内存的内容将其保存到文件中。这个第二个内核位于系统内存保留的一部分。
virsh dump
virsh dump是 libvirt的一个coredump 工具。virsh 会向 libvirt 发送 API 请求,libvirt 调用QEMU执行dump-guest-memory 命令开始实际的内存拷贝工作。
libvirt 通过调用 makedumpfile工具智能分析 vmcore 文件,实现过滤和压缩 vmcore 文件。
使用 virsh dump创建 guest 虚拟机内核的转储文件
2.2 文件大小控制策略
内存过滤
makedumpfile使用 5 个 bit 位来设置过滤 5 种内存类型:
| 选项 | 描述 |
|---|---|
| 1(0x00001) | 零页 |
| 2(0x00010) | 缓存页 |
| 4(0x00100) | 缓存私有页 |
| 8(0x01000) | 用户页 |
| 16(0x10000) | 可用页 |
文件压缩
virtio dump 提供了多种压缩格式:
您可以使用
--format=格式选项保存只内存转储。可用的格式如下:
ELF- 默认未压缩格式
kdump-zlib- kdump 压缩格式 zlib 压缩
kdump-lzo- kdump 压缩格式使用 LZO 压缩
kdump-snappy- kdump 压缩格式使用 Snappy 压缩
使用压缩后只能使用 crash工具分析(默认格式),要使用 GDB 分析需指定格式为 ELF。
2.3 makedumpfile 原理
上述两个工具对 vmcore 文件减小文件体积的处理都是使用makedumpfile,这里简单介绍一下原理。
a. 读取 coredump 和 vmlinux 数据
makedumpfile 首先解析coredump文件头,了解内存布局。
makedumpfile 解析 vmlinux 的内核符号地址(例如内核代码段地址/全局变量地址),数据结构定义(struct page/pg_data_t/zone/mem_map)以及内核配置(PAGE_SIZE/MAX_PHYSMEM_BITS)
b. 建立内存位图
初始化位图:开始时,位图被初始化,标记所有内存页为“待移除”。
标记基础保留页,其中包括 kernel 的代码段/静态数据段/栈段/dmesg buffer/其他kernel 数据结构
c. 遍历和过滤内存页
根据用户指定的转储级别 (-d level) 来决定过滤策略。转储级别是一个 4 bit 位的整数,其中每一位都代表一种类型的页面是否要被排除。
这里分为几个部分:
-
识别空闲页 (Zero Pages):这是最基础的过滤。
makedumpfile会逐页读取vmcore源,如果一个页面的所有字节都是零,就在位图中将该页标记为“移除”。这是-d 1的功能。 -
遍历页帧数组 (
mem_map):为了进行更高级的过滤,makedumpfile必须模拟内核的page管理机制。它会定位到内核的mem_map数组(在较新的内核中,这是通过pg_data_t结构体链表来管理的)。这个数组包含了描述每一个物理页帧状态的struct page结构体。 -
分析
struct page:通过检查每个struct page结构体的标志位 (flags) 和引用计数 (_refcount),makedumpfile可以判断出这个页的类型:- 缓存页 (Cache Pages):如果页面被标记为在
radix tree或xarray中用于文件缓存,并且没有被进程映射(page->mapping != NULL且page->_refcount符合特定条件),则可以被标记为“移除”。这是-d 2的功能。 - 私有缓存页 (Cache Private):同上,但包含了一些私有的、可能非共享的缓存页。这是
-d 4的功能。 - 用户态数据页 (User Data Pages):如果页面被识别为属于某个用户进程的私有数据(例如,通过反向页表映射
rmap信息判断),则可以被标记为“移除”。这是-d 8的功能。 - 空闲页 (Free Pages):通过分析伙伴系统(Buddy Allocator)数据结构,可以直接识别出哪些页面是空闲的,并将它们标记为“移除”。这是
-d 16的功能。
- 缓存页 (Cache Pages):如果页面被标记为在
d. 压缩文件
此时 makedumpfile知道最终输出文件应该包含哪些页,接下来就是生成最终的 vmcore 文件。
-
读取-过滤-压缩-写入:
makedumpfile会再次从头读取原始的vmcore源文件。- 对于每一个页,它会查询之前建立的内存位图。
- 如果位图显示该页需要“保留”就将该页的数据读入缓冲区。
- 如果位图显示该页需要“移除”就直接跳过不读取该页。
- 收集到一定数量的“保留”页(形成一个数据块,Block)后,调用指定的压缩库(如 zlib, lzo, snappy)对这个数据块进行压缩。
- 将压缩后的数据块写入到新的输出文件中。
-
生成新文件头:输出的文件也是有特定格式的。
makedumpfile会生成一个新的文件头(可以是kdump的压缩格式头,也可以是ELF格式头),这个头中包含了如何解压和重构内存布局的元数据。分析工具(如crash)读取这个文件时,会首先解析头部,然后按需解压数据块来还原出分析所需的内存内容。
3. 使用条件
3.1 kernel 符号信息
生成 minidump 需要符号表信息,需要为 vmlinux 开启 CONFIG_DEBUG_INFO 配置,这会导致 vmlinux 文件大小从 50M 增加到 200 M。但 minidump 对 vmlinux 不是强依赖,可以使用 vmliux 生成一份 vmcoreinfo,文件大小为 2K。
部署时可继续使用精简的 vmlinux,同时额外构建一个带 debug_info 的匹配版本,将其导出 minidump 要用的 vmcoreinfo 信息:
makedumpfile -g vmcoreinfo -x vmlinux
3.2 磁盘占用
生成 minidump 需要先生成完整的 coredump,原始的 coredump 文件大小 > VM 内存规格,在 minidump 生成结束前磁盘瞬时占用会较大,minidump 生成成功后会删除完整的 coredump 文件。
4. 测试
4.1 性能测试:minidump 速度和压缩大小测试
guest 启动后,在内核态和用户态分别申请 1/3 内存并写入非 0 随机值
guest 执行:
root@ubuntu-fc-uvm:~# insmod mem_occupier.ko # 分配1/3的内核态内存
root@ubuntu-fc-uvm:~# ./user_memset & # 分配1/3的用户态内存
root@ubuntu-fc-uvm:~#
不同规格下的测试
| 规格 | 过滤页 | 不压缩 | snappy | zstd | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| 文件大小 | 压缩率 | 耗时 | 文件大小 | 压缩率 | 耗时 | 文件大小 | 压缩率 | 耗时 | ||
| 2C1G<br/><br/>原始 vmcore 文件1.1G | 零页 | 780M | 29.09% | 0.613s | 704M | 36% | 0.815s | 690M | 37.27% | 1.719s |
| 缓存页 | 999M | 9.18% | 0.672s | 699M | 36.45% | 0.752s | 678M | 38.36% | 1.640s | |
| 缓存私有页 | 997M | 9.36% | 0.683s | 699M | 36.45% | 0.744s | 678M | 38.36% | 1.634s | |
| 用户页 | 695M | 36.81% | 0.466s | 386M | 64.91% | 0.552s | 362M | 67.09% | 1.219s | |
| 可用页 | 790M | 28.18% | 0.552s | 703M | 36.09% | 0.722s | 689M | 37.36% | 1.641s | |
| 以上全部 | 411M | 62.64% | 0.281s | 356M | 67.64% | 0.390s | 347M | 68.45% | 0.883s |
| 规格 | 过滤页 | 不压缩 | snappy | zstd | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| 文件大小 | 压缩率 | 耗时 | 文件大小 | 压缩率 | 耗时 | 文件大小 | 压缩率 | 耗时 | ||
| 2C4G<br/><br/>原始 vmcore 文件 4.1G | 零页 | 2.8G | 31.71% | 2.319s | 2.7G | 34.15% | 2.741s | 2.7G | 34.15% | 5.633s |
| 缓存页 | 4.0G | 2.44% | 2.718s | 2.7G | 34.15% | 2.719s | 2.7G | 34.15% | 5.915s | |
| 缓存私有页 | 4.0G | 2.44% | 2.685s | 2.7G | 34.15% | 2.744s | 2.7G | 34.15% | 5.918s | |
| 用户页 | 2.0G | 51.22% | 1.860s | 1.5G | 63.41% | 1.812s | 1.4G | 65.85% | 3.856s | |
| 可用页 | 2.9G | 29.27% | 1.920s | 2.7G | 34.15% | 2.269s | 2.7G | 34.15% | 5.259s | |
| 以上全部 | 1.5G | 63.41% | 0.995s | 1.4G | 65.85% | 1.228s | 1.4G | 65.85% | 2.749s |
| 规格 | 过滤页 | 不压缩 | snappy | zstd | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| 文件大小 | 压缩率 | 耗时 | 文件大小 | 压缩率 | 耗时 | 文件大小 | 压缩率 | 耗时 | ||
| 2C16G<br/><br/>原始 vmcore 文件 17G | 零页 | 11G | 35.29% | 8.945s | 11G | 35.29% | 9.901s | 11G | 35.29% | 20.685s |
| 缓存页 | 17G | 0 | 10.362s | 11G | 35.29% | 10.065s | 11G | 35.29% | 22.450s | |
| 缓存私有页 | 17G | 0 | 10.481s | 11G | 35.29% | 10.159s | 11G | 35.29% | 22.523s | |
| 用户页 | 11G | 35.29% | 23.050s | 5.7G | 66.47% | 20.300s | 5.4G | 68.24% | 39.952s | |
| 可用页 | 11G | 35.29% | 45.403s | 11G | 35.29% | 47.024s | 11G | 35.29% | 13.297s | |
| 以上全部 | 5.7G | 66.47% | 3.641s | 5.4G | 68.24% | 4.345s | 5.3G | 68.82% | 10.142s |
4.2 功能测试:panic测试
guest 内执行:
echo c > /proc/sysrq-trigger
调试信息:
crash> bt 1277
PID: 1277 TASK: ffff8880047e4200 CPU: 1 COMMAND: "bash"
#0 [ffffc90000b2fd20] panic at ffffffff8116df49
#1 [ffffc90000b2fda0] sysrq_handle_crash at ffffffff817ef2c9
#2 [ffffc90000b2fdb0] __handle_sysrq at ffffffff817ef8c8
#3 [ffffc90000b2fde8] write_sysrq_trigger at ffffffff817efef7
#4 [ffffc90000b2fe18] proc_reg_write at ffffffff81506afe
#5 [ffffc90000b2fe38] vfs_write at ffffffff81468edf
#6 [ffffc90000b2fed8] ksys_write at ffffffff814693cb
#7 [ffffc90000b2ff18] __x64_sys_write at ffffffff8146947e
#8 [ffffc90000b2ff28] x64_sys_call at ffffffff81008d45
#9 [ffffc90000b2ff38] do_syscall_64 at ffffffff81af84bb
#10 [ffffc90000b2ff50] entry_SYSCALL_64_after_hwframe at ffffffff81c0012f
RIP: 00007f91bc114887 RSP: 00007ffcc392c4e8 RFLAGS: 00000246
RAX: ffffffffffffffda RBX: 0000000000000002 RCX: 00007f91bc114887
RDX: 0000000000000002 RSI: 0000560587070b40 RDI: 0000000000000001
RBP: 0000560587070b40 R8: 0000000000000000 R9: 0000560587070b40
R10: 0000000000000077 R11: 0000000000000246 R12: 0000000000000002
R13: 00007f91bc21b780 R14: 00007f91bc217600 R15: 00007f91bc216a00
ORIG_RAX: 0000000000000001 CS: 0033 SS: 002b
crash> log
.......
[ 0.337292] systemd-journald[766]: Received client request to flush runtime journal.
[ 0.431148] virtio_net virtio1 ens2: renamed from eth0 (while UP)
[ 9.507282] sysrq: Trigger a crash
[ 9.508049] Kernel panic - not syncing: sysrq triggered crash
[ 9.509971] Kernel Offset: disabled
[ 9.510760] ---[ end Kernel panic - not syncing: sysrq triggered crash ]---
4.3 功能测试:soft lockup 测试
guest 内执行:
insmod soft_lockup.ko
调试信息:
crash> bt
PID: 220 TASK: ffff8881365dc000 CPU: 1 COMMAND: "insmod"
#0 [ffffc900000a8e48] panic at ffffffff81a8ff0d
#1 [ffffc900000a8ec8] watchdog_timer_fn.cold at ffffffff81a96d57
#2 [ffffc900000a8ef8] __hrtimer_run_queues at ffffffff8110331c
#3 [ffffc900000a8f60] hrtimer_run_queues at ffffffff81103fcc
#4 [ffffc900000a8f80] update_process_times at ffffffff8110264d
#5 [ffffc900000a8f98] tick_sched_handle at ffffffff811147c2
#6 [ffffc900000a8fb0] tick_nohz_handler at ffffffff81114ba2
#7 [ffffc900000a8fd8] smp_apic_timer_interrupt at ffffffff81c0273a
#8 [ffffc900000a8ff0] apic_timer_interrupt at ffffffff81c01c2f
--- <IRQ stack> ---
#9 [ffffc90000743d28] apic_timer_interrupt at ffffffff81c01c2f
[exception RIP: delay_tsc+53]
RIP: ffffffff81a85b75 RSP: ffffc90000743dd8 RFLAGS: 00000246
RAX: 00000000b19e1c84 RBX: ffffffffa006c000 RCX: 0000000000000001
RDX: 0000000000000060 RSI: 0000000000000001 RDI: 00000060b19e1ad0
RBP: 0000000000000000 R8: 0000000000000001 R9: 0000000000006979
R10: 0000000000000001 R11: 0000000000000000 R12: ffffc90000743e88
R13: 0000000000000003 R14: 0000000000000000 R15: 0000000000000000
ORIG_RAX: ffffffffffffff13 CS: 0010 SS: 0018
#10 [ffffc90000743de0] do_one_initcall at ffffffff81000e26
#11 [ffffc90000743e50] do_init_module at ffffffff811217ca
#12 [ffffc90000743e70] __do_sys_finit_module at ffffffff811230d5
#13 [ffffc90000743f38] do_syscall_64 at ffffffff81002465
#14 [ffffc90000743f50] entry_SYSCALL_64_after_hwframe at ffffffff81c000a4
RIP: 00007fb61adf588d RSP: 00007fff21b066a8 RFLAGS: 00000246
RAX: ffffffffffffffda RBX: 000055f102ecd480 RCX: 00007fb61adf588d
RDX: 0000000000000000 RSI: 000055f10148ecd2 RDI: 0000000000000003
RBP: 0000000000000000 R8: 0000000000000000 R9: 0000000000000000
R10: 0000000000000003 R11: 0000000000000246 R12: 000055f10148ecd2
R13: 000055f102ecc3f0 R14: 000055f10148d888 R15: 000055f102ecd590
ORIG_RAX: 0000000000000139 CS: 0033 SS: 002b
小结
基于 kernel 的符号表信息,minidump 可以过滤 VM 内存中的零页/空闲页/用户页以及压缩算法来显著减少 core 文件大小,压缩算法会导致dump 耗时增加 50% 以上但文件压缩较小暂不启用。
附录
soft_lockup.c:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/delay.h>
static int __init soft_lockup_init(void)
{
printk(KERN_INFO "soft_lockup: Module loaded.\n");
printk(KERN_INFO "soft_lockup: Starting an infinite loop to trigger a soft lockup...\n");
// This is the problematic part. The while(1) loop
// will cause the CPU to be stuck in this kernel thread
// forever, without yielding to the scheduler or responding
// to interrupts.
while (1) {
// We can add a very small delay to prevent the CPU
// from overheating, but it won't prevent the soft lockup.
// It just makes the loop slightly less aggressive.
udelay(10);
}
return 0;
}
static void __exit soft_lockup_exit(void)
{
// This part will never be reached because of the infinite loop
printk(KERN_INFO "soft_lockup: Module unloaded.\n");
}
module_init(soft_lockup_init);
module_exit(soft_lockup_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("A kernel module to demonstrate a soft lockup.");