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有几条回收路径,理解它们的优先级和适用场景很重要:
- Ballooning(气球驱动):通过
virtio-balloon驱动,让虚拟机"主动"把暂时不用的内存还给宿主机。这是最温和的方式,因为它是虚拟机内部配合的。 - KSM(Kernel Samepage Merging,内核同页合并):扫描不同虚拟机的内存页,把内容完全相同的页合并成一份,写时复制。适合跑大量相同系统的场景。
- Swap:把不常用的内存页换到磁盘。这个对虚拟机性能影响最大,能不用就不用。
- 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_millisecsKSM的收益在"大量同构虚拟机"场景下非常可观,我实测过跑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.conf3.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_sharing5.4 NUMA绑定后性能反而下降
这种情况通常是因为绑定策略太激进,导致某个节点内存不够,虚拟机被迫用swap。检查:
virsh numatune <vm-name> numactl --hardware # 看各节点内存余量如果节点内存确实不够,要么放宽策略(用preferred),要么把虚拟机迁到内存充足的节点。
5.5 常见问题速查表
| 现象 | 可能原因 | 排查命令 | 解决方向 |
|---|---|---|---|
| 虚拟机卡顿但内部内存充足 | 宿主机超分过度/swap | vmstat 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共享量,三个信息一摆出来,问题基本就定位到方向了。剩下的就是顺着方向深挖,比盲目加内存靠谱得多。