最近我们陆续注意到,有用户在使用 NodeQuality(NQ) 对部分 Geelinx Compute VPS 进行测试时,会遇到一个相当离谱的结果:内存读写带宽看起来并没有明显异常,但 Sysbench 给出的延迟却直接冲到了两万甚至两万六千纳秒。
如果你是我们对应产品的客户,请直接进入 Geelinx 控制台,对实例执行一次「硬重启」。我们已经完成相关虚拟化配置修复并推送补丁,实例在重新创建虚拟化运行环境后即可应用新的 CPU 配置,内存延迟测试将恢复到正常水平。
这次问题不是宿主机超开,也不是物理内存性能异常。
本帖抽送 1 台 HK.HKG.INTL.Lion 有效期 3 个月,不可续期
省流:
Sysbench 内存延迟测试需要极高频率地进行 Event 计时,而虚拟机中的 ACPI PM Timer 访问开销远高于 TSC,因此原本约 150ns 的测试结果会被放大到 26000ns 左右。由于部分虚拟化特征被隐藏后,新的固定 CPU Model 没有同时向 Guest 正确提供稳定的 TSC 时间特征,导致部分实例完整启动后,Linux Guest 的高性能时钟源不可用,只能退化到 ACPI_PM 导致出现此问题。使用 kvm-clock 或 tsc 即可。
一个比较典型的结果是:
Sysbench:
读取 19315.7 MB/s
写入 13821.2 MB/s
延迟 26379 ns
如果只看最后这一行,第一反应确实很容易是:宿主机是不是超开了?内存是不是已经争抢到爆炸?NUMA 是不是完全没处理?毕竟 26000ns 已经不是“比正常内存慢一点”的程度,而是足足 26 微秒。如果把这个数字直接理解成传统意义上的 DRAM Access Latency,那已经不是性能不好看,而是几乎不符合常识。
偏偏更奇怪的地方就在这里:读写带宽并没有一起掉到一个离谱的水平。 一台内存真的已经恶化到平均访问需要 26μs 的机器,很难同时还能维持十几乃至接近二十 GB/s 的连续读写性能。
所以我们没有急着把它归结成“跑分误差”,也没有直接认为是宿主机资源问题,而是决定把 NQ 这项测试完整拆开,看看这两万多纳秒究竟是怎么来的。
一、先抛开 NQ,本机直接复现它的 Sysbench 测试
NodeQuality 的硬件测试中,所谓“内存延迟”并不是另外调用一套专业的内存延迟测试工具,而是建立在 Sysbench Memory Test 上。
为了排除 NQ 自身运行环境、结果解析或者其他脚本逻辑造成的影响,我们先在问题实例的系统中直接安装 Sysbench,然后按照同样的方式手工运行 64 Byte Random Read:
sysbench memory \
--memory-block-size=64 \
--memory-total-size=1000G \
--memory-oper=read \
--memory-access-mode=rnd \
--time=5 run
第一次得到的结果却完全正常:
Total operations: 32287705 (6456191.95 per second)
General statistics:
total time: 5.0002s
total number of events: 32287705
按照相同的计算方式:
5.0002 / 32287705 × 1,000,000,000
≈ 154.9 ns
也就是说,同一台虚拟机、同样的 Linux 系统、同样的 Sysbench Memory Random Read,手工执行时得到的是大约 155ns,而此前 NQ 报告里的结果却是 26379ns。
我们又进一步分别测试了 Random Read、Random None、Sequential Read、Sequential None。结果全部稳定在 140~155ns 左右:
RND READ ≈ 154.9 ns
RND NONE ≈ 143.5 ns
SEQ READ ≈ 143.2 ns
SEQ NONE ≈ 141.9 ns
到这里,事情开始变得非常有意思。
因为它说明 Sysbench 本身没有天然测出 26000ns,这台虚拟机也完全有能力跑出正常的 150ns 级结果。问题显然不是简单的“这台机器内存就这么慢”。
二、去 HK.INTL.Peak 所在宿主机做一组对照实验
为了避免一直围绕问题机器自己打转,我们又去 HK.INTL.Peak 所在的另一台宿主机新开了一台虚拟机,作为完全独立的对照组。
Peak 所在宿主机上的实例运行同样的 NodeQuality 测试,得到的是:
Sysbench:
读取 23477.4 MB/s
写入 19374.0 MB/s
延迟 150 ns
这个结果非常重要。
因为它证明了一件事:NodeQuality 本身完全可以正常给出 150ns 左右的结果。
也就是说,不能简单地认为“NodeQuality 这个延迟指标本身就是假的”“它只是把某个大块传输时间算成了延迟”,否则另一台机器就不可能在完全相同的脚本下得到 150ns。
所以问题重新被缩小为:
为什么两台虚拟机都能跑出正常的内存吞吐,但其中一边的 Sysbench 64B Random Memory Test 却慢了一百多倍?
这时候我们开始逐项比较两台虚拟机的运行环境。而最终真正把问题暴露出来的,并不是内存参数,而是一个看起来和“内存跑分”毫无关系的东西:
Clocksource。
三、真正的杀手:ACPI PM Timer
在问题实例重新完整关机并启动、确保当前虚拟化配置真正生效之后,我们检查:
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
输出变成了:
acpi_pm
再看可用 Clocksource:
cat /sys/devices/system/clocksource/clocksource0/available_clocksource
结果更加直接:
acpi_pm
也就是说,这台虚拟机此时甚至不是“Linux 觉得 ACPI PM 比 TSC 更合适”,而是:
Guest 里压根没有一个可用的高性能 TSC Clocksource,只能使用 ACPI PM Timer。
带着这个状态重新跑刚才完全相同的 Sysbench:
Total operations: 191277 (38244.96 per second)
General statistics:
total time: 5.0004s
total number of events: 191277
换算:
5.0004 / 191277 × 1,000,000,000
≈ 26142 ns
而此前 NodeQuality 的结果是:
26379 ns
两个数字几乎完全重合。
到这里,这个问题实际上已经完成了最关键的闭环:
NQ 的 26000+ns 确实是真的,但它并不是“物理内存访问一次需要 26μs”,而是 Sysbench 在使用极慢的虚拟机计时路径后,测出来的每个 Memory Event 耗时。
四、为什么换一个 Clocksource,所谓“内存延迟”能差一百多倍?
这也是整个问题最有意思的地方。
很多人看到 NodeQuality 中的:
Memory Latency: 26379 ns
会自然地把它理解成 CPU 从内存控制器读取一份数据,DRAM 返回这份数据所花费的时间。也就是我们平时讨论 DDR4、DDR5、NUMA、本地内存和远程内存时说的那种 Memory Access Latency。
但 Sysbench 的测试机制不是这样。
它把整个测试过程拆分成大量 Event。每一个 Event 都需要进行计时,Sysbench 要知道 Event 什么时候开始、什么时候结束,才能最后计算吞吐、延迟、百分位等数据。
在一个正常的 Linux Guest 中,如果 TSC 被认为稳定可靠,clock_gettime() 可以通过 vDSO 等快速路径完成。我们在正常状态下实际测量过:
clock_gettime: 27.83 ns/call
clock_gettime: 28.15 ns/call
这种成本非常低。
但当 Guest 失去可靠 TSC、只能依靠虚拟化环境中的 acpi_pm 获取时间时,情况就完全不同了。ACPI PM Timer 本质上是一个传统的平台定时器接口,在虚拟机中访问它的成本远高于读取稳定的 TSC。此时 Sysbench 进行的是一个非常小的 64 Byte Random Memory Event,但为了测量这么一个极小操作,它却需要付出一个相当昂贵的“看表”成本。
于是就会出现一个极其反直觉的结果:
内存本身没有慢 170 倍,但用来测量这次内存操作花了多久的时间源慢了几个数量级。
最终整个 Event 被拖到了两万多纳秒,然后 NodeQuality 又把这个 Event Latency 作为“内存延迟”展示出来。
所以用户最终看到的是:
读取:19315.7 MB/s
写入:13821.2 MB/s
延迟:26379 ns
看起来像是“内存吞吐正常,但是 DRAM 延迟已经比别人高了一百多倍”。
实际上真正慢下来的,是 Sysbench 高频调用的计时路径。
五、为什么我们的虚拟机会掉到 ACPI PM?
这就要说到 Compute VPS 的一个比较特殊的设计需求。
Geelinx Compute VPS 定位本身就是计算型产品,所以除了普通 VPS 的使用场景之外,我们还希望它能够正常支持 Nested Virtualization。简单来说,就是客户购买一台 Compute VPS 后,仍然可以在这台虚拟机里面继续运行 KVM、Hyper-V 等虚拟化环境。
因此,我们需要给 Guest 保留 AMD-V/SVM 等嵌套虚拟化能力,同时为了兼容不同 Guest、避免宿主机某些较新的 CPU Feature 直接被透传进虚拟机,以及处理部分嵌套虚拟化场景下的兼容性,我们没有简单使用完全的 Host Passthrough,而是采用了固定的 Guest CPU Model,并隐藏了部分虚拟化特征。
其中几个主要目标其实是彼此独立的:
固定 CPU Model
→ 避免宿主机新增的某些指令集无条件暴露给 Guest
SVM
→ 允许 Guest 内继续运行 KVM / Hyper-V
隐藏部分 Hypervisor / KVM 特征
→ 满足部分嵌套环境和兼容性需求
问题就出在这个组合产生了一个非常隐蔽的副作用。
在隐藏部分 KVM Paravirtualization 特征后,Guest 本身已经不能依赖 kvm-clock。而采用新的固定 CPU Model 后,我们又没有同时向 Guest 声明稳定的 Invariant TSC。
两个条件叠加之后,就形成了一个几乎教科书式的坑:
没有 KVM PV Clock
+
Guest 不认为 TSC 是稳定时钟源
=
只能使用 acpi_pm
CPU 运算没有炸,内存吞吐没有炸,Nested Virtualization 也仍然正常工作,所以正常业务场景下这个问题其实非常不显眼。
直到 NodeQuality 使用一个极高频、极小 Event 的 Sysbench Random Memory Test,把 Clocksource 的性能差异硬生生放大了一百多倍。
六、修复:我们没有重新打开 Host Passthrough,只补回了缺失的时间特征
找到原因以后,修复反而比排查简单得多。
我们没有重新切回 host / Host Passthrough,因为固定 Guest CPU ABI 本身有它存在的意义;也没有为了单纯让 NQ 分数变好而取消 Nested Virtualization 相关设计。
真正缺少的只是:
invtsc
也就是向 Guest 明确暴露 Invariant TSC 能力,让 Guest 可以把 TSC 当作稳定可靠的时间基础。
修改后,虚拟机重新完整关机启动,再运行完全相同的 Sysbench:
Total operations: 32314737 (6461782.80 per second)
General statistics:
total time: 5.0002s
total number of events: 32314737
换算:
5.0002 / 32314737 × 1,000,000,000
≈ 154.7 ns
从问题状态下的:
≈ 26142 ns
恢复到:
≈ 155 ns
也就是说,在 vCPU 数量没变、虚拟机内存没变、宿主机内存没变、NUMA 没变、Sysbench 参数没变 的情况下,仅修正 Guest 的时间源,就让这项测试的结果改善了大约:
169 倍
这个 A/B 对照也让整个问题没有太多争议空间了。
七、所以这到底是不是“超开”?
不是。
而且这次问题很适合作为一个例子,说明为什么只看 Benchmark 的一个最终数字,很容易得出完全相反的结论。
如果 26000ns 真的是由于严重的 CPU 超售、内存过度分配、NUMA 争抢或者物理内存子系统异常造成的,那么修改 Guest Clocksource 不可能凭空把真正的物理内存延迟从 26μs 变成 155ns。
但我们实际完成的 A/B 测试正是:
同一台虚拟机
同一套 Guest
同一套宿主机资源
同样的 Sysbench
同样的测试参数
问题状态:
Clocksource: acpi_pm
191277 events / 5.0004s
≈ 26142 ns
修复状态:
Clocksource: TSC
32314737 events / 5.0002s
≈ 154.7 ns
如果是资源争抢造成的真实性能问题,改变时间源不会让一次真实 DRAM Access 快一百多倍。
所以这次的两万多纳秒并不是一个“宿主机已经超到没法用了”的证据,而是一个非常典型的 Benchmark Measurement Path 问题。
当然,这不代表“只要 NQ 延迟高就一定是 Clocksource”。如果一台机器确实存在严重 CPU Steal、NUMA Remote Access、Memory Reclaim 等问题,照样可能在测试中体现出来。
技术问题最忌讳的其实就是:
看到一个数字,先决定结论,再寻找能够证明它的原因。
八、那是不是 NodeQuality 测错了?
我们也不打算简单把这个问题甩给 NodeQuality。
严格来说,NQ 并没有凭空捏造 26379 ns。
问题状态下,同样的 Sysbench 命令手工运行就是:
≈ 26142 ns
NodeQuality 给:
26379 ns
甚至可以说测得相当准。
真正容易产生歧义的地方,是“内存延迟”这四个字很容易被理解为一个纯粹的硬件指标。
实际上这里测到的是:
Sysbench Memory Event 在当前 Guest 软件、计时路径、CPU、内存等共同作用下的 Event Latency。
其中当然包含内存访问成本,但并不等价于拿专业 Pointer Chasing 测试得到的传统 DRAM Load-to-Use Latency。
正常情况下,计时路径只需要几十纳秒,这部分成本不会离谱到影响结论,所以大家通常也不会注意它。
而我们这次恰好把 Guest Clocksource 弄到了 acpi_pm,于是这个平时可以忽略的变量被直接放大到了足以主宰整个测试结果的程度。
某种意义上来说,NodeQuality 反而真的帮我们找到了一处平时很难被发现的虚拟化配置问题。
只是最终那一行:
26379 ns
如果脱离上下文,很容易被理解成完全不同的东西。
九、完整的 A/B 数据
为了方便以后再遇到类似问题时查证,我们也把这次最核心的数据整理一下。
问题状态
Guest Clocksource:
acpi_pm
Available Clocksource:
acpi_pm
Sysbench 64B Random Read:
Total operations: 191277
Total time: 5.0004s
换算 Event Latency:
≈ 26142 ns
NodeQuality:
≈ 26379 ns
正常状态
Guest Clocksource:
TSC
Sysbench 64B Random Read:
Total operations: 32314737
Total time: 5.0002s
换算 Event Latency:
≈ 154.7 ns
此前正常环境中的时间调用性能
clock_gettime:
27.83 ns/call
28.15 ns/call
而在 HK.INTL.Peak 所在宿主机上的对照实例中,NodeQuality 本身给出的结果也在:
约 150 ns
整个证据链最终形成了一个非常完整的闭环:
正常 Clocksource
→ Sysbench ≈ 150ns
异常 Clocksource
→ Sysbench ≈ 26000ns
恢复正常 Clocksource
→ Sysbench 再次 ≈ 150ns
十、为什么这个 Bug 平时几乎感觉不到?
因为这不是一个“所有计算都慢 170 倍”的问题。
正常 CPU 运算、内存连续读写、磁盘 IO、网络收发,并不会每执行一点点工作就强制按照 Sysbench 这种方式高频获取时间。
所以在日常使用中,用户可能只会觉得这台机器:
CPU 正常
内存带宽正常
网络正常
磁盘正常
甚至业务本身完全没有异常。
但是 Sysbench 的这个测试非常特殊:每个 Event 都很小,测试频率极高,因此用于统计 Event 的时间获取成本占比会迅速放大。
它就像你想测一个人按键盘一次需要多久,但使用的秒表每按一次开始、停止都要卡 20 毫秒。
最后得到的数字当然很大。
但慢的并不是人的手。
慢的是秒表。
这也是为什么这个 Bug 最终是通过 NodeQuality 被集中暴露出来,而不是用户先反馈“服务器实际业务性能下降了一百倍”。
十一、最后
这次问题的起点其实很普通:有用户把 NodeQuality 的测试结果发到了论坛,大家看到:
Memory Latency: 20000+ ns
自然会产生疑问,也有人第一时间猜测是不是宿主机超开或者资源争抢。
从结果本身看,这种怀疑完全可以理解。
但我们自己看到以后也觉得这个数字在物理意义上过于反常,所以没有停留在“这只是跑分不准”或者“业务正常就行”的解释上,而是一路把 Sysbench、Guest CPU、Clocksource 和虚拟化配置拆开做了完整的对照实验。
最后发现,真正的原因既不是内存,也不是超开,而是我们为了 Compute VPS 的 CPU ABI 兼容和 Nested Virtualization 做调整时,留下了一处不起眼的 Clocksource 配置问题。
它没有让内存真的慢一百多倍。
却成功让测内存的“秒表”慢了一百多倍。
目前对应的虚拟化配置已经完成修正。对于此后重新完整启动、应用新 CPU 配置的相关实例,Guest 将能够正常使用稳定的 TSC 时间源。
我们其实挺喜欢这种问题。
因为很多 Benchmark 最有价值的地方,并不是那个最终分数到底比别人高 5% 还是低 10%,而是:
当一个数字离谱到不符合常识的时候,它通常是在提醒你,系统里藏着一个值得挖出来的东西。
这次 NQ 确实把它炸出来了。
所以以后如果再看到一台机器:
内存读写:正常
NQ 延迟:20000+ ns
除了“超开”之外,也可以先顺手看看:
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
如果屏幕上安静地蹦出来:
acpi_pm
那可能不是内存先破防了。
是负责测它有多快的那块时钟,先破防了。
也有可能我先破防
Geelinx LTD
2026.8.24
看懂了,大概意思就是时钟不准
@80hou 季抛机
参加
参与
来了来了
那必须支持
参加一下
嗨 鸡