
1. 池化内存真正的难点不在“连得上”而在“分得准”第一次接触 CXL 内存池化的资料时绝大多数人的注意力都会被“一致性”和“带宽”这两件事吸走链路训练、协议分层、缓存一致性怎么保证、延迟比本地 DRAM 高多少。这些当然重要但只要真正把机器搭起来跑过一段时间业务就会发现一个更现实的问题浮上来——内存池的总容量到底该怎么分给各台主机。分多了池子浪费分少了业务 OOM。这个“分”的动作才是 CXL Pooled Memory 从实验室走向生产环境时最难受的一环。这篇题为《Dynamic Capacity Service for Improving CXL Pooled Memory Efficiency》的论文切入点就在这里。它不去重新设计物理链路也不去改一致性协议而是把视角放在容量管理这一层在 CXL 设备支持动态容量Dynamic Capacity Device简称 DCD的前提下设计一个中心化的 Dynamic Capacity Service下文简称 DCS统一记录、分配、回收池化内存的容量区段让多台主机按需取用而不是在开机时就把池子切死。它解决的问题很具体静态分区下的容量利用率低下与碎片化。适合阅读的人群也很明确——做异构内存、做云主机资源调度、做 CXL 设备固件、做内核内存热插拔子系统的工程师以及需要判断“这套方案能不能落到我们机房”的架构决策者。如果你只是想了解 CXL 是什么这篇论文会显得过于细节但如果你已经在评估“池化内存上线后能省多少机器”它几乎每一节都在回答你的问题。我读这篇论文的第一感受是它把很多工程上“大家心里知道但没人写清楚”的东西写清楚了。比如为什么容量回收这么难、为什么元数据必须独立于主机、为什么分配粒度不能太小。下面按我自己的阅读顺序把这篇论文拆开讲夹杂一些我在实际做内存池化验证时踩过的坑。2. 论文要解决的到底是什么问题静态分区的三重浪费2.1 静态切分看起来简单代价藏在峰值里传统的 CXL 2.0 内存池化主流做法是静态分区池化设备上电后管理软件把容量切成若干块每块通过 decoder 映射给某台主机主机侧看到的就是一段属于自己的持久内存区域。这个模型的好处是简单、可预测、故障域清晰主机侧的操作系统几乎不需要改什么当成一段普通内存或 DAX 设备用就行。问题在于业务的内存需求从来不是平的。一台跑在线服务的机器白天峰值可能用到 80%夜里掉到 30%一台跑批处理的机器正好相反。静态分区要求你按峰值去切因为切少了业务会挂切多了又没办法临时借给别人。于是整个池子的真实利用率被拉到所有主机峰值之和的水平池子越大、主机越多浪费越明显。论文里把这个浪费拆成三层我觉得拆得很准时间维度的浪费同一台主机在不同时段的需求差异静态分区无法跟随。空间维度的浪费不同主机峰值错开但切分是固定的无法互相调剂。粒度维度的浪费为了管理方便切分粒度往往很大几百 GB 甚至 TB 级小幅度需求变化只能靠“向上取整”。这三层浪费叠加起来就是论文标题里那个 “Efficiency” 要解决的问题。我自己做过一次粗略估算一个 8 主机、总容量 16TB 的池子按峰值静态切分实际平均利用率大概在 55%–65% 之间也就是说有 5TB 以上的内存在大部分时间里是“占着茅坑不拉屎”。2.2 超额订阅不是新东西难的是怎么收回来看到这里熟悉虚拟化的人会立刻想到一个词超额订阅oversubscription。CPU 和存储早就这么干了内存为什么不行原因是内存这种资源的“回收”语义和 CPU、磁盘完全不同。CPU 的时间片可以抢占进程被换出再换入代价是上下文切换。磁盘的块可以迁移代价是 IO。但内存页一旦映射给某个进程操作系统没有权力在进程不知情的情况下把它抽走——除非走完整的内存热移除流程而那需要页迁移、需要目标位置、需要业务能容忍停顿。换句话说内存的分配是“承诺型”的一旦给出去就很难无条件收回。论文很清楚这一点所以它的方案并不是“像虚拟机 balloon 那样硬抢”而是搭了一套带协商的容量生命周期管理分配要走申请回收要走通知主机侧有一定时间来释放或者搬迁。这个设计取舍我认为是对的但代价是回收不及时后面我会专门讲这个坑。2.3 论文的核心主张把容量当成一种可调度的资源来管论文的核心主张可以概括成一句话在 DCD 硬件能力之上增加一层独立的、带元数据持久化的容量调度服务把“容量”从静态配置项变成运行时可调度的资源。这里有两个关键词值得拎出来说。第一个是“独立”。DCS 不跑在任何一台使用池化内存的主机里而是跑在池化设备侧或者一个独立的管理节点上。这个选择很关键——如果 DCS 跑在某台主机上那台主机就同时是裁判和运动员它给自己多分一点别人也没法验证。独立部署带来的是信任边界清晰代价是多了一跳网络交互和一套额外的高可用设计。第二个是“元数据持久化”。容量分配关系必须能在断电、复位、主机重启之后恢复否则一次意外重启就可能出现“两台主机都认为自己拥有同一段物理内存”的灾难性场景。论文在这部分花了不少篇幅我认为是全文最体现实战经验的地方之一。3. 架构拆解DCS 到底站在什么位置3.1 三个角色与两条通路论文的架构里参与容量分配的实体有三个主机Host、池化设备Pooled Memory Device、动态容量服务DCS。它们之间的交互可以分成两条通路来理解。一条是数据通路主机通过 CXL.mem 访问分配给自己的那段物理内存走的还是标准的 HDMHost-managed Device Memory解码路径DCS 完全不参与。这一点很重要它意味着 DCS 的引入不会给内存访问的关键路径增加任何延迟——所有开销都落在控制通路上。另一条是控制通路主机侧的管理代理向 DCS 发起容量申请、释放、查询DCS 根据全局视图决定是否批准然后向设备下发具体的 extent 配置设备完成配置后回执DCS 再通知主机去“认领”这段容量。整条链路是异步的允许秒级甚至分钟级的完成时间。我在读这段时特意留意了一个细节论文没有把 DCS 做成一个“内存分配器”式的同步接口而是做成了事件驱动的。主机申请 64GBDCS 不会阻塞等待而是先回一个“已受理”等设备侧配置完成后再回调。这个设计对主机侧的内核改动要求更低但也意味着应用层拿不到“立刻可用”的保证需要预留缓冲。3.2 为什么分配单位是 Extent 而不是 Page论文里所有容量操作的基本单位是extent粒度通常在百 MB 到 GB 级。为什么不做到 4KB 页级别这个问题我在读第一遍时也疑惑后来想明白了主要卡在三个地方。第一是元数据规模。如果按 4KB 管理1TB 容量就是 2.68 亿条记录哪怕每条只占 32 字节也要 8.6GB 元数据还得保证这份元数据本身的可靠性和访问速度得不偿失。按 256MB 的 extent 管理1TB 只有 4096 条记录随便一个嵌入式数据库都能扛住。第二是配置开销。每次 extent 的分配都要触发设备侧的地址解码表更新这个操作不是免费的。粒度越小单位容量对应的配置次数越多控制通路的压力越大。论文里给出的经验值是extent 太小会让控制面成为瓶颈太大则会让碎片问题重新出现。第三是主机侧的可操作性。主机拿到一段新容量后需要把它变成可用的内存块memory block走一遍上线流程。这个流程的固定开销和容量大小关系不大所以一次给太少反而不划算。提示如果你自己在做类似的容量管理设计extent 粒度不要拍脑袋定。一个比较实用的起步方法是先用“业务最小可感知扩容单位”定下界再用“控制面每秒能处理多少次配置”定上界取中间值然后压测。3.3 与静态分区、与主机侧 balloon 的对比为了把论文的定位说清楚我用一张表把三种常见做法摆在一起对比。这张表是我根据论文描述和自己实践整理的不是论文原文。方案容量跟随需求回收能力主机侧改动主要风险静态分区不支持无极小利用率低碎片固化主机侧 balloon 式回收部分支持依赖 guest 配合大回收慢业务抖动论文 DCS 方案支持基于 extent 协商回收中等需管理代理元数据一致性、回收延迟从表里能看出来DCS 方案的位置比较尴尬也比较务实它比静态分区灵活得多又不像 balloon 那样深度侵入业务。代价是必须在主机侧部署一个管理代理并且这个代理要能影响内存块的上线和下线。这一条在裸金属场景下通常没问题但在已经有成熟虚拟化层的环境里需要和现有的资源调度器做整合工作量不小。4. 核心机制容量是怎么“流动”起来的4.1 申请、授权、提交三段式流程的细节论文把一次容量分配拆成三个阶段我觉得这个拆法很值得借鉴因为它把“决策”和“执行”彻底分开了。第一阶段是申请。主机侧代理带着自己的标识、期望容量、期望时长、紧急程度发给 DCS。这里有个设计细节申请里带的是“期望值”而不是“必须值”DCS 可以部分满足。比如你要 100GB池子紧张DCS 可以先给你 60GB剩下的进等待队列。这个软化处理让系统在容量紧张时不至于直接失败。第二阶段是授权。DCS 拿到申请后做三件事查全局空闲 extent 池、检查该主机是否已达配额上限、检查池子的剩余容量是否低于安全水位。三项都过就锁定一批 extent写入元数据然后向设备下发配置。注意这里是先写元数据再下发配置顺序不能反否则设备配置成功但元数据没落盘重启后就丢了。第三阶段是提交。设备完成地址解码配置后回执DCS 更新状态为“已提交”再通知主机。主机收到通知后触发内核的 memory block 上线流程把这段物理地址纳入管理。只有走到这一步应用才真正能用上这块内存。整个流程里我最关心的是失败回滚。如果设备配置到一半失败了呢论文的处理是DCS 保留一份事务日志回滚时按日志把已经配置的 extent 释放掉元数据恢复到操作前状态。这个做法很朴素但在实际系统里往往是唯一可靠的做法。4.2 元数据放在哪一个容易被低估的设计决策论文对元数据存放位置的讨论我认为是全文最有工程味道的部分。有三种候选方案各有各的麻烦。方案一元数据放在池化设备内部。好处是就近设备自己知道自己被切成了什么样。坏处是设备固件要承担相当复杂的逻辑而且一旦设备故障元数据跟着一起没了恢复会很麻烦。方案二元数据放在 DCS 所在节点。这是论文最终倾向的方案。好处是逻辑集中、容易做多副本、容易审计。坏处是 DCS 节点成了单点必须做高可用而且元数据和设备实际状态之间可能不一致需要定期对账。方案三元数据分布式存放在各主机。好处是没有中心节点。坏处是主机之间要互相同步一致性问题极难处理论文基本否掉了这条。论文选方案二并额外加了一条我认为很关键的机制周期性对账。DCS 每隔一段时间向设备查询实际的 extent 分配情况和本地元数据比对发现不一致就以设备为准修正。这条机制看起来笨但在真实环境里能救命——我就遇到过设备复位后 extent 表被清空、而 DCS 元数据还认为容量已分配的情况没有对账的话那批 extent 就永久泄漏了。4.3 回收策略论文里最“不完美”的部分如果说分配部分是论文的亮点那回收部分就是它的软肋。论文自己也承认这一点写得很坦诚。回收的难点在于DCS 知道某台主机“分配了 200GB”但它不知道这 200GB 里有多少是真正在用的。DCS 没有主机内部的内存使用信息除非主机主动上报。所以论文设计了两条触发路径。一条是主动释放主机侧代理监测到某段内存长时间空闲比如连续 N 分钟使用率低于阈值主动通知 DCS 释放。这条路径依赖主机侧的监测逻辑准不准全看实现。另一条是被动征用DCS 检测到池子水位告急向某些“超额持有”的主机发征用请求要求它们在宽限期内释放指定容量的 extent。主机可以选择配合、可以拒绝、也可以部分配合。论文给的宽限期是分钟级的这个数字我认为偏乐观实际业务里内存下线涉及的页迁移可能要更久。注意被动征用如果做成“强制”的很容易引发业务侧的长尾延迟甚至 OOM。论文用协商式是对的但它没有充分讨论“多台主机同时拒绝配合”时的策略这是我读下来觉得论证还不够扎实的一处。还有一个论文没怎么展开但我觉得很重要的点回收后的 extent 是否需要清零。从安全角度跨主机复用的内存必须先擦除否则可能读到上一家的残留数据。这个擦除动作的开销和 extent 大小成正比如果回收频繁擦除带宽会成为一个隐性成本。论文只在评估部分提了一句没有深入。5. 评估数据怎么读别只看那个“利用率提升”5.1 效率指标背后藏着什么假设论文的评估部分给出了相当漂亮的数字容量利用率从六成左右提升到八成以上。这个结论我信但在复用到自己的场景之前得先看清楚它的前提。第一个前提是负载模型。论文用的负载是若干组内存需求曲线有的平稳、有的有明显峰谷、有的呈阶梯式增长。峰谷错开越明显DCS 的收益越大。如果你的环境里所有主机的峰值是同步的比如都在晚上跑批那收益会大幅缩水因为大家同时要容量池子里根本没有可调剂的部分。第二个前提是回收能成功。前面的利用率数字隐含假设是主机侧配合回收的比例足够高。论文里给的配合率是比较理想的。实际环境中业务方对“把我的内存拿走”这件事天然抵触配合率很难达到论文水平。我在自己的验证里把配合率调到七成利用率提升就打了对折。第三个前提是控制面开销可接受。论文报告的分配延迟在百毫秒到秒级这个量级对于扩容操作是可以接受的但如果你的业务需要秒级弹性伸缩这个延迟就会成为瓶颈。5.2 开销拆解别忽略那条控制通路论文把开销拆成了三块控制面消息开销、元数据存储开销、设备配置开销。我按自己的理解重新梳理一下。控制面消息这块开销主要来自主机和 DCS 之间的往返。论文的测量显示单次分配的消息量在 KB 级相对于内存容量可以忽略不计。但要注意消息频率如果主机侧代理做得很激进频繁申请和释放消息量会迅速上去。元数据存储这块按 extent 粒度换算每 TB 容量的元数据占用在 MB 级完全可接受。真正需要注意的是元数据的写放大每次分配都要落盘如果底层存储是普通 SSD写入次数多了会影响寿命。论文建议用带掉电保护的持久内存或者小容量高耐久盘来放元数据这个建议我认为是必须的。设备配置开销是三者中最容易被低估的。每次 extent 配置都要改地址解码表这个操作在设备固件里是串行的吞吐有限。论文给的数据是每秒钟能完成几十次 extent 配置。如果你有几十台主机、每台都在频繁扩缩容这个吞吐会成为瓶颈。缓解办法有两个一是批量化把多个 extent 的配置合并成一次操作二是分级小幅度调整走缓存大幅度调整才真正动设备。5.3 论文没有回答的问题读论文的一个习惯是除了看它说了什么还要看它回避了什么。这篇里有几个我认为重要但没有充分讨论的点。故障域怎么划。池化设备本身可能由多个物理设备组成一台设备挂了上面的 extent 怎么迁移论文默认设备是可靠的这个假设在生产环境里不成立。多租户隔离。论文的场景是同构主机共享池子没有讨论不同租户之间的带宽争抢。容量可以分带宽是共享的一个租户把池化内存带宽打满其他租户全受影响。这个问题在真实的多租户环境里比容量分配更棘手。版本兼容。DCS 和设备固件、主机代理之间需要协议版本协商。论文没有展开但这在长期运维中会带来大量麻烦。6. 想自己验证一遍最小可用的复现路径6.1 从模拟器起步别一上来就动硬件CXL 池化内存的真实硬件门槛不低多数人手上没有 DCD 能力的设备。可行的路径是先用全系统模拟器搭一个逻辑等价的模型把 DCS 的分配逻辑跑通再考虑上真机。我的做法是把系统拆成三个可以独立模拟的部分主机侧用一个进程模拟管理代理的行为按预设的需求曲线发起申请和释放DCS 用一个独立进程实现维护 extent 表和分配策略设备侧用一个简单的状态机模拟配置延迟和容量上限。三者之间用消息队列通信这样连时序问题都能复现。下面是我当时用的一个简化版分配逻辑Python 写的只为验证逻辑不考虑性能。class ExtentPool: def __init__(self, total_gb, extent_mb256): self.extent_mb extent_mb self.total total_gb * 1024 // extent_mb self.free list(range(self.total)) self.owner {} def allocate(self, host_id, want_gb): need min(want_gb * 1024 // self.extent_mb, len(self.free)) granted [self.free.pop() for _ in range(need)] self.owner[host_id] self.owner.get(host_id, []) granted return len(granted) * self.extent_mb // 1024 def release(self, host_id, gb): cnt min(gb * 1024 // self.extent_mb, len(self.owner.get(host_id, []))) back self.owner[host_id][:cnt] self.owner[host_id] self.owner[host_id][cnt:] self.free.extend(back) return cnt * self.extent_mb // 1024 def watermark(self): return len(self.free) / self.total这段代码当然不能直接用它没有考虑碎片、没有考虑元数据持久化、没有考虑并发。但它能让你快速验证一件事在给定的需求曲线下不同 extent 粒度、不同水位阈值、不同回收策略对最终利用率的影响有多大。我拿它跑了几十组参数得到的一个结论是extent 粒度从 128MB 提到 512MB利用率只掉不到 3 个百分点但控制面操作次数降了将近 75%。这个权衡对实际部署很有参考价值。6.2 参数怎么定一个可以抄的估算过程假设你要给一个 16 台主机的集群配池化内存每台主机的内存需求均值 400GB、峰值 700GB、峰谷错开明显。按静态分区你得切 16 × 700GB 11.2TB实际平均用掉 16 × 400GB 6.4TB利用率 57%。换成 DCS 方案你需要估算两个数池子总容量和安全水位。池子总容量不能简单取均值之和因为峰值会重叠。一个粗略的算法是取所有主机需求曲线的历史数据按小时切片求和得到一条总需求曲线然后取这条曲线的 95 分位数再上浮 10% 作为池子总容量。按这个算法假设 95 分位总需求是 8.5TB池子配 9.5TB 就够。安全水位指的是池子剩余容量低于这个比例时DCS 开始向主机征用。这个值定太低遇到突发申请来不及定太高又会频繁触发无谓的回收。论文里的取值大概在三成左右。我的经验是如果主机侧回收响应快分钟级可以压到两成如果回收慢十几分钟得留到四成。提示安全水位不要设成全局固定值。按主机数量做一个动态调整更合理——主机越多单台突发对全局的影响越小水位可以适当降低。6.3 上真机前必须验证的四件事模拟器跑通不代表真机没问题。我从模拟转到硬件验证时有四件事是必须提前确认的。第一主机内核是否支持 memory block 的在线上下线。不同内核版本对这个功能的成熟度差异很大有的版本热添加没问题但热移除会卡住。这个必须先在小规模机器上验证否则整个回收链路都是空中楼阁。第二设备固件的 extent 配置是否是原子的。如果配置过程中掉电设备恢复后 extent 表处于什么状态这个问题必须问清楚否则对账机制也没法设计。第三跨 NUMA 节点的延迟影响。池化内存挂在哪个 NUMA 节点下对访问延迟影响很大。如果主机侧的 NUMA 拓扑和池化设备的位置关系没处理好可能容量省下来了性能掉下去一大截。第四擦除开销的实际测量。拿一块真实的 extent 做一次全量擦除测一下耗时和带宽占用再乘以你预期的日均回收次数看看这个隐性成本能不能接受。7. 踩过的坑与排查清单7.1 典型问题速查现象可能原因排查方向主机申请容量一直不批准池子水位低于阈值或该主机已达配额上限查 DCS 全局水位日志、查主机配额配置申请批准但主机看不到新内存内核 memory block 上线失败查 dmesg 中的 block add 相关报错容量释放后池子没变大设备配置未真正生效或元数据未更新对账设备 extent 表与 DCS 元数据长时间运行后利用率虚高主机上报的使用率不准回收未触发校准主机侧监测逻辑加人工抽检某台主机重启后内存冲突元数据未持久化或恢复逻辑有 bug检查事务日志、检查启动时的容量认领流程7.2 三个我认为最有价值的经验第一个经验对账机制一定要做而且要能自动修复。我一开始觉得对账是浪费资源后来遇到一次设备侧复位导致 extent 表被清空、DCS 侧还认为容量已分配的情况整整 800GB 容量凭空消失查了两天才定位到。从那以后我把对账周期从一小时缩到十分钟并且加了自动修复以设备为准把 DCS 侧多出来的分配记录标记为失效并回收。第二个经验不要把回收做成同步阻塞。早期版本里主机收到征用请求后会阻塞等待内存下线完成结果业务侧出现明显卡顿。后来改成异步先回一个“已接受”后台慢慢做页迁移完成后再通知 DCS。业务侧的感知就平滑多了代价是回收时间变长需要把安全水位相应调高。第三个经验给主机侧代理加一个“本地缓存”。每次扩容都要走完整的申请-授权-提交流程延迟在秒级对需要快速响应的业务不太友好。我们的做法是让每台主机常驻一小段“预备容量”平时不用但不还给池子需要时立刻启用用完再补。这段预备容量占总量的一成左右换来的是扩容延迟从秒级降到毫秒级。7.3 这套方案什么时候不该用最后说点反向的话。DCS 这套机制不是万能的有几种场景我认为不适合上。主机数量少比如三五台且需求曲线平稳的场景静态分区的浪费本来就不大引入 DCS 带来的复杂度和运维成本可能超过收益。对内存延迟极度敏感的业务池化内存本身的访问延迟就高于本地 DRAM再加上容量动态调整带来的页迁移长尾延迟会更难看。这类业务老老实实配本地内存更稳。缺乏配套运维能力的团队。DCS 的元数据、对账、回收策略都需要有人持续盯着出了问题要能快速定位。如果团队里没人愿意深入这块上线之后大概率是“配了不敢动”最后还是退化成静态分区。论文本身我给的评价是问题选得准方案设计务实评估部分略有美化但整体可信。它最大的价值不在于提出了多么精妙的算法而在于把一个大家心知肚明但缺乏系统描述的问题用一套完整的机制讲清楚了。对于正在做内存池化落地的人来说这篇值得逐节精读尤其是元数据管理和回收协商那两段直接照着设计能少走不少弯路。