解决qemu虚拟机中内存偏小的问题

问题描述:
最近测试部报了一个问题,云平台中设置大于4GB的内存并设置numa,启动linux2.6.32内核的客户机,之后操作系统中查看实际内存是1.9G,比设置内存小了大概2.1GB。

使用版本信息如下:
QEMU version: 3.0.0
guest os kernel version: 2.6.32
host kernel version: 4.9.0

问题排查如下:
1)在系统中排查问题
通过如下命令查看60系统下内存槽硬件信息,说明内存卡硬件识别正常:

$ dmidecode -t memory

解决qemu虚拟机中内存偏小的问题_第1张图片
解决qemu虚拟机中内存偏小的问题_第2张图片
通过$ free -m查看,系统识别出来却有问题:
解决qemu虚拟机中内存偏小的问题_第3张图片
通过测试如下命令启动QEMU虚拟机必然复现该问题:

$ qemu-system-x86_64 -enable-kvm -name guest=vm1,debug-threads=on -machine pc-i440fx-2.6,accel=kvm,usb=off,dump-guest-core=off -cpu Westmere -m size=4194304k,slots=16,maxmem=16777216k -realtime mlock=off -smp 1,sockets=1,cores=1,threads=1 -numa node,nodeid=0,cpus=0 -uuid fd3535db-2558-43e9-b067-314f48211343 -no-user-config -rtc base=localtime -no-shutdown -boot menu=on,strict=on -device piix3-usb-uhci,id=usb,bus=pci.0,addr=0x1.0x2 -drive file=/opt/issue32/generic.qcow2,format=raw,if=none,id=drive-ide0-0-0 -device ide-hd,bus=ide.0,unit=0,drive=drive-ide0-0-0,id=ide0-0-0,bootindex=1 -spice port=5900,addr=0.0.0.0,disable-ticketing,seamless-migration=on -k en-us -device cirrus-vga,id=video0,bus=pci.0,addr=0x4 -device virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x3 -msg timestamp=on

(这里的slots为16,所以先抛下一个问题:“为什么实测slots为3的时候不会出现2.1G的内存占用?”)
2)接下来在2.6.32内核源码中排查该问题:
通过debug发现是bootmem释放未占用的内存到buddy内存系统的时候少了2.1G,接下来继续研究bootmem的内存分配。

setup_node_bootmem->init_bootmem_node->free_boot_mem_with_active_regions->early_res_to_bootmem(这里干的好事)

bootmem是系统初始化期间的临时内存分配系统,通过位图来标识内存占用,bootmem自身的初始化很简单,首先将位图所有位置1,表示所有页已保留,之后通过free_boot_mem_with_active_regions将可用页帧置0,而free_boot_mem_with_active_regions中会调用early_res_to_bootmem将early_res分配器占用的内存页继承到bootmem中来,early_res是在bootmem之前更早的内存分配器,排查发现是early_res分配器使用过程中占用了2.1G的内存页,而bootmem继承了它导致未释放至buddy子系统。
因此继续向前排查,在这里发现了问题:

start_kernel->setup_arch->initmem_init->acpi_scan_nodes->compute_hash_shift->allocate_cachealigned_memnodemap->reserve_early

CONFIG_ACPI_NUMA确定此选项后内核会编译acpi_numa_init函数,获取numa支持,从硬件系统的acpi表中得到物理硬件的nodes信息,ACPI是在系统启动阶段由BIOS/UEFI收集各方面信息并创建的。而2.1G的内存分配正是在allocate_cachealigned_memnodemap中通过reserve_early完成的。
allocate_cachealigned_memnodemap分配的大小计算如下:

static int __init allocate_cachealigned_memnodemap(void)
{
	。。。
	nodemap_size = roundup(sizeof(s16) * memnodemapsize, L1_CACHE_BYTES);
	。。。
	reserve_early(nodemap_addr, nodemap_addr + nodemap_size, "MEMNODEMAP");
	。。。
}

memnodemapsize定义如下:

#define memnodemapsize memnode.mapsize

而memnode.mapsize的计算和设置是在compute_hash_shift->extract_lsb_from_nodes中完成的:

/*
 * The LSB of all start and end addresses in the node map is the value of the
 * maximum possible shift.
 */
static int __init extract_lsb_from_nodes(const struct bootnode *nodes,
					 int numnodes)
{
	int i, nodes_used = 0;
	unsigned long start, end;
	unsigned long bitfield = 0, memtop = 0;

	for (i = 0; i < numnodes; i++) {
		start = nodes[i].start;
		end = nodes[i].end;
		if (start >= end)
			continue;
		bitfield |= start;
		nodes_used++;
		if (end > memtop)
			memtop = end;
	}
	if (nodes_used <= 1)
		i = 63;
	else
		i = find_first_bit(&bitfield, sizeof(unsigned long)*8);
	memnodemapsize = (memtop >> i)+1;
	return i;
}

extract_lsb_from_nodes中将所有内存节点的start值进行了一个位或。之前埋下过一个问题也可以解释了“为什么slots为3的时候不会出现2.1G的内存reserve?”其实答案在于最后memnodemapsize = (memtop >> i)+1中,将unsigned long转换成了unsigned int,而i=0,直接导致了高32位的精度丢失,而在slot=3的时候恰好1全在高32位上面,导致mapsize=1,因此没有内存占用。所以这里又可以抛出一个问题了 ;)
(为什么内存区域会随着slots个数而变化?)

node区域范围有如下四个:
a) 0-655,360
b)1,048,576-3,221,225,472
c)4,200,000,000-5,300,000,000
d)35,433,480,192-35,433,480,191
最后一个区域范围恰好是hotplug的内存区域范围,很明显这里的hotplug区域范围有问题,怎么可能只有1。由于start只比end小1,也就直接导致了根据start位与计算的memnodemapsize值偏大,并且还出现了精度丢失!

好了,现在知道问题的直接导致是热插拔内存区设置的有问题,但是为什么会有问题? hotplug内存区参数是怎么读取的,之后想起开机启动的时候好像也报了和hotplug区相关的问题:
在这里插入图片描述
系统启动过程报hotplug区的过小,而hotplug参数也是来自于SRAT表的解析,SRAT(静态资源亲和性表)是ACPI规范的一部分,SRAT表解析流程如下:

start_kernel->setup_arch()->acpi_numa_init()->acpi_table_parse_srat()->acpi_table_parse_entries()
->acpi_parse_memory_affinity()->acpi_numa_memory_affinity_init()->update_nodes_add()

update_nodes_add函数中:

    if ((signed long)(end - start) < NODE_MIN_SIZE) {
        printk(KERN_ERR "SRAT: Hotplug area too small\n");
        return;
    }

这里的SRAT表是前期BIOS存放在内存中的,其根本来源还是qemu。

3)因此就开始了在qemu源码中排查问题的旅途
qemu中建立SRAT表的流程:

main->qemu_run_machine_init_done_notifiers->notifier_list_notify->pc_machine_done->acpi_setup->acpi_build->build_srat

build_srat是构建SRAT表的函数,其中有:

    if (hotplugabble_address_space_size) {
        build_srat_hotpluggable_memory(table_data, machine->device_memory->base,
                                       hotplugabble_address_space_size,
                                       pcms->numa_nodes - 1);
    }

这里的hotplug memory的基址是machine->device_memory->base=5,368,709,120,这个数值从前面的区域范围表中看起来是合理的,为什么这个base addr进入虚拟机后就变成
35,433,480,191了?

    ram_addr_t hotplugabble_address_space_size =
            object_property_get_int(OBJECT(pcms), PC_MACHINE_DEVMEM_REGION_SIZE,
                                		   NULL);

往前看,前面获取的hotplugaable_address_space_size=30064771072恰好是(35,433,480,192 - 5,368,709,120),也就是说end地址传入虚拟机是正确的。

最终在如下代码段找到了问题所在,在info为null是,build_srat_memory设置的start=end-1,size=1,和虚拟机中读取到的完全相符,看来是这里导致的该bug。

static void build_srat_hotpluggable_memory(GArray *table_data, uint64_t base,
                                           uint64_t len, int default_node)
{
    MemoryDeviceInfoList *info_list = qmp_memory_device_list();
    MemoryDeviceInfoList *info;
。。。
    for (cur = base, info = info_list;
         cur < end;
         cur += size, info = info->next) {
        numamem = acpi_data_push(table_data, sizeof *numamem);

        if (!info) {
            build_srat_memory(numamem, end - 1, 1, default_node,
                              MEM_AFFINITY_HOTPLUGGABLE | MEM_AFFINITY_ENABLED);
            break;
        }
。。。

通过参考最新版的的代码以及老版本(2.x)的代码稍作修改即可。

PS:这里其实也可以通过设置device模型为pc-dimm,让info不为null,比如如下:

-numa node -object memory-backend-ram,policy=default,size=4G,id=mem-mem1 -device pc-dimm,node=0,id=dimm-mem1,memdev=mem-mem1

#)
这里最后再解释下为什么内存区域会随着slots个数而变化:
hotplugin内存区域初始化流程如下:

main->machine_run_board_init->pc_init_v3_0->pc_init1->pc_memory_init

如下可知device_mem_size = max_size - ram_size + 1G * slots

device_mem_size = machine->maxram_size - machine->ram_size
        if (pcmc->enforce_aligned_dimm) {
            /* size device region assuming 1G page max alignment per slot */
            device_mem_size += (1 * GiB) * machine->ram_slots;
        }

device_mem_size = 12G(16G-4G) + 1G * 16(slots) = 28G(30064771072)。这里的1G * slots是考虑到大页对齐的问题,支持单页最大1G。所以hotplug区域也会随着slots而变化。

你可能感兴趣的:(虚拟化,虚拟化,云计算,qemu,ovirt,内核)