
在云计算的世界里,服务提供商通常会采用一种名为内存超售(memory overcommitment)的实践——即分配给虚拟机(VM)的内存总量超过了物理上实际存在的内存。这种经济上的博弈是提高效率的关键,但当博弈失败、内存耗尽时,会发生什么呢?这一挑战引出了一个根本性的选择:是诉诸于虚拟机监控程序(hypervisor)进行的低效、暴力的交换,还是采用一种更优雅、更具协作性的策略。本文将深入探讨后一种更优的策略:内存气球(memory ballooning)。它解决了虚拟机监控程序对稀缺资源的全局视角与客户机操作系统对其自身资源的孤立视角之间的知识鸿沟。在接下来的章节中,您将全面了解这项关键技术。第一章“原理与机制”将剖析气球技术的工作原理、其相对于宿主机级别交换的优势,以及交换风暴和死锁等潜在陷阱。随后,“应用与跨学科联系”一章将探讨其作为云数据中心经济工具的角色,及其与系统和硬件架构的深层联系,揭示其作为现代虚拟化核心组织原则的地位。
在我们理解计算机内部运行的虚拟世界的旅程中,我们遇到了一个根本问题:一台物理宿主机如何应对其众多租户——即虚拟机(VM)——的内存需求?宿主机通常会采取一种被称为内存超售的大胆的统计乐观主义行为。这就像航空公司售出的机票数量超过了飞机上的座位数,赌的是总有几位乘客不会出现。云服务提供商为其虚拟机分配的总内存量超过了物理服务器实际拥有的内存,赌的是这些虚拟机不会在同一时间全部要求其全额内存。这场赌博是云经济效率的秘密。但如果赌输了怎么办?当所有乘客都到场,宿主机的物理内存耗尽时,会发生什么?这正是魔术师的困境,其解决方案是一场关于协作与控制的、优美而复杂的舞蹈。
当虚拟机监控程序发现其内存耗尽时,必须从其客户机中回收一些内存。它面临着两种截然不同策略的选择:充当一个生硬的工具,或是一个微妙的外交官。
第一种策略是宿主机级别的交换(host-level swapping),这是一种暴力强制的形式。虚拟机监控程序对客户机虚拟机的内部运作一无所知,它只是简单地挑选它们的一些内存页面,并强行将其写出到自己的存储磁盘——即其交换空间。它跨越了一道“语义鸿沟”,完全不知道这些内存页面对客户机来说究竟意味着什么。想象一下,一个房东需要为新房客清理出一个房间。由于不知道什么东西有价值,房东开始随机打包箱子,可能会把昨天的报纸包起来,却把一个无价的花瓶暴露在外。
这种盲目性可能极其低效。考虑客户机操作系统中一种常见的内存类型:干净的文件系统页面缓存(clean file system page cache)。这些页面包含从磁盘上的文件中读取但未被修改的数据。它们是已存在于别处的数据的完美副本。如果客户机操作系统需要释放这部分内存,它可以简单地丢弃这些页面,产生的磁盘 I/O 为零。如果再次需要这些数据,可以从原始文件中重新读取。但盲目的虚拟机监控程序并不知道这一点。当它选择一个干净的缓存页面进行交换时,它会执行一次不必要的磁盘写入到其交换文件中。如果客户机之后再次需要该页面,虚拟机监控程序必须从其交换文件中执行一次磁盘读取才能将其恢复。这种浪费的循环被称为 I/O 放大(I/O amplification)。对于每一个本可以免费回收的页面,虚拟机监控程序执行了两次 I/O 操作(一次写入和一次可能的读取),从而极大地降低了性能。
这就是第二种更优雅的策略——内存气球(memory ballooning)——发挥作用的地方。这是一种温和说服的技术。虚拟机监控程序在客户机操作系统内部安装一个特殊的软件,一个被称为气球驱动程序(balloon driver)的伪设备驱动程序。可以把它想象成一个生活在客户机领土内的间谍或大使。当虚拟机监控程序需要内存时,它向这个驱动程序发送一个命令:“给气球充气。”
气球驱动程序的响应方式与客户机内部的任何其他应用程序一样:它向客户机操作系统申请大量内存。随着气球“膨胀”,它会消耗客户机的内存页面。这在客户机内部制造了内存压力,诱使客户机操作系统相信自己快要用完内存了。作为回应,客户机操作系统会执行其设计好的任务:激活其自身复杂的内存回收程序来释放空间。客户机操作系统分配给气球的物理内存页面随后会报告给虚拟机监控程序,后者可以回收它们并将其分配给其他更需要的客户机。房客被要求腾出空间,而由他自己来决定要收起什么。
气球技术的魔力在于它跨越了语义鸿沟。牺牲哪些页面的决定权被委托给了唯一知道其价值的实体:客户机操作系统本身。但客户机如何做出这个关键选择呢?这本身就是一个引人入胜的问题,是在预测未来与最小化犯错成本之间的一种平衡。
现代操作系统不会随机选择牺牲品。它们使用巧妙的算法,如增强型二次机会(ESC)算法,该算法根据处理器设置的两个简单的硬件标志对页面进行分类:一个引用位(reference bit,),表示页面最近被访问过;以及一个修改位(modify bit,),表示页面已被写入(即“脏页”)。这就产生了四类页面,每一类都有不同的驱逐优先级:
通过遵循这个层次结构,客户机操作系统试图在对正在运行的应用程序造成最小干扰的情况下回收内存。然而,即使是这种智能策略也可能被愚弄。任何页面置换策略的目标都是保护应用程序的工作集(working set)——即它为完成其工作所积极需要的页面集合。如果气球迫使操作系统从这个工作集中回收内存,性能就会急剧下降,这种状态被称为颠簸(thrashing)。例如,一个总是倾向于丢弃文件缓存页面的幼稚策略,最终可能会驱逐对性能至关重要的热数据库缓存,而忽略了应用程序分配但数小时未曾访问过的冷匿名内存。因此,一个真正成熟的客户机必须采用超越简单分类的算法,使用近期性和频率的启发式方法来准确估算应用程序的真实工作集,并确保只有工作集之外的页面才被提供给气球。
因此,气球技术并非免费的午餐。它是一种根本性的权衡。当虚拟机监控程序为解决整个宿主机的内存短缺而在 VM-A 中给气球充气时,它提高了整个系统的稳定性。但这样做也缩小了 VM-A 可用的内存,增加了其内部内存管理器的压力。如果回收请求过于激进,客户机的工作集将无法容纳在其可用内存中,其缺页中断率将急剧飙升。
这产生了一种动态的推拉效应。随着客户机中气球的膨胀,宿主机级别的内存压力降低,低效的宿主机交换风险也随之减小。与此同时,客户机级别的内存压力增加,由于其自身的分页活动,客户机的性能可能会开始受到影响。虚拟机监控程序扮演着中央银行家的角色,为每个客户机小心翼翼地调整“利率”(气球大小),以维持整个经济的稳定,同时避免在任何一个单独的州(虚拟机)中引起衰退(颠簸)。
当这种微妙的平衡行为失败时会发生什么?后果可能是灾难性的,导致级联故障的反馈循环。考虑一个场景:一个面临严重内存不足的虚拟机监控程序,同时在所有客户机虚拟机中激进地给气球充气。如果这样做没有考虑到它们各自的工作集,可能会将其中许多虚拟机同时推入颠簸状态。
这会触发一场交换风暴(swap storm)。首先,客户机开始疯狂地将页面交换到它们的虚拟磁盘。这股 I/O 请求的洪流淹没了虚拟机监控程序。为了应对 I/O 负载,宿主机操作系统必须分配越来越多自身的物理内存用于 I/O 缓冲区和缓存。宿主机自身内存使用的突然飙升,在宿主机上造成了新的、甚至更严重的内存不足。现在,宿主机本身也被迫进行交换,将可能属于其他健康客户机的内存,甚至是其自身内核的一部分,分页出去。整个系统陷入停顿,困在一个恶性循环中:解决内存压力的方案(交换)只会制造更多的内存压力。
一种更微妙的病态是嵌套交换(nested swapping)。一个客户机操作系统,在气球的压力下,决定将一个页面换出到其虚拟交换文件。从虚拟机监控程序的角度来看,那个虚拟交换文件只是其自身文件系统上的一个普通文件。如果宿主机在自身的内存压力下,已经将客户机试图写入的那个文件块交换出去了,那会怎么样?现在,客户机的单个缺页中断在宿主机层面触发了第二次缺页中断。为了服务客户机的请求,虚拟机监控程序必须首先从它自己的交换磁盘中读取数据,仅仅是为了给客户机的交换磁盘提供存储空间。这种双重故障级联可能会严重削弱 I/O 性能。先进的虚拟机监控程序通过更智能的协调来应对这种情况,例如监控客户机的缺页中断频率(PFF),并且当它检测到客户机正在交换时,将客户机的交换文件“钉”(pinning)在宿主机内存中,以保证它始终存在,永远不会成为宿主机级别交换的牺牲品。
我们已经看到,内存气球的操作需要客户机和宿主机之间持续的对话。但这个对话发生在一个并发的世界里,多个虚拟机中的多个线程以及宿主机本身都在同时尝试管理内存。这种协调需要锁来保护共享数据结构,而有锁的地方就潜伏着死锁(deadlock)的危险。
想象两个线程,一个在客户机中(),一个在宿主机中(),需要进行协调。客户机线程锁住它自己的内存映射(),然后向宿主机发出一个 hypercall,这个操作需要宿主机的内存锁()。与此同时,宿主机线程可能已经获取了 ,并且需要回调到客户机中检查某些东西,这个过程需要获取 。一个致命的循环出现了: 持有 并等待 ,而 持有 并等待 。它们被卡住了,永远地等待对方。
解决这个隐藏危险的方案是计算机科学中最优雅的原则之一:全局锁顺序(global lock ordering)。为了防止这种循环等待,所有参与者——每个客户机和宿主机——都必须同意一个严格的排序协议。例如,他们可能规定,任何线程在持有 时都不得请求 。或者,更稳健地,他们建立一个总顺序,比如 ,并强制锁必须始终按该升序获取。一个同时需要这两个锁的客户机必须首先获取 ,即使其自然的倾向是先从自己的锁开始。这个简单而不可侵犯的礼仪规则确保了死锁循环在结构上是不可能发生的。它优美地证明了一个理念:即使在最复杂、分层的系统中,可靠性也常常建立在简单、形式化的规则基础上,这些规则支配着机器深处一场看不见的舞蹈。
理解了内存气球的机制后,我们可能会倾向于认为它只是一个巧妙但孤立的工程技巧。事实远非如此。实际上,内存气球是一种基础语言,一个至关重要的沟通渠道,它使得现代计算机系统的众多独立层次——从全球范围的云编排器到单个处理器核心的硅片——能够协作、协商和适应。它是一条线索,将经济学、操作系统设计和硬件架构编织成云的无缝织物。让我们踏上一段旅程,从云数据中心的鸟瞰视角到处理器的微观世界,看看这个简单的想法如何绽放成一幅丰富的应用与联系的织锦。
想象一下,你正在运营一个庞大的云数据中心。你最宝贵和昂贵的资源是物理内存。对效率的压力是巨大的。如果你能以某种方式向你的客户承诺总共 GiB 的内存,而服务器中只有 GiB 的物理 RAM,你就可以托管更多的客户并运营一个更盈利的业务。这种做法,即内存超售(memory overcommitment),是云计算的经济引擎。这很像一家航空公司卖出的机票比飞机上的座位还多,赌的是有些乘客不会出现。在云中,“缺席者”是虚拟机内闲置的内存页面。
但这是一场高风险的赌博。如果所有客户突然同时要求他们所有的内存,系统将在“交换风暴”中陷入停顿——一场灾难性的交通堵塞,系统在快速内存和慢速磁盘存储之间疯狂地移动数据。你如何才能在不冒崩溃风险的情况下,获得超售的经济利益?
这就是内存气球在一个复杂的资源管理策略中成为明星角色的地方。一个设计良好的云平台不是把气球技术当作一个生硬的工具,而是在一个更大的制衡系统中的一个精确工具。例如,一个稳健的策略可能会设定一个特定的超售比率,比如 ,但仅针对启用了气球驱动程序的客户机。它会为宿主机系统本身保留一部分内存,并在空闲内存低于安全阈值(比如 )时主动给气球充气。最关键的是,它会为每个虚拟机建立一个“内存底线”,确保气球永远不会回收过多的内存以至于侵占客户机的活动工作集,从而防止客户机被迫进行交换。在出现意外的需求激增时,系统有一个逃生计划:它可以自动将虚拟机实时迁移到不那么拥挤的宿主机上,就像城市调度员将交通绕开事故现场一样。
交换风暴的风险不仅仅是定性的;它可以用惊人的清晰度进行建模。我们可以为一个虚拟机定义一个“工作集赤字”,即由于超售而被推到磁盘上的其活动内存量。这个赤字的每一吉字节都会产生一定速率的交换 I/O。随着超售比率 的增加,赤字增长,宿主机上所有虚拟机的总交换 I/O 也会攀升。由于磁盘子系统具有有限的带宽,存在一个最大的超售比率 ,超过这个比率,交换 I/O 将超过安全限值,性能急剧下降。通过理解这种关系,云提供商可以数学上确定盈利能力与危险之间的精确边界。
经济计算可以更加精细。如果必须回收内存,谁应该承担成本?从运行关键、内存密集型数据库的虚拟机中回收内存,远比从一个大部分时间处于空闲状态的虚拟机中回收内存的破坏性大得多。这变成了一个优化问题:如何在从一组虚拟机中回收总共 个页面的同时,最小化整个系统的总性能下降?答案在于一个优美的贪心方法。对于每个虚拟机,我们可以计算一个“边际成本”——即从该虚拟机回收每一页对整个系统造成的性能冲击。这个成本取决于虚拟机工作负载的内存敏感度以及它影响的用户数量等因素。为了达到最佳的全局结果,编排器应首先从边际成本最低的虚拟机回收页面,然后是次低的,依此类推,直到达到回收目标。这确保了负担总是被放在会造成最小整体伤害的地方。
当我们把内存气球看作是连接不同世界的桥梁时,它的真正美妙之处就显现出来了。虚拟机监控程序生活在宿主机物理内存的世界里,敏锐地意识到整体的稀缺性。客户机操作系统生活在它自己孤立的宇宙中,相信它拥有一块私有的“客户机物理内存”。这两个世界对彼此的现实是盲目的。气球技术充当了翻译器,让虚拟机监控程序能够用客户机能理解的语言来传达其对内存的需求。
这种沟通对于诊断性能问题至关重要。想象一下,一个系统管理员看到一个运行缓慢的虚拟机。原因可能是两种截然不同的现象之一:客户机操作系统因内存不足而将其自己的页面交换到其虚拟磁盘;或者宿主机因内存不足而在背后将虚拟机的页面交换出去。区分这两者需要关联来自两个世界的信息。客户机级别的交换通过客户机内部的高交换计数器来揭示,这对应于宿主机上该客户机虚拟磁盘文件的高 I/O 流量。另一方面,宿主机级别的交换则通过宿主机上的高交换计数器和流向宿主机专用交换设备的 I/O 流量来揭示,这通常发生在气球驱动程序显示显著膨胀之后。只有通过观察这两组信号,才能正确诊断病症。
我们可以将这种合作更进一步。如果能让客户机操作系统意识到虚拟机监控程序的意图会怎样?考虑 CLOCK 算法,这是客户机操作系统在需要空闲页面时选择要驱逐哪个内存页面的常用方法。它就像一个表针扫过页面,寻找一个最近没有被使用过的页面(其“引用位” 为 )。现在,假设虚拟机监控程序知道它即将通过气球技术回收一个特定的页面。它可以向客户机发送一个“提示”,在该页面上设置一个假设的提示位 。一个聪明的客户机操作系统可以修改其 CLOCK 算法,定义一个新的“有效”引用位 。现在,从客户机的角度来看,即将被气球回收的页面似乎是“正在使用”的。客户机的页面置换算法会跳过它,明智地避免了驱逐一个即将被虚拟机监控程序拿走的页面的徒劳工作。这是一个跨层优化的优美例子,它防止了冗余的工作。
当然,并非所有的优化都能和谐共存。有时,特性会发生冲突。其中一个冲突源于巨页(huge pages)。为了加速内存访问,现代系统可以使用大的 页面来映射内存,而不是标准的 页面,从而减少处理器转译后备缓冲器(TLB)的压力。然而,这些巨页通常被“钉”在内存中,不能轻易地被气球驱动程序回收。这就产生了一个直接的权衡:租户想要巨页以获得更好的性能,但提供商失去了气球技术的灵活性,这损害了超售和效率。这可以被建模为效用的冲突。可以计算租户的性能增益,也可以量化提供商因回收灵活性降低而造成的损失。通过找到租户的边际增益等于提供商的边际损失的点,系统可以找到一个最优平衡,也许可以通过对使用不灵活的巨页收取更多费用来实现。
层间的对话一直延伸到宿主机的物理内存分配器。现代操作系统通常使用伙伴系统(buddy system)来管理物理内存,它擅长查找和合并相邻的空闲块以形成更大的块。这对于创建巨页至关重要。在这里,气球技术不仅可以用来回收任何内存,还可以用来回收特定的内存。想象一下宿主机上一个 对齐的内存块,它几乎完全空闲,只有几个分散的页面分配给了一个虚拟机。一个智能的虚拟机监控程序可以指示该虚拟机的气球驱动程序专门针对那几个页面进行回收,而不是从整个系统中随机回收页面。通过释放它们,宿主机的伙伴分配器可以将小块合并成一个单一、连续的 巨页,供高性能应用程序使用。这是一个卓越的例子,说明了如何使用气球技术作为碎片整理的外科手术工具,将一个被动的回收机制转变为一个主动改善系统结构的工具。
像内存气球这样的高层策略的影响并不会在软件边界停止;它们会一直波及到硬件层面,影响着硅片本身的性能。
考虑一台拥有多个处理器的现代服务器,每个处理器都有自己的本地内存库——一种非统一内存访问(NUMA)架构。访问本地内存速度快,而访问连接到另一个处理器的内存则明显较慢。虚拟机监控程序自然会尝试将虚拟机的内存放置在其虚拟 CPU 运行的同一 NUMA 节点上。但是,当那个本地节点面临内存压力时会发生什么?虚拟机监控程序可能会给虚拟机的气球充气,回收本地内存,结果客户机再次访问那些页面,迫使虚拟机监控程序在远程、压力较小的节点上重新分配它们。结果如何?虚拟机的一部分内存访问现在不得不长途跋涉跨越互连,增加了平均内存访问延迟,并可衡量地降低了虚拟机的吞吐量。这表明,为了获得最佳性能,气球操作和内存放置都必须是 NUMA 感知的。
硬件的回响可能更加微妙。每个处理器核心都包含一个用于内存地址转换的小型快速缓存,称为转译后备缓冲器(TLB)。当虚拟机监控程序通过气球技术回收一个客户机页面时,它必须使页表中的相应映射失效。这种失效意味着任何核心上缓存了这个现已过时的转换的任何 TLB 条目都必须被刷新。这会触发在芯片上广播失效消息。虽然单个失效的成本很小,但一个回收数千页面的大型气球操作可能会触发一场此类失效的风暴。使用一个简单的概率模型,我们可以计算出在所有核心上将被刷新的 TLB 条目的预期总数。对于一个有 个核心,每个核心有 个条目的 TLB 的系统,从一个大小为 页的工作集中回收 个页面,预期的总失效数就是 。这个优雅的公式量化了气球技术的一个隐藏硬件成本,提醒我们,在复杂的系统中,没有哪个操作是真正免费的。
最后,让我们考虑客户机和虚拟机监控程序之间的对话是如何物理实现的。最有效的方式是 hypercall,一种专门的指令,充当从客户机到虚拟机监控程序的直接、私密的电话线。另一种方式是模拟一个硬件设备。客户机写入一个特殊的内存地址(MMIO),这会陷入到虚拟机监控程序,然后后者必须唤醒一个独立的用户空间进程,该进程再向宿主机内核进行系统调用才能完成工作。对每个步骤的微秒级延迟——VM 退出、上下文切换、系统调用——进行仔细核算后,会发现直接的 hypercall 路径要快得多。它绕过了模拟设备路径的繁琐官僚程序,提供了一种精简高效的机制,这对于像内存管理这样对性能敏感的操作至关重要。
从云规模的经济学到硬件缓存的复杂性,内存气球揭示了它不仅是一个特性,而是虚拟化系统的核心组织原则。它证明了现代计算优雅的分层设计,其中简单而稳健的原语能够实现复杂而智能的行为,创造出一个远大于其各部分之和的整体。