1. 从一次虚拟机集体卡顿说起内存虚拟化的真实分量去年帮一个做后端开发的朋友排查他的测试环境他抱怨说宿主机上跑了四台虚拟机只要其中一台编译代码另外三台就像被点了穴一样SSH 敲个命令都要等两三秒才回显。他第一反应是 CPU 不够准备加核。我让他先别急着加硬件把virsh dominfo和virsh nodeinfo的输出发过来一看宿主机 32GB 内存四台虚拟机各分了 8GB加起来正好 32GB宿主机自己一点余量都没留。问题根本不在 CPU而在内存——宿主机为了给虚拟机凑够内存已经在疯狂地做内存回收和换页磁盘 I/O 被打满所有虚拟机跟着遭殃。这件事特别典型。很多人接触 KVM 虚拟化注意力几乎全在 CPU 和磁盘上内存这块往往就是“给个数字就完事”。但恰恰是内存决定了你的虚拟化环境是“稳如老狗”还是“一碰就崩”。CPU 超分一点顶多是慢内存超分不当轻则性能雪崩重则触发 OOM虚拟机被内核直接干掉。这篇就专门聊 KVM 的内存技术。我会从内存虚拟化的底层机制讲起把 EPT、影子页表、气球驱动、KSM、大页、NUMA 这些概念一个个拆开讲清楚它们各自解决什么问题、在什么场景下该开、开了之后怎么验证效果。内容偏实战参数和命令都是可以直接抄的也会穿插一些我踩过的坑。如果你正在维护 KVM 环境或者准备自己搭一套虚拟化平台这篇应该能帮你少走不少弯路。2. 虚拟机眼里的内存和宿主机眼里的内存根本不是一回事2.1 地址翻译的三层结构GVA、GPA、HPA要理解 KVM 内存技术先得把三个地址概念分清楚这是所有后续讨论的地基。虚拟机里运行的进程用的是客户机虚拟地址GVAGuest Virtual Address。这个地址经过客户机操作系统自己的页表翻译变成客户机物理地址GPAGuest Physical Address。注意这个“物理地址”是虚拟机以为自己拥有的物理地址它并不是真正的硬件物理地址。而宿主机上真正对应到内存条颗粒的地址叫宿主机物理地址HPAHost Physical Address。KVM 要做的核心工作之一就是把 GPA 再翻译成 HPA。所以完整链路是GVA →客户机页表→ GPA →KVM 的二级翻译→ HPA。第一层翻译由客户机内核负责KVM 管不着第二层翻译才是 KVM 内存虚拟化的主战场。2.2 影子页表早期方案为什么被淘汰最早 KVM 用的是影子页表Shadow Page Table。思路很直接既然客户机有自己的页表宿主机也有一套 MMU 用的页表那 KVM 就帮客户机“抄”一份把 GVA 直接映射到 HPA让硬件 MMU 直接用这份影子页表。这样一次翻译就到位了看起来很美。但问题在于维护成本。客户机每修改一次自己的页表KVM 都得同步更新对应的影子页表。客户机操作系统换页、缺页、进程切换这些操作极其频繁每一次都可能触发 KVM 的陷入VM Exit从客户机模式切到宿主机模式去处理。进程越多、内存访问越密集这个开销就越夸张。我早年在一台老服务器上跑影子页表模式虚拟机里开个稍微重一点的数据库CPU 的 system 时间直接飙到 40% 以上全耗在页表同步上了。2.3 EPT 与 NPT硬件辅助翻译才是正解后来 Intel 推出EPTExtended Page TableAMD 对应的是NPTNested Page Table思路变了既然软件维护影子页表太累那就让硬件来干这第二层翻译。CPU 的 MMU 直接支持两级页表——客户机页表负责 GVA 到 GPAEPT 负责 GPA 到 HPA硬件自动完成嵌套翻译不需要 KVM 频繁介入。这个改变是革命性的。开启 EPT 后页表同步的陷入几乎消失内存密集型应用的性能提升非常明显。判断你的环境有没有开 EPT可以这样查# 检查 CPU 是否支持 EPTIntel或 NPTAMD grep -o -E ept|npt /proc/cpuinfo | sort -u # 查看 KVM 模块是否启用了 EPT cat /sys/module/kvm_intel/parameters/ept # 输出 Y 表示已启用如果输出是N或者压根没这个文件那你的虚拟机大概率还在用影子页表性能损失是实打实的。这种情况通常出现在很老的 CPU 上或者 BIOS 里没打开虚拟化相关的硬件辅助功能。我建议先确认硬件支持再检查 BIOS 设置最后看内核模块参数一层层往下排。提示EPT 和 NPT 是硬件能力不是软件开关。CPU 不支持的话任何配置都开不出来。买服务器做虚拟化之前务必确认这一项。3. 内存超分这件事气球驱动和 KSM 到底该怎么配合3.1 内存超分的诱惑与风险宿主机 64GB 内存想跑十台虚拟机每台分 8GB加起来 80GB超了。但实际运行时十台虚拟机不可能同时把 8GB 用满可能平均只用 3GB。于是就有了内存超分Memory Overcommit允许分配给虚拟机的内存总和超过宿主机物理内存靠运行时动态回收来兜底。超分能大幅提高硬件利用率但它是把双刃剑。回收机制跟不上分配速度就会触发宿主机 OOM Killer随机杀进程虚拟机可能直接消失。所以超分必须配合可靠的内存回收手段KVM 主要提供两个气球驱动Ballooning和KSMKernel Samepage Merging。3.2 气球驱动让虚拟机“主动”吐内存气球驱动的原理很形象。在虚拟机里装一个叫virtio_balloon的驱动它就像一个可以充气放气的气球。宿主机内存紧张时通过 virtio 通道告诉虚拟机“把气球充大一点。”虚拟机收到指令就向自己的内核申请一批内存页交给气球驱动。这些页在虚拟机内部被标记为“已占用”但实际上宿主机可以把它们回收给别的虚拟机用。等宿主机内存宽裕了再让气球放气把内存还给虚拟机。配置气球驱动分两步。宿主机这边虚拟机 XML 里要有 balloon 设备devices memballoon modelvirtio stats period10/ /memballoon /devices虚拟机内部要加载驱动Linux 一般自带。然后可以用virsh动态调整# 查看当前气球大小和实际可用内存 virsh dommemstat vm01 # 把虚拟机目标内存调整为 4GB从 8GB 压缩 virsh setmem vm01 4G --live # 恢复 virsh setmem vm01 8G --live这里有个关键点setmem调整的是虚拟机的当前可用内存不是最大内存。虚拟机启动时配置的maxMemory才是上限。如果启动时没设maxMemory动态调大就会失败。我踩过这个坑虚拟机跑起来想临时加内存结果报错说超出最大值只能关机改配置重启。气球驱动的局限也很明显它依赖虚拟机内部的驱动配合。如果虚拟机里驱动没装、或者被禁用宿主机就指挥不动它。而且气球回收内存是“温和”的虚拟机内部会感知到内存减少如果它自己内存压力也大可能拒绝配合。所以气球适合“虚拟机实际用量远低于分配量”的场景不适合硬压。3.3 KSM把相同的内存页合并成一份KSM 走的是另一条路。它扫描宿主机上所有进程包括虚拟机的内存找出内容完全相同的页面把它们合并成一份只读页多个虚拟机共享。一旦某个虚拟机要写这个页再触发写时复制COW给它单独复制一份。这个机制在什么场景下最有效同一批用同一个镜像克隆出来的虚拟机。比如你开了十台 CentOS 虚拟机做测试它们的内核代码、系统库、甚至很多空闲页内容都是一样的KSM 能把这些重复页合并掉省下大量内存。我实测过十台同镜像的虚拟机开启 KSM 后宿主机内存占用能降 30% 到 40%。开启 KSM 的步骤# 检查 KSM 是否可用 cat /sys/kernel/mm/ksm/run # 0 表示关闭1 表示开启 # 开启 KSM echo 1 /sys/kernel/mm/ksm/run # 调整扫描频率页/秒默认 100调大可加快合并但吃 CPU echo 1000 /sys/kernel/mm/ksm/pages_to_scan # 查看合并效果 cat /sys/kernel/mm/ksm/pages_shared cat /sys/kernel/mm/ksm/pages_sharing cat /sys/kernel/mm/ksm/pages_sharingpages_sharing表示被共享的页数乘以页大小通常 4KB就是省下的内存。pages_shared是实际保留的共享页数。但 KSM 不是没有代价。它要持续扫描内存做哈希比对吃 CPU合并后的页一旦被写要触发 COW有额外开销。所以对性能极度敏感、内存又充足的场景我一般不开 KSM。反过来内存紧张、虚拟机同质化高的测试环境KSM 是救命的。3.4 气球和 KSM 的配合策略这两个机制不是二选一而是配合使用。我的经验是KSM 做常态化的内存压缩气球做应急的动态调节。日常让 KSM 开着把重复页合并掉宿主机内存水位自然就降下来了。当内存还是不够时再用气球从“用量虚高”的虚拟机那里回收。判断哪台虚拟机用量虚高看virsh dommemstat里的available和rss对比available 远大于 rss 的就是可以压的对象。机制触发方式回收粒度CPU 开销适用场景气球驱动宿主机主动指令整块内存低虚拟机用量虚高、驱动可用KSM内核持续扫描相同页合并中同质化虚拟机、测试环境交换Swap内核自动页级高兜底尽量避免注意宿主机上的 swap 是最后一道防线不是常规手段。一旦虚拟机内存被换到 swap性能会断崖式下跌。宁可少开几台虚拟机也别让宿主机频繁 swap。4. 大页与 NUMA让内存访问少绕几个弯4.1 透明大页和显式大页的区别CPU 访问内存时每次都要查页表做地址翻译。页表本身也存在内存里翻译一次可能要访问好几级页表。页越大同样大小的内存需要的页表项就越少翻译开销就越低TLB旁路转换缓冲的命中率也越高。传统页大小是 4KB而大页Huge Page通常是 2MB 或 1GB。2MB 大页相比 4KB 页页表项数量减少到 1/512TLB 能覆盖的内存范围大得多。对内存密集型应用比如数据库、缓存服务大页带来的性能提升很可观。Linux 有两种大页机制透明大页THPTransparent Huge Pages和显式大页HugeTLB。THP 是内核自动管理的对应用透明但它的分配和回收时机不好控制可能造成延迟抖动。显式大页需要手动预留应用显式申请可控性强适合对延迟敏感的场景。KVM 虚拟机用大页推荐用显式大页。配置方法# 预留 2MB 大页比如预留 4096 个即 8GB echo 4096 /proc/sys/vm/nr_hugepages # 查看预留情况 grep HugePages /proc/meminfo然后在虚拟机 XML 里指定用大页memoryBacking hugepages page size2 unitM/ /hugepages /memoryBacking这样虚拟机的内存就来自预留的大页池不会走 THP 的自动路径。4.2 大页的代价预留了就占着大页不是白拿的。你预留了 8GB 大页这 8GB 就固定占住了别的进程用不了哪怕虚拟机没跑起来。所以预留多少要算清楚预留过多浪费内存预留过少虚拟机起不来。我一般这样估算把所有需要大页的虚拟机的内存加起来再留 10% 余量。比如三台虚拟机各 4GB那就预留 12GB 加 1.2GB约 13GB换算成 2MB 大页就是 6656 个。另外大页不支持 swap也不支持气球驱动回收。用了大页的虚拟机内存就是实打实占住的超分能力基本丧失。所以大页和超分是两条路选了大页就别指望超分选了超分就别用大页。这个取舍要在规划阶段就想清楚。4.3 NUMA 架构下内存分配不能乱来现代服务器基本都是NUMA非统一内存访问架构。简单说CPU 和内存被分成多个节点Node每个 CPU 访问自己节点内的内存快访问别的节点的内存慢。跨节点访问的延迟可能是本地访问的一倍以上。如果虚拟机用的 vCPU 和内存跨了 NUMA 节点性能就会受影响。KVM 提供了 NUMA 绑定能力可以把虚拟机的 vCPU 和内存固定到同一个物理节点。查看宿主机 NUMA 拓扑numactl --hardware # 或者 lscpu | grep NUMA输出会显示有几个节点、每个节点的 CPU 和内存分布。然后可以在虚拟机 XML 里做绑定vcpu placementstatic4/vcpu cputune vcpupin vcpu0 cpuset0/ vcpupin vcpu1 cpuset1/ vcpupin vcpu2 cpuset2/ vcpupin vcpu3 cpuset3/ /cputune numatune memory modestrict nodeset0/ /numatunememory modestrict表示内存只从节点 0 分配配合 vCPU 也绑在节点 0 的 CPU 上这样虚拟机内部的内存访问就都是本地的。这里有个容易忽略的点虚拟机的内存大小要和 NUMA 节点的内存容量匹配。如果节点 0 只有 16GB 内存你给虚拟机分 20GB 还绑到节点 0就会分配失败。所以绑定前先看清楚每个节点的内存容量。4.4 大页加 NUMA 的组合效果大页和 NUMA 绑定一起用效果是叠加的。大页减少页表翻译开销NUMA 绑定减少跨节点访问延迟两个都是针对内存访问路径的优化。我在一台双路服务器上做过对比测试一个内存密集型的缓存服务默认配置下 QPS 是 8 万左右开了 2MB 大页加 NUMA 绑定后QPS 涨到 11 万提升接近 40%。当然这个数字因负载而异但方向是明确的。不过要提醒一句这些优化都有前提。如果你的虚拟机本身内存用量不大或者负载不是内存密集型开大页和 NUMA 绑定的收益就很有限反而增加了管理复杂度。优化要针对瓶颈来别为了优化而优化。5. 内存回收的暗坑OOM、swap 抖动和 KSM 的 CPU 代价5.1 宿主机 OOM 是怎么发生的宿主机 OOM 的触发链条通常是这样的内存超分 → 虚拟机实际用量上涨 → 宿主机可用内存下降 → 内核开始回收先回收缓存再 swap再气球→ 回收速度跟不上分配速度 → 可用内存跌破水位线 → OOM Killer 启动 → 挑一个“分数最高”的进程杀掉。问题是OOM Killer 挑进程是按oom_score来的它可不管哪个是虚拟机。有时候它杀掉的是虚拟机的某个 vCPU 线程虚拟机直接崩溃有时候杀掉的是宿主机上的管理进程整个管理面板失联。更糟的是OOM 发生时系统已经卡得不行你连登录排查都困难。预防 OOM核心是控制超分比例和设置内存水位告警。我的经验是生产环境超分比例不要超过 1.5:1也就是 64GB 内存最多分配 96GB 给虚拟机。测试环境可以激进一点2:1 甚至 3:1但要接受随时可能出问题的风险。监控方面重点看这几个指标# 宿主机可用内存 free -h # 各虚拟机的内存统计 virsh dommemstat vm01 # KSM 合并情况 cat /sys/kernel/mm/ksm/pages_sharing # swap 使用情况 vmstat 1 5vmstat里的si和so两列是 swap 换入换出如果持续非零说明内存压力已经很大了得赶紧处理。5.2 swap 抖动为什么比 OOM 更折磨人OOM 是一刀切疼但干脆。swap 抖动是钝刀子割肉系统不会崩但会慢到让你怀疑人生。当宿主机内存不足内核把虚拟机的内存页换到 swap 分区。虚拟机访问这些页时又触发缺页中断从 swap 读回来。如果虚拟机的活跃内存大于物理内存就会陷入“换出-换入-再换出”的死循环磁盘 I/O 被 swap 打满所有虚拟机跟着卡。我见过最惨的一次宿主机 128GB 内存跑了二十多台虚拟机超分到 200GBswap 设了 64GB。结果 swap 被用满si/so常年几十万整个宿主机 load average 冲到 200 以上SSH 都连不进去。最后只能硬重启丢了一批测试数据。避免 swap 抖动最根本的还是别超分太狠。如果实在要超分把宿主机的vm.swappiness调低# 默认 60调到 10 表示尽量不用 swap echo 10 /proc/sys/vm/swappiness这样内核会优先回收缓存而不是换出内存页能缓解一些。但记住这只是缓解不是解决。内存不够就是不够调参数救不了。5.3 KSM 的 CPU 开销什么时候会咬人KSM 省内存但扫描和比对是要吃 CPU 的。默认pages_to_scan是 100也就是每秒扫描 100 个页这个量很小CPU 开销几乎可以忽略。但如果你为了加快合并把pages_to_scan调到几千甚至几万CPU 开销就上来了。我做过一个测试把pages_to_scan设成 10000在一台 32 核的服务器上KSM 内核线程ksmd的 CPU 占用能到 5% 到 8%。如果服务器本身 CPU 就紧张这个开销就不能忽视了。更隐蔽的问题是 KSM 的 COW 开销。合并后的页是只读的任何虚拟机要写这个页都得先复制一份。如果被合并的页恰好是高频写的页COW 就会频繁触发性能反而下降。KSM 有merge_across_nodes和smart_scan等参数可以调优但根本上还是要看负载特征。我的建议是KSM 开着但pages_to_scan别调太高默认或稍微调大即可。同时监控ksmd的 CPU 占用如果持续超过 5%就得考虑是不是合并收益不值这个开销了。5.4 一个真实的内存回收排查案例回到开头那个朋友的案例。四台虚拟机各 8GB宿主机 32GB没有余量。我让他先做了几件事第一步看宿主机内存和 swap 状态。free -h显示可用内存只剩几百 MBswap 用了 2GB 多vmstat的si/so持续非零。确认是内存压力导致的卡顿。第二步看各虚拟机的实际用量。virsh dommemstat显示四台虚拟机的rss分别是 3GB、2.5GB、4GB、3.5GB加起来 13GB远低于分配的 32GB。说明虚拟机分配的内存大量闲置但宿主机却因为“分配了就不能动”而紧张。第三步开 KSM。四台虚拟机是同一个镜像克隆的重复页很多。开启 KSM 并把pages_to_scan调到 500 后pages_sharing涨到 80 多万省下约 3GB 内存。宿主机可用内存回到 3GB 以上swap 不再增长。第四步用气球驱动进一步压缩。把用量最低的那台虚拟机从 8GB 压到 4GB又释放出 4GB。至此宿主机内存充裕卡顿消失。这个案例的教训是内存分配要留余量超分要配合回收机制回收机制要根据虚拟机特征选。如果一开始就只分配 4GB 而不是 8GB根本不会有这个问题。分配内存时手松后面就要用各种机制去补何必呢。6. 把内存参数调对一份可落地的配置清单6.1 虚拟机 XML 里的内存相关字段KVM 虚拟机的内存配置集中在 XML 的几个地方我把关键字段列出来方便对照domain typekvm !-- 最大内存热插拔上限 -- maxMemory slots16 unitKiB16777216/maxMemory !-- 启动时分配的内存 -- memory unitKiB8388608/memory !-- 当前内存可动态调整 -- currentMemory unitKiB8388608/currentMemory memoryBacking !-- 使用大页 -- hugepages page size2 unitM/ /hugepages !-- 锁定内存防止被 swap -- locked/ /memoryBacking numatune memory modestrict nodeset0/ /numatune devices memballoon modelvirtio stats period10/ /memballoon /devices /domain几个要点maxMemory一定要设否则动态加内存会失败locked配合大页用防止内存被换出memballoon的stats period设小一点监控数据更及时。6.2 宿主机内核参数的调整宿主机这边几个内核参数对内存行为影响很大# 降低 swap 倾向 echo 10 /proc/sys/vm/swappiness # 控制脏页比例避免大量脏页堆积导致 I/O 尖峰 echo 10 /proc/sys/vm/dirty_ratio echo 5 /proc/sys/vm/dirty_background_ratio # 开启 KSM echo 1 /sys/kernel/mm/ksm/run echo 500 /sys/kernel/mm/ksm/pages_to_scan # 预留大页 echo 6656 /proc/sys/vm/nr_hugepages这些参数要持久化的话写进/etc/sysctl.conf和/etc/rc.local或者用 systemd 服务管理。6.3 监控指标和告警阈值内存这块我建议至少监控这几个指标并设好告警指标查看方式告警阈值说明宿主机可用内存free -h低于总内存 10%内存压力预警swap 换入换出vmstat的 si/so持续大于 0内存严重不足KSM 合并页数pages_sharing突降可能虚拟机负载变化虚拟机 rssvirsh dommemstat接近分配值该虚拟机内存吃紧OOM 计数dmesggrep -i oom出现即告警监控工具用 Prometheus 加 node_exporter 和 libvirt_exporter 就能覆盖大部分配置起来也不复杂。6.4 不同场景的配置模板最后给几个场景的配置建议可以直接参考场景一生产环境内存充足追求稳定不超分每台虚拟机分配内存不超过实际需求开启大页和 NUMA 绑定关闭 KSM避免 CPU 开销和 COW 抖动气球驱动保留但不主动压缩swappiness 设为 10场景二测试环境内存紧张虚拟机同质化高超分比例 2:1 左右开启 KSMpages_to_scan 设 500 到 1000开启气球驱动定期回收闲置内存不用大页大页和超分冲突监控 OOM 和 swap设好告警场景三开发环境虚拟机频繁创建销毁适度超分1.5:1KSM 开启合并效果明显气球驱动开启配合自动化脚本回收内存分配留 20% 余量定期清理僵尸虚拟机内存虚拟化这块说到底就是“分配、翻译、回收”三件事。分配要留余量翻译要靠硬件辅助回收要选对机制。把这三点吃透KVM 环境的内存问题基本就能拿捏住了。我在实际运维中最大的体会是内存问题从来不是突然出现的它是一点点积累出来的。今天多分 1GB明天多开一台虚拟机等到 OOM 那天才反应过来已经晚了。平时把监控做好把水位盯住比出事后任何补救都管用。