+关注

一个被忽视的CPUID,如何悄悄吞掉你的IOPS
点赞

2026-03-03 更新
3 次观看
一个被忽视的CPUID,如何悄悄吞掉你的IOPS

问题现象

虚机内使用PoleFS(共享文件系统),在对PoleFS进行性能压测的过程中,我们观察到一个反直觉现象:相同配置的虚机,仅仅因为是否暴露Hypervisor CPUID,IOPS表现出现明显差异(暴露后的顺序读写IOPS是暴露前的两倍)。整个过程中,虚机IO路径、网络链路、存储后端均无变化,这说明真正的瓶颈,很可能藏在虚机系统默认行为中(Hypervisor CPUID的影响)。

结论先行

经过完整的调用链追踪与KVM侧验证,我们最终确认:性能差异的根因不在存储,而在CPU Idle策略。更准确地说,是这条路径:Hypervisor CPUID->haltpoll governor启用->Busy Poll替代HLT->VMEXIT显著减少->IPI唤醒延迟下降->IOPS提升,这条链路揭示了一个经常被忽视的事实:在虚拟化环境中,CPU调度策略对性能的影响,可能远超IO路径本身。

问题定位

热点分析

分别对未暴露和暴露Hypervisor指令的场景进行Perf分析,结果如下:

上图为未Hypervisor指令集时的perf数据,热点集中在__rawspin_unlock_irqrestore,且占比异常偏高。

上图为暴露Hypervisor指令集时的perf数据,__rawspin_unlock_irqrestore函数的热点明显下降,这非常关键,这个现象表明,锁本身没有变,变的是“等待锁的CPU行为”。

内核链路

我们继续沿调用链深入,最终定位到:kvm_para_available(),该函数用于判断CPU是否暴露Hypervisor特征位。一旦成立,Linux将启用:haltpoll cpuidle driver、haltpoll governor。

static void __wake_up_common_lock(struct wait_queue_head *wq_head, unsigned int mode,
			int nr_exclusive, int wake_flags, void *key)
{
	unsigned long flags;
	wait_queue_entry_t bookmark;

	bookmark.flags = 0;
	bookmark.private = NULL;
	bookmark.func = NULL;
	INIT_LIST_HEAD(&bookmark.entry);

	do {
		spin_lock_irqsave(&wq_head->lock, flags);
		nr_exclusive = __wake_up_common(wq_head, mode, nr_exclusive,
						wake_flags, key, &bookmark);
		spin_unlock_irqrestore(&wq_head->lock, flags);
	} while (bookmark.flags & WQ_FLAG_BOOKMARK);
}

#define raw_spin_unlock_irqrestore(lock, flags)		\
	do {							\
		typecheck(unsigned long, flags);		\
		_raw_spin_unlock_irqrestore(lock, flags);	\
	} while (0)

通过分析linux kernel代码,kvm_para_available的核心引用在drivers/cpuidle/cpuidle-haltpoll.c:113和drivers/cpuidle/governors/haltpoll.c:143两处,代码如下:

kvm_para_available:
bool kvm_para_available(void)
{
	return kvm_cpuid_base() != 0;
}

static inline uint32_t kvm_cpuid_base(void)
{
	static int kvm_cpuid_base = -1;

	if (kvm_cpuid_base == -1)
		kvm_cpuid_base = __kvm_cpuid_base();

	return kvm_cpuid_base;
}

static noinline uint32_t __kvm_cpuid_base(void)
{
	if (boot_cpu_data.cpuid_level < 0)
		return 0;	/* So we don't blow up on old processors */

	if (boot_cpu_has(X86_FEATURE_HYPERVISOR))
		return hypervisor_cpuid_base("KVMKVMKVM\0\0\0", 0);

	return 0;
}
drivers/cpuidle/cpuidle-haltpoll.c:113引用如下
static int __init haltpoll_init(void)
{
	int ret;
	struct cpuidle_driver *drv = &haltpoll_driver;

	/* Do not load haltpoll if idle= is passed */
	if (boot_option_idle_override != IDLE_NO_OVERRIDE)
		return -ENODEV;

	cpuidle_poll_state_init(drv);

	if (!kvm_para_available() || !haltpoll_want())
		return -ENODEV;

	ret = cpuidle_register_driver(drv);
	if (ret < 0)
		return ret;

	haltpoll_cpuidle_devices = alloc_percpu(struct cpuidle_device);
	if (haltpoll_cpuidle_devices == NULL) {
		cpuidle_unregister_driver(drv);
		return -ENOMEM;
	}

	ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "cpuidle/haltpoll:online",
				haltpoll_cpu_online, haltpoll_cpu_offline);
	if (ret < 0) {
		haltpoll_uninit();
	} else {
		haltpoll_hp_state = ret;
		ret = 0;
	}

	return ret;
}
drivers/cpuidle/governors/haltpoll.c:143引用如下:
static struct cpuidle_governor haltpoll_governor = {
	.name =			"haltpoll",
	.rating =		9,
	.enable =		haltpoll_enable_device,
	.select =		haltpoll_select,
	.reflect =		haltpoll_reflect,
};

static int __init init_haltpoll(void)
{
	if (kvm_para_available())
		return cpuidle_register_governor(&haltpoll_governor);

	return 0;
}

HLT和BusyPoll,这两段内核逻辑实际上在决定一件极其重要的事情:当VCPU短暂空闲时,是“睡眠”,还是“等待”?

HLT模式:看似省电,实则昂贵。当haltpoll未启用时:VCPU执行HLT->触发VMEXIT->控制权回到宿主机,随后若锁被释放:宿主机注入中断->VMENTRY恢复执行,整个链路包含多次特权切换,这些切换带来的影响就是CPU执行延迟。

BusyPoll模式:用CPU换延迟。当haltpoll启用时:1.VCPU保持运行态;2.主动轮询IPI pending位;3.避免频繁VM切换。为防止无限自旋,KVM引入:PLE(Pause Loop Exiting),超过阈值才触发VMEXIT。这是一种非常经典的系统设计哲学:用可控的CPU消耗,换取确定性的低延迟。

实验证明

为了排除偶然性,我们进行了一个“带有攻击性”的实验,在未暴露hypervisor的情况下,强制启用haltpoll。将drivers/cpuidle/cpuidle-haltpoll.c:113和drivers/cpuidle/governors/haltpoll.c:143两处代码调整为如下:

将cpuidle-haltpoll.c:113调整:
原逻辑:
if (!kvm_para_available() || !haltpoll_want())
调整后:
if (kvm_para_available() || !haltpoll_want())

将haltpoll.c:143调整:
原逻辑:
if (kvm_para_available())
调整后:
if (!kvm_para_available())

重新编译内核后,fio测试Polefs的IOPS与有hypervisor指令集时基本一致,perf数据如下:

相关逻辑

cpuidle-haltpoll.c和haltpoll.c两处相关逻辑决定了CPU Idle时,是进入HLT还是BusyPoll。在HLT模式下,VCPU VMEXIT到宿主机,执行__rawspin_unlock_irqrestore后,通过IPI(核间中断)触发其他等待lock的VCPU,再次产生VMEXIT,由宿主机VMM触发其他VCPU VMENTRY唤醒。在BusyPoll模式下,VCPU一直处于自旋状态,为避免长时忙等浪费CPU资源,KVM层面设置PLE限制,自旋一定时间后触发限制,然后VMEXIT到宿主机。BusyPoll模式下,由于等锁的VCPU处于运行态,直接检查对应IPI Pending寄存器,没有VMEXIT\VMENTRY,所以延迟较低。

上图为没有hypervisor指令集时,宿主机的KVM Perf数据,VMEXIT中,HLT占比 22.59%,APIC_WRITE占比76.93%

上图为有hypervisor指令集时,宿主机的KVM Perf数据,HLT占比14.67%(-8%),PAUSE_INSTRUCTION占比15.07%,EXTERNAL_INTERRUPT占比8.93%,MSR_WRITE占比60.94%(非hypervisor指令集下使用APIC_WRITE,是老旧CPU架构下的xAPIC中断模式,使用mmio方式,MSR_WRITE是新架构下的x2APIC中断模式,使用MSR寄存器,x2没有APIC和mmio的数量限制)。EXTERNAL_INTERRUPT是BusyPoll模式下,触发PLE的操作。

上图是没有hypervisor指令集,修改Linux Kernel cpuidle-haltpoll.c:113和haltpoll.c:143两处代码后的宿主机KVM Perf数据,HLT占比14.86%,PAUSE_INSTRUCTION占比14.42%,EXTERNAL_INTERRUPT占比8.60%,APIC_WRITE占比62.00%,与有hypervisor指令集的数据基本一致。

综合以上宿主机层面的KVM Perf数据,hypervisor指令集之所以能够影响__rawspin_unlock_irqrestore的性能,核心在于CPU Idle场景下,目标VCPU的行为,HLT带来的VMEXIT\VMENTRY提升了IPI的延迟,Hypervisor指令将VCPU从“休眠 + 中断唤醒”转向“忙等 + 快速响应”。

针对这类性能问题,我们的常规视角是:应用->IO->网络->存储,但在虚拟化背景下,真实路径是:应用->锁竞争->CPU Idle策略->VMEXIT->调度延迟->底层IO,在真正的IO之前,隐藏着更复杂的链路,这正是现代系统性能分析最容易被误判的地方。

HaltPoll的本质:虚拟化时代的“自旋锁哲学”。haltpoll其实代表一种重要思想:短Idle ≠ 值得睡眠,尤其在NVMe、分布式存储、高QPS数据库,在这些场景中,HLT反而是一种错误优化。

优化建议

优化点:

1.默认开启暴露Hypervisor CPUID,尤其适用于:高IOPS虚机、数据库、低延迟服务等。

2.建立VMEXIT观测体系,在宿主机侧进行VMEXIT观测,分析虚拟化层VMEXIT原因并进行优化

通过本次链路分析,会发现瓶颈越来越多出现在“调度与虚拟化边界”。未来的高性能优化也将包含:Guest-Hypervisor、调度-中断等这些“看不见”的地方。



Copyright©2005-2026 360.cn  版权所有  360互联网安全中心   北京举报中心