news 2026/10/15 1:46:24

KVM内存管理深度解析:超分、气球、KSM与大页调优实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
KVM内存管理深度解析:超分、气球、KSM与大页调优实战

1. 从一次内存告警说起:为什么KVM内存管理值得单独拎出来讲

前阵子帮一个朋友排查他实验室里的一台虚拟化宿主机,现象很典型:宿主机物理内存128GB,上面跑了十来个KVM虚拟机,每个分配8GB,按理说总共也就80GB出头,但free -h一看,可用内存常年只剩几个GB,swap用得飞起,虚拟机内部还时不时卡顿。他第一反应是"内存不够,加条子",但我让他先别急着掏钱,因为KVM这套内存机制里,你分配给虚拟机的内存,和宿主机实际被占用的内存,压根不是一回事。

这就是我打算把KVM内存技术单独拿出来写一篇的原因。CPU虚拟化那套东西,讲来讲去核心就是VT-x、调度、vCPU绑定,逻辑相对线性;但内存这块,水要深得多——它牵扯到地址翻译、页表、内存回收、超分、气球、KSM、大页、NUMA亲和性等一大堆机制,而且这些机制之间还会互相影响。你如果只懂"给虚拟机配多少内存",那遇到性能问题基本只能靠猜。

这篇内容适合谁看?如果你正在用KVM做实验环境、私有云底座、或者单纯想搞明白虚拟化底层怎么玩内存,那这篇就是给你写的。我会从整体设计思路讲起,把每个核心机制的原理、参数、实操命令、踩坑经验都摊开说,尽量做到你照着就能复现、能调优。需要说明的是,文中涉及的具体参数取值和调优建议,一部分来自我自己的实操记录,一部分是基于KVM通用实践的合理补充,不同硬件和负载下表现会有差异,你落地时务必以自己环境的实测为准。

先把一个核心认知摆在前面:KVM的内存虚拟化,本质上是"多了一层地址翻译 + 一整套内存资源的动态调度"。理解了这句话,后面所有机制你都能找到它的位置。

2. 内存虚拟化的整体设计与核心思路拆解

2.1 从"影子页表"到"硬件辅助"的路线选择

要理解KVM内存为什么这么设计,得先回到一个根本问题:虚拟机里的程序用的是虚拟地址(VA),它以为自己在独占一整块连续内存;但宿主机上跑着好几个虚拟机,物理内存是共享的。所以从虚拟机的虚拟地址,到真正的物理内存,中间必须经过翻译。

在早期没有硬件支持的时候,这个翻译靠的是影子页表(Shadow Page Table)。简单说,就是Hypervisor帮每个虚拟机维护一份"虚拟地址直接到宿主机物理地址"的页表,虚拟机自己以为在操作自己的页表,实际上被Hypervisor偷偷换成了影子页表。这个方案能用,但问题很明显:虚拟机每次改自己的页表,Hypervisor都得同步更新影子页表,上下文切换和页表同步的开销非常大,而且实现复杂、容易出bug。

后来硬件厂商在CPU里加了**EPT(Intel)和NPT(AMD)**这类二级地址翻译机制,KVM就转向了硬件辅助方案。它的思路是:虚拟机里的页表还是老老实实做"虚拟地址→虚拟机物理地址(GPA)"的翻译,然后CPU硬件再自动做一层"虚拟机物理地址→宿主机物理地址(HPA)"的翻译。两层翻译都由硬件完成,Hypervisor基本不用插手,性能一下就上来了。

提示:现在你几乎不可能遇到需要手动开影子页表的场景了,现代CPU默认都支持EPT/NPT。但理解这段历史很有必要,因为它解释了为什么KVM内存的性能瓶颈往往不在"翻译"本身,而在"内存资源的调度"上。

2.2 内存超分:把"分配"和"占用"解耦

回到开头那个朋友的困惑。他给10个虚拟机各配8GB,为什么宿主机没有被占满80GB?因为KVM默认允许内存超分(Memory Overcommit)。

这里有个关键概念要分清:

  • 配置内存(Configured Memory):你给虚拟机配的、虚拟机自己看到的内存大小,比如8GB。
  • 常驻内存(RSS, Resident Set Size):宿主机上真正被这个虚拟机占用的物理内存。

虚拟机刚启动时,它并不会立刻把8GB全用上,可能只用了1GB。KVM配合Linux的**按需分页(Demand Paging)**机制,只有当虚拟机真正访问某块内存时,宿主机才分配对应的物理页。所以10个虚拟机配80GB,实际常驻可能只有30GB,这就是超分的价值——提高内存利用率,让物理内存能被更多虚拟机共享。

但超分是把双刃剑。如果所有虚拟机同时把内存用满,宿主机物理内存不够了,就会触发内存回收,甚至OOM Killer直接杀进程。所以超分必须配合下面这些机制一起用,否则就是给自己埋雷。

2.3 内存回收的几条路径

当宿主机内存吃紧时,KVM/Linux有几条回收路径,理解它们的优先级和适用场景很重要:

  1. Ballooning(气球驱动):通过virtio-balloon驱动,让虚拟机"主动"把暂时不用的内存还给宿主机。这是最温和的方式,因为它是虚拟机内部配合的。
  2. KSM(Kernel Samepage Merging,内核同页合并):扫描不同虚拟机的内存页,把内容完全相同的页合并成一份,写时复制。适合跑大量相同系统的场景。
  3. Swap:把不常用的内存页换到磁盘。这个对虚拟机性能影响最大,能不用就不用。
  4. OOM Killer:最后手段,直接杀进程。

这四条路径的"温和程度"是递减的,性能影响是递增的。一个健康的KVM环境,应该尽量在前两条路径上解决问题,而不是等到Swap和OOM。

2.4 为什么还要关心NUMA和大页

上面讲的是"资源调度"层面,还有两个"性能"层面的东西不能忽略:

  • NUMA(Non-Uniform Memory Access):在多路CPU的服务器上,CPU访问本地内存和远端内存的速度不一样。如果虚拟机的vCPU和它的内存跨了NUMA节点,性能会明显下降。所以KVM提供了NUMA亲和性绑定。
  • 大页(Huge Pages):传统内存页是4KB,页表项多、TLB命中率低。用2MB甚至1GB的大页,能显著减少页表开销、提升TLB命中率,对内存密集型负载(数据库、缓存)效果明显。

这两块是"锦上添花"型的优化,但一旦你的负载对内存性能敏感,它们就从"可选"变成"必调"。

3. 核心机制细节解析与实操要点

3.1 内存超分到底怎么配:从参数到验证

超分不是某个开关,而是一组行为的组合。核心在于Linux的vm.overcommit_memory参数和KVM自身的内存分配策略。

先看宿主机层面的参数:

# 查看当前超分策略 cat /proc/sys/vm/overcommit_memory

这个值有三个含义:

取值含义适用场景
0启发式超分,内核自己判断默认,一般够用
1总是允许超分想最大化密度,风险自担
2禁止超分,按比例限制追求稳定,牺牲密度

我个人的经验是:实验环境用0,生产环境如果内存不是特别紧张,也建议用0。用1虽然能塞更多虚拟机,但一旦集体爆发,OOM来得又快又狠。用2则太保守,超分的意义就没了。

再看虚拟机层面,virsh里有个关键配置:

<memory unit='GiB'>8</memory> <currentMemory unit='GiB'>8</currentMemory>

memory是最大内存,currentMemory是启动时分配的内存。如果你想让虚拟机支持热插拔内存,可以把memory设大、currentMemory设小,启动后再按需加。但注意,热插拔内存需要虚拟机内部配合,且不是所有Guest OS都支持。

验证超分效果,最直接的是看宿主机的内存和每个虚拟机的RSS:

# 看宿主机整体 free -h # 看每个qemu进程实际占用的物理内存 ps -eo pid,rss,comm | grep qemu

注意:ps里的RSS是包含共享内存的,如果开了KSM,多个虚拟机的共享页会被重复计算。更准确的方式是看/proc/<pid>/smaps里的Pss(按比例分摊的共享内存)。

3.2 Ballooning:让虚拟机"主动吐内存"

气球驱动是我最推荐的内存回收方式,因为它足够温和。原理很形象:在虚拟机内部装一个"气球",Hypervisor想让虚拟机还内存时,就把气球吹大,气球占用的内存页在虚拟机看来是"被占用了",但实际上这些页被Hypervisor回收给了宿主机;想让虚拟机多用内存时,就把气球放小。

配置上分两步。第一步,虚拟机XML里加上balloon设备:

<devices> <memballoon model='virtio'> <stats period='10'/> </memballoon> </devices>

stats period是统计周期,单位秒,方便你监控气球状态。第二步,虚拟机内部要装virtio-balloon驱动,现代Linux内核基本都自带,Windows需要装virtio驱动包。

配置好之后,可以用virsh动态调整:

# 把虚拟机内存收缩到4GB(气球吹大) virsh setmem <vm-name> 4G --live # 再放回8GB virsh setmem <vm-name> 8G --live

这里有个大坑:setmem只能把内存调到不超过memory配置的值,而且如果虚拟机内部没有balloon驱动,这个命令会失败或者无效。我见过有人调了半天没反应,最后发现是Guest里根本没装驱动。

还有一个经验:气球回收的内存,虚拟机内部是感知不到的。也就是说,你在虚拟机里free -h看到的可用内存,可能和宿主机实际给它的不一致。这在排查问题时容易误导人,要心里有数。

3.3 KSM:同页合并的收益与代价

KSM的思路很聪明:宿主机上跑10个一模一样的Linux虚拟机,它们的内核代码、系统库、甚至很多只读数据页,内容是完全相同的。KSM定期扫描这些页,发现内容一致就合并成一份,所有虚拟机共享,谁要写就复制一份出来(Copy-on-Write)。

开启KSM:

# 启动KSM服务 systemctl start ksmtuned systemctl enable ksmtuned # 查看KSM状态 cat /sys/kernel/mm/ksm/run # 1表示开启 cat /sys/kernel/mm/ksm/pages_shared # 已合并的页数 cat /sys/kernel/mm/ksm/pages_sharing # 共享页被引用的次数

ksmtuned是个守护进程,会根据内存压力自动调整KSM的扫描频率。你也可以手动调:

# 设置扫描速度(页/秒),0表示暂停 echo 1000 > /sys/kernel/mm/ksm/pages_to_scan # 设置两次扫描间隔(毫秒) echo 20 > /sys/kernel/mm/ksm/sleep_millisecs

KSM的收益在"大量同构虚拟机"场景下非常可观,我实测过跑20个相同镜像的虚拟机,内存能省下30%以上。但代价也有:

  • CPU开销:扫描和比对内存页要消耗CPU,虚拟机越多、内存越大,开销越明显。
  • 延迟抖动:COW机制在写入共享页时会触发复制,可能带来微小的延迟抖动,对延迟极度敏感的负载要谨慎。

提示:KSM对"内容相同"的判定是逐字节比对,所以它对同构系统效果好,对异构系统(比如一个跑数据库、一个跑Web)基本没收益。别指望它万能。

3.4 大页配置:透明大页 vs 静态大页

大页分两种,很多人搞混:

  • 透明大页(THP, Transparent Huge Pages):内核自动管理,对应用透明,你什么都不用做它就可能用上2MB大页。方便,但行为不可控,有时反而带来延迟抖动。
  • 静态大页(HugeTLB):需要你手动预留,虚拟机明确使用。可控性强,适合对性能要求高的场景。

先看THP:

cat /sys/kernel/mm/transparent_hugepage/enabled # 可能的值:[always] madvise never

对KVM来说,THP的always模式有时会导致内存回收困难(大页不容易被换出),所以很多调优指南建议设成madvise,让KVM自己决定。我一般这么设:

echo madvise > /sys/kernel/mm/transparent_hugepage/enabled

再看静态大页。假设你要给虚拟机预留大页,先算需要多少:

# 假设要预留8GB的2MB大页 # 8GB / 2MB = 4096 个 echo 4096 > /proc/sys/vm/nr_hugepages # 验证 cat /proc/meminfo | grep HugePages

然后在虚拟机XML里指定使用大页:

<memoryBacking> <hugepages> <page size='2' unit='MiB'/> </hugepages> </memoryBacking>

这里有个关键计算:大页必须提前预留,而且预留后普通进程用不了。如果你预留了4096个2MB大页(8GB),那这8GB就从宿主机可用内存里"扣掉"了。所以预留多少要算准,预留多了浪费,少了虚拟机起不来。

我踩过的坑:有一次预留大页后忘了改nr_hugepages的持久化配置,重启后大页没了,虚拟机全部启动失败。所以记得写进/etc/sysctl.conf:

echo "vm.nr_hugepages = 4096" >> /etc/sysctl.conf

3.5 NUMA亲和性:别让vCPU跨节点访问内存

在多路服务器上,NUMA是个绕不开的话题。你可以先用numactl看看拓扑:

numactl --hardware

输出会告诉你几个NUMA节点、每个节点的CPU和内存。理想情况下,虚拟机的vCPU和它的内存应该落在同一个节点上。

KVM提供了几种NUMA策略,在XML里配置:

<numatune> <memory mode='strict' nodeset='0'/> </numatune> <cpu> <numa> <cell id='0' cpus='0-3' memory='8' unit='GiB'/> </numa> </cpu>

mode='strict'表示严格绑定到节点0,如果节点0内存不够就直接失败,不会退而求其次用远端内存。这个策略适合对性能要求高的场景。如果追求灵活性,可以用preferred,优先用指定节点,不够再用别的。

注意:NUMA绑定和超分是有冲突的。如果你严格绑定NUMA节点,那这个节点的内存就不能被超分到别的节点去,密度会下降。所以NUMA调优本质上是"性能换密度"的取舍。

4. 完整实操流程:从零调优一台KVM宿主机的内存

4.1 环境确认与基线采集

动手之前,先摸清家底。这一步很多人跳过,结果调优全靠拍脑袋。

# 1. 看CPU和NUMA拓扑 lscpu | grep -i numa numactl --hardware # 2. 看内存总量和当前使用 free -h cat /proc/meminfo | head -20 # 3. 看当前超分策略和THP状态 cat /proc/sys/vm/overcommit_memory cat /sys/kernel/mm/transparent_hugepage/enabled # 4. 看现有虚拟机的内存配置和实际占用 for vm in $(virsh list --name); do echo "=== $vm ===" virsh dominfo $vm | grep -i memory done ps -eo pid,rss,comm | grep qemu

把这些数据记下来,作为调优前的基线。没有基线,你根本不知道调优有没有效果。

4.2 分场景的参数配置方案

不同负载对内存的需求完全不同,我给三套典型配置,你可以对号入座。

场景一:实验/测试环境,追求密度

特点是虚拟机多、负载轻、可以容忍偶尔卡顿。配置思路是激进超分 + KSM。

# 允许超分 echo 0 > /proc/sys/vm/overcommit_memory # 开启KSM并调高扫描频率 systemctl start ksmtuned echo 2000 > /sys/kernel/mm/ksm/pages_to_scan echo 10 > /sys/kernel/mm/ksm/sleep_millisecs # THP设为madvise echo madvise > /sys/kernel/mm/transparent_hugepage/enabled

虚拟机层面,给每个虚拟机配balloon设备,但不预留大页。

场景二:生产Web/应用服务器,追求稳定

特点是负载中等、要求稳定、不能随便OOM。配置思路是温和超分 + 气球回收 + 适度大页。

# 启发式超分 echo 0 > /proc/sys/vm/overcommit_memory # KSM保持默认,不激进扫描 systemctl start ksmtuned # 预留适量静态大页 echo 2048 > /proc/sys/vm/nr_hugepages

虚拟机层面,配balloon + 静态大页 + NUMA绑定。

场景三:数据库/内存密集型,追求性能

特点是内存消耗大、对延迟敏感、不能超分。配置思路是不超分 + 大页 + 严格NUMA。

# 禁止超分 echo 2 > /proc/sys/vm/overcommit_memory # 关闭KSM(避免COW抖动) systemctl stop ksmtuned echo 0 > /sys/kernel/mm/ksm/run # 大量静态大页 echo 16384 > /proc/sys/vm/nr_hugepages

虚拟机层面,静态大页 + 严格NUMA绑定 + 关闭balloon(避免内存被回收)。

4.3 关键参数的验证与压测

配完不是就完事了,得验证。我一般用这几个手段:

# 1. 确认大页生效 cat /proc/meminfo | grep -i huge # 2. 确认虚拟机用了大页(看qemu进程的smaps) grep -i huge /proc/<qemu-pid>/smaps | head # 3. 确认NUMA绑定生效 virsh numatune <vm-name> # 4. 确认balloon工作 virsh dommemstat <vm-name>

压测方面,如果条件允许,用stress-ng在虚拟机内部压内存:

# 在虚拟机内部跑,压8GB内存 stress-ng --vm 4 --vm-bytes 2G --timeout 300s

同时观察宿主机的free、vmstat、以及KSM的pages_sharing变化。这样你能直观看到调优前后的差异。

4.4 监控指标的持续采集

调优不是一锤子买卖,得持续看。我习惯用一套简单的脚本定期采集:

#!/bin/bash # kvm-mem-monitor.sh echo "=== $(date) ===" echo "宿主机内存:" free -h | grep -E "Mem|Swap" echo "KSM共享页: $(cat /sys/kernel/mm/ksm/pages_sharing)" echo "大页使用:" grep -i huge /proc/meminfo echo "各虚拟机RSS:" ps -eo pid,rss,comm | grep qemu

丢进crontab每5分钟跑一次,日志留着,出问题时回溯特别有用。

5. 常见问题与排查技巧实录

5.1 虚拟机内存"看起来够用"但实际卡顿

这是最典型的"超分后遗症"。虚拟机内部free显示还有内存,但宿主机已经内存紧张,触发了swap或者气球回收,导致虚拟机实际拿不到内存。

排查思路:

# 1. 先看宿主机整体压力 vmstat 1 5 # 关注si/so(swap换入换出),如果非0,说明在swap # 2. 看气球状态 virsh dommemstat <vm-name> # 关注available和rss,如果available远小于配置值,说明气球被吹大了 # 3. 看KSM是否过度合并 cat /sys/kernel/mm/ksm/pages_sharing

解决办法:要么给宿主机加内存,要么降低超分比例,要么把关键虚拟机的balloon关掉、内存锁定。

5.2 大页预留后虚拟机启动失败

报错通常是"cannot allocate memory"或者"hugepages not available"。原因基本是预留的大页不够,或者被别的进程占了。

# 看实际可用大页 cat /proc/meminfo | grep -i huge # HugePages_Free 是真正可用的

如果HugePages_Free小于虚拟机需要的数量,就得增加nr_hugepages。注意,增加大页需要宿主机有足够的连续物理内存,如果内存碎片化严重,可能加不上去,这时候得重启宿主机。

5.3 KSM开了但没效果

先确认KSM真的在跑:

cat /sys/kernel/mm/ksm/run # 必须是1

如果pages_sharing一直是0,可能是:

  • 虚拟机内存内容差异太大(异构系统),没有可合并的页。
  • pages_to_scan设得太小,扫描太慢。
  • 内存压力不大,ksmtuned没触发扫描。

可以手动调高扫描频率观察:

echo 5000 > /sys/kernel/mm/ksm/pages_to_scan sleep 30 cat /sys/kernel/mm/ksm/pages_sharing

5.4 NUMA绑定后性能反而下降

这种情况通常是因为绑定策略太激进,导致某个节点内存不够,虚拟机被迫用swap。检查:

virsh numatune <vm-name> numactl --hardware # 看各节点内存余量

如果节点内存确实不够,要么放宽策略(用preferred),要么把虚拟机迁到内存充足的节点。

5.5 常见问题速查表

现象可能原因排查命令解决方向
虚拟机卡顿但内部内存充足宿主机超分过度/swapvmstat 1降超分、加内存
虚拟机启动失败大页不足grep Huge /proc/meminfo增加nr_hugepages
KSM无收益异构负载/扫描慢cat pages_sharing调扫描频率或放弃
NUMA绑定后变慢节点内存不足numactl --hardware放宽策略
气球调不动Guest无驱动virsh dommemstat装virtio-balloon驱动
内存回收导致抖动THP always + 回收cat thp/enabled改madvise

5.6 几条压箱底的经验

第一,超分比例不要超过1.5倍。这是我踩过坑之后的保守建议。1.5倍以内,配合气球和KSM,基本能稳住;超过2倍,OOM就是迟早的事。

第二,关键虚拟机一定要锁内存。在XML里加:

<memoryBacking> <locked/> </memoryBacking>

这样虚拟机内存不会被swap出去,代价是宿主机不能超分这部分内存。数据库、缓存类虚拟机强烈建议加。

第三,监控比调优更重要。我见过太多人调完参数就不管了,结果负载一变,之前的配置全失效。持续采集pages_sharing、HugePages_Free、各虚拟机RSS这几个指标,比任何一次性调优都管用。

第四,别迷信"最佳实践"。网上很多KVM内存调优的文章,参数都是抄来抄去,但你的硬件、负载、内核版本都不一样。我上面给的参数是起点,不是终点,一定要在自己的环境里压测验证。

最后分享一个我常用的快速诊断组合,遇到内存问题先跑这三条:

free -h && echo "---" && vmstat 1 3 && echo "---" && cat /sys/kernel/mm/ksm/pages_sharing

宿主机内存、swap活动、KSM共享量,三个信息一摆出来,问题基本就定位到方向了。剩下的就是顺着方向深挖,比盲目加内存靠谱得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 1:45:56

iText高清PDF图像保真:绕过自动压缩与缩放的底层控制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:45:43

QT+FFMPEG播放器开发:解码链路核心实现与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:45:37

AI应用层人才缺口暴涨!Java开发者转型AI

9月12日&#xff0c;Anthropic的CEO达里奥阿莫迪发了一篇长文&#xff0c;标题叫《We Must Pace the Frontier》&#xff0c;核心意思就一句话&#xff1a;AI行业该踩一脚刹车了&#xff0c;安全措施得跟上技术发展的速度。 消息一出&#xff0c;硅谷炸了。OpenAI的奥特曼、马斯…

作者头像 李华
网站建设 2026/10/15 1:44:56

MDK警告L6989W:分散加载文件执行区为空的排查与修复

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:44:38

基于MP2642与PIC32的双串磷酸铁锂电池均衡系统设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:44:36

JFinal实战教程:从设计哲学到企业级架构演进

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华