FAST-存储大会-2025-笔记-全-
FAST 存储大会 2025 笔记(全)
001:主题演讲——从交付两代AI超级计算机中获得的洞见

在本节课中,我们将学习一位资深工程师分享的个人职业旅程,以及构建和优化用于人工智能(AI)工作负载的云原生超级计算机系统的技术洞见。我们将涵盖AI工作流的全貌、系统设计原则、性能优化策略以及存储架构面临的挑战。
个人旅程与建议
上一节我们介绍了课程概述,本节中我们来看看演讲者分享的个人职业发展故事和建议。
我的职业生涯始于IBM研究院,当时我正在攻读研究生。我加入了IBM的高性能计算(HPC)小组,参与了包括Blue Gene系列在内的超级计算机的构建工作,专注于文件系统和性能工具。
在职业生涯的前5到10年,是进行深度技术工作的最佳时期。这个阶段通常管理负担较轻,可以专注于技术成长和建立广泛的专业社区网络。积极参与行业会议,与同行交流,建立一个强大的导师和同行社区至关重要,这对个人和职业发展都有长远益处。
随后,我转向了云计算领域。从一切追求性能的HPC,转向注重可用性、易用性和弹性的云平台,这是一个巨大的思维转变。这段经历表明,拥抱变化、学习新领域是持续成长的关键。
近年来,我的工作重心转向了构建AI系统。结合HPC的性能专长和云计算的灵活性,我们致力于设计能够支持完整AI工作负载生命周期的云原生AI超级计算机。
我强烈鼓励大家参与指导他人和教学活动。无论是指导同事、教授学生,还是帮助社区中的年轻人,这不仅能带来成就感,也是从日常工作中获得平衡的有效方式。
AI工作流全景
上一节我们了解了演讲者的背景,本节中我们来看看现代AI,特别是基础模型,所涉及的完整工作流程。
AI模型开发远不止最终的训练阶段。一个完整的流程包括多个计算和存储密集型阶段:
以下是AI工作流的主要阶段:
-
数据准备
- 这是起点,如同“沙里淘金”。我们从数十PB的原始数据开始,通过过滤、清理和标注,最终得到可能包含数万亿令牌(tokens)的高质量数据集。
- 此过程需要大量数据工程人力,计算任务可能持续数小时到数天,涉及数百甚至数千个CPU核心,虽然GPU使用相对较少,但步骤极其繁多且需要反复迭代。
-
分布式训练
- 在准备好数据并确定模型架构和超参数后,启动大规模的分布式训练。这类似于为马拉松进行长期训练。
- 实际训练前的准备工作(如小规模试验)所消耗的基础设施资源可能与正式训练相当甚至更多。训练本身可能持续数周甚至数月,需要动用成千上万个GPU。
-
模型微调
- 训练得到的基础模型不能直接使用,需要针对特定用例进行微调。这就像完成基础教育后进入专业领域。
- 微调可能涉及数百个任务,使用较少的GPU资源,并行尝试多种路径,然后评估选择最佳结果。
-
推理部署
- 这是模型产生实际价值的阶段。模型被部署以服务终端用户请求。
- 推理是高度交互式、计算和内存密集型的任务。其性能通常受GPU内存带宽限制。关键指标是每百万令牌的成本(美元),优化成本对于AI服务的长期成功至关重要。
AI系统的计算需求
上一节我们梳理了AI工作流,本节中我们来量化训练这些模型所需的计算资源。
驱动AI模型训练规模的两个核心因素是:数据量(D) 和 模型参数量(N)。自2020年以来,这两个维度都在急剧增长。数据集从千亿令牌级别跃升至十万亿令牌级别,模型参数从十亿级别增长到万亿级别。
所需计算量的估算公式非常简单,一个六年级学生也能记住:
- 预训练计算量 ≈ 6 × N × D
- 其中,
N是模型参数量,D是训练数据令牌量。公式中的系数6是一个经验值,考虑了前向传播、反向传播等操作。
- 其中,
- 推理计算量 ≈ 2 × N × D
- 推理主要涉及前向传播,计算量约为预训练的三分之一。
- 微调计算量
- 微调使用的数据量
D较小,但模型参数量N保持不变,因此计算量介于预训练和推理之间。
- 微调使用的数据量
这些公式表明,AI对计算资源的需求呈组合式爆炸增长。
云原生AI超级计算机设计
上一节我们了解了AI的巨大算力需求,本节中我们来看看如何设计系统来满足这些需求。
我们的设计目标是构建一个云原生AI超级计算机(代号Vela),它需要兼顾高性能和云的灵活性。
以下是核心设计原则:
- 在以太网上实现高性能:尽管云环境主要基于以太网,而非InfiniBand,但我们仍需通过优化实现接近裸金属的性能。
- 支持全工作负载生命周期:系统必须同时支持训练、微调和推理,而不仅仅是训练。
- 具备云的操作灵活性与弹性:能够快速部署、扩展,并适应全球不同规模和形态的数据中心。
- 优化功耗、空间和冷却效率:AI硬件功耗巨大,必须创新设计以提高数据中心机架密度和能效。
系统采用标准的云数据中心脊柱-叶子(Spine-Leaf)网络架构,通过虚拟化提供计算资源,但关键是通过技术优化(如GPU Direct RDMA)使虚拟机能获得接近裸机的性能。
我们通过启用GPU Direct RDMA over Ethernet技术,让GPU能够直接跨节点通信,避免了数据经CPU拷贝的开销。这使得网络吞吐量提升了2-4倍,网络延迟降低了约10倍,从而能将20B参数模型的训练时间缩短至合理范围。
面对高功耗挑战,我们重新设计了电源方案。传统云机柜为冗余保留一半电源容量。我们改为让GPU节点在检测到电源故障时自动降频(功耗在1.5秒内从700W降至150W),而非直接崩溃,从而实现了机柜计算密度的翻倍,提高了资源利用率。
存储架构与挑战
上一节我们探讨了计算和网络优化,本节中我们聚焦于AI工作负载中的存储数据架构。
AI系统中的数据流主要涉及三层存储:
以下是AI存储架构的三层:
-
对象存储
- 这是数据的源头和归宿。容纳PB级别的原始数据集、处理后的数据以及最终模型。提供极佳的成本效益和规模,但性能通常不足以支撑高强度训练。
-
分布式文件系统
- 这是高性能计算的“热点数据缓存层”。训练和微调作业从此处读取输入数据,并写入检查点(checkpoint)和中间结果。它需要提供高带宽和低延迟,以避免GPU空闲。
- 关键需求:与对象存储的同步必须是自动化和无缝的,而非手动拷贝。
-
本地存储
- 位于GPU节点内部,速度最快。常用于缓存频繁访问的数据,如微调数据集或推理时的KV缓存。
- 挑战:容量有限,若被长期独占会导致整体基础设施利用率低下。
当前的主要挑战在于数据在存储层间的透明迁移。用户仍需手动管理数据在不同层级间的移动。未来的研究方向包括:使数据迁移对用户不可见;让分布式文件系统缓存层能根据需求弹性伸缩;以及开发用于存储系统的自动化运维工具(“自动驾驶仪”)。
在我们的实践中,使用IBM Spectrum Scale(GPFS)作为高性能缓存层,显著改善了数据加载时间和作业执行的稳定性。
经验总结与未来展望
在本节课中,我们一起学习了从构建两代AI超级计算机中获得的核心洞见。
首先,AI工作负载变化迅猛。与其完全相信研究人员对未来的预测,不如假设AI将取得巨大成功,并以此为前提进行系统设计,前瞻性地思考百倍规模下可能出现的问题。
其次,在实践中学习和迭代优于追求完美。尽早将系统交予用户使用,收集反馈并持续改进,这比闭门造车能学到更多。当然,初版系统需具备基本可用性。
再者,自动化运维至关重要。我们需要更智能的工具(如“自动驾驶仪”)来自动诊断硬件故障、性能降级等问题,否则工程师将陷入无尽的调试工作,无法推进根本性创新。
最后,AI革命才刚刚开始。其中蕴含着巨大的机遇。构建这些系统是数百人团队协作的成果。我鼓励大家积极参与到这个旅程中,与社区交流,共同应对挑战。


本节课总结了个人在HPC、云计算和AI领域的交叉经验,深入探讨了支持现代基础模型全生命周期的云原生AI超级计算机的设计、优化和存储考量。核心在于平衡高性能与云的弹性,并通过自动化应对运维复杂度,以支撑AI的持续演进。
002:Ananke - 快速透明的文件系统微内核恢复

在本节课中,我们将学习一项名为“Ananke”的研究工作,它旨在为文件系统微内核提供快速、透明的进程崩溃恢复。我们将探讨其核心挑战、设计原理以及如何实现高效恢复。
概述
文件系统微内核将文件系统服务构建为用户空间进程,无需操作系统内核参与。这种架构因其性能优势、易于开发和升级以及更好的故障隔离而受到关注。然而,当文件系统进程崩溃时,会面临与传统内核文件系统崩溃(等同于断电)不同的恢复挑战。Ananke 通过引入名为 Plog 的内存日志和 M 算法,旨在正确、高效地恢复内存与磁盘之间的“状态间隙”,实现快速透明的恢复。
文件系统微内核与恢复挑战
上一节我们介绍了文件系统微内核的基本概念。本节中我们来看看其独特的崩溃恢复模型所面临的挑战。
当内核文件系统崩溃时,会导致整个操作系统崩溃,所有应用程序都会失去进度,因此其恢复方式与断电恢复类似,仅使用磁盘状态。然而,当作为进程的文件系统微内核崩溃时,只会导致该进程崩溃,操作系统和其他应用程序可以继续运行。
这带来了新的机遇,但也带来了核心挑战:恢复状态间隙。文件系统的更新首先发生在内存中,因此在磁盘状态和应用程序视图之间存在一个“状态间隙”。此外,后续操作或后台行为(尤其是非顺序操作)可能会任意改变这个状态间隙。
Ananke 的核心设计
为了解决上述挑战,Ananke 设计了核心机制。接下来,我们将深入探讨其核心数据结构 Plog 和恢复算法 M。
Plog:为进程崩溃恢复设计的内存日志
Plog 是嵌入在文件系统进程中的内存日志,用于记录操作和其他信息。当文件系统崩溃时,操作系统协调控制权转移,通知一个新的文件系统进程,然后使用 Plog 来恢复状态间隙。
Plog 的条目设计用于高效追踪操作如何影响状态间隙。每个条目包含:
- 一个数组:记录操作可能影响的目标(例如文件描述符、inode号)。
- 一个位图:追踪该操作对这些目标的更新是否仍然对状态间隙有贡献。
以下是 Plog 条目结构的简化表示:
struct plog_entry {
int targets[MAX_TARGETS]; // 可能影响的目标标识符数组
bitmap_t contribution_bits; // 贡献位图,每位对应一个目标
// ... 其他元数据(如操作类型、参数)
};
通过检查位图,可以快速判断一个已记录的操作是否仍需在恢复时被考虑,这有助于降低正常路径的开销。
M 算法:决定恢复时的操作
Plog 记录了“发生了什么”,而 M 算法(代表 Act、Ignore 或 Modify)则决定在恢复时“该怎么做”。它负责将 Plog 中的记录转换为可由原始文件系统实现执行的动作。
M 算法对每个已记录的操作做出三个决策之一:
- 忽略:如果该操作对当前状态间隙已无贡献,则忽略它。
- 执行:直接重放该操作,使用原参数。
- 修改执行:需要采取行动,但必须以修改的形式(例如,不同的操作类型或参数)。
这个决策基于 Plog 条目中的贡献位图以及恢复期间维护的路径名到 inode 的映射视图。
状态间隙的复杂性及解决方案
上一节我们介绍了 Ananke 的核心组件。本节中我们来看看状态间隙为何复杂,以及 Plog 和 M 算法如何协同解决。
状态间隙复杂的原因主要有两点:
- 后续操作移除更新:例如,
close操作会移除文件描述符,fsync操作会使某些更新持久化到磁盘,从而将它们从状态间隙中移除。 - 后续操作改变前置条件:例如,一个文件被打开、写入,然后重命名。崩溃后,需要重放写入操作,但无法使用原始路径名重放打开操作,因为路径映射已改变。
Plog 和 M 算法共同应对这些挑战:
- Plog 通过位图动态追踪每个操作的更新是否仍“有效”(即仍属于状态间隙)。
- M 算法在恢复时扫描日志,识别像
fsync和rename这样的操作,回溯并更新那些依赖于已改变路径映射的先前操作的参数(例如,将打开操作的路径名从旧名改为新名),从而实现正确的修改执行。
评估与总结
我们已了解了 Ananke 的设计原理。最后,我们来看看它的实际效果如何。
对 Ananke 的全面评估表明:
- 正确性:在超过 3 万次故障注入实验中,Ananke 均提供了故障透明性(即应用程序无感知)。
- 低开销:在大多数情况下,正常路径的性能开销低于 2%。
- 恢复速度快:恢复时间极短,即使对于具有挑战性的工作负载,对应用程序而言也仅表现为瞬间的性能下降。
总结
本节课中我们一起学习了 Ananke,一个为文件系统微内核提供快速透明进程崩溃恢复的系统。其核心创新在于 Plog 内存日志和 M 算法,它们能够高效、正确地恢复崩溃进程留下的状态间隙,而无需在正常路径引入额外的刷盘操作,从而兼顾了低开销和快速恢复。
更广泛地说,将进程崩溃恢复与全系统崩溃恢复分离是一个重要范式转变。进程崩溃不同于断电,它为实现不丢失状态间隙的透明恢复提供了巨大机遇。我们期望进程崩溃恢复能够提升当今大规模系统中本地文件系统服务的可靠性,减轻全局恢复的负担。


003:优雅提升文件系统性能——面向磁盘文件系统的透明NVM预写日志


在本节课中,我们将学习如何利用非易失性内存(NVM)来优雅地加速传统磁盘文件系统。我们将介绍一种名为 enlog 的透明预写日志方案,它能够在不修改应用程序和文件系统的情况下,显著提升同步写入操作的性能,同时保留磁盘缓存(D-cache)在处理其他操作时的高效率。
概述:为何需要NVM加速磁盘文件系统?
在过去的十年中,非易失性内存(NVM)技术兴起。与传统磁盘相比,NVM具有更高的持久化性能和更细粒度的写入支持。然而,专为NVM设计的文件系统并非完美解决方案。
NVM文件系统面临一些挑战:NVM容量通常小于磁盘;将数据从旧磁盘文件系统迁移到新NVM文件系统给用户带来不便;NVM文件系统的一致性语义可能与磁盘文件系统不同;新编写的NVM文件系统代码可能不如久经考验的磁盘文件系统稳定。
从性能角度看,虽然NVM本身比磁盘快,但NVM文件系统并不总是优于磁盘文件系统。这是因为磁盘文件系统通常由D-cache加速,而大多数NVM文件系统会绕过D-cache。
以Nova和EXT4为例:对于读操作,当缓存热时,磁盘文件系统性能优于NVM文件系统;当缓存冷时,NVM文件系统性能更优。对于写操作,带有D-cache的磁盘文件系统在异步写入时性能优于NVM文件系统,而NVM文件系统在处理同步写入时性能更优。
综上所述,NVM文件系统的优势主要体现在两个方面:缓存冷时的读操作,以及同步写操作。对于其他可以被D-cache加速的操作,磁盘文件系统性能更好。
因此,我们面临一个问题:能否结合磁盘文件系统和NVM文件系统两者的优势?答案是肯定的。我们可以在NVM上加速同步写入,同时保留D-cache处理快速操作的能力。
现有方案的局限性
当前已有一些利用NVM加速磁盘文件系统的方案,但它们未能达到最佳实践。
- SPFS:通过预测同步事件将部分数据接管到NVM来加速磁盘文件系统。但在同步操作不规则或不频繁的场景下,它无法提供有效加速。接管数据后,还可能拖慢后续的读写操作。
- Pettoc:允许从D-cache快速读取,但代价是写入性能。它将所有数据(无论同步还是异步)都写入NVM,这显然会导致异步写入工作负载的性能下降。
这些加速器模糊地接管了大量数据,而不是精确地只加速缓慢的同步操作。此外,由于需要频繁访问NVM数据,它们必须在运行时为NVM建立索引,这引入了复杂性和性能开销。同时,它们还需要持续占用大量NVM空间。
我们发现,先前的工作忽略了两点关键因素:
- D-cache已经提供了高效的索引和数据检索机制。因此,NVM应只专注于持久化同步数据,应避免为NVM建立索引和从NVM检索数据的需求。
- NVM写入和磁盘写入可能以随机顺序交错。先前的工作为了避免这种交错导致的数据版本不一致,接管了所有操作和大量数据。我们发现,理清跨不同设备的写入时序关系,有助于减少不必要的数据和操作被接管,从而充分利用RAM的高性能。
enlog的设计目标
我们希望将NVM高效地集成到现有系统中,它应具备以下属性:
- 透明性:对上层应用程序和底层磁盘文件系统都透明。这样可以避免要求修改用户程序,同时仍能重用成熟的磁盘文件系统,并避免数据迁移的开销。
- 一致性保证:不损害磁盘文件系统中同步操作的一致性保证。同时,无需在非同步场景引入强一致性,因为这只会降低性能。
- 无性能降级:确保在任何场景下(无论是读或写,同步或异步)都不会对现有磁盘文件系统造成性能降级。
- 轻量高效:设计并实现一个轻量级、高效的解决方案,占用尽可能少的NVM空间。
enlog的核心设计
我们设计了 enlog。与先前工作不同,enlog 不是一个文件系统,而是一个专门为磁盘文件系统设计的预写日志。
类似于数据库中的重做日志,enlog 专注于记录写入事件,以便在崩溃后可以重放尚未提交到磁盘的数据。它拥有预写日志的所有优点。
但与数据库重做日志不同,enlog 不要求将所有数据都写入磁盘,因为那样会降低性能。相反,enlog 只记录受同步操作影响的数据,以绕过缓慢的磁盘I/O。
我们在VFS层通过 vfs_fsync_range 拦截同步写入,将所有其他操作留给更快的D-cache处理。同时,我们提供细粒度的数据持久化,并解决NVM和磁盘写入交错引起的一致性问题。
enlog的数据结构
在介绍同步写入步骤前,首先介绍 enlog 用于在NVM上存储数据的数据结构。
enlog 采用日志型结构。首先,inode log 用于持久化单个文件的同步数据。对于每次写入,数据按页边界划分,每个部分由一个 inode log entry 保存,如上图中的绿色块所示。
其次,对于每个 inode log,其头部和尾部信息存储在一个特殊的 super log entry 中,即图中的小蓝色块。所有的 super log entry 共同构成了整个 super log。为了方便恢复,这个 super log 的起始位置被放置在NVM的零地址。
需要注意的是,当数据按页划分时,其头部和尾部可能不对齐页边界,这可能导致数据块小于一页。中间部分可能包含页对齐的、包含完整页数据的块。对于未对齐的小数据块,我们使用 in-place log entry 将数据按字节写入日志条目本身,如图中的条目0和条目3。对于完整的页数据块,我们将数据写入单独的NVM页,并使用 out-of-place entry 链接它们,如图中的条目1和条目2,以便于后续的垃圾回收。
同步写入流程
以下是传统磁盘文件系统的同步写入步骤。可以看到,一旦同步发生,就需要等待缓慢的磁盘I/O。由于磁盘的最小写入粒度是块,此操作无法实现细粒度持久化。
在 enlog 中,我们不等待缓慢的磁盘I/O,而是将同步数据写入NVM中的 enlog 并立即返回。这些页将在后台异步写回磁盘。
对于 O_SYNC 写入,我们知道正在写入数据的偏移量和长度。由于NVM支持字节粒度写入,我们可以为小的 O_SYNC 写入实现细粒度持久化。
我们进一步说明同步进程与日志条目的对应关系。对于 O_SYNC 写入,写入的未对齐部分使用 in-place entry 处理以实现细粒度写入,如图中黄色部分。对齐的完整页数据则使用 out-of-place entry 写入,如图中红色部分。
对于独立的同步调用,如 fsync 和 fdatasync,由于我们只知道调用时哪些页是脏的,但不知道具体哪些字节是脏的,我们只能在页粒度上吸收同步写入。如图所示,现在我们知道,在 fsync 调用中,少量粉色的用户写入可能导致一大块红色数据被写入存储介质。
但由于NVM支持字节粒度写入,我们能否使同步操作也实现细粒度?当然,跟踪每个字节的脏状态是不切实际的。然而,我们也知道在 O_SYNC 写入中,enlog 可以以字节粒度吸收数据。基于此观察,我们的想法是预测文件的同步模式,并主动为每个文件标记或清除 O_SYNC 标志。通过这种主动同步机制,当文件上发生连续、分散的小同步写入时,enlog 可以用更细的粒度吸收它们中的大部分。
处理写入交错与一致性
由于存在两种不同的写入路径:一种从D-cache到NVM,另一种从D-cache到磁盘,它们之间的任何交错都可能导致一致性问题。换句话说,如果顺序未知,在崩溃恢复期间,一些过时的NVM数据可能会覆盖磁盘上已更新的数据。
我们的解决方案是在NVM上记录必要的磁盘写回事件,如图中紫色气泡所示,以建立一个全局时钟。更多细节可以在我们的论文中找到。
垃圾回收
为了理解垃圾回收过程,我们需要知道哪些NVM记录(即条目)不再需要。由于我们在写入时沿页边界分割数据,我们可以看到每个条目对应一个单独的页。
如果整个页被覆写,即存在一个 out-of-place entry,那么与该页相关的所有先前条目都可以被丢弃。或者,如果一个页已被写回磁盘,这意味着该页之前的所有写入都已保存,因此在该写回事件之前该页的条目也可以被丢弃。
之后,我们只需要调度一个后台线程来回收这些未使用的条目和关联的数据。以下是一些例子:这些 out-of-place entries 和 rep entries 将使该页的先前条目过期。
崩溃恢复
对于崩溃恢复,我们首先需要识别每个页的所有相关条目。我们对整个 enlog 进行全扫描,将与同一页相关的所有元素链接成一个链表。然后,我们顺序访问每个页,并将未过期的条目重放到磁盘上。
实验评估
以下是我们的实验设置。由于时间限制,此处不详细展开,但请注意我们所有的评估都是在缓存热的情况下进行的。一方面,缓存命中是大多数工作负载的常态;另一方面,我们只加速同步写入,因此需要避免缓存未命中造成的性能干扰。
首先,我们测试了多种文件系统和加速器在不同读写和同步比例下的性能。我们的 enlog 在完全没有同步操作时保持了D-cache的高性能,在这种情况下我们可以超越NVM文件系统及其全同步写入。它可以匹配NVM文件系统的性能,并且在处理混合读写和异步请求时,在所有系统中实现了最佳的整体性能。这种高性能的关键原因是 enlog 精确地加速了同步操作,同时允许D-cache更快地处理所有其他请求。
接下来,我们进行了纯同步写入性能测试。我们可以为磁盘文件系统提供高达15倍的加速,对于小的同步写入,其性能甚至可以通过Nova四倍,因为 enlog 支持细粒度写入。
需要注意的是,在16kB大块同步写入的情况下,enlog 的性能比Nova差,因为Nova只需要写入NVM一次,而 enlog 需要同时写入NVM和D-cache。然而,我们认为需要纯大块同步写入的应用程序相对较少,因此,在更广泛的使用场景中,牺牲一点同步写入性能以获得更好的异步写入和读取性能是值得的。
接下来,我们测试了垃圾回收性能。可以看到,在GC的帮助下,enlog 只临时占用少量的NVM空间。
我们还使用真实世界的工作负载测试了 enlog。结果表明,在各种任务下,enlog 至少可以匹配磁盘文件系统和NVM文件系统中最佳者的性能,证明了其广泛的适用性。
总结
本节课中,我们一起学习了一种利用NVM透明加速磁盘文件系统的新方法。通过将NVM作为预写日志集成到系统中,我们可以精确地吸收那些缓慢的同步写入到NVM,同时将其他操作留给D-cache。这种设计确保了 enlog 在任何情况下都不会导致磁盘文件系统的性能降级。
此外,我们采用了轻量级的设计和实现,仅使用少量的NVM空间。我们相信,这种对NVM的无痛使用更有可能被传统存储系统的用户广泛接受。
我们建议需要大量细粒度同步写入的写密集型数据库或类似应用程序选择Nova。除此之外,对于具有混合读写工作负载的服务器、PC和移动设备,我们相信 enlog 具有更大的潜力。

最后,该原型已在Github上开源。这就是我今天要分享的内容。谢谢。





004:面向CMM-H SSD的目录粒度文件系统日志技术


概述
在本节课中,我们将学习一种名为DJFS(Directory-Granularity Filesystem Journaling)的新型文件系统日志技术。该技术旨在解决传统日志机制在现代高性能存储设备(如CMM-H SSD)上导致的性能瓶颈问题。我们将从硬件演进背景出发,分析传统日志的问题,然后深入探讨DJFS的设计目标、核心原理、关键技术挑战及其性能评估。
硬件快速演进
我们首先回顾CPU和存储设备从1990年至今的发展历程。
在过去30年里,CPU核心数量增加了128倍。对于存储设备,其I/O延迟在20年内降低了约10,000倍。
传统日志机制的性能瓶颈
为了提供文件目录抽象层的崩溃一致性,日志或写时复制技术被广泛使用。
然而,日志机制会导致严重的性能下降。
幻灯片中的图表展示了文件创建基准测试(MDtest)的结果,对比了JBD2和F2FS的fsync提交。结果显示,使用日志机制会导致约70%的性能下降。
任何使用文件目录抽象层的应用程序都可能因日志机制而遭受性能下降。
诸如AIM、RocksDB、MySQL、Git和虚拟机等应用程序都依赖于文件目录抽象。文件系统更新通过日志机制反映到存储设备,因此应用程序无法充分利用像CMM-H这样的现代高性能存储设备的性能。
日志机制的瓶颈可归纳为以下四个方面:
以下是四个主要瓶颈:
- 事务锁争用:在许多日志技术中,多个操作将元数据更新插入到单个事务中。为了防止线程间的竞态条件,必须获取事务锁。
- 事务冲突:当要插入到事务中的对象已经属于另一个事务时,就会发生事务冲突。当冲突发生时,新事务必须等待前一个事务提交。
- 串行提交:许多通用文件系统在日志区域内串行化事务提交。即使发出了提交请求,在大多数情况下也必须等待前一个事务首先提交。
- 事务锁延迟:在组提交期间,没有事务的文件操作必须在事务提交开始前完成。为了防止组提交延迟,在此期间不能将新的文件操作插入到事务中,发起新文件操作的线程必须等待。
现有解决方案及其局限性
许多研究被提出来解决日志的可扩展性问题。
- 2014/2015年:ISF2和SPAP引入了按分区的事务分配。
- 2017年:I-Journaling提出了按inode的事务分配。
- 2021年:Z-Journaling引入了按路径组件(Popcorn)的事务分配。这种分配在事务冲突很少发生的情况下表现良好。
- 2023年:CJ-Journaling提出了专为并发提交设计的技术。这在事务冲突频繁的Workload中显示出令人印象深刻的性能。
- 2024年:F-Journaling结合了逻辑日志和物理日志。这最小化了频繁文件操作的日志大小,从而减少了写放大。
然而,每种日志技术都有其局限性。
- 按分区的事务分配需要修改磁盘布局。
- 按inode的事务分配难以从组提交效果中受益。
- 按路径组件的事务分配经常遇到事务冲突,需要链接多个影子页来解决,这增加了写放大。
- 并发提交是基于原子持久性I/O设计的,使其只能在特定设备上使用。
- 混合日志在事务日志提交期间缺乏细粒度的事务锁,显著增加了锁持有时间。
CMM-H SSD与设计目标
现在,这张幻灯片介绍了CMM-H,这是一种支持基于CXL域协议进行I/O操作的SSD。CMM-H支持两种I/O模式:通过CXL.io的块I/O和通过CXL.mem的缓存行I/O。
这两种I/O模式的区别可以类比为卡车和特斯拉汽车。块I/O具有高带宽但高延迟,而缓存行I/O具有低带宽但低延迟。
通过分析先前的研究,我们为日志可扩展性确立了三个目标:
以下是我们的三个设计目标:
- 最小化开销:减少事务锁持有期间被阻塞的文件操作数量。
- 最小化冲突:防止由事务冲突引起的不必要的事务串行化。
- 最大化事务合并度:减少每次事务提交的缓存刷新开销。
应用行为分析与目录粒度事务
为了探索满足这三个目标的事务分配方法,我们分析了八个应用程序的文件操作模式。这些应用包括AIM、Lustre、MySQL、Git、Mercurial、Btrfs和HDFS。
我们发现了三个关键属性:
以下是分析得出的三个关键属性:
- 专用目录(属性D):应用程序在其自己的目录内工作。例如,AIM在
school和mail目录中执行文件操作,RocksDB在系统管理员指定的目录内操作,MySQL也在指定的目录中执行文件操作。 - 目录更新(属性U):更新文件内容涉及创建和删除文件。一个众所周知的例子是原子重命名。此外,随着日志结构写入的日益普及,这种趋势变得更加明显。例如,RocksDB不操作现有的SST文件,而是创建一个新的SST文件并向其中写入数据。
- 共享目录(属性S):应用程序更新属于同一目录的多个文件。例如,在AIM中,每封邮件的邮件数据和邮件头都在同一个
school目录中创建。
基于专用目录属性,每个应用程序在其目录上工作,与其他应用程序隔离。因此,如果我们按目录分配事务,一个应用程序在恢复期间的事务不会影响其他应用程序的事务。
其次,使用按目录的事务,我们可以通过属性U和S携带多个相互关联的文件操作。
由于按目录事务的上述好处,我们决定将目录设置为日志事务的单位。
DJFS核心设计
在DJFS中,更新的元数据被插入到路径字符串最深层组件的父目录的运行事务中。
这是一个时间序列示例。当调用create(“/D”)时,以下对象被插入到D的运行事务中:D inode、D directory entry和A inode。然后当调用open(“/D/F1”)时,以下对象被插入到D1的事务中:F1 inode和F1 directory entry。当调用link时,以下对象被插入到D2的事务中:F2 inode、D2 directory entry和F2 inode。
这张幻灯片从三个轴(锁开销、冲突和合并度)分析了先前的工作。与其他日志技术不同,我们实现了所有三个目标。
在DJFS中,每个目录最多可以有一个运行事务和一个提交事务。
当文件操作被调用时,它识别目录并将日志记录插入到该目录的运行事务中。运行事务可以定期提交,也可以通过fsync提交。
在DJFS中,多个事务可以同时提交,如果在短时间内调用了几个fsync。
我们的DJFS仅记录文件元数据,例如inode、文件映射和目录项。文件系统元数据在崩溃恢复期间重建,就像在I-Journaling或Concurrent Commit中一样。这是为了减少事务冲突的频率。
对于inode,DJFS使用256字节的物理日志。对于索引和目录块,它使用基于Bmap的缓存行粒度差异日志。DJFS采用细粒度日志的原因是为了确保日志能够放入CMM-H缓存中。
该图说明了相同X动态映射的随机输出,作为数据集大小的函数。随着数据集大小的增加,吞吐量下降。
关键技术挑战与解决方案
为了实现按目录事务,存在三个挑战。
以下是三个主要挑战及其解决方案:
- 事务选择:一个inode可以被多个目录项引用。那么同一个inode可能存在多个运行事务。日志模块需要选择事务。
- 多事务原子性:单个文件操作可以更新多个目录。我们需要一种机制以原子方式处理多个目录更新。
- 解决目录事务冲突:冲突下的事务需要正确同步。
解决方案1:基于路径的事务选择
一个文件可以有多个链接,因此可以有多个父目录。在这种情况下,我们使用一种称为基于路径的事务选择的技术。如果文件操作需要路径,则文件操作基于路径选择运行事务。当文件操作接收到文件描述符时,它通过从关联的文件结构跟踪目录项来跟踪路径。
解决方案2:事务队列
单个文件操作可能更新多个目录。在这种情况下,文件操作的原子性可能会受到损害。为了解决这个问题,我们开发了事务队列技术。事务队列将两个事务合并为一个。在此示例中,D1事务和D2事务被排队。
解决方案3:运行-运行冲突处理
当一个事务尝试更新属于另一个运行事务的元数据对象时,会发生运行-运行冲突。如果文件系统检测到运行-运行冲突,文件系统将两个事务排队,并为冲突操作更新现有的日志记录。
解决方案4:运行-提交冲突处理
当文件操作更新属于提交事务的元数据对象时,会发生运行-提交冲突。如果文件系统检测到运行-提交冲突,文件系统会延迟提交运行事务,直到冲突下的提交事务在存储中持久化。
性能评估
这是评估设置。我们使用了双路Intel SPR机器和CMM-H原型。我们使用Ubuntu 22.02和Linux内核5.18。我们使用了六种日志模块:JBD2、F2FS commit、CJ-Journaling、Z-Journaling、I-Journaling和DJFS。
其中,JBD2、F2FS commit、I-Journaling、DJFS使用CXL.mem接口来持久化事务。此外,我们将DJFS的细粒度日志应用于JBD2和I-Journaling。
我们使用了四个宏基准测试:Postmark、MDtest、AIM和RocksDB fill-sync。
我们展示了MDtest和RocksDB fill-sync的吞吐量。MDtest是一个元数据密集型基准测试,DJFS显示出比F2FS commit高2.4倍的吞吐量。在RocksDB fill-sync中,它显示出与F2FS commit几乎相似的吞吐量。
然后,我们测量了所有四个基准测试中每个工作负载的事务锁持有间隔长度。由于细粒度的事务粒度,DJFS显示出事务锁持有时间的显著减少。
接下来,我们计算了由于事务冲突而导致事务被阻塞的次数,我们称之为冲突计数。按目录和按路径组件的事务分配几乎消除了事务冲突,因为它们是面向每个文件系统数据结构的事务分配。
然后,我们计算了每个事务的文件操作数量。按目录的事务成功地比按路径组件的事务携带了更多的文件操作。
总结
本节课中,我们一起学习了DJFS的设计与实现。
我们分析了应用程序的文件数据行为,并得出结论:它围绕专用目录展开。
我们提出了一种新的文件系统日志单位:目录。
对于CMM-H,我们需要一种能够利用CMM-H访问局部性的新日志文件系统。
因此,我们提出了DJFS,即目录粒度日志文件系统。




005:ScaleLFS - 为商用SSD设计的具有可扩展垃圾回收功能的日志结构文件系统 🗂️

概述
在本节课中,我们将学习一种名为ScaleLFS的新型日志结构文件系统。该系统通过并行化垃圾回收过程,显著提升了文件系统的持续性能。我们将深入探讨其设计原理、核心组件以及性能表现。
日志结构文件系统与垃圾回收瓶颈
上一节我们概述了ScaleLFS的目标。本节中,我们来看看其要解决的核心问题:传统日志结构文件系统中的垃圾回收瓶颈。
日志结构文件系统(LFS)通过仅追加的方式写入数据来优化写操作。在LFS中,新数据被写入连续的LBA,以利用顺序写入的高性能。当数据被覆写时,之前的页面变为陈旧并被标记为无效。如果无效页面积累导致空闲空间不足,LFS会通过垃圾回收来回收空间。在GC过程中,LFS选择一个连续的LBA范围作为受害者段,并将有效页面从受害者段迁移到空闲空间。
然而,频繁的GC会显著降低LFS的持续性能。因为GC在单线程中运行,它会形成一个瓶颈。如图所示,GC触发后,应用带宽最多下降了68倍。
一个直观的改进方法是引入GC多线程。然而,如下图所示,增加GC线程数并未提升性能。这个结果是由于当前GC过程中锁争用的增加导致的。因此,需要并发且可扩展的GC技术。
传统LFS中的可扩展性瓶颈
上一节我们看到了GC的瓶颈。本节中,我们基于广泛使用的FFS文件系统,分析阻碍GC可扩展性的四个关键瓶颈。
以下是四个主要瓶颈:
- 共享资源:GC的目标段和页缓存被多个线程共享,导致同步开销。解决方案是为每个GC线程分配专用资源。
- 受害者选择的过度争用:受害者选择操作被互斥锁独占保护,形成了可扩展性瓶颈。解决方案是允许并发受害者选择。
- 过度的元数据同步:段元数据被单个互斥锁保护,迫使所有线程等待。解决方案是启用并发段元数据更新。
- 粗粒度的数据保护:GC线程和I/O线程在文件级别竞争访问,造成争用。解决方案是切换到页面级别以减少争用。
ScaleLFS的核心组件
上一节我们分析了问题所在。本节中,我们来看看ScaleLFS提出的三个核心解决方案组件。
ScaleLFS建立在三个关键组件之上:专用垃圾收集器、可扩展受害者管理器和可扩展受害者保护器。
- 专用垃圾收集器
DGC是一个拥有自己专用页缓冲区和目标段的GC线程,这减少了资源共享开销。 - 可扩展受害者管理器
SVM允许多个DGC使用原子位图并发选择受害者。它还允许宽松的同步更新,因此多个线程可以同时更新段元数据。 - 可扩展受害者保护器
SVP通过使用每文件并发哈希表来启用页面级别的垃圾回收,以减少争用。
组件一:专用垃圾收集器详解
上一节我们介绍了三个组件。本节中,我们首先深入探讨专用垃圾收集器的细节。
DGC包含一个每GC线程数据缓冲区和写入流。专用数据缓冲区有助于避免昂贵的页缓存开销。如图所示,当GC启动时,DGC从SVM选择一个受害者段。它不使用全局共享的页缓存,而是直接将有效页面加载到其专用页缓冲区中。然后,DGC将缓冲区数据写入其专用写入流。此时,专用写入流支持以顺序方式进行无锁LBA管理。
组件二:可扩展受害者管理器详解
上一节我们了解了DGC。本节中,我们来看看可扩展受害者管理器如何工作。
SVM引入了并发受害者选择,利用两个原子位图:一个用于脏段,另一个用于受害者段。每个DGC选择有效页面最少的段作为其受害者。在受害者选择期间,脏段位图用于并发识别候选受害者,而受害者段位图则解决并发受害者选择中的冲突。多个DGC选择同一段作为受害者的数据竞争通过原子测试并置位操作解决。成功设置受害者段位的DGC获胜。通过采用CVS,DGC可以并发选择受害者而不会遇到锁争用。
SVM的另一个关键特性是宽松同步更新。它允许SVM并发更新有效页面位图(指示需要GC迁移的页面)和有效页面计数(被视为GC成本)。我们为VPB使用原子位图,为VPC使用原子整数。然而,LSU引入了两个副作用:次优受害者选择(增加GC成本)和误判GC读取(导致不必要的读I/O)。
次优受害者选择的例子在图的左侧描述。DGC0最初由于最低的VPC而选择段1作为受害者。同时,其他线程更新了VPC,使得段0成为最优受害者。不幸的是,由于对VPC的更新不是紧密同步的,这对DGC0可能是不可见的。这导致了GC写放大,但在我们的评估中,由LSU引起的写放大可以忽略不计。
第二个副作用,误判GC读取,在图的右侧描述。当DGC1发现其受害者段的VPB中第一个位为1时,它开始对该受害者的第一页进行GC读取。然而,与此同时,另一个线程清除了第一个位,表明第一页已变为陈旧。由于LSU,DGC1无法识别此更新。这种误判GC读取可能导致数据一致性问题。我们第一次误判GC读取的数据是陈旧的,因此,这些陈旧数据不应提供给用户以确保数据一致性。
为了实现数据一致性,我们通过利用节点数据(如索引节点或直接节点)来过滤掉陈旧页面。如果节点内的LBA与GC读取的LBA不同,ScaleLFS将相应的寻址页面确定为陈旧并跳过其GC写入。此过程受节点级日志保护,该日志已在FFS中用于保护节点数据免受并发更新。因此,它不会造成额外的日志开销。
组件三:可扩展受害者保护器详解
上一节我们解决了SVM的副作用。本节中,我们来看最后一个组件:可扩展受害者保护器。
SVP通过利用带有比较并交换操作的每文件并发哈希表来启用页面级别GC。它在访问页面之前插入该页面的文件偏移量。当访问完成时,插入的条目被移除以防止读取已释放的内存空间。采用逻辑删除。逻辑删除的条目由首先发现它的其他线程实际移除。如果一个线程发现相同的文件偏移量已经插入到SVP中,该线程会等待其被移除。为了减少争用,SVP根据文件偏移量将请求分发到多个桶中。通过SVP,DGC在页面级别执行GC,从而降低了争用。
性能评估:微基准测试
上一节我们详细了解了所有组件。本节中,我们通过评估来看看ScaleLFS的实际表现。
我们在配备英特尔CPU、160GB DRAM和三星983 SSD的机器上评估ScaleLFS。我们使用4KB随机写入进行微基准测试。此外,我们采用文件服务器、邮件服务器和OLTP工作负载进行宏基准测试。作为真实世界应用,我们使用YCSB工作负载评估MySQL。
为了比较,我们使用了Linux内核中的FFS和运行在纯LFS模式下的FFS-L。此外,我们还评估了作为最先进可扩展LFS的MapFS和作为并行GC方案的PGC。ScaleLFS是在LFS模式下的FFS上构建的原型。
在微基准测试中,我们使用两种容量配置评估我们的想法:30GB的小分区和完整的SSD容量。小分区的评估通过最小化SSD内部GC的影响来隔离文件系统性能。在此配置中,我们提交了120GB的随机写入。如左图所示,ScaleLFS在执行时间上比其他LFS快达7倍。当其他LFS经历显著的带宽下降时,ScaleLFS保持了相对较高的带宽。这些结果表明,ScaleLFS在高I/O密集型工作负载下有效地扩展了LGC,从而保持了持续的吞吐量。
此外,为了评估ScaleLFS如何有效地利用SSD,我们测量了提交给SSD的写入带宽。如右图所示,与其他LFS相比,ScaleLFS实现了高达19.6倍的带宽。这一结果表明,即使频繁进行GC,ScaleLFS也能高效利用SSD。
为了在更实际的存储配置上进行评估,我们也使用了完整的SSD容量。在此评估中,我们提交了10.3TB的随机写入。如图所示,与FFS相比,ScaleLFS减少了37分钟的执行时间。运行在纯LFS模式下的系统(如FFS-L和PGC)在LFS和SSD GC都启动后,显示出11MB/s的持续写入带宽。相比之下,ScaleLFS实现了最高的性能,没有明显的性能下降。这一结果意味着即使与SSD GC并行,ScaleLFS也是高效的。
性能评估:宏基准测试与真实应用
上一节我们看到了微基准测试的优异结果。本节中,我们看看它在更复杂工作负载和真实应用中的表现。
对于宏基准测试,我们使用Filebench中的三个写入密集型合成工作负载。在此评估中,与其他LFS相比,ScaleLFS将吞吐量提高了高达83.1%。由于大量文件删除或应用线程间的争用导致的GC频率较低,性能改进低于微基准测试的结果。
为了展示在真实应用中的性能,我们在YCSB工作负载下使用MySQL数据库。最初,插入650万条记录以达到总分区容量的70%。在工作负载A(更新密集型)中,执行时间减少了高达70.4%。在工作负载B(读取密集型)中,执行时间减少了27.3%。尽管读取密集型工作负载为ScaleLFS带来了更具挑战性的条件,但结果表明它仍然带来了性能改进。
总结



在本节课中,我们一起学习了ScaleLFS,一个旨在通过多线程友好优化来加速垃圾回收的日志结构文件系统。ScaleLFS利用具有专用策略的多个GC线程,并通过并发数据结构和宽松的受害者管理来增强GC并发性。此外,它引入了页面级别保护机制以减少线程间的冲突。评估结果表明,与现有的LFS相比,ScaleLFS将性能提升了高达7倍。
006:重新思考文件系统的请求到I/O转换过程以实现高速SSD的充分利用 🚀



在本教程中,我们将学习一篇来自FAST‘25存储大会的研究。该研究探讨了现代文件系统在利用高速NVMe SSD时遇到的性能瓶颈,并提出了一种名为OrFS的新型异构文件系统架构来解决这些问题。我们将逐步解析问题的根源、解决方案的设计思路以及其带来的性能提升。
概述:高速SSD时代的性能瓶颈
过去几年,固态硬盘(SSD)的性能经历了爆炸式增长,带宽从不到1 GB/s提升到了超过10 GB/s。然而,尽管底层硬件性能大幅提升,许多数据密集型应用(如图处理、科学计算)的实际性能却未能同步增长。本研究发现,传统的文件系统I/O栈在处理用户请求时存在显著的“写入低效”问题,导致无法充分利用高速SSD的带宽潜力。
问题诊断:为何文件系统成为瓶颈?
上一节我们概述了性能不匹配的现象,本节中我们来看看具体是哪些因素导致了“写入低效”。研究通过一系列测试和分析,定位了三个核心问题。
首先,研究者运行了单线程随机写入测试,发现文件系统的吞吐量仅能达到底层SSD带宽的三分之一或四分之一。即使将测试聚焦在接近1MB的大尺寸写入上,性能提升依然有限。
以下是导致性能瓶颈的关键因素分析:
-
I/O对齐开销:文件系统使用页缓存(Page Cache)将用户请求转换为内存页对齐的I/O。对于非页对齐的写入,需要进行耗时的“读-修改-写”操作。这涉及从SSD读取数据、在内存页中修改、再写回SSD。由于SSD访问延迟较长,这些小规模操作变得极其缓慢。
公式表示:非对齐写入耗时 = SSD读取延迟 + 内存修改时间 + SSD写入延迟 -
页缓存管理开销:随着写入尺寸增大,页缓存本身的管理开销(如查找脏页、组装BIO)变得显著。对于1MB的写入,页缓存管理开销可占总写入时间的40%以上。
-
I/O并发度不足:大多数存储系统被动依赖SSD的内部并行性。但由于I/O调度的复杂性,单一线程很难最大化SSD性能。测试表明,使用多线程处理较小的I/O请求能更好地利用SSD带宽。
解决方案:OrFS异构文件系统设计
在明确了问题根源后,研究者提出了名为OrFS的解决方案。其核心思想是引入非易失性内存(NVM)作为辅助存储,协同高速SSD工作,而非简单地在内存中合并请求或强制使用对齐要求苛刻的直接I/O。
异构数据布局
OrFS将文件数据分布在两种存储介质上:
- 元数据:全部存放在NVM上。
- 文件数据:根据策略分布在NVM和SSD上。
系统定义了三种存储单元类型:
SD Blocks: 用于存放32KB对齐的数据段。NVM Pages: 用于存放4KB对齐的数据段。NVM-U Pages: 用于存放剩余的未对齐数据片段。
写入请求分割策略
当写入请求到达时,OrFS会将其智能地分割为三部分:
- 块对齐的SSD I/O
- 页对齐的NVM I/O
- 未对齐的NVM-U I/O
分割过程遵循两项策略:
- 对齐优先策略:按块对齐 > 页对齐 > 未对齐的优先级顺序分割请求。
- 碎片最小化策略:尽可能将文件数据放置在更少的存储单元中,以减少碎片。
统一文件映射结构:HR-Tree
为了高效管理分布在三种存储单元上的数据,OrFS设计了异构Radix树(HR-Tree)。它由传统的Radix树和一个底部的异构层组成。
- Radix树部分将文件逻辑偏移映射到逻辑块。
- 异构层记录每个逻辑块内部的数据构成(即哪些部分在SSD块,哪些在NVM页)。
通过HR-Tree,OrFS只需一次树遍历就能定位一个文件请求关联的所有存储单元。
并行I/O引擎
为了高效、透明地处理分割后的异构I/O并保证数据一致性,OrFS实现了并行I/O引擎。
- 并行处理:SSD I/O和NVM I/O被并行处理。
- 多线程I/O:主动使用多线程来处理SSD I/O,以充分挖掘其并发性能。
- 数据一致性:每个I/O线程在其独占的数据范围内执行操作,避免冲突。
- I/O路径优化:
- SSD写入:使用直接I/O模式,绕过页缓存。
- SSD读取:使用缓冲I/O模式,受益于预对齐写入带来的高效页缓存命中。
- NVM访问:使用高效的内存映射路径。
性能评估:OrFS的表现如何?
研究者将OrFS与多类文件系统进行了对比测试,包括传统SSD文件系统(EXT4, F2FS)、NVM文件系统(NOVA, AFS)和混合文件系统(Strata, SPFS, 以及基于OrFS思想实现的PHFS)。
以下是核心测试结果:
- 单线程随机写入性能:OrFS在各种写入大小下都表现出显著更低的平均写入延迟。
- 数据分布分析:在大写入(如1MB)场景下,OrFS有超过90%的数据被直接写入SSD,性能主要来源于高速SSD。而像Strata这样的系统,其性能主要来源于昂贵的NVM。
- 读取性能:由于写入时数据在SSD侧总是32KB对齐的,这使得后续的读取操作能更高效地利用页缓存,因此OrFS的读取吞吐量也显著高于基线系统。
- 多线程性能:OrFS仅需少量用户线程即可最大化SSD的写入性能,证明了其主动提升I/O并发度的有效性。
- 实际应用:
- 在充满小请求的LevelDB工作负载中,OrFS使用NVM处理多数请求,而让SSD快速处理大的压缩写入,表现优异。
- 在混合大小请求的图处理工作负载中,OrFS将处理时间减少了近70%。
总结与资源
本节课中我们一起学习了现代文件系统在高速SSD上面临的“写入低效”挑战。我们深入分析了其三大根源:I/O对齐开销、页缓存管理开销和I/O并发度不足。接着,我们详细介绍了OrFS异构文件系统的设计方案,它通过引入NVM作为辅助存储、智能分割写入请求、使用HR-Tree统一映射以及并行I/O引擎,有效地克服了这些瓶颈,在读写性能上都实现了显著提升。
这项研究已开源,您可以通过提供的链接查看其详细实现。


总结要点:
- 问题:传统文件系统I/O栈导致高速SSD带宽无法被充分利用(写入低效)。
- 根源:非对齐I/O操作、页缓存管理开销、单线程I/O并发不足。
- 解决方案:OrFS文件系统,采用NVM+SSD异构存储、请求分割、HR-Tree和并行I/O引擎。
- 结果:大幅提升了随机写入、读取以及实际应用(如LevelDB、图处理)的性能。
007:FlacIO - 面向容器镜像服务的扁平化与聚合I/O方案


概述
在本节课中,我们将学习一篇来自FAST25存储大会的技术分享,主题是“FlacIO:面向容器镜像服务的扁平化与聚合I/O方案”。我们将深入探讨传统容器启动的瓶颈、FlacIO的核心设计思想、其关键组件以及它如何显著提升容器冷启动性能。内容将力求简单直白,适合初学者理解。
传统容器存储栈与启动瓶颈 🐢
首先,我们来了解传统的容器存储栈。容器通常在Overlay文件系统上构建其文件系统,用于合并镜像的多个层,并为容器提供一个统一的视图。
启动容器的传统方式是将整个镜像加载到节点。然而,这种方式效率低下,因为它会引发大量的I/O放大,并导致启动延迟。
为了加速启动,延迟加载成为了主流解决方案。它只将镜像的元数据拉取到节点,让容器快速启动,而实际的数据I/O则在容器运行过程中按需进行。
另一种优化是减轻冷启动开销,例如使用主机端缓存共享或P2P加载。但这些方案需要消耗主机资源,而主机资源是有限的,因此无法完全解决冷启动问题。本工作将重点聚焦于冷启动加速。
延迟加载的典型过程与I/O分析 🔍
上一节我们介绍了冷启动的瓶颈,本节中我们来看看延迟加载的典型过程。
延迟加载过程主要分为三个阶段:
- 获取元数据:获取构建容器运行时所需的元数据。
- 创建运行时:在主机上创建运行时环境,例如创建Cgroup、创建文件系统。
- 启动服务:启动服务并执行容器内的入口点。
在这个过程中,数据会先被拉取到延迟加载模块的预取缓存中,然后加载到本地文件系统。这个阶段会触发大量的I/O操作。
现在,让我们分析现有解决方案中冷启动开销的构成。在我们的实验中,CFS和Lindle是文件级加载系统,而DADI是块级加载系统。
从结果可以看出,延迟加载方案确实能加速部署阶段,但它们在就绪阶段会遭受非常长的延迟。
当我们深入分析现有系统的I/O行为时,发现它们在就绪阶段存在严重的I/O放大和低效的网络访问问题。
问题的根源:传统镜像组织方式 🧩
上一节我们分析了延迟加载的I/O问题,本节我们来探讨其根本原因。
问题的关键在于传统的镜像组织方式。传统镜像是全局导向的,意味着一个镜像被多个服务使用;同时它也是存储导向的,主要反映磁盘状态。
这种组织方式的缺点如下:
- 由于镜像上的随机访问,使得延迟加载系统难以聚合I/O。
- 延迟加载单元与容器访问模式不匹配,导致高I/O放大。
- 由于不同容器间的I/O行为不同,I/O局部性难以优化。
- 需要一个复杂的I/O路径来将磁盘状态转换为内存状态。
解决方案:运行时镜像组织方式 🚀
基于对传统方式缺点的分析,我们提出了运行时镜像组织方式。
其核心思想是:为每个服务预计算其内存状态。因此,它是服务导向的(一个镜像对应一个服务),并且是内存导向的(只反映内存状态,而非磁盘状态)。
这种组织方式的好处是:
- 网络友好:可以通过一次网络I/O精确拉取冷启动所需的数据。
- 支持快速loop文件系统构建:因为运行时镜像已经包含了内存数据的索引和数据本身,这使得loop文件系统可以直接挂载在给定的运行时镜像上。
FlacIO的设计挑战与概述 ⚙️
然而,基于运行时镜像设计FlacIO并非易事。我们面临两大挑战:
- 如何高效地组织运行时镜像:我们需要考虑容器启动时准确的I/O追踪,同时要兼顾镜像大小并与现有生态系统兼容。
- 如何在主机上实现轻量级的I/O栈:当前内核不支持从给定的内存状态挂载文件系统,我们需要新的操作原语。同时,FlacIO运行在延迟加载系统之上,因此需要与传统的I/O栈兼容。
现在,让我介绍FlacIO的总体架构。它包含四个关键组件:
- FlacIO驱动:作为控制平面,连接系统中的其他组件。
- I/O追踪器:用于在容器首次启动时记录I/O。
- 运行时页缓存:内核中的一个特殊文件系统页缓存,允许将运行时镜像注入内核并挂载到文件系统。
- 运行时镜像服务:位于注册节点,用于管理和生成运行时镜像。
以下是工作流程:
当用户调用运行时镜像创建API时,系统会启动容器,收集I/O,并将I/O记录发送到注册表进行离线生成。
在容器冷启动期间,FlacIO驱动会先获取运行时镜像元数据,然后通过一次大粒度的I/O拉取镜像数据,并将其注入RPC。
在容器运行期间,RPC处理对位于运行时镜像中的页的读写和缺页错误。对于那些在运行时镜像中缺失的I/O,则会被重定向到传统流程处理。
准确的I/O追踪机制 🎯
上一节概述了FlacIO的架构,本节我们深入了解其基石——准确的I/O追踪。
运行时镜像的生成依赖于容器启动时准确的I/O追踪。I/O追踪并非新概念,例如DADI就使用了块级追踪机制进行I/O预取。
我们采用了基于感知点(PoP)的文件级I/O追踪机制。最终用户可以使用 RT_CREATE API 来生成运行时镜像,该API包含三个参数:镜像名、入口点(用于计算唯一服务ID)和 PoP(用于告知I/O追踪器何时停止追踪)。
PoP有两种类型:
- 外部PoP:在容器外部运行的程序,例如HTTP探测。这对于像Redis、MySQL这样的网络服务容器非常有用。
- 内部PoP:在容器内部运行,例如容器的入口点。我们可以使用导入核心库作为PoP,这对于像TensorFlow这样的框架容器非常高效。
当 RT_CREATE 被调用时,系统会启动一个容器进行追踪。我们使用eBPF在容器命名空间下记录读写和缺页I/O。最后,I/O追踪记录被发送到注册表进行离线生成。
这种设计的优势在于:
- 比之前的方案更准确,因为我们可以在PoP捕获到相应事件时停止追踪。
- 适用于任何延迟加载系统,因为它在VFS层实现。
运行时镜像的组织与生成 🗂️
由于运行时镜像是按服务生成的,如果我们不考虑不同运行时镜像间的数据重复,可能会急剧增加主机的内存使用量。
根据我们的分析,使用相同基础镜像的服务间,数据重复率非常高;而使用不同基础镜像的服务间,数据重复率则很低。
因此,我们提出了分组结构。它将属于同一个基础镜像的服务归为一组。组内的数据会被去重,并存储在组数据区中。
每个运行时镜像的服务元数据是私有的,它记录了文件索引以及其在组数据区中的页映射。它使用一个位图来记录其数据在组数据区中的分布情况。
运行时镜像的生成在离线阶段进行。当接收到I/O追踪记录时,它会初始化相关的元数据和组元数据,然后从基础镜像中取出记录中的页,计算页的指纹并检查它是否已存在于数据区中。如果是一个新页,则更新重复数据删除指纹并将其添加到数据区。最后,更新元数据中的页表。
冷启动流程与文件系统操作 🖥️
在冷启动期间,驱动首先从注册表获取服务元数据,并调用一个新的OS原语 prefetch 来获取缺失页的预测。
如下图所示,每个组对应一个位图,每个RPC维护一个位图来记录哪些页已被加载。prefetch 通过比较服务元数据中的组位图和主机中的位图,返回一个需要预取的位图。
通过将位图发送到注册表,所需的页将被拉取到主机并注入内核。接着,我们使用 inject 系统调用来将服务元数据和页加载到内核。最后,容器平台使用一个特殊的挂载方式,基于运行时镜像构建文件系统。它搜索运行时镜像,然后将文件系统超级块指向它。
RPC中的文件系统操作很简单,因为它只负责缓存。我们只需要处理位于镜像中的页的读写和缺页。
- 在文件打开时,它在服务元数据中搜索文件索引,并将文件节点指向相应的页表。
- 在文件读取和缺页时,它搜索页表并返回结果。如果页不存在,则回退到传统的VFS流程处理。
实现、对比与优势 ⚔️
现在,让我们把一切整合起来。FlacIO在OpenEuler中实现,并适配到两个主流加载系统CFS和Lindle。我们为每个系统定制了容器引擎中的parking snap结构。
图中的流程展示了工作流。FlacIO可以在镜像清单加载后,才开始加载元数据、执行数据拉取和注入。因此,许多FlacIO流程可以与容器平台流程重叠执行。
以下是FlacIO与现有优化方案的对比:
- 对比预取优化:现有方案只是简单地扩大加载单元,依赖于容器I/O行为具有强局部性。
- 对比优先级文件预取:它严重依赖用户经验,并且加载单元很大。
- 对比追踪回放:它未能很好地解决准确追踪和高效聚合的问题。
相比之下,FlacIO在I/O追踪上更准确,在I/O聚合上更高效,并且整体设计更轻量级。
性能评估 📊
我们在一个24核的服务器上运行测试,通过25G网络连接到注册表。对于TensorFlow,我们使用内部PoP(将加载其核心框架库作为PoP)。对于其他容器,我们使用HTTP探测作为外部PoP。
结果显示,FlacIO在冷启动上可比次优系统提升高达2.7倍的性能。此外,在热启动对比中,使用FlacIO的系统几乎没有引入额外开销。
从tcpdump的结果来看,FlacIO将网络数据量和数据包数量减少了超过1.6倍。在不同内存占用场景下,FlacIO也更友好,相比其他系统可节省超过70%的内存占用。原因在于FlacIO具有更低的I/O放大和更短的缓存层次。
权衡与真实场景应用 ⚖️
FlacIO的主要权衡点是后端存储开销。我们为9个主流容器生成了运行时镜像并计算了开销。
结果显示,运行时镜像的大小分别占TensorFlow和Nginx总镜像大小的9%和4.7%。这意味着FlacIO可以用大约5%的存储开销,换取高达2.4倍的启动速度提升。同时,存储开销还可以通过压缩进一步降低。
最后,我们在一些真实应用中进行了评估:
- 对象存储场景:启动MinIO对象存储,结果显示FlacIO能使其启动更快。
- 机器学习训练场景:启动一个TensorFlow容器进行MNIST数据训练,FlacIO消除了训练过程中的按需加载开销,从而减少了总训练时间。
- 自动扩缩容场景:在集群中启动64个容器,结果显示FlacIO将动态扩展的尾部延迟降低了超过22%。
总结 🎉



在本节课中,我们一起学习了FlacIO方案。我们首先分析了现有容器镜像加载方案的瓶颈,然后提出了创新的“运行时镜像”组织方式。基于此,我们设计了FlacIO系统,它通过准确的PoP追踪、高效的分组去重镜像组织以及轻量级的RPC文件系统,实现了扁平化和聚合I/O。该方案已适配到两个主流加载系统,评估结果表明,FlacIO能显著提升容器冷启动性能,同时保持较低的网络、内存和存储开销。
008:现代云架构中的存储服务研究




在本教程中,我们将学习如何构建并分析一个名为“Cloudscape”的数据集,该数据集旨在揭示现代云架构中存储服务的真实使用情况。我们将从数据集的构建方法开始,逐步深入到对云存储层多样性、流行服务、存储数据特性以及服务交互模式的具体分析。
概述
云平台已成为主流的软件部署环境,构成了一个价值数千亿美元的产业。然而,尽管云服务被广泛使用,但描述真实世界云架构如何构建的数据集却非常稀缺。现有的研究往往缺乏对哪些云服务真正流行的洞察。本工作通过分析一系列由AWS维护的“This is my architecture”视频,构建了第一个大规模、结构化的真实云架构数据集——Cloudscape。该数据集涵盖了近400家不同公司的架构,时间跨度近五年,为我们理解云存储服务的实际应用提供了宝贵资源。
数据集构建方法论
上一节我们概述了Cloudscape项目的背景和目标。本节中,我们将详细介绍从非结构化视频数据构建结构化数据集的具体方法。
我们的数据源是YouTube上的技术分享视频。这些视频平均时长6分钟,包含密集的技术信息,并通过音频和视觉渠道(如口头描述或图表连线)传递。视频内容主题广泛,我们的目标是从所有视频中可靠地提取可用数据。
为了将视频转化为结构化数据集,我们采用了“迭代编码”的实践方法。这里的“编码”并非指编程,而是指系统化的分类标记过程。我们的方法源于经过充分研究的协作式定性编码实践。
以下是构建过程的核心步骤:
-
独立编码与初步共识:三名团队成员独立观看相同的五个视频,尝试提取可可靠获取的数据,并各自制定初步的编码指南。随后,团队讨论各自的方法,就一套初步的编码指南达成共识。例如,我们决定将架构格式化为图结构,节点代表服务。
-
迭代与精炼:以上述初步指南为起点,将其应用于更大的视频子集。这个过程会进一步精炼指南,一些规则被舍弃,新的规则被添加。我们多次重复此过程,不断优化编码指南,直到指南不再有更新为止。
-
扩展与验证:由于数据集规模较大,我们引入了另外三名编码员。我们对他们进行培训,使其掌握最终的编码指南。他们对数据集的补充标注都经过了验证。
数据结构示例:以Snapchat为例
了解了构建过程后,我们来看看数据在Cloudscape中是如何呈现的。本节将以Snapchat的架构为例进行说明。
在Cloudscape中,架构被编码为图。图的节点是所使用的AWS服务以及系统中的其他实体(如用户手机)。服务之间的交互被捕获为边。我们区分携带数据的交互和仅作为请求触发器或确认响应的交互。
此外,我们还捕获了“工作流”,即完成一个工作单元所需的一系列交互集合。每个架构的工作流概念是独特的。以Snapchat为例,它是一个消息服务。当发送者想要发送照片时,它遵循红色工作流来存储图像,然后采取蓝色路径在接收端触发通知。
最后,我们为边赋予了序列号,这提供了这些交互之间的顺序信息。
研究发现:存储层的多样性与异构性
我们已经介绍了数据集的构建方法并浏览了一个示例。现在,我们转向通过研究这400个不同架构所学到的关于存储层的知识。我们首先提出的问题是:如何描述现代云应用的存储层?它由什么组成?
研究发现,存储层具有惊人的多样性。Cloudscape中的架构使用了多达14种不同的存储服务。
接下来,我们考虑每个架构使用了多少种不同的存储服务。数据显示,10%的架构没有使用任何特定的云存储服务,这意味着90%的架构确实使用了专门的云存储服务。超过三分之一的架构使用一种存储服务来满足需求。数据还揭示,存储层是异构的,超过一半的架构依赖于两种或更多不同的存储服务。
因此,我们发现现代云架构确实已经接受了这些专门的云存储服务,并将存储需求卸载给它们。任何关于构建云原生和多租户存储服务的持续工作都可能产生相当大的影响,并有望看到实际部署。
研究发现:最流行的存储服务
在了解了存储层的整体构成后,我们接下来探讨:在所有这些不同的存储服务中,哪一种最受欢迎?为了便于分析,我们将提供相同数据模式的服务进行了归类。
总体而言,我们发现对象存储是最受欢迎的,其次是NoSQL存储,然后是SQL存储。具体到服务,最流行的三个服务是S3、DynamoDB和RDS。事实上,S3的使用率是第二名服务的两倍。
最后,尽管有大量关于分布式文件系统的研究,但遗憾的是,它们在此类应用场景中似乎并不那么重要。
S3的高使用率引人深思,我们利用Cloudscape对其进行了更深入的研究。左图显示了我们已知的信息:S3在整个数据集中68%的架构中被使用。Cloudscape的一个功能是根据架构的关键目标对其进行分类。考虑第二个图的X轴,我们将架构大致分为数据摄取密集型、延迟敏感型或计算密集型。我们发现S3在所有类型的架构目标中都被广泛采用。此外,令人惊讶的是,在所有类别中,许多架构除了S3之外没有使用任何其他服务。尽管如此,仍有很大一部分架构将S3与其他存储服务配对使用。
因此,我们发现对象存储已被广泛采用,而分布式文件系统在此背景下则较为罕见。任何关于对象存储的持续研究都将继续产生巨大影响。这也凸显了捕获云上对象存储所承受的真实工作负载的必要性。
研究发现:存储数据的语义理解
上一节我们分析了服务的流行度,本节我们将更深入地探讨存储在这些服务中的数据本身,能否从语义上更好地理解它?为此,我们考察了两个最流行的服务:S3和DynamoDB。
不出所料,我们发现S3存储了丰富多样的数据,包括媒体、日志、配置文件、CSV/Parquet格式的分析文件等等。然而,我们也能够理解这些数据是如何被使用的上下文。我们观察到,看似相同的数据可能具有不同的重要性或不同的可用性要求。
以日志和配置文件为例。两者在对象存储看来都是文本。然而,日志通常用于分析,下游分析引擎或许可以容忍数据延迟可用,并且可以在数据可用前推迟工作,而不会牺牲吞吐量。另一方面,配置文件可能用于启动集群。如果它不可用,可能会使整个设置停滞。因此,我们发现S3为所有数据片追求高可用性,它平等地对待它们。因此,让服务了解下游服务的这些可用性要求,从而可能构建更便宜的系统,或许是富有成效的。
我们还考察了存储在DynamoDB中的数据。正如预期,DynamoDB最常用于存储业务特定数据。然而,我们发现了两个有趣的用例。首先,一些架构运行着跨服务的长时间作业,而作业状态本身被维护在DynamoDB中。其次,我们发现有些情况下,DynamoDB存储的是某些原始数据的精简版本。例如,它存储了S3中对象的元数据,并指向S3中的该对象。这揭示了存储层中发生的紧密耦合。
我们认为,现在是时候制定一些原则性框架和指南,用于引用这些跨服务分布的数据,同时仍能保持数据的有效性和一致性。
研究发现:与存储交互的服务
最后,我们考察哪些服务最频繁地从存储读取和写入存储。该图显示了与存储交互的前10名服务。计算服务用橙色标出。因此,毫不奇怪,我们发现计算服务是与存储交互的主要接口。
然而,有趣的是Lambda的流行度。我们认为这凸显了理解无状态函数如何处理状态的重要性。Lambda的高使用率很有趣。作为系统研究人员,我们很好奇这种采用背后的原因。与S3类似,我们发现Lambda在65%的架构中被使用,同样覆盖所有目标类别。此外,在我们观察的所有架构中,大约有20%的架构仅使用Lambda就足以作为唯一的计算服务。
因此,我们发现计算服务,特别是Lambda,对云的存储层构成了压力。这凸显了开始捕获真实的无服务器存储工作负载的必要性。
总结
在本教程中,我们一起学习了Cloudscape数据集的构建与分析。Cloudscape是数月人工工作的成果,形成了一个捕捉云架构如何构建的数据集。我们将这个数据集公开,研究人员可以运行查询来激发和夯实他们未来的研究问题。我们还提供了一个基于Web的交互式探索器,以便快速理解数据。
我们希望我们的工作是第一步。我们认为我们的社区需要更多这样的数据集,以更好地理解这些服务如何被组合在一起的现实。我们愿意与其他云提供商合作,重复这项研究并将其扩展到更多维度。更广泛地说,我们欢迎关于如何系统化解决这个问题的讨论。




区块链存储优化:P09:Maat - 区块链存储超额收费分析与优化


在本教程中,我们将学习区块链存储中的“超额收费”问题。我们将首先了解区块链的基本概念和状态数据,然后深入分析三种具体的超额收费问题及其财务影响。最后,我们将介绍一个名为 Maat 的优化工具,它如何通过调整收费规则来解决这些问题,并评估其性能表现。
区块链基础与状态数据
首先,我们来介绍一些关于区块链及其状态数据的基本概念。
众所周知,区块链是一种分布式账本,由一系列区块组成。每个区块包含与区块链数据进行交互的交易。区块链状态数据包含了所有账户和合约的信息,并以 Merkle Patricia Trie (MPT) 结构组织,其中 MPT 节点存储在硬盘上。
为了提高效率,区块链会维护一个状态数据的缓存,该缓存驻留在内存中,并由相邻的几个区块共享。
对于状态数据的读取操作,交易在执行时首先会从内存缓存中获取数据。如果缓存未命中,交易才会去访问硬盘上存储的相应 MPT 节点。
对于状态数据的写入操作,交易会先将更新后的数据写入内存缓存。当一个区块内的所有交易执行完毕后,更新后的数据才会被写入硬盘。
交易费用机制与问题
接下来,我们将介绍交易费用机制以及由此产生的问题。
为了防止对状态数据的无限访问和修改,区块链引入了交易费用机制,根据用户对矿工资源(如磁盘 I/O)的使用情况向用户收取交易费,这通过 Gas 来衡量。每笔交易和合约操作都会消耗 Gas。
Gas 成本的设计初衷是反映实际的资源消耗。例如:
- 从内存缓存中读取账户数据大约消耗 100 gas。
- 从硬盘中读取账户数据大约消耗 2100 gas。
在我们的工作中,我们重点关注存储相关的 Gas 成本,原因有二:
- 像以太坊这样的区块链一直饱受高交易费之苦,这是一个公认的巨大问题。例如,数据显示,用户在 2023 年支付了约 24 亿美元 的交易费。
- 存储相关的 Gas 成本占这些交易费的 70% 以上。
超额收费问题分析
我们工作的第一部分是分析超额收费问题。我们通过研究区块链规范和实施,首先识别出三种超额收费问题。
在我们的语境中,超额收费问题指的是账户和合约操作的 Gas 成本高于实际资源消耗的情况。例如,当区块链将一次内存加载操作按磁盘加载操作收费时,我们就认为这种情况属于超额收费。
以下是识别出的三种超额收费问题:
第一种问题 发生在同一个区块内,两笔交易连续读写同一个数据对象时。在这种情况下,区块链会错误地将针对区块内内存缓存的读写操作,按磁盘读写操作的 Gas 成本收费。
请看右侧示例图,一个区块内有两笔交易:Alice 分别向 Bob 和 Cat 转账。在第一笔交易中,Alice 的余额从硬盘加载并写入区块内内存缓存。在第二笔交易中,Alice 的余额从区块内内存缓存加载,但区块链错误地认为是从硬盘加载,从而收取了磁盘读取费。
第二种问题 发生在 128 个区块内,两笔交易连续读取同一个数据对象时。在这种情况下,区块链会错误地将针对跨区块内存缓存的读取操作,按磁盘读取操作的 Gas 成本收费。
右侧同样有一个示例图,Alice 分别向 Bob 和 Cat 转账。在第一笔交易中,Alice 的余额从硬盘加载并写入跨区块内存缓存。在第二笔交易中,余额从跨区块内存缓存加载,但区块链再次错误地收取了磁盘读取费。
第三种问题 发生在两笔交易部署包含重复字节码的合约时。在这种情况下,区块链会为在硬盘上存储重复的字节码收取不必要的磁盘写入费。
右侧示例图中,有两笔交易部署了具有相同字节码的合约。在第一笔交易中,字节码(最高价值的字节码)将被存储到硬盘上的键值存储中。在第二笔交易中,第二个合约的字节码已经存在于键值存储中,但区块链仍会为写入该字节码收取费用,尽管实际上没有发生状态变更。
我们进一步探讨了这三种超额收费问题的财务影响。我们测量了它们在以太坊和 BSC 区块链上的影响。对于 BSC 区块链,由于其复用了以太坊的代码,同样会遭受这三种问题。我们的数据集包含约 100 万个区块。
结果显示:
- 在以太坊上,超过 70% 的交易和 40% 的交易费受到影响。
- 在 BSC 上,超过 90% 的交易和 40% 的交易费受到影响。
Maat 优化方案
我们工作的下一部分是优化区块链中识别出的超额收费问题,并提出了名为 Maat 的解决方案。
Maat 的核心方法是根据存储操作的实际负载来调整其相应的 Gas 费用。Maat 包含两个主要组件:
- 第一个组件从区块和交易的执行过程中,收集存储操作及其当前的 Gas 费用。
- 第二个组件应用我们提出的优化规则,针对第一个组件收集到的实际超额收费案例,优化其 Gas 费用。
接下来,我将详细介绍四条优化规则。
前两条规则针对第一种问题,即重复的内存读写操作在区块内缓存中被错误地按磁盘读写操作收费。我们的解决方案是,将这些错误的磁盘读写操作收费,替换为内存读写操作的费用。
第三条规则旨在解决第二种问题,即重复的内存读取操作在跨区块缓存中被错误地按磁盘操作收费。我们的解决方案是,将这些错误的磁盘操作收费,替换为内存操作的费用。
最后一条规则旨在缓解第三种问题,即部署包含重复字节码的合约会产生冗余的磁盘写入费。我们的解决方案是,消除部署包含重复字节码的合约所产生的费用。
Maat 的设计考量
我将通过回答两个问题来强调 Maat 的设计考量。
第一个问题:如何确保优化后的 Gas 费用在所有区块链节点间保持一致?
我们通过使用区块链固有的数据结构来形式化四条优化规则的条件,从而确保一致性。这些固有数据结构包括区块、交易、账户、合约和合约字节码。所有区块链节点对这些数据结构的执行都是确定性的,这由区块链共识机制保证。
第二个问题:不同节点的缓存配置和大小可能不同,如何确保存储操作的一致性?
前三条规则依赖于区块内和跨区块缓存来进行优化。然而,不同的配置和大小可能会改变缓存的内容。我们通过使用一种资源定位技术来确保存储操作在所有节点间保持一致。该技术可以预先定位前 128 个区块中访问过的数据,从而在所有节点间提前分配缓存内容。在我们的估算中,这种情况下最大内存消耗约为 200 MB,因此 Maat 的部署不会引入显著的内存开销。
性能评估
在评估中,我们选择 EIP-2929 作为基线,该提案旨在解决重复内存读取操作在区块内缓存中被错误收费的问题,这与我们的第一个问题部分重叠。
我们同样使用 100 万个区块来评估 Maat 的性能。结果显示,Maat 实现了显著的优化:
- 在以太坊上节省了超过 1100 万美元 的交易费。
- 性能表现约为基线(EIP-2929)的 3 倍,节省了约 570 万美元。
总结与扩展
最后,我们总结一下。我们识别了区块链中的三种超额收费问题,它们影响了超过 70% 的交易和 40% 的交易费。我们提出了名为 Maat 的工具,通过优化由这三种问题导致的超额 Gas 费用来解决它们。我们在以太坊区块链上评估了 Maat 的性能,结果显示 Maat 每周可实现约 500 万美元 的交易费节省,并且性能超出基线三倍。
如果您对我们的工作感兴趣,我们还评估了不同优化规则及其组合对三种超额收费问题的有效性,并测量了 Maat 的性能、空间开销和时间开销。结果表明,Maat 不会引入巨大的空间或时间开销。
此外,我们探索了 Maat 在其他 50 个复用以太坊代码的区块链上的可扩展性。结果显示,Maat 同样可以优化这些区块链中的超额收费问题。
感谢聆听。




010:重温网络编码在温数据存储中的应用


大家好。我是陈平。我很荣幸代表本文作者进行这次演讲。
这篇论文来自中国的华中科技大学。
这项工作由余崇虎教授和丹枫教授指导。
我们知道,Blob存储被广泛部署用于存储大量抽象数据,例如照片和文档。
实际的Blob存储系统将数据保存在不同性能的存储中,以权衡性能。
例如,在Facebook中,新数据首先存储在热存储中以保证高访问性能。
当数据变冷时,Facebook将这些数据移动到温存储中。
我们发现两个有趣的现象。我们发现温数据在存储中占主导地位,在Facebook中超过80%。
其次,大多数Blob都很小。在Facebook中,几乎所有的照片和60%的视频都小于1MB。
在存储系统中,纠删码提供故障可靠性并被广泛部署。
它将k个数据块编码成n个编码块以形成一个条带。
条带中任意k个编码块可以恢复数据。
这些图说明了单块修复,其中恢复一个丢失块需要读取两个块,导致高修复成本。
因此,提出了一些技术来解决这个问题。
再生码技术被提出来解决高成本问题。
我们知道它是修复友好的,该技术将大块分割成更小的子块,减少了数据传输大小。
子块的数量被称为子块化级别。
MSR是最有效的再生码方法之一。
它可以最小化存储冗余,并分为两种类型。
第一种是非系统MSR,第二种是系统MSR。
前者只存储校验块,而后者同时包含数据和校验块。
系统MSR更受欢迎,因为它们能以高编码率直接访问原始数据。
因此,系统MSR提供了良好的原始数据访问性能。然而,它在修复操作中表现不佳,因为需要访问多个子块,导致大量非连续I/O。
这张图展示了单块修复过程。我们可以看到,一个块被分成许多子块,修复一个丢失块需要从每个节点读取多个子块。
我们通过实验证实了这一发现。
这张图显示了不同n和k值下,针对不同Blob大小的修复性能。
我们可以看到,随着Blob大小的减小,修复性能急剧下降,这表明对于小Blob,修复性能受到限制。
为了解决第一个限制,现有方法使用连续布局将小块合并成均衡的大块,以提高访问和I/O效率。
然而,由于不必要的数据检索,它仍然会导致读放大。
这张图显示,随着合并大小的增加,降级读性能显著降低。
同时,随着块大小的减小,读性能也急剧下降。
这两个问题促使我们进行研究。幸运的是,我们发现非系统MSR码在读取小数据时表现更好。
它拥有更少的子块,因此比系统MSR码产生更少的I/O。例如,在图A中,我们可以看到修复一个丢失块只需要读取三个子块。
而在图B中,系统MSR码需要读取6个子块。显然,图A中的技术更好。
因此,在本文中,我们设计了NB,它结合了系统和非系统MSR码的关键思想。对于小数据,我们采用非系统MSR方法;而对于大数据,我们保留系统方法。
为了解决一些关键挑战,我们进一步提出了三个解决方案。
首先,虽然非系统MSR提供了更好的修复性能,但在访问原始数据时会导致读放大,因为读取一个块需要从k个节点解码k个块。例如,如图A所示,如果你想读取任何原始数据,你必须解码条带中的两个块。
而在图B中,原始数据可以直接读取,因为它存储了原始数据块。因此,我们的主要目标是减少非系统MSR的读放大。
我们发现Blob数据表现出良好的访问局部性。
Blob数据中存在两种访问局部性:Blob内局部性和Blob间局部性。
前者意味着小Blob通常被一起访问,例如相册中的照片。因此,如果读取了该Blob中的任何部分,其余部分应该被预取。
后者意味着多个Blob经常被一起访问,例如属于同一用户ID的Blob。
数据局部性促使我们通过将数据在组内编码来加速读取性能。
例如,考虑两个具有访问局部性的数据块D1和D2。
在图A中,不考虑局部性,读取D1和D2需要解码四个块。
而在图B中,考虑到局部性,读取D1和D2只需要解码两个块。并且当D1被读取时,D2有很大可能立即被读取。
这是我们设计的工作流程。我们设计了“分割-合并-编码”方案来改善Blob内局部性,以及“合并-分割-编码”方案来改善Blob间局部性。设计细节在论文中提供。
其次,非系统MSR码能有效修复小Blob,但处理大Blob仍然具有挑战性。
观察到Clay码在大Blob上表现良好,因此我们采用混合编码架构:对小Blob使用非系统MSR码,对大Blob使用Clay码(系统MSR)。
问题是如何确定小Blob和大Blob之间的阈值。
我们选择磁盘时间占主导地位的数据大小作为阈值,如图中红色圆圈所示。左侧区域是大Blob,右侧区域是小Blob。
我们还发现现有的系统MSR码只支持最多两个块损坏的场景。
因此,我们设计了子块轮转技术来支持更多故障情况。更多细节和证明在论文中展示。
我们在Linux中实现了我们的NB。具体来说,我们实现了局部性管理器模块来管理访问局部性。我们基于Jerasure实现了编码和分发模块,并基于RS实现了修复模块。
在阿里巴巴集群上的所有实验表明,与MSR相比,我们的方法在保持可比较的读取吞吐量的同时,将单块修复时间减少了高达45%,全节点修复时间减少了高达38%,这证明了我们方法的效率。关于NB的进一步实验在论文中呈现。
这是结论。我们已经开源了代码,以方便研究人员使用。任何技术问题,请联系作者的邮箱。谢谢。




011:Mooncake - 用存储换计算,一种以KV缓存为中心的架构


在本节课中,我们将学习Mooncake系统。这是一个为大型语言模型(LLM)推理服务设计的、以KV缓存为中心的高效架构。它通过“用更多存储换取更少计算”的核心思想,显著降低了长上下文场景下的服务成本。
背景:LLM推理的挑战
首先,让我们了解基于Transformer的大语言模型推理过程。请求处理分为两个阶段:预填充阶段和解码阶段。
- 预填充阶段:处理所有输入令牌,并生成第一个输出令牌。
- 解码阶段:以自回归方式,一次处理一个令牌,生成后续所有输出令牌。
我们使用两个延迟指标来衡量推理性能:
- TTFT:从输入到生成第一个输出令牌的时间,即预填充时间。
- TBT:生成每个输出令牌之间的时间间隔,即解码时间。
预填充阶段一次性完成所有计算,计算量随输入长度快速增长。例如,处理128K的输入,获得第一个响应令牌可能需要超过5秒。解码阶段则对带宽更敏感,计算资源利用率较低,但其延迟必须满足用户的阅读速度(例如每秒10个令牌)。
因此,LLM服务面临巨大挑战:冗长的上下文预填充消耗大量GPU计算时间,同时严格的TBT限制限制了预填充与解码混合批处理中请求的计算效率。
系统概览与核心问题
当我们跨多个实例应用推理过程,并引入一个中央调度器(称为指挥器)时,就得到了一个在线LLM服务系统。该系统提供流式API,每个生成的令牌都会实时返回给用户。作为服务质量保证,TTFT和TBT延迟不应超过预定义的阈值。
Mooncake正是为Kimi提供动力的在线LLM服务系统。目前,Kimi为数百万用户服务,每日处理超过千亿令牌,并提供流畅的实时响应。
Kimi最受欢迎的功能之一是支持高达100万令牌的上下文长度。根据缩放定律,更大的模型和更长的上下文可以带来更高的智能。然而,这也增加了推理成本和响应延迟,尤其是在使用高峰期。因此,一个自然的问题产生了:如何在大规模长上下文服务场景中,在不损害用户体验的前提下,提高计算资源利用率和系统整体吞吐量?
关键洞察:前缀缓存的潜力与挑战
针对长上下文LLM推理的一个重要优化是前缀缓存。KV缓存可以在具有相同前缀的请求之间共享,以减少计算。例如,如果有一个之前的请求是“今天星期几?”,那么后续的请求“昨天星期几?明天星期几?”就只需要计算最后两个令牌的KV缓存,可以复用前四个令牌的缓存。
我们对Kimi的在线工作负载进行了分析,结果令人惊讶:在真实世界的工作负载中,大约50%的令牌其KV缓存可以被重用。这意味着,如果我们有足够的空间来存储所有的KV缓存,就可以节省近一半的GPU计算。
然而,当我们只使用单个节点的本地缓存(容量约为300万令牌)时,缓存命中率会显著下降。这表明,我们应该扩展KV缓存容量,以“用更多存储换取更少计算”。
Mooncake的设计理念
尽管想法简单,但与传统缓存相比,KV缓存对存储系统提出了更大的挑战。每个令牌可能只有几个字节,但经过大模型处理后,可能会膨胀数千倍,达到数十甚至数百KB。此外,KV缓存随序列长度线性增长。例如,对于Llama 3 70B模型,即使采用分组查询注意力等技术来减少KV缓存,一个100万令牌上下文的K缓存体积仍会达到320GB。
因此,为了充分利用前缀缓存来降低计算成本,所需的KV缓存存储量将远远超过单个节点的可用容量。更重要的是,KV缓存的传输速度要求非常高。如果I/O成为瓶颈,将导致宝贵的GPU计算资源闲置。
为了构建一个高效的LLM服务系统,我们需要实现大容量KV缓存存储、低延迟和高带宽的KV缓存传输。在系统层面,我们还需要实现KV缓存感知的调度,以平衡缓存局部性和实例负载。
总而言之,以KV缓存为中心的架构是你所需要的全部。基于此,我们设计并构建了Mooncake——一个以KV缓存为中心的LLM服务架构。
Mooncake架构详解
Mooncake构建了底层基础设施Mooncake Store,它提供了一个快速、可扩展且容错的KV缓存存储与传输引擎。在此基础上,我们构建了一个大规模预填充-解码分离的推理集群,在保持实时响应的同时,极大地提高了吞吐量。
工作流程:预填充与解码分离
首先,让我介绍Mooncake中处理请求的工作流程。LLM推理有许多调度算法,例如预填充优先、解码优先和分块预填充(将预填充执行分段以减少对解码阶段的干扰)。然而,所有这些方法在同时满足高模型浮点利用率(MFU)和TBT要求方面都面临挑战。
因此,我们采用了预填充-解码分离架构,将两个阶段分离到两个不同的实例上,如右图所示。在预填充实例完成增量预填充后,KV缓存被传输到另一个实例进行解码阶段。
这种分离架构避免了混合批处理中预填充和解码之间的干扰。同时,每个阶段可以根据自身的计算特性使用不同的资源和并行方法,从而进一步提高MFU和吞吐量。在大规模部署中,预填充与解码分离被扩展到两个节点池:预填充池和解码池。来自任何节点的KV缓存都可以被快速传输,从而实现更灵活的请求调度。支持这一切的基础设施就是Mooncake Store。
核心组件:Mooncake Store
Mooncake Store在推理节点的GPU显存和SSD上构建了一个全局缓存池,并支持推理引擎与存储之间的高带宽KV缓存传输。作为一个已部署的系统,Mooncake Store提供了高可扩展性和容错性,支持在线推理系统的弹性伸缩。
在设计上,Mooncake Store独立于推理引擎,并提供灵活的API结构。推理引擎(如最广泛使用的开源LLM推理框架vLLM)可以自行实现前缀缓存卸载和预填充-解码分离功能。
在内部,Mooncake Store使用基于前缀哈希的对象存储来管理KV缓存,其中每个块由16到512个令牌组成。每个块附加一个哈希键,该键由其自身哈希及其前缀决定,用于去重。
当新请求到达时,系统会逐个匹配令牌块,直到出现不匹配,从而识别出可重用的KV缓存块和新的KV缓存块。预填充实例生成增量的KV缓存块,然后将其传输到解码实例。当节点上的KV缓存块数量达到容量限制时,我们使用LRU策略来驱逐旧的缓存块。
Mooncake Store提供了一个高性能、容错的KV缓存传输引擎,支持多种传输方法,如GPU Direct RDMA、TCP和基于fabric的NVMe。它针对多RDMA网卡场景进行了优化,与其他协议(如NCCL)相比,能更高效地利用网卡带宽。此外,该传输引擎更加灵活,因为它不需要构建精确的通信组,从而支持部署节点的弹性伸缩。
作为上层LLM引擎和下层硬件之间的中间层,Mooncake Store在外部集成方面表现出色。对于上层,Mooncake Store提供零拷贝的对象put和get接口,不仅支持vLLM等推理引擎,还支持Megatron等训练引擎用于其检查点的加载和传输。对于下层,Mooncake Store支持本地内存、远程内存、SSD以及其他第三方内存存储。
性能评估
根据我们对Kimi的历史统计,Mooncake使Kimi在H800和A800 GPU集群上分别能多处理115%和107%的请求。为了在实验部分验证这些结果,我们在Mooncake上进行了一系列端到端和消融实验,以解答以下问题:
- Mooncake在真实场景中是否优于现有的推理系统?
- 与传统的仅限本地的前缀缓存方法相比,Mooncake Store的设计是否显著提高了Mooncake的性能?
为了模拟在线请求分布,我们从在线请求中采样构建了三个跟踪数据集:对话、代理和合成。这些数据集的平均输入长度约为10K令牌,与常用的短请求数据集不同,更接近真实世界的分布。
为了比较Mooncake和其他推理系统在真实场景中的性能,我们选择了有效请求容量这一指标,它指的是在给定工作负载中满足延迟要求的请求数量。这个指标反映了系统的真实有效吞吐量,也称为“良吞吐”。
结果显示,三个基线系统无法避免混合批处理中预填充和解码之间的干扰,导致大量延迟违规。另一方面,Mooncake通过预填充-解码分离和KV缓存内存池化,实现了灵活的分离调度,从而避免了上述干扰。因此,Mooncake的有效请求容量要高得多,提升高达498%。
接下来,我们测量了预填充阶段的GPU计算时间,这反映了MFU和前缀缓存的实际利用率。更低的GPU成本意味着更高的MFU和更高的可重用KV缓存比例。从柱状图中可以看出,与Mooncake相比,vLLM和采用分块预填充的vLLM的GPU成本要高出两到三倍。而采用前缀缓存的vLLM虽然可以重用缓存,但其缓存仅限于单个节点,仅适用于缓存热度高度集中的场景(如对话和代理负载)。Mooncake通过实现KV缓存池化,在所有跟踪数据集上极大地提高了前缀缓存利用率。因此,我们可以在预填充阶段节省29%到61%的GPU计算成本。
由于时间限制,我没有详细介绍Mooncake推理引擎和调度部分的实现细节。更多信息,请参阅我们的论文。我们也欢迎您阅读论文中的消融实验,以获得对Mooncake系统更全面的理解。
未来路线图
在最后,我想介绍一下我们将Mooncake Store集成到vLLM中的路线图,我们计划分为两个阶段。
- 第一阶段:我们使用Mooncake传输引擎实现vLLM的分离推理,目前仅支持一个预填充实例和一个解码实例。这部分代码已经合并到vLLM中,您可以参考我们的代码库获取教程。
- 第二阶段:我们将基于Mooncake的分布式KV缓存和中央调度服务,支持任意数量的预填充和解码实例进行分离推理。我们正在努力实现这一点,以使vLLM支持更灵活的分离推理。
总结
本节课我们一起学习了Mooncake系统。我们对论文进行了总结:我们提出了“用更多存储换取更少计算”的理念,以降低为Kimi提供大规模长上下文LLM服务的成本。基于此,我们构建了Mooncake——一个以KV缓存为中心的LLM服务架构,在我们的实际部署中节省了一半以上的成本。
Mooncake的核心基础设施是Mooncake Store,它提供了一个快速、可扩展且容错的KV缓存存储与传输引擎。基于Mooncake Store,Mooncake实现了高效的预填充-解码分离、大容量KV缓存存储和灵活的KV缓存调度,从而在保证实时响应的同时,显著提高了LLM服务吞吐量。
欢迎您查看我们的论文和GitHub代码库以获取更多细节。感谢聆听。




012:面向高吞吐与低延迟的十亿级向量搜索


概述
在本节课中,我们将学习一种面向十亿级向量数据库的高吞吐、低延迟近似最近邻搜索方法。该方法通过CPU与GPU协同处理,结合分层索引、乘积量化与启发式重排序等技术,在保证高精度的同时,显著提升了搜索性能并降低了成本。
背景介绍
向量搜索,也称为Top-K最近邻搜索,其目标是在一个包含N个向量的数据集中,为给定的查询向量找到距离最近的K个向量。每个向量是一个D维的表示。
给定查询向量 q,目标是找到数据集 X = {x1, x2, ..., xN} 中的Top-K向量,使得它们与 q 的距离最小。距离通常使用欧几里得距离或余弦相似度等度量。
由于“维度灾难”问题,精确的最近邻搜索在大规模高维数据上计算成本极高。因此,研究者提出了多种近似最近邻搜索算法,主要分为四类:暴力搜索、基于哈希、基于树和基于图的方法。其中,基于图的方法在精度和性能上表现出了显著优势。
基于图的ANN方法构建一个近邻图,其中向量被抽象为节点,边代表两个节点向量之间的距离。对于每个查询,算法从给定节点开始导航图,顺序遍历可能包含近似最近邻的相邻节点。
动机与挑战
随着大语言模型的快速发展,向量搜索在检索增强生成中扮演着关键角色,用于扩展模型的知识库。然而,嵌入向量数据的爆炸式增长,要求ANN算法能够处理包含数十亿向量的数据集。
基于图的ANN方法通常需要将整个索引加载到内存中,这导致了高昂的内存成本。为了降低内存消耗,微软的商业系统SPANN引入了分层索引,将大部分索引存储在SSD上。
具体来说,在离线阶段,SPANN使用聚类算法将数据划分为多个分区。为确保聚类质量,它会将边界向量复制到相邻分区的倒排列表中。然后,它将所有倒排列表存储在SSD上,同时在内存中使用一个基于分区中心构建的图索引。查询时,SPANN遍历内存中的图以找到最接近的M个倒排列表,将它们加载到内存,然后在这些列表中通过精确计算找到Top-K最近邻。
尽管SPANN实现了与内存方法相当的低延迟,但其吞吐量受限。我们发现,其查询延迟主要来自两个阶段:内存中的图遍历和处理来自SSD的倒排列表。当并发查询增多时,多个查询会竞争读取SSD上的倒排列表,导致I/O拥塞和高延迟。
乘积量化是一种有前景的压缩原始高维向量为低维码字的技术,能有效减少I/O请求的大小。然而,PQ将向量间的距离计算转换为多次内存查找操作,这对传统CPU架构构成了挑战,因为CPU的访存带宽有限且并行度不足。因此,PQ通常由GPU加速,以利用其大规模并行核心和高带宽内存。
一个直观的想法是将分层索引、乘积量化和GPU加速技术结合起来,以实现最优的ANN解决方案。但我们发现这并非易事。
初步方案与存在的问题
我们首先讨论一个结合了PQ和分层索引的GPU加速ANN方案。查询时,ANN引擎首先遍历导航图以确定最接近的M个倒排列表,然后将这些列表的原始或压缩数据加载到CPU主存或GPU的HBM中进行距离计算。
由于PQ会对查询精度产生负面影响,我们增加了一个重排序过程来提升精度。在重排序阶段,需要对Top-N个候选向量的原始表示与查询向量进行比较,以找到最终的Top-K最近邻。
令人失望的是,上述方案并未达到预期的高性能,无论是单线程延迟还是多线程峰值吞吐量。
我们面临三个主要挑战:
- 跨设备的大量数据传输:PQ技术虽然能将倒排列表的总大小从多个页面减少到一个页面粒度,但由于重排序的需要,I/O请求数量反而增加了17%。更重要的是,大量倒排列表数据需要在CPU和GPU之间传输,这抵消了GPU加速带来的收益。根本原因在于,即使经过PQ压缩,GPU的HBM仍然无法容纳所有数据,导致CPU-GPU间的数据传输成为新的性能瓶颈。
- 重排序数量的不确定性:为了达到相同的查询精度,通常需要重排序过程来优化中间结果。但我们发现,不同查询所需的最小重排序数量差异很大。如果为所有查询固定重排序的向量数量,会导致不必要的I/O操作和距离计算。
- 重排序的I/O粒度不匹配:重排序过程需要从SSD读取原始向量,但一个原始向量的大小通常只有几百字节,而现代SSD的最小操作单位是一个页面(例如4KB)。这种粒度不匹配导致了显著的读放大,使得重排序阶段的I/O效率极低。
FusionANN:我们的解决方案
为了应对上述挑战,我们提出了FusionANN,这是一个用于十亿级ANN搜索的CPU与GPU协同处理架构。
FusionANN包含三个关键设计,分别针对一个特定挑战:
- 多层索引与CPU-GPU协同过滤机制,减少数据传输。
- 基于轻量级反馈控制模型的启发式重排序,减少不必要的计算。
- 基于优化存储的I/O感知去重,提升I/O效率。
1. 多层索引与协同过滤
首先,我们采用分层平衡聚类算法迭代地将数据集划分为多个倒排列表。每个列表包含多个向量ID及其对应的向量内容。
数据集聚类后,我们使用所有分区的中心点构建一个导航图索引,并将其存储在内存中。
更重要的是,与SPANN存储所有倒排列表ID不同,我们仅提取每个倒排列表的向量ID作为合并数据存储在内存中。
当图数据和合并数据生成后,中间的倒排列表就可以被丢弃。由于图和合并数据的内存占用相对较小,我们可以使用通用服务器以内存高效的方式支持十亿级向量。
由于PQ能显著减少高维向量的内存占用,即使是中端GPU的HBM也能容纳十亿级数据集的全部压缩向量。
在FusionANN中,我们将所有压缩向量预加载到GPU的HBM中,避免了GPU和CPU之间的数据交换。
在线查询流程如下:
- CPU遍历内存中的导航图,识别出与查询向量最接近的Top-M个倒排列表。
- CPU查询合并数据,收集这些候选列表中的所有向量ID。
- CPU将这些向量ID发送给GPU,并调用GPU内核进行进一步处理。
- GPU收到向量ID后,首先从HBM中读取每个ID对应的PQ压缩向量,并计算其与查询向量的PQ距离。
- GPU对所有距离进行排序,并将Top-N个向量ID返回给CPU。
- 由于使用PQ向量得到的中间结果不够精确,CPU根据这些向量ID从SSD读取原始向量进行进一步的重排序。
- CPU返回最终的Top-K最近邻。
这种CPU-GPU协同过滤机制可以在避免向量数据在CPU和GPU间传输的同时,过滤掉最不相关的向量。
2. 启发式重排序
为了减少不必要的I/O操作和计算,我们提出了启发式重排序机制。
核心思想是:当当前Top-K结果已经稳定时,后续的重排序将不会改变最终结果。
我们将重排序过程划分为多个小批次顺序执行。在此过程中,我们使用一个优先队列来维护当前的Top-K最近邻。当一个小批次完成后,我们计算优先队列的变化率。如果连续多个小批次的变化率都小于给定阈值,则提前终止重排序过程。
3. I/O感知去重
为了进一步提高I/O效率,我们首先将高相似度的向量存储在同一SSD页面上,以提升空间局部性。
我们设计了两种I/O去重机制:
- 合并小批次内的I/O:将映射到同一SSD页面的I/O请求进行合并。
- 利用缓冲区消除冗余:利用读缓冲区消除后续小批次中的冗余I/O操作。
举例说明:
假设重排序过程有两个小批次:
- 批次0任务:重排序向量2、4、6。
- 批次1任务:重排序向量5、8、9。
执行批次0时,查询映射表得到向量对应的SSD页面ID。发现向量2和6都存储在页面0上,因此合并这两个I/O请求,只读取一次页面0。假设页面0和页面2不在读缓冲区中,则通过两次I/O操作读取它们。
执行批次1时,虽然向量5、8、9存储在不同页面,但读缓冲区中已包含存有向量5的页面2。因此,批次1只需读取页面1和页面3,共两次I/O操作。
实验评估
我们在配备V100 GPU和三星SSD的服务器上进行了实验,使用了三个十亿级别的真实世界向量数据集。
我们将FusionANN与三种代表性的十亿级ANN解决方案进行了比较,包括两种基于SSD的解决方案和一种GPU加速的内存解决方案。
关键结果如下:
- 与SPANN相比,FusionANN在达到相同低延迟的同时,吞吐量提升了高达3倍。
- 与DiskANN相比,FusionANN在吞吐量上提升了高达4倍。
- 更重要的是,与GPU加速的内存解决方案相比,FusionANN在保持相似低延迟的同时,吞吐量提升了高达5倍。
- 在性能可扩展性方面,随着线程数增加,FusionANN表现出了最佳的扩展性。
- 在成本和内存效率方面,FusionANN也表现最佳。
总结
本节课我们一起学习了FusionANN,一个用于十亿级近似最近邻搜索的CPU与GPU协同处理架构。它通过多层索引、协同过滤、启发式重排序和I/O感知去重等关键技术,仅使用一块中端GPU,就同时实现了高吞吐、低延迟、高成本效益和高精度。
FusionANN为解决大规模向量搜索中的性能与成本平衡问题提供了一个有效的设计范例。




013:IMPRESS - 面向大语言模型推理的基于重要性的多层级前缀KV存储系统


概述
在本节课程中,我们将学习一篇来自FAST'25存储大会的研究工作:IMPRESS。这是一个专为大语言模型推理设计的、基于重要性感知的多层级前缀键值存储系统。我们将了解大语言模型推理中共享前缀KV缓存带来的性能瓶颈,以及IMPRESS如何通过创新的技术来优化存储和缓存,从而显著降低推理延迟。
背景与动机
大家好,我是魏建辰,来自佐治亚大学,导师是许冰河教授。
很荣幸介绍我们的工作IMPRESS,这是一个面向大语言模型推理的、基于重要性感知的多层级前缀KV存储系统。这是与华为云合作完成的工作。
大语言模型获得了广泛关注,并已应用于聊天机器人和搜索引擎等多个领域。现代LLM应用通常在将用户查询发送给模型进行推理之前,会附加信息丰富的内容前缀。
许多请求共享相同的前缀标记,例如GPT插件中的系统提示,或RAG系统中检索相同文档的相似问题。
由于共享的前缀标记在先前阶段会生成相同的KV缓存,近期研究提出将这些共享的前缀KV缓存存储在GPU或CPU内存中。
当具有相同前缀标记的新请求到达时,KV缓存可以被异步加载和复用,从而减少冗余计算时间。
右图展示了一个简单3层LLM推理的“首词元时间”。这表明,与重新计算相比,该方法显著降低了TTFT。
如左下角图所示,在我们的测试中,从128个标记到8000个标记的各种前缀长度下,这种优势都存在。
然而,随着请求数量的增加,GPU和CPU内存变得不足,KV缓存必须存储在SSD等磁盘上。
不幸的是,从SSD加载KV缓存会引入显著的I/O延迟,这无法再通过异步加载完全隐藏,并暴露在关键路径上。
我们的测试表明,I/O时间占TTFT的50%以上,使得从SSD到GPU的KV加载成为新的性能瓶颈。
为了解决这个问题,先前的工作可以分为两类。第一类仅依赖GPU/CPU,受限于缓存大小,在大规模场景下仍需要大量重新计算。第二类利用调度信息,在任务到达前将不必要的KV前缀从SSD预取到CPU内存。然而,在高请求负载或抢占式调度下,这种方法效率低下。
我们思考,是否有可能减少需要加载的可复用KV数据量,从而减少I/O时间。
近期研究表明,在解码阶段仅保留重要的KV可以达到相似的推理精度,如H2O和InGen所示。这启发我们在前缀阶段仅加载重要的KV,以缓解I/O瓶颈并降低TTFT。
面临的挑战
然而,这种方法面临挑战。
挑战一:识别重要KV会引入显著的I/O开销。
例如,对于一个有N个标记的请求和模型某一层的三个注意力头,前缀K和V存储在SSD中。我们需要将所有前缀键加载到GPU内存中以生成注意力矩阵,然后识别每个头中最重要的KV,最后仅从SSD加载重要的值。
我们发现,KV的减少仅限于步骤2,因为在每个头中识别重要KV需要大量的I/O。
有人可能认为,既然KV是共享的,可以根据历史信息静态预判哪些KV是重要的,并仅加载这些。然而,我们在两个开源模型上的测试表明,虽然这种静态方法减少了I/O数据量,但与动态识别重要KV相比,会导致高达5%的精度损失,这是不可接受的。
挑战二:现有的存储和缓存系统并非最优,因为它们没有考虑标记KV的重要性。
具体来说,存储系统存在读取放大问题,因为一个标记的KV被打包到同一个数据块中,混合了重要和不重要的KV。左图显示了当仅加载重要KV时,每个数据块中重要KV的比例,不重要的KV也被加载到内存中。
另一方面,缓存系统基于最近最少使用或访问频率。右图显示,重要性与频率之间没有直接关联。一个访问频率高的数据块可能具有较低比例的重要KV。
接下来,我们将介绍我们的观察和设计,以克服上述两个挑战。
IMPRESS系统架构
这是IMPRESS的整体架构。它包括一个重要标记识别模块来应对挑战一,以及KV重排序和缓存替换来优化存储和缓存以应对挑战二。
挑战一解决方案:相似性引导的重要标记识别
为了减少前缀K的加载,我们观察到在LLM的同一层内,不同注意力头之间的重要标记索引集合具有高度相似性。
这是一个例子。假设两个头的重要K索引分别是{0, 2}和{0, 1}。我们使用Jaccard索引来衡量相似性,在本例中是1/3。第二张图显示了OPT 300亿参数模型中某一层任意两个头之间的真实相似性,均高于0.9。右图表明,这种相似性存在于不同的LLM规模和重要KV比例下。
基于这一观察,我们提出了相似性引导的重要标记识别。核心思想是使用少数选定头的重要标记索引集合来近似其余头的重要标记索引集合。
让我们看一个例子。左图展示了没有我们方法的过程,右图展示了启用我们方法后的过程。它首先仅加载少数头(例如一个头)的前缀键,然后识别该头中的重要标记分布。接着,我们近似其他头中的重要K分布,并仅加载这些重要的键和值。与左侧的现有方法相比,我们的方法显著减少了步骤一中加载的前缀键数量。
由于并非所有层都具有足够高的相似性,我们选择性地应用此方法。仅对高相似性层使用此方法;对于低相似性层,我们仍使用左侧的现有方法,以确保推理精度保持在相似水平。
挑战二解决方案:KV重排序与基于分数的缓存管理
为了解决存储读取放大的挑战,我们基于标记重要性对KV进行重排序,将重要和不重要的KV分离到不同的数据块中。这减少了需要加载的块数,缓解了读取放大。
然而,重排序会影响前缀树结构。图A展示了一个包含两个请求的例子。一个请求有标记t0到t7,另一个有t0到t3,然后是t8到t11。其中,橙色标记t0、t3、t4和t9是重要标记。我们不能直接重排序以将这四个橙色标记聚集到一个节点中,因为这会破坏树结构,例如t4和t9不是共享标记。
因此,我们避免跨节点重排序,并在每个节点中添加映射列表。映射列表(如m0)用于记录重排序前后标记索引的映射关系,以便在推理期间可以恢复原始的标记序列。
对于缓存挑战,我们引入了基于分数的缓存准入和替换方法。
我们不仅依赖访问频率,还考虑每个数据块中重要标记的比例。例如,假设块1和块2的访问频率分别为1.5和1,重要KV比例分别为50%和100%。LRU会优先缓存块1,而我们的方法会优先缓存块2,因为块2的分数高于块1。这将缓存未命中次数从20次减少到15次。
实验评估
我们的实验在一台配备80GB显存的A100 GPU、128GB CPU内存和2TB SSD的服务器上进行。我们测试了四个公开数据集,前缀大小从55GB到65GB不等。
我们将IMPRESS与四个系统进行比较:Recomp(不使用前缀KV缓存)、Async(执行异步KV加载,无调度信息,适用于更通用的场景),以及另外两个在Async基础上添加H2O并使用LRU或LFU进行缓存管理的系统。
为了防止运行时内存不足,我们将GPU HBM的缓存空间设置为10GB,CPU内存设置为32GB。默认情况下,每个数据块包含64个标记的键或值。
由于我们的第一项技术使用近似方法来识别重要KV,我们进行了精度测试。结果显示,IMPRESS达到了与H2O相当的精度,与重新计算相比,平均精度损失小于0.2%。
我们还测试了TTFT结果。IMPRESS优于其他方案,比现有最优解决方案有1.2倍到2.8倍的提升。这主要归功于I/O时间的减少,如左下角图所示。右下角图展示了启用每项技术后TTFT的变化,显示每项技术都进一步降低了TTFT。
此外,我们在不同的块大小、数据集规模和流行的Llama模型上测试了IMPRESS。结果表明,在各种条件下,IMPRESS始终优于现有最优系统。更多实验结果,请参阅我们的论文。
总结
在本节课中,我们一起学习了IMPRESS系统。我们认识到,当从SSD加载共享前缀KV用于LLM推理时,I/O成为瓶颈。我们的核心思想是在前缀阶段仅加载重要的KV。然而,识别重要KV会引入I/O开销,且现有的存储和缓存系统并非最优。
为了解决这些问题,IMPRESS采用了三项技术:相似性引导的重要标记识别、KV重排序和基于分数的缓存管理。实验结果表明,IMPRESS在保持相同推理精度的同时,性能优于其他方案。


感谢大家的关注。我很乐意回答任何问题和评论。
014:GPHash - 一种支持字节粒度持久内存的高效GPU哈希索引


在本教程中,我们将学习GPHash,这是一种专为GPU持久内存系统设计的高效哈希索引。我们将了解其设计动机、核心挑战、关键技术以及性能优势。
概述
随着计算吞吐量和内存带宽的显著提升,GPU已被广泛应用于深度学习、科学计算和自动驾驶等领域以提升性能。这些GPU应用通常由大规模数据驱动,例如,为了获得更高的推荐精度,深度推荐系统需要存储数十亿规模的嵌入向量来表示更丰富的特征。
为了容纳不断增长的数据规模并确保数据可靠性,GPU应用通常将数据存储在大容量的持久存储设备中,例如SSD,并依赖CPU来管理数据。具体来说,数据首先被传输到CPU内存,然后再传输到GPU。虽然这种数据管理方式实现了大容量和持久性,但也带来了高昂的数据传输开销和额外的CPU消耗。
为了缓解数据传输开销并消除额外的CPU消耗,GPU直接存储技术提供了GPU内存与存储设备之间的直接路径,提供了块粒度的读写接口。数据可以直接传输到GPU。这种直接数据访问方式提供了更高效的数据传输。然而,使用GDS编程数据结构很困难。此外,由于其块粒度特性,可能导致外部数据传输。
近年来,字节粒度持久内存已经出现,它提供了包括大容量、字节粒度、持久性和高性能在内的诱人特性。配备持久内存的GPU系统提供了字节粒度的加载和存储指令,从而实现了从GPU到PM的直接数据访问。GPU-PM系统可以通过利用统一虚拟寻址技术将PM映射到GPU的虚拟地址空间来实现。GPU-PM系统不仅提供了大容量和持久性,还提供了成本效益高且细粒度的数据传输。此外,使用GPU-PM编程数据结构很容易。
哈希索引被广泛用于管理数据,它提供了恒定时间复杂度的点查询,并且非常适合并行访问。因此,支持GPU-PM的哈希索引有望提升GPU应用中的数据管理性能。
然而,由于以下挑战,实现一个高效的GPU-PM哈希索引并非易事。
核心挑战
以下是实现高效GPU-PM哈希索引面临的主要挑战:
-
GPU并行特性:GPU通过使用数千个线程实现高并行度,这表现出独特的特性。GPU中最小的调度单位是线程束,它由32个线程组成。当遇到分支指令时,同一线程束中的线程如果执行不同的指令,则会发生线程束分化。此外,当一个线程束加载和存储的数据落在同一个GPU缓存行时,GPU硬件会将它们合并为一次缓存访问,称为合并内存访问。然而,在哈希索引中,线程访问的键的地址是分散的,因此会导致未合并的内存访问。总体而言,严重的线程束分化和未合并的内存访问会导致性能下降。
-
数据一致性:由于GPU-PM哈希索引直接在PM中管理数据,因此在发生崩溃时保证数据一致性非常重要。PM的低原子性内存写入大小受内存总线宽度限制,例如,64位CPU为8字节。因此,当写入大小超过8字节的数据时,可能导致部分更新。如果发生系统故障,数据将被损坏。通过使用日志等技术,可以从崩溃中恢复数据。然而,这会引入双写开销。通常,崩溃一致性保证会带来很高的开销。
-
带宽差距:在执行索引操作时,GPU需要并发访问PM。虽然GPU内存的带宽很高,例如NVIDIA V100可以达到900 GB/s,但PM的读写带宽通常只有数十GB/s。因此,存在显著的带宽差距。带宽有限的PM无法高效处理来自GPU内核的大量并发访问。这种巨大的带宽差距限制了GPU高并行度的利用。
GPHash设计
为了应对这些挑战,我们提出了GPHash,这是一种用于GPU-PM系统的高效哈希索引。
为了应对挑战1和2,我们提出了一种GPU感知且PM友好的哈希表结构,它支持无日志的插入和查询操作。为了进一步缓解带宽差距,我们利用基于冻结桶的缓存,将热桶缓存在GPU内存中,从而减少对PM的访问。
GPU-PM友好哈希表结构
在GPHash中,每个桶包含多个槽,每个槽存储一个键值对。GPHash利用桶内共享来处理更多的哈希冲突,以提高内存效率。
此外,GPHash具有多哈希收集和单次线程束访问的特性。它利用线程束的并行性,使得GPHash可以一次性访问给定键的所有候选桶的所有槽。
除了传统的基于指针的键放置方案会导致地址分散外,GPHash利用就地键放置来促进合并内存访问。
GPHash的结构在GPU系统上简单高效,展现出GPU友好、写入优化和内存高效等优势。
无日志崩溃一致性操作
基于这个哈希表结构,GPHash支持无日志的插入和查询操作。这里简要介绍插入操作。
一个线程束中的线程协作执行一次插入操作。每个线程首先检查对应的槽,查看键是否存在。然后,在负载较轻的桶中找到一个空槽作为目标槽。
一个专用线程尝试使用compare-and-swap原语将槽状态设置为“插入中”。如果失败,线程束需要回到第一步。如果成功,该专用线程继续写入键和值指针。写入完成后,该线程再次使用compare-and-swap原语将槽状态设置为该键的哈希值。
崩溃后,GPHash会检测状态为“插入中”的槽,并通过将槽状态设置为“空”来恢复它们。
基于冻结桶的缓存
为了弥合GPU和PM之间的带宽差距,GPHash将热桶缓存在GPU内存中。
执行索引操作时,线程会检查桶是否被缓存。如果命中,线程直接从缓存中读取键值。否则,线程访问PM来读取键值。
为了避免严重的线程争用,GPHash定期识别热桶并将其加载到桶缓存中。缓存桶的成员是冻结的,即在两个相邻的加载阶段之间不会改变。
为了减少缓存加载的开销,GPHash并发地加载热桶,包括首先使旧缓存桶失效,然后获取新桶,最后验证新缓存桶。
关于GPHash的更多细节,包括线程束协作方式和更多索引操作,请参阅我们的论文。
性能评估
我们在配备两个Intel 26核CPU、1个NVIDIA V100 GPU和768 GB Intel Optane DCPM的服务器上评估GPHash。
我们将GPHash与以下方案进行比较:
- 传统的CPU辅助方案:使用CPU通过持久化哈希索引来管理数据。
- GPU-PM启用的方案:利用GPU-PM哈希索引来管理数据。
我们使用YCSB基准测试和多个真实世界工作负载来评估GPHash和对比方案。
下图显示了不同方案在YCSB A和C工作负载下的吞吐量和延迟。

结果表明,在YCSB A和C工作负载下,GPHash的性能分别比CPU辅助基线方案高出4.7倍和3.7倍。这是因为GPHash通过在字节粒度直接访问数据,减少了数据传输成本。总体而言,GPHash将吞吐量提高了1.9到6.3倍。这种改进源于GPHash能够高效利用GPU的高并行度,同时以适中的开销确保崩溃一致性。
我们进一步深入分析了不同方案的详细延迟分解。结果表明,CPU辅助方案受困于高传输开销,而简单的GPU-PM启用方案则受困于严重的线程束分化和高开销的一致性保证。GPHash充分利用了GPU的高并行度,并提供了低开销的一致性保证,从而带来了更好的性能。

总结
在本节课中,我们一起学习了GPHash。现有的GPU应用数据管理方法受困于高昂的数据传输开销和额外的CPU消耗。为了解决这些问题,我们提出了GPHash,这是一种高效的GPU-PM启用的哈希索引,它包含多项专为GPU-PM系统量身定制的设计。我们已在GitHub上发布了开源代码供公众使用。


感谢阅读。
015:GeminiFS - 一个面向GPU的伴侣文件系统


概述
在本节课中,我们将学习一篇来自厦门大学的研究论文,它介绍了一种名为GeminiFS的新型文件系统。这个系统专为图形处理器设计,旨在解决GPU应用内存需求快速增长而GPU高速内存容量增长缓慢的矛盾。我们将了解传统GPU存储方案的局限性,以及GeminiFS如何通过创新的设计来提升性能、降低延迟并简化编程。
背景与动机
首先,让我们看看这项工作的背景。众所周知,GPU应用程序变得极其庞大,需要更多内存,例如大语言模型。然而,GPU高带宽内存的容量增长非常缓慢。
为了适应应用程序的需求,必须扩展GPU内存。一种方法是基于内存的扩展,例如使用多GPU聚合或主机直接访问。这种方式在可预见的未来成本相当高昂。另一种方法是使用存储,将GPU内存访问范围扩展到存储设备,例如NVMe。随着高性能NVMe存储的发展,例如英特尔的傲腾系列,基于存储的扩展已被证明是一种高效且廉价的方式。
GPU存储系统的演进
上一节我们介绍了扩展GPU内存的需求,本节中我们来看看GPU存储系统的演进过程,以理解我们的设计动机。
在最初阶段,我们仅使用主机上的缓冲区将数据从CPU传输到GPU。我们首先从存储读取数据到CPU,然后调用GPU API将数据从CPU缓冲区传输到GPU缓冲区。这会增加编程复杂性、额外的内存拷贝、I/O延迟和CPU开销。
随后,一些工作如GPUFS和Direct提供了GPU内核的POSIX接口。这使得编程更容易。然而,它们仍然依赖于CPU上的守护进程来拉取GPU进程的I/O请求,并将其提交给主机文件系统。为了从NVMe获取数据,仍然需要内存拷贝来传输。额外的控制平面开销、I/O路径和内存拷贝问题仍然存在。
另一种流行的解决方案称为GPU直接存储。它利用点对点内存拷贝来跳过缓冲区,并在执行NVMe I/O时降低CPU开销。然而,它们仍然需要一个名为CUfile的专有接口,这使得编程困难。它需要修改NVMe驱动程序。这种方式仍然依赖于CPU来编排GPU I/O。
以上所有都是“以CPU为中心”的GPU解决方案。它们最终都依赖CPU来编排GPU I/O,并面临共同的问题。第一个问题是复杂的存储软件栈增加了I/O延迟。第二个问题是当执行高并发GPU I/O时,会受到有限CPU核心数的限制。
我们通过测试GPUFS和GDS来评估这个问题。左边的图表显示GPUFS的尾部延迟在GPU线程数增加并超过CPU核心数时急剧上升。右边的图表显示GDS的延迟随着I/O大小增大而降低。然而,软件开销仍然占约90%。
伴侣文件系统的核心理念
为了解决以CPU为中心方案的问题,一些解决方案如BEN提供了以GPU为中心的解决方案。它们在GPU高带宽内存上创建RAID,并使用进程级驱动程序进行注册。它们使用类似SPDK的设备控制平面,该平面完全由GPU进程管理。这使得编程极其简单。但它仍然面临一些问题。当使用GDS或SPDK时,第一个问题是SPDK类用法对NVMe设备的独占访问,阻止了CPU和GPU之间以及GPU进程之间的设备共享。第二个问题是由于缺乏文件系统管理,使得数据处理变得困难。第三个问题是当访问由主机文件系统管理的数据时,仍然需要许多内存拷贝。
那么,理想的解决方案是什么?结合CPU和GPU解决方案的优点。我们认为理想的GPU存储软件应具备以下特性:首先,它必须基于以GPU为中心的架构,完全避免CPU参与。其次,它还应提供文件系统管理能力。第三,它为CPU和GPU提供统一的命名空间。这些特性使得我们必须在GPU上构建一个文件系统。
然而,根本问题是GPU不适合构建文件系统。一个文件系统通常需要具备以下核心能力:第一,它需要提供目录和文件抽象,并维护相关的元数据。第二,它应提供磁盘空间管理并实现高效的地址映射,例如XFS中的extent。这需要具有复杂逻辑和分支跳转的软件。但GPU并非为此设计。GPU主要设计用于并发和简单的大规模计算。
因此,我们为GPU提出了一种新型文件系统,称为“伴侣文件系统”。我们称之为GeminiFS。我们在文件系统设计中分离了CPU和GPU的职责。
GeminiFS的设计挑战与解决方案
基于上述理念,GeminiFS的设计面临几个关键挑战。以下是这些挑战及对应的解决方案:
挑战一:元数据同步
元数据由于硬件隔离而难以高效同步。主机和GPU实际上是两个不同的系统,它们需要通信。此外,文件操作总是与数据操作紧密交织。
解决方案:元数据嵌入
我们通过将元数据嵌入到文件本身来实现高效的数据共享。这就像虚拟化信息中使用的特定文件格式,例如QCOW2或VMDK。在我们的场景中,CPU和GPU使用一种称为GVDK的文件格式。不同之处在于,嵌入的元数据包含文件的物理布局信息。因此,GPU可以通过解析文件来获取文件数据和地址映射。此策略基于我们对AI应用的观察:短期数据(如训练中生成的激活值)无需持久化;长期数据(如检查点和键值缓存)是只写的,生成后不再更改;大多数存储访问具有可预测的模式。文件大小通常由模型大小决定,因此我们可以预分配文件及其元数据。这避免了GPU处理运行期间的元数据同步。我们只嵌入每个文件私有的特定元数据,例如文件类型、文件大小和索引结构。其他复杂结构如目录仍由CPU管理以降低复杂性。我们使用简单的两级映射表,而不是树形结构(如扩展树),以降低地址转换的复杂性。
挑战二:设备驱动程序共享
现有的设备驱动程序无法在多个设备(CPU和GPU)之间共享。
解决方案:共享NVMe驱动程序
我们提供了一个CPU-GPU共享的NVMe驱动程序。关键的洞见是GPU只需要I/O队列,而不是管理队列。我们修改驱动程序,在初始化阶段将I/O队列设置到GPU内存中,并通过GPU和驱动程序提供的接口,使得GPU和CPU可以具体地构建控制平面。
挑战三:GPU页缓存低效
这来自两个方面:首先,页缓存只能在GPU进程内构建;其次,GPU的高并行性导致查询页时争用更高。
解决方案:GPU专用页缓存设计
首先,我们启用GPU进程间的页缓存共享,以在多个GPU打开同一文件时最小化内存成本。其次,我们降低了争用:我们在warp级别而非线程级别获取页。我们还为插入、删除和查找操作设计了一个恒定时间复杂度的容器。GeminiFS还允许用户为页缓存设置页大小和预取策略,以优化其应用程序。
挑战四:GPU编程复杂性
此解决方案需要在CPU和GPU上进行复杂的内存和环境设置,使得编程困难。
解决方案:LibGemini库
GeminiFS提供了一个名为LibGemini的库,它包含一系列类似POSIX的接口,以隐藏复杂性并降低编程难度。
性能评估
我们评估了GeminiFS的I/O性能、页缓存性能和端到端测试。
I/O性能
我们与GPUFS和GDS进行了比较。GeminiFS直接从GPU提交文件系统请求,有效降低了I/O延迟,如右图所示。左图显示GeminiFS的I/O不受CPU限制,并且可以在4KB粒度下最大化设备带宽。
页缓存性能
我们从预取策略、并行性和页大小方面评估了页缓存性能。第一张图显示预取可以充分利用页缓存带宽。第二张图显示,随着warp数量增加,页缓存可以提供超过600 GB/s的带宽。它不会成为系统的瓶颈。最后一张图显示,如果页大小设置为256KB,页缓存带宽可以最大化。
端到端训练评估
我们还在GPT-2训练上评估了GeminiFS。左图显示我们禁用了训练中激活值的卸载,以展示对检查点的好处。结果表明,与原生直接拷贝写入相比,检查点延迟降低了65%;与GDS相比降低了50%。右图显示我们启用了激活值卸载,以展示对于大批次训练的好处。总训练时间分别比原生方式和GDS低94%和91%。与将激活值保留在HBM中相比,GeminiFS仅高出4倍。这个差距可以通过扩展NVMe设备数量在未来进一步缩小。


总结


本节课中,我们一起学习了GeminiFS,一个为GPU设计的伴侣文件系统。我们回顾了传统GPU存储方案的局限性,深入探讨了GeminiFS如何通过元数据嵌入、共享NVMe驱动、GPU专用页缓存和简化编程库这四大核心设计,有效解决了以CPU为中心方案带来的延迟、扩展性和编程复杂度问题。性能评估表明,GeminiFS能够显著降低I/O延迟,最大化存储带宽,并加速大规模AI模型的训练过程。
016:3L-Cache - 一种低开销、高精度的基于学习的缓存淘汰策略

概述
在本教程中,我们将学习一种名为 3L-Cache 的新型缓存淘汰策略。该策略旨在通过机器学习方法,在显著降低计算开销的同时,保持甚至提升缓存命中率。我们将从缓存系统的基本概念讲起,逐步深入到3L-Cache的设计原理、核心优化技术及其性能评估。
缓存系统与淘汰策略简介
上一节我们介绍了教程的概述,本节中我们来看看缓存系统的基本概念。
当用户发出请求时,请求首先会经过一个离用户更近的边缘缓存系统。如果请求的数据在缓存中(即缓存命中),缓存系统会直接响应该请求,而无需访问原始服务器。这不仅加快了用户的响应时间,也显著减少了网络运营商的网络流量。
然而,与互联网上的海量数据相比,缓存空间是有限的。当缓存空间已满时,缓存淘汰策略 就变得至关重要,它决定了应该从缓存存储中移除哪些数据。评估淘汰策略有两个关键指标:
- 对象未命中率:反映请求的响应速度。
- 字节未命中率:反映减少的网络流量。
这两个指标对于评估淘汰策略都非常重要。
基于学习的淘汰策略演进
上一节我们了解了缓存淘汰策略的重要性,本节中我们来看看其发展历程。
随着时间的推移,人们提出了各种方法来改进缓存淘汰策略。观察近年趋势,我们发现过去五年中,基于学习的策略 比传统的启发式方法获得了更多关注。
这种转变的主要原因是,基于学习的方法能够适应不同的工作负载并微调参数,从而相比启发式方法有助于降低未命中率。因此,本工作专注于基于学习的淘汰策略。
在学术界,我们根据学习粒度将基于学习的淘汰策略分为三类:
以下是三种主要的学习粒度分类:
- 策略级学习:在专家策略(如LRU和LFU)之间进行选择。
- 组级学习:预测组的淘汰权重,并基于组级淘汰权重来淘汰对象。
- 对象级学习:对多个对象进行采样,预测每个对象的淘汰权重以做出淘汰决策。
通过从每类中选择代表性策略进行评估,我们发现策略级和组级方法在某些情况下可能表现出更差的未命中率,而对象级学习 在两种未命中率上都表现良好且非常稳定。
但是,对象级学习有一个很大的缺点:计算成本最高。以RRB为例,其计算成本可能比LRU高172倍。此外,分析HLB在Twitter数据集上的实时CPU开销时,我们看到了巨大的波动,CPU使用率可能在1.5倍到3倍之间摆动。
3L-Cache的设计目标与挑战
基于上述观察和分析,我们提出了核心问题:能否在保持对象级学习未命中率优势的同时,显著降低其CPU开销? 如果可以,它将成为理想的基于学习的淘汰策略。
我们首先分析了对象级学习策略的计算开销。结论是:训练和预测共同主导了计算开销,但它们的影响随缓存大小而变化。在小缓存中,较高的未命中率使得预测占开销的83%。在大缓存中,预测降至45%,而训练开销保持稳定并成为主要开销。这凸显了需要同时降低训练和预测成本以提高效率。
我们总结了面临的关键挑战:
以下是3L-Cache需要解决的三个核心挑战:
- 如何在不降低模型准确性的情况下减少训练中的开销浪费?
- 如何在不牺牲未命中率的情况下减少预测开销?
- 如何提高策略在不同数据轨迹上的泛化能力?
我们的目标是实现一个具备低开销、低字节未命中率和低对象未命中率的策略,我们称之为 3L-Cache。
3L-Cache的核心设计
上一节我们明确了设计目标与挑战,本节中我们将深入探讨3L-Cache的具体设计。
这是3L-Cache的设计概览。首先,为了在不损害未命中率的前提下实现低频训练,我们引入了训练数据收集机制。其次,我们设计了更高效的采样和淘汰方法。第三,我们开发了高效的参数自动调整机制,以动态调整参数来增强整体系统性能。
1. 高效训练数据收集
我们的训练数据收集机制会过滤不必要的缓存请求,在确保高预测精度的同时降低训练频率。
我们采用滑动窗口调整,使用一个超参数滑动因子来动态快速地调整窗口大小。
高效的采样和标注包含四个步骤:
以下是采样与标注的四个步骤:
- 采样:每个请求随机采样一个对象。
- 去重记录:存储采样时间,并拒绝重复记录,直到该对象被再次请求。这是我们设计的关键之一。
- 标注:标记被再次请求的对象,并记录其间隔时间。
- 更新:在累积了M个样本后重新训练模型。在我们的实验中,发现M应在32K到128K之间。
这种方法确保了高效且轻量级的训练。
我们使用梯度提升机(GBM) 来预测对象的下次访问间隔时间的对数。选择GBM是因为它能有效处理缺失的历史特征,而无需昂贵的插补操作。
3L-Cache使用6个输入特征来预测未来访问时间,实验结果表明这些特征足以保持较高的模型准确性。
2. 双向采样策略
我们的第二个关键设计是双向采样策略,它通过优先处理非热门对象来提高淘汰效率,从而减少预测开销。
在LRU缓存队列中,热门和非热门对象混合在一起,但非热门对象倾向于在队列尾部积累。我们利用这种分布特点,采用两种采样策略:
以下是两种采样策略:
- 从尾部采样:我们从队列最后8%的部分采样所有对象,其余对象仅在其访问频率小于或等于阈值F时才被采样。
- 从头部采样:这是因为新到达的对象遵循Zipf分布,意味着它们中的大多数实际上是非热门的。由于新对象和热门对象在头部混合,我们使用一个记录队列来跟踪新到达的对象,以便更快地识别和采样。这个过程持续到新对象占缓存的比例少于2%。
通过结合这两种方法,我们通过采样非热门对象来优化淘汰决策。
为了进一步提高效率和淘汰精度,我们为未被淘汰的对象引入了累积预测结果。这增加了淘汰候选对象的数量,从而做出更精确的淘汰决策。
然而,累积的预测结果可能增长到很大。因此,我们还提出了一种高效的管理方法,通过结合堆和哈希表来高效处理大规模的累积数据。
通过这些优化,我们成功地将淘汰率提高到50%,显著降低了预测开销,同时保持了高淘汰精度。
3. 参数自动调优
为了实现策略在不同数据轨迹上的泛化,我们还设计了序列缓存中的参数自动调优机制。具体细节请参阅论文。
性能评估
上一节我们介绍了3L-Cache的核心设计,本节中我们来看看它的实际性能表现。
在评估中,我们使用了8个开源数据集,覆盖超过4K条轨迹,来评估策略性能。我们比较了12种缓存淘汰策略,其中6种是基于启发式的,6种是基于学习的。
字节未命中率总结:
在小缓存容量下,3L-Cache在约30%的测试轨迹中实现了最低的未命中率。在腾讯小容量缓存评估中,相比LRU,我们平均降低了12%的未命中率。
在大缓存容量下,如图所示,也能观察到性能收益。
对象未命中率总结:
在小缓存容量下,3L-Cache在66%的测试数据轨迹中实现了最低的对象未命中率。在腾讯小容量缓存评估中,3L-Cache在90分位处将LRU的对象未命中率降低了超过44.8%,平均降低22.3%。
CPU开销总结(这是本文的主要目标):
在小数据量下,3L-Cache的平均CPU开销仅比LRU高2.6倍,比RHD高6.4倍。
在大数据量下,与LRU相比,3L-Cache的平均CPU开销仅为3.4倍。
总结
本节课中我们一起学习了3L-Cache缓存淘汰策略。总而言之,3L-Cache具备低开销的在线训练设计和低开销的淘汰设计。它实现了低对象未命中率和低字节未命中率,并在许多场景下超越了现有的先进淘汰策略。


如果您想查看代码,请访问我们的GitHub仓库。
017:LeapGNN - 利用以特征为中心的模型迁移加速分布式GNN训练

概述
在本节课中,我们将学习一种名为LeapGNN的新框架,它旨在加速分布式图神经网络(GNN)的训练过程。我们将探讨传统分布式GNN训练的性能瓶颈,理解LeapGNN提出的以特征为中心的模型迁移方法,并详细介绍其核心的三大优化技术。
GNN训练与分布式挑战
图神经网络(GNN)是专门为处理图结构数据而设计的神经网络模型。在我们的日常生活中,人与人、人与产品之间的关系都可以被抽象成图。因此,GNN被广泛应用于推荐系统、社交网络分析等领域。
对于大规模图数据,基于采样的GNN训练是一种标准方法。让我们通过一个简单的文本标签预测例子来快速回顾GNN的训练过程。
训练数据包含两部分:
- 图拓扑结构:表示顶点(vertices)和边(edges)之间的连接关系。部分顶点带有标签(例如,表示用户偏好),部分则没有。
- 顶点特征:每个顶点都有一个嵌入向量,称为特征。
一个GNN模型是一个多层神经网络。以一个两层GNN、最小批处理大小为1为例,训练开始时随机选取一个带标签的顶点(例如顶点5)。然后进行两跳采样以创建子图。接着,工作节点收集子图中所有顶点的特征。最后,将子图拓扑和收集到的特征送入GNN模型,通过前向和后向传播来更新模型参数。训练好的GNN模型可用于预测未知顶点的标签。
然而,现实世界中的图通常太大,无法放入单台机器的内存中。利用多台机器的分布式内存是一个有效的解决方案。
在这种场景下,图拓扑被分割成两部分(例如红色和蓝色)。红色部分的拓扑及其对应特征存储在服务器0上,蓝色部分存储在服务器1上。一小部分拓扑会被冗余存储以减少采样时的通信时间。
性能瓶颈分析
让我们通过一个分布式训练迭代的例子来突出性能瓶颈。假设图和特征分布在两台服务器(标记为红和蓝)上,最小批处理大小为2,即每台服务器的worker随机选择两个带标签的顶点进行训练。
以下是训练步骤:
- 每个worker通过两跳采样生成一个子图。
- 每个worker收集其子图中所有顶点的特征。如果顶点特征存储在本地,则直接从本地机器收集;否则,需要从远程机器获取。
- 每个worker的模型使用其子图拓扑和收集到的特征执行前向和后向传播,随后进行梯度同步和参数更新。这就完成了一次训练迭代。
我们发现,从远程机器获取特征的过程是整个训练过程的主要性能瓶颈。在各种数据集和GNN模型上的测试表明,这部分通信时间占比超过44%。
现有方法与局限性
近期的工作提出了许多方法来缓解这个瓶颈,例如图分区优化、从本地服务器采样更多顶点、在GPU内存中缓存顶点特征以及引入新的训练方案。然而,这些方法都存在一些局限性。我们将这些方法称为以模型为中心的方法,因为在它们的系统中,当模型需要位于远程机器上的特征时,它们会将这些特征获取到本地机器。
LeapGNN的核心观察与设计
我们观察到,在GNN训练中,从远程服务器获取的数据量远大于模型参数本身。基于这一观察,我们受到启发,利用模型迁移来减少GNN训练期间的数据传输量。我们的目标是将模型移动到顶点特征所在的服务器,而不是从远程服务器获取特征。
朴素的模型迁移方法
让我们用同一个例子来说明朴素的模型迁移方法。首先关注一个模型(模型0)的计算,它仍被分配用于训练顶点6和3。因此,worker首先生成子图0。
以下是其计算流程:
- 模型0收集子图第一层中存储在本地顶点的特征,并执行部分前向计算。我们将这些顶点标记为红色圆圈。
- 接着,模型0及其中间结果迁移到服务器1,在那里它收集剩余顶点的特征,并完成第一层的前向计算。
- 最后,模型0及其中间数据迁移回服务器0,以完成第二层的前向计算以及整个模型的后向传播。
这两个模型在两台机器之间交替移动并并行执行计算。在两个模型都完成前向和后向传播后,梯度被同步,参数在最后更新。
与现有的以模型为中心的方法相比,朴素的模型迁移方法完全消除了两台机器之间的特征数据传输。
然而,这种朴素的模型迁移方法只在某些情况下比原始方法更高效,在其他情况下则不然。这主要是因为朴素的模型迁移引入了中间数据传输,而这些数据是后向传播所需要的。因此,我们需要进一步改进朴素的模型迁移。
LeapGNN的三大关键技术
1. 基于微图的GNN训练
我们首先注意到,当采样的子图被划分为更小的子图时,数据局部性可以得到改善。具体来说,我们首先定义了一个微图,它是一种特殊的子图,只包含一个根顶点。例如,从最小批处理6和3采样得到的子图0,就由微图6和微图3组成。
我们发现,微图中的大多数顶点特征都位于根顶点所在的同一台机器上。例如,在微图6中,大多数顶点特征与根顶点6在同一台机器上,因此它们都是红色的。我们使用各种图分区算法、采样算法、采样层数或服务器数量进行验证,发现微图的局部性总是高于整个子图的局部性。我们认为这是因为图分区算法倾向于将相邻的顶点放在同一台机器上。
基于微图增强的局部性,我们提出了基于微图的GNN训练。
让我们再次关注模型0,与朴素模型迁移相比,有两个关键区别:
- 第一,一个微图的前向和后向传播在单台机器上完成。例如,微图6仅在服务器0上用于训练,微图3仅在服务器1上用于训练。这样做的好处是,在后向传播完成后,除了梯度之外的所有中间数据都被释放,避免了朴素模型迁移中传输大量中间数据的开销。
- 第二,它允许从远程服务器获取微图中非本地的顶点特征。请注意,由于我们之前提到的微图增强的局部性,只有少量特征需要从远程获取。
总体而言,它结合了模型迁移和微图的局部性,以减少跨服务器的数据传输量。同时,模型1遵循类似的过程,但在另一台机器上。在两个模型都完成训练后,梯度被同步,模型参数被更新。此外,由于该方法不改变训练顶点的随机分配和每个模型的训练过程,因此保持了模型精度。
2. 顶点特征预收集技术
我们进一步发现,同一服务器上不同时间步之间的特征传输是冗余的。例如,顶点1和4的特征被获取了两次。
因此,我们设计了顶点特征预收集技术来减少冗余传输。其核心思想是主动检查在接下来的几个时间步中需要哪些顶点,并一次性从远程获取多个顶点的特征,从而减少冗余的获取请求。
3. 微图合并技术
当集群规模较大时,一次迭代涉及更多时间步的计算和同步,导致时间开销增加。例如,三台服务器需要三个时间步来完成一次迭代训练。
为了解决这个问题,我们提出了微图合并技术,它通过合并一些微图组来减少时间步之间的同步时间开销。合并后,一次迭代训练只需要两个时间步。
随之而来的问题是:应该合并哪些微图以及合并多少个微图?我们设计了一个简单的贪心算法来解决这些问题,更多细节请参阅我们的论文。
实验评估与总结
我们的实验在四台服务器、单块RTX 8000 GPU上进行。我们选择了五种具有不同隐藏大小和层数的流行GNN模型,以及五种不同大小的数据集。我们在性能方面将LeapGNN与DGL、P3和NeuStar进行了比较。此外,我们还比较了LeapGNN与朴素模型迁移方法,以证明我们优化的有效性。
实验结果表明,无论是在小批量训练还是全批量训练下,LeapGNN在各种数据集和模型上的性能均优于其他系统,加速比达到1.1倍至4.8倍。
此外,我们依次启用了每项技术,结果表明每项技术都进一步加速了分布式GNN训练。具体来说:
- 第一项技术(基于微图的训练)显著降低了特征的本地缺失率。
- 第二项技术(特征预收集)最小化了冗余的特征请求。
- 第三项技术(微图合并)自适应地调整了迭代中的时间步数,自动平衡性能提升和额外开销。
我们还通过改变特征维度、批处理大小、采样扇出大小和分布式服务器数量来测试LeapGNN。结果表明,LeapGNN始终优于其他方案。
总结



在本节课中,我们一起学习了LeapGNN框架。我们首先识别出特征传输已成为分布式GNN训练瓶颈的问题。为了解决这个问题,我们引入了以特征为中心的模型迁移方法来减少特征传输量。为了使该方法真正有效,我们开发了三大关键技术:基于微图的GNN训练、顶点特征预收集和微图合并。这些创新使LeapGNN相比最先进的对比系统P3,实现了高达4.2倍的加速。我们的代码已开源,并成功通过了制品评估。
018:面向DPU的混合索引方案HiDPU

概述
在本节课中,我们将学习一篇来自FAST‘25存储大会的研究工作,题为“HiDPU: A DPU-Oriented Hybrid Indexing Scheme for Disaggregated Storage Systems”。该工作旨在解决在分布式存储系统中,将索引卸载到数据处理单元(DPU)时所面临的内存限制、交互开销和计算能力不足等挑战。我们将详细解析其提出的混合索引方案HiDPU的核心思想、组件构成以及性能优势。
背景与动机
在现代数据中心,数据处理单元(DPU)正在取代传统的网络接口卡,负责处理与计算服务器的通信。通过集成计算、网络和存储能力,DPU作为数据路径上的智能可编程处理器,具有加速数据处理和减轻CPU负载的潜力。
例如,在收到一个I/O请求时,DPU可以从CPU端的存储控制器获取物理设备地址,并构造PCIe命令直接访问存储设备。这种方式消除了CPU内存之间的冗余数据拷贝,提高了数据效率。
然而,在这个过程中,CPU仍然需要参与地址转换。在分布式存储系统的存储服务器中,CPU资源有限,涉及CPU计算还会带来额外的开销。当处理海量并发I/O请求时,这种集中式地址转换的延迟可能很高。
因此,我们的动机是将映射表的索引卸载到DPU中,并在DPU内完成地址转换。
面临的挑战
然而,在DPU中实现索引面临几个挑战。首先,与映射表的大小相比,DPU的内存极其有限。现阶段,DPU必须将大部分数据存储在自身内存中,但通过PCIe DMA进行交互的开销也可能很高。此外,DPU需要处理大量并发工作负载,其每个核心的计算能力有限。
为了应对这些挑战,一个高效的索引结构至关重要。
现有索引结构分析
我们首先探索了当前用于存储映射表的索引结构。
- 多级页表:查找速度快,但由于其每一级子表固有的空洞,空间效率较差。
- 学习型索引:构建学习模型或映射表以实现更好的空间效率,但由于预测不准确,需要额外的本地搜索。
- 完美哈希表:提供良好的查找和空间效率,但不支持实时索引更新,且重建开销高。
因此,遗憾的是,目前没有专为DPU设计的索引能够有效应对这些挑战。
HiDPU方案总览
在这项工作中,我们提出了HiDPU。它由三个主要组件构成。
- 混合索引:包含一个能放入DPU有限内存的内部索引,同时外部索引针对高效搜索进行了优化。
- DPU端缓存:用于进一步减少与CPU内存的不必要交互。
- 异步索引更新策略:支持在CPU端处理复杂的索引更新操作,确保DPU专注于简单但频繁的索引查询。
接下来,我们将从混合索引开始详细介绍HiDPU。
混合索引设计
混合索引由一个学习型索引和基于连续性的分段组成。
首先,一个分段算法将映射表划分为固定大小的段。HiDPU会为具有不同连续性水平的段创建不同的索引。对于连续性高的段,使用基于完美哈希的段,其二级哈希参数(称为pilots)作为辅助索引。
其次,使用每个段的第一个键构建一个学习型索引。分段和学习型索引确保了内部索引的大小能够适应受限的DPU内存空间。
为了找到逻辑地址对应的映射条目,HiDPU首先使用学习型索引确定它属于哪个段。
- 如果它属于一个连续性高的段,则通过基地址和一个偏移量获得目标位置。
- 如果逻辑地址属于一个完美哈希段,HiDPU将首先通过一次哈希和DMA操作检索
pilot值,然后使用pilot值执行第二次哈希,以找到映射表中的最终目标位置。 - 基于线性的完美哈希段与完美哈希段的工作原理类似,只是
pilot值的位置是通过除法运算而非哈希运算确定的。
对于连续性高的段,一次DMA操作就足以获取映射条目,但对于完美哈希段和线性完美哈希段,需要额外的DMA操作来获取pilot值。
Pilot缓存机制
为了解决上述问题,HiDPU在DPU中引入了pilot缓存。它使用乐观锁机制,将整个缓存组加载到线程的私有空间中,并通过比较ID来找到正确的pilot值。
在更新缓存前,它会检查组计数器以找到缓存组中下一个正确的pilot位置,这有助于防止写冲突。此外,线性完美哈希段利用具有相当连续性的段,将相邻的键分组到一个桶中并共享相同的pilot值。
实验结果表明,这种设计有效地提高了缓存命中率。
异步索引更新策略
在HiDPU中,DPU处理简单且频繁的读操作,而由写操作引起的复杂且不频繁的索引更新则由CPU处理。
写操作可能需要更新、删除或插入映射表条目。前两种操作可以在映射表内原地完成,但插入操作可能导致索引重建。
为了解决这个问题,新的插入操作被存储在一个基于布谷鸟哈希的缓冲区中,这也允许DPU通过一轮DMA操作检查最新的键。当缓冲区填满时,会触发本地重建以重建段索引。
缓冲区首先根据段的第一个键被划分到各个段,每个段的重建是独立进行的。段元数据中的版本号有助于识别过期的缓存条目。
需要注意的是,本地重建不需要更新学习模型,也不会影响其他段的查询。HiDPU只在全局重建期间更新学习模型,这在实际中很少发生。
实验评估
我们在一个华为DPU上实现了HiDPU原型。该DPU拥有4MB的内部内存,由所有线程共享,每个DPU线程还配备了4kB的私有内存。
我们还在DPU中实现了几项优化,以提高处理学习型索引的效率,包括定点模型计算、基于二分查找的本地搜索和数据重用压缩。
我们实现了两个基线索引:D-Page(三级页表)和D-Learned(学习型索引)。实验中使用的负载来自两个来源:微软剑桥研究院的块I/O追踪,以及从运行各种真实世界应用的基于文件服务器捕获的实时I/O请求。
性能结果分析
总体性能图显示了不同客户端数量下的吞吐量和平均延迟。
- D-Page每次查询总是需要两次随机DMA操作,效率较低。
- 当地址连续性高时,D-Learned只需要一次DMA操作将预测的映射条目取入DPU,这使其比D-Page更高效。然而,当地址连续性低时,它需要将更多数据取入DPU内存并执行本地搜索以找到目标映射条目的正确位置,这也会降低其效率。
- 借助
pilot缓存,HiDPU有超过92%的概率仅通过一次DMA操作就获得目标映射条目,这使其在所有追踪中表现最佳。
因此,HiDPU可以实现最低1.5倍到最高6.3倍的性能提升。
所有索引的DPU内存使用量都已限制在4MB以内。这里,我们展示了不同索引的CPU内存开销。
- D-Page的内存开销来自其每个表中的空洞。
- D-Learned必须存储逻辑地址及其映射条目以供本地搜索。
- HiDPU的空间开销主要来自其完美哈希表,它通过在映射表中允许可接受的空洞来换取索引构建的复杂性。
如图所示,在所有测试追踪中,HiDPU显示出稳定且最低的内存开销。一个例外是prxy追踪,其中D-Page的内存开销更低,因为该追踪主要由大文件组成,在页表中留下的空洞很少。
可扩展性与鲁棒性测试
我们随后将测试规模扩展到PB级别的存储,以测试所有索引的可扩展性。在扩展后的追踪中,D-Page和HiDPU都成功构建了索引并将其卸载到DPU。然而,随着数据集增长,D-Page中的空洞数量也增加,导致显著的CPU内存浪费。另一方面,随着数据集扩大,D-Learned很快无法构建能够上传到DPU的学习型索引。
我们接着评估了索引重建如何影响HiDPU的性能。由于本地重建在后台进行,它不会阻塞索引查询。唯一的影响是可能导致缓存过期。如图所示,当本地重建发生时,pilot缓存可能会经历过期和未命中。但由于线性完美哈希段的存在,过期导致的缓存命中率下降不超过10%,整体性能下降可以忽略不计。
与最新工作的对比
我们还与最新的LearnedFTL进行了比较,后者使用缓存的映射表和学习型索引来加速查询。不幸的是,由于其固定大小的位图(需要为每个可能的映射条目分配一位),LearnedFTL难以被上传到DPU。因此,为了公平比较,我们也实现了一个完全在CPU和内存中运行的HiDPU版本,称为HiDPU-H。我们确保HiDPU-H与LearnedFTL具有相同的旧内存使用量。
由于HiDPU-H和LearnedFTL在CPU上运行,它们都会为每次查询带来涉及CPU的开销,这显著降低了它们的性能。
总结


在本节课中,我们一起学习了HiDPU,一个面向DPU的混合索引方案。我们探讨了利用DPU加速分布式存储系统中数据路径的动机,以及将索引卸载到DPU所面临的内存、交互和计算挑战。HiDPU通过结合混合索引、pilot缓存和异步更新策略,有效地克服了这些问题。研究团队在真实DPU上实现了原型并评估了其有效性,结果表明HiDPU在性能和内存效率上均优于现有方案。这项工作为未来在智能网卡和DPU上构建高效存储系统提供了有价值的思路。
019:PIMLex - 一种基于存内计算的高性能学习索引


在本教程中,我们将学习一篇名为《PIMLex - A High-Performance Learned Index with Processing in Memory》的论文。我们将探讨传统索引的局限性、学习索引的优势,以及如何利用存内计算技术来解决学习索引面临的内存墙问题。我们将详细介绍PIMLex的设计,包括其双层解耦结构、PIM友好的模型结构以及负载均衡机制。
概述 📖
索引结构是存储系统中的重要组成部分。经典索引如B+树提出已有数十年,其通过逐层查找键值,时间复杂度为 O(log n)。近年来,学习索引的研究利用机器学习模型来适应数据分布,显示出超越传统索引的优势。如果模型足够精确,我们可以直接定位数据,因此学习索引比传统的B+树快得多。
然而,执行数据查找时,学习索引需要在模型层进行预测和逐步搜索,一旦预测出数据可能的位置,还需要在数据层进行最后的精确搜索。模型搜索和精确搜索都需要大量的内存访问。在传统的计算机架构中,我们面临一个称为“内存墙”的大问题。内存墙背后的原因很简单:CPU速度快,但内存速度跟不上CPU。这种差距导致了昂贵的内存访问,最终给学习索引带来了大量开销。根据我们的测试,学习索引的大部分执行时间都受限于内存访问。
存内计算技术为内存问题提供了一个有前景的解决方案。通过将处理能力直接集成到内存设备中,PIM可以减少在内存和CPU之间移动数据的需要,这促使我们利用强大的PIM来克服学习索引的内存墙问题。
挑战与动机 🤔
然而,当我们尝试将学习索引应用于PIM时,遇到了一些挑战。
挑战一:空间需求不匹配
第一个挑战是学习索引所需的大空间与PIM有限容量之间的不匹配。学习索引需要大量空间来处理海量数据,但PIM设备的容量很小。例如,商业PIM DIMM通常只提供8GB容量。
挑战二:模型结构与PIM特性不匹配
第二个挑战是学习索引的模型结构与PIM特性之间的不匹配。学习索引的模型预测需要多次浮点运算,但PIM设备不擅长浮点计算。例如,由于硬件限制,UPMEM PIM仅支持原生整数运算。浮点运算必须依赖软件模拟,这要慢得多。当我们在PIM上测试一个简单的学习索引时,我们发现大约18%的时间花在了模型预测上。
挑战三:倾斜工作负载导致的负载不均
第三个挑战是由倾斜工作负载引起的负载不平衡。基于PIM的索引通常将数据划分为多个分区,并将它们分布在多个PIM模块上。然而,在现实环境中,倾斜工作负载非常普遍。某些数据分区被访问的频率远高于其他分区。这导致一些PIM模块过载,而其他模块则处于空闲状态。在我们的测试中,我们发现学习索引在倾斜工作负载下的性能仅为均匀工作负载下的10%。
为了应对这三个挑战,我们提出了高性能的基于PIM的学习索引——PIMLex。
PIMLex 设计详解 🏗️
上一节我们介绍了学习索引面临的挑战,本节中我们来看看PIMLex是如何设计的。
双层解耦结构
PIMLex采用双层解耦结构,包含位于PIM侧的搜索层和位于DRAM侧的数据层。
搜索层使用锚点键构建。通过对数据层进行采样和范围分区,搜索层被划分为许多小分区,并分布在多个PIM模块上。在每个分区内,我们创建一些机器学习模型来提高搜索效率。同时,我们维护一个分区表,记录搜索层的每个分区位于数据层的哪个位置。
数据层包含按顺序存储的键值对主数据,负责处理读取和值更新操作。我们还在数据层使用缓冲页来处理新数据的插入。
PIMLex遵循三步执行流程来处理读写请求:
- PIMLex检查分区表,确定请求键属于哪个PIM模块。
- PIM在搜索层执行查找,获取请求键的近似位置。
- PIM定位请求键在数据层中的实际位置,并在主数据和缓冲页上执行读写操作。
这种双层解耦结构可以最小化PIM空间的使用。PIMLex从主数据中选择锚点键,确保搜索层的规模保持较小。同时,PIM处理了大量的内存访问。例如,要查找键K6,我们首先在搜索层搜索,找到小于K6的最大锚点键,比如K5。这一步基于具有固定误差范围 ε 的模型预测,搜索范围是 2ε。之后,PIMLex检查数据层,在锚点区间内的数据中搜索键。
这种设计有效应对了第一个挑战(空间需求不匹配)。
PIM友好的模型结构
PIM具有有限的计算能力但强大的内存能力,这对原始的多级模型结构并不友好。因此,我们为PIMLex提出了一种PIM友好的模型结构。核心思想是用更多的内存访问换取更少的计算,以应对第二个挑战。
学习索引通常采用多级模型结构,并使用基于模型的搜索方法。每一模型级别的搜索操作都基于上一级别的预测,然后学习索引在预测位置周围误差范围内的区间进行二分查找。因此,基于模型的搜索总成本是预测成本和搜索成本之和。这种方法带来了一些计算开销。
我们也可以执行另一种搜索方法——全局二分搜索。全局二分搜索将在同一级别的所有模型范围内执行二分查找,它用更多的内存访问来减少计算开销。
基于对上述两种模型搜索方法的成本分析,PIMLex使用自底向上的策略,为每个模型级别选择成本较低的搜索方法。这将原始的多级模型结构转换为一个对PIM友好的结构。例如,在这个例子中,底层使用基于模型的搜索,而其上一级使用全局二分搜索。
我们还为基于浮点运算的模型提出了基于查找表的模型。每次预测都需要将搜索键与线性回归模型的斜率相乘。基于查找表的模型减少了浮点运算,它会预先计算结果并存储到查找表中。例如,对于搜索键F1,其对应的F10和F20,它们的计算结果分别是P1和P2。我们可以将P1和P2存储到查找表中,并用P1和P2来代表实际的计算结果。
当使用基于查找表的模型时,我们可以在P1-ε到P2+ε的搜索范围内找到请求键。对于基于浮点运算的模型,预测结果更精确,搜索范围是基于查找表的2ε。根据两种模型类型的查找成本和预测成本,我们可以为PIM选择成本较低的模型类型。
负载感知的副本复制机制
当使用相同数量的PIM模块处理不同的搜索层分区时,倾斜工作负载会导致某些分区成为热点。这导致一些PIM模块过载,而其他模块空闲。由于工作负载分布不均会降低整体性能,我们需要保持不同PIM模块间的负载均衡。我们的核心思想是为热点分区在更多的PIM模块上创建额外的副本。这可以应对第三个挑战。
我们的副本复制机制需要确定每个分区的副本数量。所有分区的副本总数应与PIM模块总数匹配。我们用 T_i 表示分区i处理的查询数量。
基于副本数量 R_i 和查询数量 T_i,我们可以为每个分区定义一个负载因子,它代表了该分区的繁忙程度。全局负载因子是所有分区中最繁忙分区的繁忙程度。我们的目标是通过配置 R_i 来最小化全局负载因子。
为了实现这一点,我们的负载均衡算法会计算不同分区处理的查询数量。通过比较每个PIM模块应处理的平均查询数与 T_i,我们可以确定副本数量。更多细节可以在我们的论文中查阅。
当工作负载分布发生变化时,PIMLex仍然需要保持负载均衡。为此,我们使用了一种副本调整方法。它会重新定位所有分区的副本,但速度较慢。因此,我们还提出了快速副本调整方法。它只将一些空闲分区的PIM模块重新分配给繁忙的分区。这种方法能实现快速调整,但负载均衡效果稍差。
性能评估 📊
上一节我们深入了解了PIMLex的设计,现在让我们看看它的实际表现。
我们实现了一个基于PIM的学习索引基线(Basic-Learned-Index),它直接将基于学习的方法(如PGM索引)移植到PIM上,没有任何像模型结构优化或负载均衡这样的自动化机制。我们还比较了PIMLex与其他基于DRAM的学习索引,如IL、Leap、Finedex和S。
评估使用了五种真实数据集和倾斜工作负载。我们使用四个DRAM条和四个PIM DIMM来评估基于PIM的索引。为了确保公平比较,对于基于DRAM的索引,平台配备了8个DRAM条。
与PIM基线相比,PIMLex显示出性能优势。这种改进得益于PIM友好的模型结构和PIMLex的负载均衡方法。
与基于DRAM的学习索引相比,PIMLex在纯读取工作负载和混合工作负载下仍然优于其他索引。这是因为PIMLex利用了PIM强大的内存能力,可以减少内存访问的开销。
对于纯插入工作负载,PIMLex的性能优于Leap和Finedex,但在某些数据规模上表现不如S。这是因为对于PIMLex,数据层的数据写入仍然在DRAM侧处理,这会带来一些开销,因此在某些数据规模上无法超越S。
我们还评估了PIMLex在包含8亿个键值对的大规模数据下的性能。PIMLex的双层解耦结构可以减少PIM上的空间使用,使其更擅长处理大规模数据。我们发现PIMLex在读取和插入性能上都优于其他索引。这里我们排除了S和Leap,因为它们的索引大小超过了总内存容量。
总结 🎯
本节课中我们一起学习了PIMLex,这篇论文迈出了探索PIM如何解决学习索引中内存墙问题的第一步。我们的评估表明,PIMLex在真实的PIM硬件上,性能优于基于PIM的基线索引和基于DRAM的学习索引。
目前,PIM硬件仍存在一些性能限制,我们期待未来PIM架构的发展,并相信这将进一步提升PIMLex的性能。




020:HaSiS - 一种用于混合事务与分析处理的硬件辅助单索引存储


概述
在本节课中,我们将学习一篇来自FAST 2025存储大会的研究工作:HaSiS。这是一种利用新型存储硬件(CSD)设计的混合事务与分析处理系统,旨在解决传统HTAP系统中数据新鲜度与性能之间的长期权衡问题。我们将从HTAP的基本概念出发,逐步解析现有设计的局限,并深入探讨HaSiS如何利用硬件特性实现突破。
HTAP系统简介与挑战
上一节我们概述了本节课的主题。本节中,我们来看看什么是HTAP系统及其核心挑战。
HTAP是一种众所周知的数据处理架构,用于同时执行联机事务处理(OLTP)和联机分析处理(OLAP)。它是现代应用中的关键组件,影响金融、广告和电子商务等多个领域。
HTAP系统的关键标准通常聚焦于OLTP和OLAP的性能。对于OLTP,它通常涉及小型的随机记录更新或搜索操作。对于OLAP,它通常涉及大规模属性或列扫描。这两种工作负载本质上存在一些冲突。
HTAP系统的另一个关键标准是实时数据分析能力。现代HTAP系统希望尽快对OLTP新摄入的更新执行实时OLAP查询。
具体到这项工作,我们关注HTAP系统内部OLTP与OLAP之间最优存储格式的矛盾。OLTP系统偏好行式存储格式,因为它可以减轻摄入小型更新或搜索操作时的I/O放大效应,并保持更好的更新效率。另一方面,OLAP系统不喜欢行式存储格式,它们青睐列式存储格式以获得更好的I/O效率,因为OLAP请求通常涉及批量扫描或加载操作。
现有HTAP设计及其局限
上一节我们介绍了HTAP的基本目标与内在矛盾。本节中,我们来看看现有的解决方案及其存在的问题。
现有的HTAP设计主流是多索引设计。它们主要通过混合行式索引和列式索引来提供HTAP服务。为了实现实时数据分析,它们依赖于数据同步,或者说跨索引域的数据迁移。
现有的多索引HTAP设计可以分为两类:
以下是多索引多存储设计:
- 它们分别维护一个带有行式索引的OLTP引擎和另一个带有列式索引的OLAP引擎,分别提供服务。
- 它们依赖跨域数据迁移来实现实时数据分析。
- 这种设计存在局限性:首先,数据迁移消耗资源且耗时;其次,这种消耗性工作会引入数据新鲜度问题,因为数据可能无法在需要执行实时OLAP查询时立即应用于列存储。
以下是多索引单存储设计:
- 它们不单独维护两个引擎,而是只保留一个单一系统,但在主列存储之上构建一个行式数据层来维护列数据。
- 这种设计较为新颖,在现代HTAP系统中部署广泛。
- 这种设计也面临一些局限:由于所有数据都维护在单一系统中,意味着无论是OLTP还是OLAP请求都需要遍历所有数据。例如,对于OLAP查询,不仅需要访问主列存储,还需要访问以行式格式维护数据的行数据层。相应地,在数据量大于内存时,这会导致性能下降,因为行数据层通常维护在内存中。
我们识别出的核心问题是:在现有设计中,数据被分离管理在不同的索引域中,跨索引的数据迁移本质上引入了显著的开销。
挑战在于,我们希望同时实现最优的OLTP性能、最优的OLAP性能,并为OLAP查询提供即时数据新鲜度。本工作的关键在于,我们不再依赖原有的分离索引设计,而是希望利用新型存储硬件带来的机遇重新思考这个问题。
新型硬件:CSD与透明压缩
上一节我们分析了现有设计的瓶颈。本节中,我们来看看本工作所依赖的新型硬件——CSD。
本工作聚焦于具有内置透明压缩功能的CSD。这是一种具有数据压缩功能的特殊计算存储驱动器。
CSD透明压缩的第一个特性是低成本压缩。它可以在SSD内部透明地提供压缩或解压缩能力,而不影响数据服务的正常流程和关键路径。这种压缩或解压缩操作非常高效且延迟低。
CSD透明压缩的另一个关键特性是它可以提供虚拟化的逻辑存储空间。这可以进一步实现稀疏数据管理。这为我们重新设计新的HTAP系统以同时实现既定目标提供了重要的机遇。
HaSiS的核心设计洞察
基于新型CSD设备,我们获得了几个关键洞察:
以下是第一个洞察:CSD可以将随机I/O放大与页面大小解耦。
- 原本,OLTP系统依赖小尺寸页面来维护数据,以减轻I/O放大。否则,就需要维护一个巨大的单体日志来累积对页面的更新。但日志大小存在权衡:日志太大,恢复性能会变差;日志太小,则无法充分累积更新。
- 引入CSD后,这个问题消失了。我们可以将原本巨大的单体日志分配到每个页面,将原始的单体日志转换为每页日志。这可以减轻恢复开销,同时为OLTP请求提供最优性能。换句话说,在我们的HTAP设计中,我们将不再依赖小尺寸页面来保证OLTP的最优性能。
以下是第二个洞察:CSD可以减轻OLAP查询的随机I/O放大。
- 典型的OLAP设计偏好将数据紧密排列在数据块(如页面)中以节省存储空间。但这种布局在执行面向列的查询时会引入随机I/O放大。
- 在CSD中,思路类似。我们可以稀疏地按列管理列式数据,而不是将它们紧密地对齐在每个4KB数据块内。这样,随机I/O放大就可以避免。
以下是第三个洞察:系统中提供并发能力的多版本记录。
- 由于我们有了CSD,原本分离管理的临时数据可以遵循相同的思路,分配到每个页面。这样,我们就不会面临OLAP性能下降和额外数据检索的问题,同时垃圾回收也比以前更简单。
HaSiS系统架构
基于这些洞察,我们提出了HaSiS设计,即硬件辅助的单索引存储。系统包含三个主要组件:混合缓冲池、小型全局日志以及基于每页日志的混合页面格式。通过这些组件,我们旨在实现CSD带来的机遇和目标。
以下是混合页面布局:
- 它遵循我们的洞察分析:我们使用大尺寸的放松B+树页面来为OLTP和OLAP提供高效服务。
- 对于列式页面,我们将稀疏地将列数据对齐到每个数据块,而不是原始紧密对齐的方式。
- 对于行式数据页面,我们为每个列页面分配,就像将原始的单体日志分布为每页日志。
以下是日志与压缩机制:
- 我们设计了高效的机制,将OLTP更新摄入主存储。
- 我们希望在实现这种摄入的同时,为OLAP请求提供最优性能。
- 我们旨在更好地利用当代SSD的高带宽,通过增加SSD设备的队列深度来高效摄入这些操作。
以下是混合缓冲池:
- 它用于隔离OLTP和OLAP之间的性能影响。
- 我们采用了一种基于验证的方法来实现这一点。
关于混合缓冲池及其他技术的更多细节可以在论文中找到。
评估与结果
上一节我们介绍了HaSiS的设计原理。本节中,我们来看看它的实际效果。
我们实现了一个全功能原型和一个基准测试工具。我们的评估构建在顶级的商用SkyFl CSD设备之上。
以下是评估结果:
首先,我们与当代OLTP和OLAP系统比较,以评估我们的性能潜力。
- 与当代OLTP系统(如MySQL、PostgreSQL)以及我们实现的一个基于B+树的行存储基线相比,我们具有可比的性能。
- 与OLAP系统(如Spark)以及一个列存储基线相比,结论相同,我们能够提供可比的性能。
其次,我们与代表性的HTAP系统TDB进行比较。
- 结论是,我们能够为OLAP请求提供更稳定和即时的数据新鲜度。
- 同时,我们在OLTP和OLAP两方面的性能与TDB相当。
最后是关于优化效果的评估,例如我们设计的存储使用情况。
- 由于我们拥有透明压缩,我们不会明显占用实际物理存储空间。
总结
本节课中,我们一起学习了HaSiS,一种CSD使能的单索引设计。它旨在解决HTAP系统中数据新鲜度与性能之间的长期权衡问题。我们通过利用CSD稀疏数据管理的能力,在一个商用CSD平台上进行了评估,证明了其有效性。




021:AegonKV - 一种高带宽、低尾延迟、低存储成本的KV分离存储系统




大家好,我是来自华中科技大学的Bing Tian。本文的作者今天无法到场,我将代表他们分享这项工作的见解。
在本教程中,我们将学习AegonKV(A-KV)的设计,这是一个旨在解决键值(KV)分离存储系统中垃圾回收(GC)瓶颈的系统。我们将从背景和动机开始,逐步解析其核心设计,包括GC管理器、调度器、无验证插入机制以及如何在智能SSD上实现硬件GC。最后,我们将通过实验数据评估其性能优势。
背景与动机 🎯
键值分离系统将值从LSM-tree架构中解耦。它包含多个Blob文件来实际存储键值对,而SSTable则存储键及其对应值在Blob文件中的地址引用。值日志的大小显著减小,从而缓解了读写放大问题。
然而,值日志面临着过期数据累积的挑战。为了减少空间冗余,系统引入了垃圾回收机制。在GC过程中,需要扫描整个Blob文件,并查询LSM-tree以确定是丢弃无效数据还是重写有效数据。但这引入了额外的I/O和计算开销。
一种广泛使用的GC策略是将值日志分区,并选择垃圾比例高的Blob文件进行GC。这种由Titan和PebblesDB采用的策略被称为直接GC。它有助于选择高收益区域,但仍需要检查LSM-tree,这实际上造成了性能瓶颈。
另一类工作,以Dostoevsky和BadgerDB为代表,试图将Blob文件的单层结构转变为多层结构。当在SSTable上执行压缩时,有效值将被直接迁移到新的Blob中。这种策略被称为压缩触发GC。虽然它增加了压缩的开销,但减少了检查LSM-tree的带宽使用,从而提升了系统性能。
现有系统的权衡分析 ⚖️
为了理解键值分离的特性,我们首先对Titan、BadgerDB和Dostoevsky进行了写性能测试,并与传统的LSM存储RocksDB进行了比较。
结果如下表所示,我们在图中进一步可视化了这些指标。

与RocksDB相比,BadgerDB和Dostoevsky的吞吐量高出17%以上,尾延迟降低了50%以上,但空间放大显著增加。RocksDB仅需9GB空间存储过期数据,而BadgerDB和Dostoevsky分别需要135GB和53GB。然而,我们看到Titan在空间使用方面表现出色,仅比RocksDB多2GB。Titan在压缩I/O和重写方面也表现最佳,但吞吐量比RocksDB下降了18%。
总结来说,我们的第一个观察是:现有键值分离系统的性能结果存在权衡,无法同时在吞吐量、尾延迟和空间使用上取得优势。
基于Dostoevsky和BadgerDB的优化方向是卸载GC操作这一事实,我们进一步关闭了Titan中的GC选项。表A显示,在没有GC的情况下,Titan的吞吐量提升了67%,这表明GC操作仍然是Titan的瓶颈。
然后,我们将可用CPU核心数设置为16,以检查资源竞争的结果。结果显示,没有GC的Titan性能仅下降6%,但Titan进一步遭遇了20-23%的骤降。为了进一步确定竞争条件是否出现,我们使用perf工具进行了分析。表B显示,主要瓶颈源于读写I/O与GC竞争计算和带宽资源。
幸运的是,我们观察到直接GC操作可以通过智能SSD上的近数据处理进行优化。
智能SSD与挑战 💡
三星的智能计算存储设备是业界首款可定制和可编程的计算存储。智能SSD内部集成了一个FPGA,数据可以通过内部总线传输,因此文件处理可以绕过复杂的软件栈和系统总线。
然而,几个挑战阻碍了在智能SSD上高效卸载GC操作:
- 硬件限制:由于硬件约束,在GC期间查询LSM-tree的逻辑难以实现,需要避免。
- 有限的计算资源:智能SSD内的计算资源无法与主机相比,因此资源控制至关重要。
- 带宽竞争:当硬件GC完成后,结果需要传输回主机,这会竞争带宽,需要设计来重新调度数据移动。
AegonKV 系统设计 🏗️
以下是AegonKV的架构设计,旨在应对数据重写的挑战。

我们在GC管理器中提出了有效位图结构。此外,为了克服智能SSD资源有限的问题,我们设计了GC调度器来协调数据移动和资源分配。我们还引入了无验证插入机制来解决GC的带宽竞争问题。最后,我们在智能SSD上设计并部署了GC计算卸载模块。
GC管理器设计
对于每个Blob文件,我们创建一个称为valid_map的位图,用于指示每个键值对的有效性,其中位0和1分别标记有效和无效记录。GC管理器利用压缩的结果信息来更新valid_map,并通过监控每个valid_map中的垃圾比例来创建GC任务。
GC调度器设计
调度器中有三个关键单元:
- I/O控制单元:充分利用智能SSD接口,在SSD和FPGA DRAM之间传输Blob文件,以及在系统总线上传输
valid_map和重打包的镜像数据。 - 计算调度单元:在软件层管理正在使用的核心数量以及为每个任务分配的FPGA资源。
- 合并插入单元:合并插入的问题是来自并发调用的写接口竞争。因此,我们添加了一个屏障,帮助协调多个磁盘的合并操作到一个统一的通道中。
无验证插入机制
尽管合并插入单元通过批处理进行了优化,但它仍然需要对每个合并数据进行查找,以避免版本冲突。如下图所示,一个包含旧数据的条目可能通过合并插入,覆盖了在GC期间写入的最新版本。

然而,这种查找操作会导致严重的带宽竞争。为了提升性能,我们提出了无验证插入。
为了应对由此带来的数据一致性问题,我们将查找验证推迟到get和compaction过程中。
- 在
get过程中,首次找到一个键时,它可能是正常数据或GC合并数据。查询到正常数据时没有问题。但当查询到合并数据时,我们不确定在GC期间是否有新的值被写入。因此,我们继续向后查询更旧的版本,并比较其值地址与合并数据中存储的原始地址,以确定哪个是最新版本。 - 在
compaction过程中,我们也采用继续读取下一个版本的策略。此外,我们在压缩期间将合并数据提升为正常数据,以最小化延迟验证的频率。
硬件GC卸载
我们将硬件卸载过程抽象为三个独立的阶段:
- 输入解码阶段:解码Blob文件和
valid_map。由于数据在文件内是连续的,硬件实现高度并行。 - 实际计算阶段:过滤模块以离散流的方式遍历
valid_map,产生信号供特征模块选择是重写还是忽略。 - 输出编码阶段:从特征模块检索数据,并将其排列为适合写入SSD的格式,无需主机端迭代。
性能评估 📊
我们在配备三星第一代智能SSD的服务器上进行了大量实验来评估AegonKV的设计。我们比较了AegonKV与Titan、BadgerDB、Dostoevsky和RocksDB。使用了YCSB基准测试以及开源的生产环境工作负载:Facebook社交图工作负载和Twitter缓存追踪。
YCSB写密集型负载
下图显示了在YCSB写密集型负载下的结果。
AegonKV表现出吞吐量提升,范围从1.3倍到4.6倍,并且非常接近无GC场景的性能。AegonKV也实现了最低的尾延迟,降低了14%到85%。在空间使用方面,AegonKV仅需0.75倍于RocksDB的额外冗余空间。键值分离的压缩数据量显著减少,并且AegonKV没有写停顿。
其他负载下的表现
- 读密集型负载:AegonKV的吞吐量和尾延迟与其他系统非常接近,因为读流本质相同,且GC操作不频繁触发。由于更新比例低,空间使用也相似。
- 扫描负载:所有系统的性能保持相对相似,AegonKV通过引入额外的压缩触发GC来调节值日志,从而输出性能。
- 真实工作负载:对于社交图和缓存集群,AegonKV的性能结果与YCSB写密集型负载基本一致,吞吐量高出1.22到2倍,尾延迟降低31%到55%,表明AegonKV非常适合真实场景。
- 混合与读密集型负载:我们观察到与YCSB读负载类似的结果,所有系统在性能各方面都很接近。除了GC频率低之外,这两个负载中大量的小值也影响了结果。
资源与效率
表A报告了硬件资源使用情况。我们充分利用硬件并部署了硬件GC,足以支持系统正常运行。表B显示了CPU利用率,AegonKV实现了约20%的节省,这表明GC卸载有效解决了竞争瓶颈。表C显示了能效,AegonKV提升了15%到17%。
参数敏感性分析
我们接下来分析了YCSB在系统参数和工作负载参数方面的敏感性。
- 系统参数:对于AegonKV,由于我们将GC从系统关键路径中分离,性能在不同垃圾比例、线程负载下保持不变。与Titan相反,增加GC线程数不影响AegonKV的性能,调整批处理大小参数对系统性能也无影响。总结:AegonKV的性能优势不受系统参数影响。
- 工作负载参数:
- 值大小:较小的值意味着读写开销减少。AegonKV对值大小的变化较不敏感。
- 数据集大小:数据集大小对每个系统影响很小。
- 工作负载比例:结果与YCSB评估一致。
- Zipfian分布:AegonKV受多线程冲突的影响最小。
- 总结:AegonKV具有良好的工作负载适应性和适度的可扩展性。
总结 🎉
我们观察到现有键值分离系统在GC方面存在竞争和权衡。因此,我们引入了AegonKV,它全面提升了吞吐量、尾延迟和空间使用率。实验证明,AegonKV实现了吞吐量1.3到3.3倍的提升,尾延迟降低37%到66%,空间开销减少15%到85%。



这就是我们工作的介绍。如需更多详细信息,请查阅我们的论文。由于作者无法到场,如果您有任何问题,可以通过电子邮件联系作者。感谢聆听。
022:D2FS - 设备驱动的文件系统垃圾回收 🗑️➡️💾

概述
在本节课中,我们将学习一种名为D2FS的新型文件系统设计。它的核心思想是让存储设备来负责文件系统的垃圾回收工作,从而显著提升系统性能。我们将从背景动机开始,逐步解析其设计原理、面临的挑战以及最终的解决方案。
背景与动机
上一节我们提到了学习D2FS的目标,本节中我们来看看其产生的背景和需要解决的问题。
日志结构文件系统(Log-structured File System, LFS)由Rosenblum于1991年首次提出。LFS是一种写优化文件系统。它将整个文件系统分区视为一个单一的日志,并将数据块写入日志的末尾。例如,当更新一个文件块时,LFS将新块写在日志末尾,并使旧块失效。它通过将文件上的随机写转换为顺序写,从而高效地处理随机写工作负载。
多年来,已经出现了许多为新兴存储设备设计的日志结构文件系统,包括JFFS、YAFFS、F2FS等。LFS因其仅追加(append-only)的特性与闪存存储(需要顺序页写入)的特性高度契合而备受青睐。
然而,当LFS与闪存存储一起使用时,写放大会增加,因为文件系统和闪存存储都会冗余地执行各自的垃圾回收例程。
下图展示了IO栈中的映射关系。要定位一个文件块的物理位置,存在两种映射:FTL映射和L2P映射。FTL映射将文件块映射到文件系统分区内的逻辑块地址(LBA)。L2P映射将LBA映射到闪存空间中的物理页地址(PPA)。
文件系统垃圾回收负责回收无效的文件系统块。它将有效的逻辑块整合到一个空闲段中,并为更新的LBA更新FTL映射。然后,受害段被释放并回收。
另一方面,设备垃圾回收负责回收无效的闪存页。它将有效的闪存页整合到一个空闲的闪存块中,并为更新的PPA更新L2P映射。结果,受害闪存块被释放并回收。
垃圾回收对应用程序性能有严重的负面影响。我们在LFS和闪存存储上执行随机写入。当文件系统和设备同时运行垃圾回收时,应用程序性能下降高达80%。
已有大量研究致力于减轻垃圾回收的开销。一些工作侧重于将具有相似生命周期的文件块聚集在一起以减少写放大。许多研究提出通过向主机暴露设备的内部几何结构,让文件系统直接清理存储设备。最近提出的IPLFS采取了相反的方法,它通过过度扩展文件系统分区来消除文件系统垃圾回收的需求。然而,为了管理如此大的文件系统分区,L2P映射结构变得巨大且复杂,难以在现实世界中部署。
总而言之,一些工作依赖文件系统进行垃圾回收,而另一些工作则依赖设备进行垃圾回收。这引出了我们的问题:系统垃圾回收和设备垃圾回收,哪个更好?
我们比较了它们的开销,发现文件系统垃圾回收比设备垃圾回收成本高得多。其背后的原因是文件系统垃圾回收涉及太多子操作,包括检查点、格式化遍历、元数据更新、主机-设备数据传输和页分配,而设备垃圾回收则不需要。
因此,我们的核心想法是让设备来清理文件系统分区。将系统从运行垃圾回收中解放出来,让设备同时清理存储空间和文件系统分区。
基本概念
上一节我们分析了传统垃圾回收的弊端,本节中我们来了解D2FS的基本工作原理。
左图显示了主机文件系统中的FTL映射,右图显示了存储设备中的L2P映射。当设备运行垃圾回收时,它会整合有效的闪存页,因此相应的PPA会被更新。同时,设备会更新被整合闪存页对应的逻辑块地址(LBA),使得相应的逻辑块也被整合。因此,设备同时更新了FTL映射和L2P映射。结果,设备垃圾回收同时清理了闪存块和逻辑空间。
然后,设备将更新后的FTL映射同步给主机文件系统。主机文件系统为更新的LBA更新相应的FTL映射,并在不显式迁移文件系统块的情况下回收空闲的文件系统段。我们将这个概念称为设备驱动的文件系统垃圾回收。
挑战
理解了基本概念后,我们来看看实现设备驱动文件系统垃圾回收所面临的三个关键挑战。
以下是实现设备驱动垃圾回收的三个主要挑战:
- 第一,传统的IO栈只允许主机更新FTL映射。 由于主机和设备都会分配LBA,在分配LBA时存在潜在的冲突。
- 第二,现有的接口机制(如中断和轮询)不适合同步更新后的FTL映射。 轮询浪费主机CPU周期;为设备到主机的同步定义一个新的专用中断需要对现有IO栈进行重大更改,例如添加新的中断或新的中断向量表或新的中断处理程序(如果我们幸运地找到一个可用的未使用中断号)。
- 第三,设备垃圾回收必须在文件系统耗尽空闲段之前及时运行。 由于设备根据自己的调度运行垃圾回收,文件系统可能会耗尽空闲段,从而暂停其操作,直到垃圾回收同步完成。
D2FS设计概述
面对这些挑战,D2FS提出了相应的解决方案。本节我们将介绍D2FS的整体设计。
D2FS由三个设计要素组成:耦合垃圾回收、迁移队列和虚拟过度配置。
- 耦合垃圾回收:设备更新FTL映射。
- 迁移队列:设备用于将更新后的FTL映射同步到主机的IO接口。
- 虚拟过度配置:我们虚拟地扩展文件系统分区,以防止文件系统耗尽空闲段。
耦合垃圾回收
首先,让我们详细了解耦合垃圾回收。
耦合垃圾回收是一种设备垃圾回收。在耦合垃圾回收中,LBA和PPA被耦合在一起并同时更新。当垃圾回收运行时,闪存页和相应的逻辑块被一起整合,从而同时回收闪存块和文件系统段。
为了防止设备与文件系统在分配LBA时发生冲突,我们将文件系统分区划分为两个区域:常规区域和垃圾回收区域。在常规区域,只有主机可以分配LBA。在垃圾回收区域,只有设备可以分配LBA,作为垃圾回收的目标区域。
耦合垃圾回收分三步进行:
- 选择受害闪存块。
- 将受害闪存块中的有效页迁移到一个空闲闪存块。
- 将目标闪存块重新映射到垃圾回收区域中的一个空闲段。
迁移队列
接下来,我们解释迁移队列。
迁移队列是设备驱动的IO机制,用于将FTL更新发送给主机。一个迁移队列包含一组旧LBA和新LBA的配对。设备将迁移队列发送给主机。然后,主机根据更新的LBA更新FTL映射。之后,主机通过轮询机制向设备发送完成信号。
当迁移队列准备就绪时,设备需要向主机发出信号。对于IO信号链接,设备将队列通知搭载在其他IO命令的完成信号上。我们称这种机制为队列搭载。队列搭载既不浪费CPU周期,也不需要定义新的中断。
在使用传统IO栈接口机制时,设备分别发出IO完成信号和队列通知。在队列搭载中,设备将队列通知搭载在后续其他IO命令的完成信号上。为此,设备在IO完成信号中设置一个新定义的队列标志。主机接收队列并从中提取更新后的FTL映射。然后,主机根据更新后的FTL映射更新文件系统状态,从而创建空闲段。
虚拟过度配置
最后,我们介绍虚拟过度配置。
虚拟过度配置的概念是将文件系统分区大小与存储容量分离,并虚拟地扩展文件系统分区大小。在传统的IO栈中,文件系统分区大小受存储容量限制。因此,设备必须在文件系统耗尽空闲段之前及时运行垃圾回收。否则,文件系统操作将暂停,直到垃圾回收同步完成。
在虚拟过度配置中,文件系统分区被虚拟扩展。它为文件系统提供足够的空闲虚拟段,允许设备按照自己的调度运行垃圾回收。
虚拟过度配置并不意味着文件系统可以存储超出存储容量的数据块。文件系统分区中有三种类型的块:有效块、无效块和空闲块。在传统IO栈中,有效块、无效块和空闲块的总大小受存储容量限制。在虚拟过度配置中,只有有效块的大小受存储容量限制,确保文件系统不会写入超出存储容量。
我们研究了需要将文件系统分区虚拟过度配置多少。我们确定2.4倍的存储容量是足够的。回想一下,D2FS文件系统分区由常规区域和垃圾回收区域组成。我们通过实验确定,常规区域需要1.4倍的存储容量。如果低于1.3倍,文件系统很可能耗尽空闲段,从而降低应用程序性能。我们将垃圾回收区域的大小设置为等于存储容量,因为存储容量足够大,可以容纳垃圾回收迁移的逻辑块。
性能评估
了解了D2FS的设计后,本节我们通过实验数据来看看它的实际效果。
以下是评估设置:我们使用一个256GB的模拟SSD(曾在FAST‘23上展示),垃圾回收单元大小为32MB。我们使用5种工作负载:FIO、TPC-C on MySQL、YCSB on MongoDB、Filebench和Fileserver。
我们比较了四种文件系统:
- F2FS:涉及文件系统垃圾回收和设备垃圾回收。
- Zoned F2FS:采用ZNS方法的F2FS版本,只有文件系统执行垃圾回收。
- IPLFS:只有设备执行垃圾回收。
- D2FS:只有设备执行垃圾回收。
它们都具有相同的垃圾回收单元大小,即32MB。
这张图显示了4KB随机写入工作负载的性能。由于垃圾回收,所有四种文件系统的性能在大约300秒后开始下降。通过消除昂贵的文件系统垃圾回收,D2FS的性能是F2FS的3倍,是Zoned F2FS的1.7倍。D2FS的性能也优于IPLFS 15%。IPLFS定期重组其L2P映射结构以减少内存占用,此活动会干扰来自主机的IO请求。D2FS没有这种开销。
在宏观基准测试中,D2FS仍然优于其他文件系统。D2FS的性能是F2FS的2.6倍,是Zoned F2FS的1.4倍,是IPLFS的1.5倍。
我们比较了设备垃圾回收和文件系统垃圾回收的开销。该图显示了不同工作负载下每次垃圾回收的延迟。我们发现文件系统垃圾回收所需时间大约是设备垃圾回收的3倍。我们观察到文件系统垃圾回收开销的主要来源是检查点、页分配和格式化遍历。
总结
本节课中,我们一起学习了D2FS设备驱动文件系统垃圾回收技术。
我们观察到文件系统垃圾回收比设备垃圾回收更昂贵。因此,我们让设备垃圾回收来清理文件系统分区,这与广泛使用的主机驱动垃圾回收方法相反。结果,D2FS的性能分别是F2FS的3倍和Zoned F2FS的1.7倍。


D2FS通过耦合垃圾回收让设备同时更新映射,通过迁移队列实现高效同步,并通过虚拟过度配置确保文件系统不会因设备调度而停滞,从而系统性地解决了性能瓶颈问题。
023:ShiftLock - 通过交接缓解单边RDMA锁争用


在本教程中,我们将学习一篇来自USENIX FAST 2023会议的研究工作——ShiftLock。这项研究由清华大学的研究团队完成,旨在利用RDMA的硬件特性来加速分布式锁协议,特别是解决高争用场景下的性能下降问题。我们将从RDMA技术基础开始,逐步深入到ShiftLock的核心设计理念、关键技术挑战及其解决方案。
RDMA技术简介 🔌
上一节我们提到了ShiftLock的研究背景,本节中我们来看看其依赖的核心技术——RDMA。
RDMA意为远程直接内存访问。它是一种用户态网络技术,允许直接访问远程内存,同时绕过远程CPU。由于无需CPU干预,RDMA操作速度极快,网络往返可在微秒级内完成。
RDMA请求被称为“动词”。主要分为两类:
以下是RDMA动词的分类:
- 单边动词:无需远程CPU参与,包括读、写和原子操作。
- 双边动词:仍需要CPU协助,包括发送和接收。
RDMA硬件发展迅速,其提供的丰富语义为开发新协议创造了机会。本工作重点关注其原子操作动词,它们能操作大于8字节的值,并提供位掩码语义,允许修改操作数的任意部分而保持其余部分不变。可以说,RDMA原子操作比其CPU对应物更强大。
RDMA锁的优势与挑战 ⚖️
上一节我们介绍了RDMA的基础,本节中我们来看看基于RDMA的锁协议。
在RDMA锁中,客户端直接读写存储在服务器上的锁条目。它们相互协调,就像在单台机器上一样,只不过它们使用RDMA动词访问内存。这种场景也被称为“单边锁”,因为只有客户端CPU是活跃的,服务器CPU几乎不做事。
与传统锁相比,RDMA锁具有优势,包括微秒级延迟、高吞吐量和低CPU使用率。然而,它也有两面性。由于服务器不活跃,客户端在获取锁失败时得不到任何反馈,必须不断重试。RDMA网络不擅长处理大量并发重试,尤其是在存在冲突时。因此,在高争用场景下,RDMA锁的性能会显著下降。例如,200个争用客户端可能导致吞吐量下降约95%。
根本问题在于,有太多来自客户端的重试,它们不断尝试获取已被他人占用的锁。这些重试会消耗I/O资源并导致性能下降。在典型的比较并交换实现中,超过85%的重试在高争用场景下会失败。
现有解决方案:锁交接 🔄
上一节我们指出了RDMA锁在高争用下的问题,本节中我们来看看一个经典的解决思路。
好消息是,这不是一个新问题。一个解决方案是让客户端排队并交接锁,这是在著名的MCS锁和CLH锁中已有的思想。
“交接”意味着一个客户端通过点对点消息直接将锁交给另一个客户端,而无需服务器介入。通过交接,客户端可以本地等待接收锁,而不是执行重试。
坏消息是,我们发现很少有RDMA锁采用这个已有34年历史的想法。那么问题出在哪里?实际上,在RDMA网络中实现MCS锁存在一些挑战。
以下是实现MCS锁于RDMA网络的主要挑战:
- 连接可扩展性:RDMA控制路径相比数据路径非常慢,在两个客户端之间建立新连接需要毫秒级时间。这导致一个困境:如果预先连接所有客户端,则不可扩展;如果不预先连接,则速度慢。
- 高网络延迟:RDMA访问比本地内存访问慢约20倍。这使得难以添加有用功能,例如为MCS锁增加读写者语义。现有的MCS锁读写者变体需要多次触碰锁来获取或释放,这在RDMA上下文中会转化为多次网络往返,增加更高延迟。
- 容错性:MCS锁假设客户端永远不会失效,但这在分布式系统中不成立。需要回答两个问题:第一,如何检测故障;第二,如何正确恢复。
ShiftLock 核心设计 🛠️
上一节我们分析了在RDMA上实现锁交接的挑战,本节中我们正式介绍ShiftLock如何应对这些挑战。
为了解决上述问题,我们开发了ShiftLock。它采用了MCS锁的思想,同时引入了几个关键设计,包括可扩展的客户端间通信、单次往返的读写者协议以及容错机制。
首先,我们快速回顾一下MCS锁协议本身。MCS的原理是将客户端组织在队列中。锁条目是一个指向队列尾部的指针。要获取锁,客户端只需将自己追加到尾部。如果它也是队列头部,则可以成功获取锁。否则,它将有一个队列中的前驱;它会写入前驱的内存以更新其后继信息,然后等待前驱通过更新其内存中的一个标志将锁交给它。
1. 可扩展的客户端间通信
现在,我们开始介绍ShiftLock的贡献。首先,讨论如何在RDMA网络中实现MCS锁所需的客户端间通信。
在分布式场景中,客户端运行在不同节点上,必须通过RDMA进行通信。我们需要为它们选择合适的动词并解决可扩展性问题。
MCS锁希望客户端写入彼此的内存,但要在RDMA上执行写入,需要大量元数据,这些元数据必须作为远程指针的一部分嵌入到锁条目中。为了节省空间,我们指出,由于客户端间通信的最终目标是发送一条最终由远程CPU处理的消息,我们可以使用双边发送动词,而不是单边写入,来执行此类通信。只要接收方准备就绪,发送操作具有与写入相似的性能。此外,这些发送不是RPC调用,客户端不需要一直消耗CPU来监控它们。相反,它们只在需要时处理这些接收到的消息。
对于可扩展性问题,由于维护传统连接不可行,我们倾向于使用RDMA动态连接。动态连接允许客户端在数据路径上连接到不同的远程端点,速度非常快,重新连接开销小于1微秒。如前所述,客户端的路由信息作为队列尾指针的一部分嵌入在锁条目中,因此其他人可以使用该信息动态连接到该客户端。
2. 单次往返的读写者锁协议
上一节我们解决了通信问题,本节中我们来看看ShiftLock如何高效支持读写者锁。
之前我们假设只有写者存在,锁是互斥锁。当存在读者时,我们必须找到一种方法来管理它们,并在读者和写者之间转移锁所有权,以避免饥饿等问题。
由于网络延迟高,将所有客户端混合在同一个队列中绝对不是好主意。相反,我们只在队列中维护写者,并使用锁条目中的三个新字段来管理读者。
以下是用于管理读者的三个关键字段:
- 读者计数器:统计正在尝试获取或正持有锁的活跃读者数量。
- 锁释放计数器:客户端释放锁时递增此计数器。
- 纪元:标识锁所有权转移的周期。
因此,客户端可以检测是否存在读者或写者,并在没有冲突时获取锁。剩余两个字段用于解决存在冲突的情况。
为了帮助写者在读者持有锁时获取锁,我们引入了第二个字段——锁释放计数器。每当客户端释放锁时,它们就递增此计数器。现在,考虑一个位于队列头部的写者。当它将自己追加到队列尾部时,它也会自动读取整个锁条目。然后它将知道在它之前有多少读者持有锁,并可以自旋等待锁释放计数器,直到其值表明所有先前的读者都已离开。这个值就是读者计数器和释放计数器的总和。
第三个字段“纪元”,帮助读者在写者持有锁时获取锁。为了解释这个名称的含义,我们知道基于队列的读写者锁的整个生命周期可以分为许多“纪元”,每个纪元由三个步骤组成:首先,0个或多个读者获取并释放锁;其次,一个写者出现并获取锁;第三,写者在队列中交接锁。现在,很明显,将锁交给读者就相当于开始一个新的纪元。
因此,当写者持有锁时,我们让读者自旋等待纪元字段。当写者释放锁时,它也会自动递增读者计数器和释放计数器,并改变当前纪元以将锁传递给读者。现在,对于下一个写者的情况与我们之前讨论的类似。当前的写者可以进行一些计算,并发送消息给下一个写者,要求它自旋等待释放计数器,直到所有先前的读者都已离开。
在这个设计中,读者获取锁的信号是纪元的改变,而不是其绝对值。纪元不像模式,而像数字电路中的时钟。它保证了在无争用情况下的单次往返锁获取,因为客户端可以在任何纪元中获取锁。
此外,为了避免读者饥饿,写者必须主动将锁交给读者。我们设置“转移计数”为连续交接的次数。如果达到阈值(在我们的实现中是16次),锁将被交给读者。
3. 容错机制
上一节我们设计了高效的锁协议,本节中我们来看看ShiftLock如何处理客户端故障。
最后,我们讨论ShiftLock应如何容错。我们关注客户端故障,因为如果服务器故障,唯一的选择是让所有客户端切换到新服务器并从头开始重试。
尽管ShiftLock引入了客户端间的锁交接,但在故障检测方面没有额外的复杂性。如果一个客户端发生故障,最终所有客户端都会被阻塞,没有人会释放锁。释放计数器将停止变化,因此我们可以采用租约的思想,如果释放计数器在一定时间内保持不变,则检测到故障。当释放计数器几十微秒不变时,客户端可以检测到故障。然而,负责执行恢复的是服务器。
具体来说,服务器维护一个“纪元”计数器,记录发生了多少次恢复。检测到故障的客户端在当前纪元向服务器请求恢复。服务器会将释放计数器增加一个非常大的值,清除锁条目中的所有其他字段,并递增纪元。释放计数器的跳跃将使其他客户端意识到这次恢复,因此它们可以重试获取锁。这个设计有效地防止了客户端执行过时的恢复(由于ABA问题,这种情况有很小的可能发生),否则会导致系统崩溃。
性能评估与总结 📊
现在我们已经完成了ShiftLock主要设计要点的介绍。总结来说,它通过双边动词和自动动态连接实现了可扩展且高效的客户端间通信。它开发了一种低延迟读写者锁协议,保证在无争用情况下的单次往返获取和释放。同时,它提供了容错能力。
我们在约7000行Rust代码中实现了ShiftLock,并在六节点测试平台上与一些现有的RDMA锁进行了比较。总体而言,ShiftLock(图表中的深蓝色线)提供了最高的性能。它在不同规模下提高了读写者锁的吞吐量并降低了延迟。
然而,与互斥锁相比,一些写者在ShiftLock中可能等待更长时间,因为如前所述,写者需要主动将锁交给读者以避免饥饿。这可能导致尾部延迟稍高。通过检查硬件计数器,我们发现ShiftLock性能优势背后的原因是它有效地减少了重试,并对服务器端RDMA网卡造成了最低的压力。对回退策略(一种减少重试次数的传统机制)的效果分析表明,它只能部分缓解问题,而不能完全解决。
ShiftLock的优势确实带来了一些恢复开销。在相同的租约时间内,客户端必须比现有的RDMA锁等待更长时间才能确定之前的故障。这是因为ShiftLock允许客户端交接锁,所以我们必须考虑交接所花费的时间。因此,其恢复性能略低于先前的工作,但仍然相似。
本节课中,我们一起学习了ShiftLock,一个针对高争用RDMA锁的优化方案。我们从RDMA基础讲起,分析了传统锁交接思想在RDMA网络中面临的挑战,并详细探讨了ShiftLock在可扩展通信、高效读写者协议和容错机制三个方面的创新设计。这些设计使其在高争用场景下显著提升了性能。




024:选择性设备端执行数据依赖型读I/O


在本节课程中,我们将学习一篇来自FAST‘25存储大会的研究工作,主题是“选择性设备端执行数据依赖型读I/O”。这项研究探讨了如何利用存储内计算技术来加速延迟敏感型工作负载,特别是那些涉及数据依赖型读操作的应用。
现代存储设备在速度和带宽方面取得了显著进步。然而,这种演进也带来了两个新问题。
第一个问题是PCIe总线与存储设备内部带宽之间存在差距。下图显示存储内部带宽显著增长,但PCIe互连带宽的增长却相对停滞。因此,即使存储内部带宽很高,数据移动本身也成为了瓶颈。
为了解决这个问题,存储内计算技术被提出,它将数据密集型计算任务转移到存储设备内部执行,以减少通过PCIe传输的数据量。存储内计算利用存储设备内部的高带宽来处理诸如图遍历和K近邻搜索等工作负载。它通常通过集成高并行度的设备端处理器(如FPGA)来实现可编程的计算存储设备。
一个存储内计算的例子是Insider,它提出了基于FPGA的可编程行计算。另一个例子是Lambda,它也采用了类似的方法。总的来说,这些技术都将数据密集型计算任务转移到存储设备内部,类似于Insider,但通过实现更快速的数据访问(特别是在页缓存等领域)来提升效率。
现代存储设备演进带来的第二个问题是,随着存储设备速度变快,内核软件栈成为了瓶颈。当我们在NVMe存储设备上测量内核栈的延迟时,发现近48%的延迟来自于内核软件栈。
为了应对这个问题,主机端近存储计算通过将延迟关键的计算任务放置在更靠近存储设备的位置,来减少内核栈的开销。例如,XRP分析了数据依赖型读I/O的性能。数据依赖型读I/O指的是读取磁盘上大型数据结构(如数据库引擎常用的B+树索引)的操作。XRP在内核中执行eBPF函数,并提交必要的读操作以绕过内核软件栈。我们将这些需要连续发出读请求的操作称为重提交任务。
值得注意的是,我们观察到现有的存储内计算研究主要关注吞吐密集型工作负载,而非延迟关键型场景。因此,我们的研究探索了存储内计算能否加速数据依赖型读I/O这类延迟关键型操作。
然而,我们发现存储内计算的两个特性并不完全适合延迟关键型场景。
第一个特性是计算存储设备的计算能力有限。常见方法通过使用高并行度的设备端处理器来优化吞吐密集型工作负载。然而,这种处理器可能会增加设备端计算延迟,因为较低的计算能力会削弱低数据访问延迟带来的优势。
第二个特性是,商业化的计算存储设备在设计上并未充分考虑访问延迟的潜在优势。例如,在三星的智能SSD中,FPGA和SSD控制器通过PCIe连接,因此它们的访问延迟并不比主机处理器低很多,这是因为控制器与设备端处理器之间的通信开销。
这两个特性带来了两个约束,使得处理延迟关键型工作负载变得困难。首先,我们必须考虑有限的计算能力,因为将繁重任务转移到设备端可能会对整体性能产生负面影响。其次,设备端处理器必须尽可能靠近控制器放置,换句话说,控制器与设备端处理器之间不应有显著的通信开销。
考虑到这些约束,我们提出了选择性设备端执行数据依赖型读I/O,简称SOD。它展示了在数据依赖型读I/O场景中,当重提交数据转移不具优势时,如何有效地利用设备端计算。SOD引入了一种混合执行模型,选择性地平衡设备端和存储内计算,以应对有限的计算能力。此外,SOD假设设备端处理器与存储控制器位于同一个片上系统中,以消除两者之间的通信开销。
接下来,让我们看看这个混合执行模型是如何工作的。当应用程序向存储设备提交一个读请求时,SOD会准备元数据来处理设备端和存储内的重提交任务。SOD通过扩展NVMe命令来发送这些元数据,然后存储设备执行读操作。
一旦读请求完成,设备端运行时可以在存储设备内部执行一次设备端重提交。如果需要进一步的重提交,运行时将完成该请求。然而,由于计算能力有限,设备端处理器可能没有可用资源。在这种情况下,执行存储内计算可能比等待设备端任务完成在延迟方面更有效。我们将这个过程称为反向卸载。
被反向卸载的任务可以在计算能力更强的主机CPU上更快地执行。然而,这些被卸载的任务会承受大约500纳秒到1微秒的PCIe往返延迟惩罚。有趣的是,我们观察到,尽管存在反向卸载的惩罚,重提交操作在延迟关键型场景中仍然可以表现出色。
这是一个SOD在B+树索引查找过程中如何操作的例子。SOD可以像XRP一样遍历磁盘上的大型数据结构,解析节点并读取数据。这些数据通常是局部访问的。如果设备端处理器没有可用资源,反向卸载机制可以弥补设备端处理器计算能力的不足。
我们的论文展示了为SOD所做的更多设计选择。例如,SOD会缓存先前的元数据以加速设备端和存储内的重提交翻译,并支持对繁重的设备端重提交任务进行并行执行。同时,它还利用MPK来减少边界检查的开销。由于时间有限,这部分内容将跳过。
为了实现SOD原型,我们扩展了一个用于NVMe存储设备的开源模拟器。我们选择了英特尔傲腾DC SSD作为超低延迟存储设备的模型,并选择ARM Cortex-A53作为我们设备端处理器的参考,因为ARM CPU在先前的研究中已被用作存储控制器。
重要的是,我们通过降低CPU频率来模拟一个计算能力较弱的设备端处理器。根据我们的微基准测试结果,将CPU频率降至1.2 GHz是模拟弱计算能力设备端处理器的合理方式,因为此时ARM CPU的性能与降频后的英特尔CPU相似。
我们在此环境中评估SOD。具体来说,我们使用102.4 GB内存和一个单节点进行计算存储模拟。我们分配了四个核心,类似于我们的参考处理器ARM Cortex-A53。我们为每个核心分配一个重提交工作线程。我们使用一个简单的B+树键值存储(称为PPF-KV)进行评估,同时也在WiredTiger上进行真实场景的评估。
评估的第一个结论是,SOD有效地消除了内核软件栈开销和PCIe往返延迟。当我们在闭环负载生成器下,使用B+树基准测试和多个线程来评估SOD和XRP时,SOD实现了比XRP更高的吞吐量和更低的延迟。值得注意的是,设备端处理了约38%的重提交任务,而存储内处理了剩余的62%。
评估的第二个结论是,SOD的性能优于单纯的设备端执行或单纯的存储内执行。当我们在适合压力测试的开放环路生成器下评估SOD的B+树基准测试时,SOD的混合重提交模型比单纯的设备端执行或单纯的存储内执行(类似于XRP)提供了更高的性能。这凸显了由于自身特性,设备端计算在处理延迟关键型工作负载时的不足。SOD比XRP性能高出10.8%,这表明混合方法可以胜任延迟关键型场景。
我们也在WiredTiger上评估了SOD,它使用基于日志结构合并树的键值存储。我们使用YCSB工作负载进行测试。研究表明,与XRP相比,SOD将重提交延迟降低了高达9.4%。然而,当工作负载D(读密集型)时,SOD并未降低延迟,因为存储内重提交次数很少。这意味着由于重提交次数少,SOD可以在设备端实现更优化的重提交。
以下是本次演讲的总结。我们观察到存储内计算的特性并不完全适合延迟关键型场景。SOD的设计基于这些特性做出了一些假设。因此,SOD提供了一种新的混合重提交设计,选择性地利用设备端和存储内计算。在评估中,SOD实现了比完全依赖设备端或存储内计算的方法更好的性能。
本节课中,我们一起学习了SOD这项研究。它针对现代存储计算在延迟关键型场景下面临的挑战,提出了一种创新的混合执行模型。通过选择性地在设备端和存储内执行数据依赖型读操作的重提交任务,SOD有效地平衡了计算能力与数据访问延迟,从而提升了整体性能。这项研究为未来计算存储设备的设计提供了有价值的思路。




025:面向安全云盘的可扩展完整性检查优化 🛡️


在本教程中,我们将学习如何优化云存储中的数据完整性检查协议。数据完整性是云安全的关键,但传统的保护方法会带来显著的性能开销。我们将探讨这些开销的根源,并介绍一种创新的、自适应的数据结构——动态默克尔树,它能根据工作负载模式动态调整,从而大幅降低完整性检查的成本。

背景:云存储与完整性挑战 💾
上一节我们提到了完整性检查的重要性,本节中我们来看看其背后的具体机制和面临的挑战。
保护存储在云端的数据完整性是云安全的重要组成部分。然而,完整性保护会带来性能成本。下图展示了实践中的完整性成本,它比较了Vembarity(Linux内核中实现最先进完整性保护的设备映射器或目标)与两个基线:一个是没有加密、没有完整性保护的磁盘,另一个是只有加密但没有完整性保护的磁盘。

Y轴是吞吐量,X轴是容量。我们看到的总体趋势是,吞吐量随着容量的增加而下降。一个4TB的磁盘吞吐量损失约75%,即使是一个小的16MB磁盘,吞吐量损失也约为50%。结论是,提供完整性保护的成本可能变得令人望而却步。
我们的工作通过开发优化的、能适应工作负载模式的完整性数据结构来解决这个性能问题,利用工作负载模式来降低完整性成本。
核心问题与解决方案概览 🔍
我们通过回答三个研究问题来解决这个问题。
以下是三个核心研究问题:
- 完整性开销的根本原因是什么?
- 在理想情况下,我们能否为最优的完整性数据结构建模?
- 如果可以,我们能否在真实系统中实现或近似这个最优结构?
提前透露一下,答案是:根本原因是哈希计算成本;如果我们有一些先验知识,我们可以构建一个最优结构;我们可以通过学习工作负载模式来近似最优结构。
完整性保护的工作原理 🛠️
上一节我们概述了问题,本节我们来深入了解完整性检查在系统中的具体工作流程。
假设你有一个典型的基础设施即服务部署,你配置了一台虚拟机,在其中运行一个应用程序,该应用程序需要访问一些快速的本地存储。应用程序通过系统调用与文件系统通信,文件系统将这些调用转换为一连串由块层处理的块操作。
在这种情况下,如果攻击者设法控制了存储设备或广义上的存储接口,他们就可以向虚拟机注入错误数据,破坏系统的完整性。在实践中,我们通过添加一个安全层(一个磁盘接口)来防止此类攻击,该接口在沿调用栈返回任何数据或确认之前执行一些完整性检查。
安全层处理两个高级例程:更新和验证。
- 每当向磁盘写入一个块时,你将数据推送到磁盘,并更新一些相关的安全元数据。
- 每当读取数据时,你检查数据是否与相关的安全元数据一致。
非正式地说,这被称为加密证明系统。
安全元数据本身由两部分组成:消息认证码和默克尔哈希树。
- MAC 提供了真实性保证,确保从磁盘读取的任何数据都是你存放的合法数据。
- 默克尔树 提供了新鲜性保证,确保从磁盘读取的数据不仅是合法的,而且是你存放的最新版本数据。
更具体地说,当我们读写块时,会回到安全层。
- 写入块时,首先计算一个新的块MAC,然后使用该MAC更新默克尔树。
- 读取块时,我们像检查其他校验和一样检查MAC与数据的一致性,然后在默克尔树中验证MAC的新鲜性。
默克尔树与性能瓶颈 🌳
上一节我们介绍了完整性检查的基本流程,本节我们深入看看默克尔树如何工作,以及它为何成为性能瓶颈。
默克尔树的工作原理如下:数据块的MAC是树中的叶子节点。更新或验证MAC的过程涉及在每个层级递归地获取兄弟节点,将它们连接起来并进行哈希计算,直到得到单个根节点。根节点通常存储在芯片上或密封在TPM中,不受攻击者控制。
因此,对于更新,你经历这个递归过程,生成一个新根并保存它。对于验证,你做同样的事情,但将计算出的根与已知的正确根进行比较。这样得到的保证是,每当你读取一些数据时,如果攻击者试图向虚拟机注入过时或无效的数据,完整性检查就会失败。
在论文中,我们提供的分析表明,在这个递归过程中进行哈希计算所花费的时间,远远超过了在快速磁盘上进行简单数据访问的时间。我们测量到一次更新或验证大约需要200到300微秒,而进行一次数据访问可能只需要几十微秒(取决于使用的磁盘类型)。因此,我们得出结论:哈希计算是瓶颈,要降低开销,就需要降低哈希计算成本。
关键洞察:利用访问局部性 🔥
我们论文中的关键见解是,有机会通过利用引用局部性来降低哈希计算成本。众所周知,存储工作负载通常表现出高度的引用局部性,即一小部分块被频繁访问。我们的想法是,通过打破平衡树结构,允许树变得不平衡,并将与热块相关的MAC保持在更靠近根节点的位置,从而利用这一点。
结果是形成一种树结构,其中与热数据相关的MAC具有更短的更新和验证路径,这意味着更少的哈希计算次数,从而降低哈希计算成本,无论是对于频繁访问的数据还是整体而言。这个直觉很简单,这就引出了一个问题:为什么现在才做?以前我们在做什么?
之前的工作主要集中在离线完整性检查上,性能不是主要关注点,因此使用平衡树足以满足他们的需求。在我们的案例中,我们假设应用程序运行在机密虚拟机内部,需要实时完整性检查。更重要的是,我们才刚刚开始意识到哈希计算是一个瓶颈,因为新兴存储设备的数据访问延迟非常低,因此计算哈希与进行数据访问的时间比率正在大幅增长。
构建最优树模型 🏆
我们的工作基于使用不平衡树结构的新想法,分为两部分。首先,我们认识到实现最小哈希计算成本是理想的,因此我们首先开发了最优哈希树结构的定义。然后,我们设计了一种启发式方法,以解决最优定义的一些局限性。
我们在论文中提供了完整的推导和技术细节,但概括来说,我们通过将其与压缩中的一个并行问题联系起来来定义最优性。在压缩中,前缀树用于将一组符号映射到码字。编码过程通过遍历树中的一条路径到达叶子节点,并根据遍历过程中移动的方向,在码字位串后追加一个0位或1位。目标是找到一种编码,能对原始数据产生最小的表示形式。
因此,当我们用像霍夫曼编码这样的算法构建最优前缀树时,得到的保证是:热符号被分配更短的码字。在这个例子中,符号B3出现频率为30%,用2位编码;而B7出现频率为5%,用4位编码。
我们以同样的方式对哈希树中的叶子节点进行建模,因为它给了我们一个非常清晰的保证:访问频率更高的MAC及其关联块将被放置在树中更高的位置,从而带来更短的更新/验证路径和更低的哈希计算成本。
以这种方式构建最优树的一个条件是,你需要事先了解一些工作负载知识。你需要知道概率分布才能构建树。这是一个限制,因为在实践中很少能获得这种知识。然而,最优树仍然是一个有用的评估工具,因为如果你确实有这种知识,你就可以构建树,从而建立性能的上限(最大吞吐量,最小开销)。在论文中,我们称之为最优树预言机。
动态默克尔树:自适应的解决方案 ⚙️
理想情况下,在真实系统中,我们希望摆脱这个假设。因此,在我们的第二个贡献中,我们通过建立与一种广泛用于垃圾回收和IP路由查找等任务的数据结构——伸展树的联系来解决这个问题。我们设计了一种适用于哈希树用例的伸展树变体,称为动态默克尔树。
驱动该树的关键机制是,每当你访问树中的一个叶子节点时,你执行一系列旋转操作,将该节点一直移动到根节点,或者至少移动到部分路径上。随着时间的推移,你最终会得到这样一种树结构:热数据倾向于聚集在树的高层,而冷数据则沉在树的低层。
伸展树有几个理想的特性,使其成为构建DMT的良好候选数据结构。总结来说,伸展树是固有的不平衡树结构。与最优树不同,它们不需要事先了解工作负载。我们可以从任何初始状态开始,树的结构将完全由工作负载模式驱动。这种自适应性特别有用,因为它允许你捕获时间模式,并随着时间动态适应工作负载的变化。
虽然直觉上,我们应该能够以这种方式利用引用局部性,但我们必须解决几个挑战。主要挑战在于执行这些旋转操作可能成本很高。当伸展树用于搜索时,旋转可能很廉价,因为你只需要更新一些指针。但在哈希树的上下文中,每当你进行一次旋转,你就改变了树的结构,我们需要通过从旋转点向上到根节点重新计算所有哈希来提交这个改变。因此,我们需要降低这些成本。
我们通过引入两个启发式参数来实现这一点:
- 伸展概率 P:这意味着我们不会在每次访问时都伸展,而只在一定百分比(例如1%)的访问中进行伸展。
- 伸展距离 D:这意味着在我们决定伸展的那些访问中,我们不会将节点一直伸展到树顶,而只伸展到一半或四分之一的路程,等等。
这两者共同有助于降低成本。
DMT的工作分为三个步骤:
- 我们为每个节点附加整数热度计数器,以跟踪相对访问频率。
- 当块层开始接收IO请求时,在我们决定伸展的那些访问上,我们按照标准的伸展规则执行旋转操作(根据节点在访问时的排列,可能是zig、zig-zig或zig-zag情况)。
- 当我们决定伸展时,我们旋转父节点而不是实际的叶子节点,这有助于避免旋转可能带来的一些风险。
实现与评估 📊
我们实现了DMT和最优树,大约5000行C++代码,它是一个使用块层框架的块设备驱动程序。该驱动程序拦截读写IO操作,并在沿调用栈返回任何数据或确认之前,调用更新和验证函数。作为优化,我们还引入了一个小型哈希缓存,允许我们在更新和验证过程中避免一些元数据IO。该驱动程序在标准Linux系统上运行,我们使用带有本地NVMe磁盘的EC2实例进行了评估。
在论文中,我们研究了各种不同的工作负载,包括一些用FIO生成的合成工作负载、阿里巴巴跟踪数据集和Filebench工作负载。今天我将重点关注两个关键的评估问题:第一,DMT能否在容量方面更好地扩展?第二,它们能否快速适应工作负载的变化?
这张图比较了DMT与DM-Verity以及其他几个基线。

图例包括:无加密无完整性的磁盘、有加密无完整性的磁盘、4叉、8叉和64叉树。作为参考,DM-Verity是二叉树。我们观察这些其他树是为了对比,在论文中我们提供了另一项分析,表明仅仅使用更宽的树不足以降低哈希计算成本,因为在存储级哈希树的背景下存在一些独特的挑战:当你使用更宽的树时,哈希函数的实际输入更大,因此每次哈希计算的实际成本更高,所以在权衡上无法达到平衡。
最后,我们还有最优树,我们通过提前记录工作负载跟踪并从中构建树来构造它。
在图中,Y轴是吞吐量,X轴是容量。我们看到的一般趋势是,基线的性能随着容量的增加而下降。与此同时,DMT的性能保持大致恒定。事实上,我们看到DMT始终提供约85%的最优吞吐量。这正是我们寻找的可扩展性保证。
在底部的图中,我们观察尾部延迟,关键结论是DMT将尾部延迟改善了高达40%。因此,尽管通过我们的设计,伸展可能成本高昂,但我们可以随着时间的推移降低这些成本。
在下一个实验中,这张图展示了一个在工作负载在均匀分布和Zipfian分布阶段之间交替时观察到的150秒吞吐量快照。每个Zipfian阶段集中在地址空间的不同区域。Y轴是吞吐量,X轴是时间。关键结论是,在Zipfian阶段开始时,DMT能够快速适应,捕获倾斜的工作负载模式并提供加速,而在均匀分布阶段则提供相当的性能。其意义在于,DMT可以适应工作负载形状的变化以及随时间变化的工作负载关注区域。
总结 📝
本节课中我们一起学习了云存储完整性检查的优化方法。总结来说,存储完整性很重要但成本高昂,默克尔树遍历是开销的根本原因,我们可以通过使用优化和设计自适应数据结构来降低这些开销。代码已可供其他研究人员使用。


核心概念公式/代码表示:
- 默克尔树验证路径哈希计算:
根哈希 = H(...H(H(叶子MAC || 兄弟MAC) || 上层兄弟)... ) - 动态默克尔树旋转条件:
if (random() < P) { splay(node, D); } - 哈希缓存优化:
if (hash_in_cache(block_id)) { use_cached_hash(); } else { compute_and_cache_hash(); }
026:Silhouette - 利用一致性机制检测持久内存文件系统中的Bug


在本教程中,我们将学习一篇来自FAST‘25存储大会的研究——Silhouette。这项研究提出了一种新颖的方法,用于检测基于持久内存的文件系统中的微妙Bug。我们将从持久内存编程的挑战开始,逐步了解Silhouette的核心思想、工作原理及其显著效果。
概述:持久内存文件系统的Bug检测挑战
持久内存系统承诺提供高性能,但由于其乱序持久性特性,它们容易产生微妙的Bug。检测这些Bug面临两大挑战:首先,程序因乱序持久性而易出错;其次,可能的崩溃场景数量随着未持久化存储的数量呈指数级增长,导致搜索空间爆炸,使得穷尽测试变得不可行。
乱序持久性详解
上一节我们介绍了检测Bug的宏观挑战,本节中我们来看看导致这些挑战的根本原因——乱序持久性。
下图阐释了乱序持久性的概念。系统中有变量A和B,初始值均为0。
初始状态:
CPU缓存: A=0, B=0
持久内存: A=0, B=0
执行序列:
1. store A = 2
2. flush A
3. store B = 3
4. fence
首先,指令store A = 2将CPU缓存中的A更新为2。但由于CPU缓存与持久内存之间的不一致性,此时持久内存中的A可能是0或2,尚未确定。接着,flush A指令试图将A从CPU缓存刷写到持久内存,但刷新指令可能被编译器或CPU重排序,因此不保证立即生效。然后,store B = 3更新了缓存中的B,同理,持久内存中的B可能是0或3。
如果在fence指令执行前发生崩溃,持久内存中变量A和B的状态组合将产生最多 2^2 = 4 种崩溃场景。fence指令可以防止指令重排序,并保证所有已刷新的变量被持久化。然而,未被刷新的变量(称为“inflight stores”)状态仍不确定。
假设在某个栅栏点有N个inflight stores,则可能产生最多 2^N 种崩溃场景。在真实的持久内存文件系统中,一个栅栏点可能存在多达20或40个inflight stores,这将导致百万甚至万亿级别的崩溃场景,使得穷尽测试无法进行。
核心观察:一致性机制与未保护存储
面对巨大的搜索空间,Silhouette提出了一个关键见解:持久内存文件系统使用已知的一致性机制(如日志)来保证操作的原子性和持久性。真正的漏洞往往存在于那些与任何一致性机制无关的存储操作中,我们称之为“未保护存储”。
研究数据显示,在测试的系统中,超过95%的栅栏点包含少于5个未保护存储,这消除了长尾分布的影响。通过将测试焦点从未知的大量inflight stores转移到数量较少的未保护存储上,可以大幅缩减需要测试的崩溃场景数量。
Silhouette的核心方法
基于以上观察,Silhouette的核心方法分为两步:
- 检测并验证一致性机制:首先检查文件系统是否正确实现了其一致性机制(如日志)。如果正确,那么所有与该机制相关的存储操作在测试中都不会被重排序,从而可以将它们排除在重排序测试范围之外。
- 启发式测试未保护存储:对于剩余的未保护存储,采用一种启发式方法进行测试。该启发式基于一个观察:持久内存程序常使用一个“关键存储”(如标志位、原子变量)来指示一组inflight stores的一致性状态。
以下是该启发式方法的具体描述:
对于N个未保护存储,传统的组合方法需要测试 2^N 种崩溃场景。Silhouette的启发式方法假设每个未保护存储都可能是“关键存储”,并仅为每个存储测试两种场景:
- 场景一:仅持久化当前这个“关键存储”,其他存储不持久化。
- 场景二:持久化其他所有存储,但当前这个“关键存储”不持久化。
因此,对于N个未保护存储,Silhouette只需生成 2N 种崩溃场景,极大地减少了搜索空间。
Silhouette的工作流程与实现
本节我们深入了解一下Silhouette如何具体实现其核心方法。
Silhouette的工作流程包含以下几个步骤:
- 动态指令追踪:使用LLVM Pass对文件系统源代码进行插桩,以便在运行时动态追踪指令执行,支持细粒度的指令级追踪。
- 存储操作收集与标注:从动态追踪记录中提取存储操作及其数据类型信息(如所属结构体、字段名)。然后,使用一个轻量级的注解文件来识别哪些存储操作与已知的一致性机制相关联。
- 一致性机制不变式检查:对与一致性机制关联的存储操作进行不变式检查,以确保机制被正确实现。例如,对于一个日志机制,需要检查:
- 顺序不变式:日志写入阶段必须按顺序持久化。
- 位置不变式:日志必须写入有效的日志区域(如头尾指针之间)。
- 数据不变式:日志中写入的数据必须与最终提交的数据匹配。
- 分类与测试:通过不变式检查的存储被标记为“受保护存储”。其余存储则被归类为“未保护存储”,并应用上述的启发式方法生成有限的崩溃场景进行测试。
注解文件非常轻量,它不需要修改源代码,仅作为一个配置文件,通过结构体名和字段名来关联一致性机制的元数据,通常每个结构体只需不到10行配置。
评估结果
Silhouette在C++和LLVM Pass中实现,并测试了PMFS、NOVA和WinFS三个持久内存文件系统。与现有工具(Winter、CHIPM)相比,Silhouette展现出显著优势:
- 有效性:发现了15个新的Bug,例如NOVA中的指针持久化错误导致段错误,以及PMFS和WinFS中因重用inode导致的数据丢失。
- 高效性:在更短的时间内发现了更多的Bug。这归功于其一致性机制检测和启发式方法。
- 可扩展性:生成的崩溃场景数量比Winter和CHIPM少了几个数量级,极大地降低了测试开销。
Silhouette项目已在GitHub上开源。
总结
本节课中我们一起学习了Silhouette,一种用于检测持久内存文件系统中Bug的创新工具。它通过以下方式解决了搜索空间爆炸的难题:
- 利用并验证文件系统内置的一致性机制,将大量存储操作排除在重排序测试之外。
- 对剩余的未保护存储,采用一种高效的启发式方法,将测试场景从指数级(2^N)减少到线性级(2N)。


Silhouette高效、可扩展,性能超越现有方法,并以10倍的速度加速了Bug发现过程,成功检测出15个新Bug。这项研究为持久内存系统的可靠性测试提供了强有力的新思路。
027:OPIMQ - 多队列块设备的顺序保持IO栈 📚

在本教程中,我们将学习OPIMQ(Order-Preserving IO stack for Multi-Queue Block Device)的核心概念。这是一种旨在多队列存储架构中高效保持存储顺序(Storage Order)的技术。我们将从问题背景出发,逐步解析现有方案的不足,并详细介绍OPIMQ的设计原理与实现机制。
为什么存储顺序很重要?🤔
现代存储系统采用多队列架构,以在操作系统层面高效处理并行的IO请求。多个请求队列允许每个CPU核心单独提交IO,避免了设备级的争用。像NVMe和UFS中的多个命令队列有助于充分利用存储带宽。
当一个应用程序发出写请求时,该请求在到达存储设备之前,会经过主机端的请求队列。在单队列系统中,所有请求被顺序处理,自然地保持了顺序。但在多队列系统中,请求被分发到多个请求队列,每个请求队列将其请求分派到对应的命令队列。
命令队列中的分布式IO可能会被存储控制器或FTL(闪存转换层)重新排序。因此,发出的IO请求本质上可能是无序的,因为请求完成的顺序可能与它们发出的顺序不同。
应用程序需要负责强制执行写顺序,这被称为存储顺序,即数据在存储中变得持久化的序列。在数据库日志记录和文件系统日志中,强制执行存储顺序对于确保崩溃时的持久性和一致性是必需的。
现有方案的局限性 🚧
应用程序必须使用代价高昂的机制来强制执行存储顺序。为了确保写操作以正确的顺序持久化,应用程序必须等待前一个写操作完全持久化后,才能分派下一个。这种严格的强制执行限制了并行性并增加了延迟。
缓存屏障命令提供了一种更有效的方式来强制执行存储顺序。它保证写操作按顺序持久化。使用缓存屏障时,如果写操作被顺序分派,后续的写操作在前一个写操作完全持久化之前不能被持久化。这防止了无序持久化,减少了应用程序手动强制执行存储顺序的需要。
为了减少强制排序的开销,已经探索了几种方法,可以大致分为两类:
- 顺序写:使用缓存屏障顺序执行写操作。
- 乱序写:最初无序写入数据,然后在发生崩溃或系统故障时,使用全局写标识符或PMR和VME等技术来恢复正确顺序。
然而,这些方法存在显著限制。例如,barrier IO stack仅适用于单队列设备,与多队列存储架构不兼容。La barrier仅限于UFS,而OPTR仅支持单个写流。其他方案如CCMVmi和RIU需要专门的硬件资源,或依赖于低效的排序机制。
这些限制凸显了需要一种能在多队列存储系统中高效强制执行存储顺序的解决方案。
多队列系统中的核心问题 🎯
上一节我们介绍了存储顺序的重要性,本节中我们来看看多队列架构带来的具体挑战。
在单队列IO栈中,缓存屏障自然地保持了存储顺序。然而,在多队列IO栈中,具有依赖关系的写请求可能被分发到不同的队列,允许存储控制器对它们重新排序。这可能导致后面的写请求比前面的更早到达存储介质,从而破坏了预期的顺序。即使使用了缓存屏障,在多队列系统中,存储顺序仍可能被破坏。
为了理解多队列系统中的问题,我们定义两个关键概念:流和纪元。
- 流:由单个线程发出的一组IO请求。
- 纪元:流内的一组写请求,它们内部可以重新排序,但必须按创建顺序持久化。每个纪元由缓存屏障界定。
核心问题是,存储顺序在不同队列的写请求之间无法自然强制执行。当具有依赖关系的请求被分配到不同的队列时,强制执行顺序变得具有挑战性,我们称之为队列间存储顺序问题。这在两种情况下发生:当依赖请求来自同一线程或不同线程时。
以下是具体挑战:
挑战一:保证线程迁移导致的流内存储顺序
由于工作窃取,来自一个流的写请求可能被分发到多个队列,导致纪元分裂——即来自同一纪元的请求最终进入不同的队列。结果,缓存屏障可能在同一纪元的其他请求之前到达存储控制器,过早地界定了纪元并错误地放置请求,从而破坏了存储顺序。
挑战二:保证流间存储顺序
当具有排序约束的写请求来自不同线程(尤其是在不同的CPU核心上)时,每个线程在其分配的队列中注册其写请求,要求系统跨队列强制执行顺序,这可能会损害存储顺序。
OPIMQ的设计与解决方案 💡
为了解决上述问题,我们引入OPIMQ——多队列块设备的顺序保持IO栈。
OPIMQ通过主机和存储端的协作来确保存储顺序。每个IO请求被分配一对流ID和纪元ID,定义了其排序约束并将其传递给存储端。在存储端,这些ID使得即使在多个队列中也能确保正确的持久化顺序。
这是OPIMQ的设计概览:
- 主机端:顺序保持文件系统根据排序约束决定缓存屏障的放置。OPIMQ将IO请求组织成流和纪元,为其请求分配相应的ID。
- 存储端:OP-FTL确保基于纪元的持久化顺序,而双流写和兄弟感知延迟映射保证了流间存储顺序。
在OPIMQ中,来自同一线程的写请求属于单个流。流ID源自线程的进程ID。在每个流内,纪元由缓存屏障界定并分配唯一标识符。块设备层为每个IO请求分配一对流ID和纪元ID。
解决方案一:防止纪元分裂
为了解决第一个挑战(保证流内存储顺序),OPIMQ引入了纪元固定。这确保了同一纪元中的所有写操作被放置在同一个队列中。如果线程在纪元界定前迁移,它会将写请求分派到其原始队列。只有在缓存屏障界定了纪元之后,新请求才会被放置到迁移后核心的队列中。这防止了在存储端过早地进行纪元界定。
解决方案二:确保流间顺序
第二个挑战是确保跨流的存储顺序,这需要保持来自不同流的纪元之间的顺序。OPIMQ引入了双流写——一个同时属于两个流的写请求。
当强制执行流间顺序时,OPIMQ将来自前序纪元的写操作指定为双流写。例如,线程A的纪元E12必须在线程B的纪元E6之前持久化。为了强制执行这一点,OPIMQ将W1和W2标记为双流写,意味着它们属于流A的E12,同时也是流B的E5的一部分。
一个双流写携带两对流ID和纪元ID。除了其原始的流ID和纪元ID,它还分配一个次要的流ID和纪元ID。
OP-FTL通过引用请求中的流ID和纪元ID,确保写操作在每个流内按纪元顺序持久化。关键思想是,数据可以以任何顺序刷新到闪存介质以最大化并行性,而映射表的更新则严格按纪元顺序进行。
为了强制执行流间存储顺序,双流写携带的两对流ID和纪元ID代表了两个排序约束。OP-FTL解释这些约束,并且仅在两个条件都满足时才持久化该写操作。如果只满足一个条件,OP-FTL会延迟映射更新,直到两个条件都满足。这被称为兄弟感知延迟映射。
性能评估 📊
现在让我们看看OPIMQ的实际表现。我们在一个多核服务器上测试了所有工作负载,使用三星980 Pro作为存储设备。OPIMQ在Linux内核中实现以进行评估。我们选择EXT4作为代表性文件系统,并在实验中将其称为OP-EXT4。
我们将OPIMQ与三种配置进行比较:带有传统IO栈的EXT4、带有单命令队列的barrier IO stack,以及MQF in CCM Realmi。
首先,我们比较了OP-EXT4和传统EXT4的fsync延迟。在EXT4日志记录中调用fsync时,文件数据和日志块必须在日志提交块之前写入以保持一致性。为了强制执行此顺序,传统EXT4在分派提交块之前,必须等待文件数据和日志完全写入存储设备。OPIMQ消除了强制执行顺序所涉及的DMA传输延迟和v flag开销。因此,OP-EXT4中的fsync延迟减少到EXT4的三分之一。
对于宏观基准测试,我们使用Filebench的per mail、dbench和si bench OLTP insert在云环境中评估OPIMQ。我们通过增加运行容器的数量来测量IOPS以评估可扩展性。OPIMQ在per mail中性能是原生Linux IO栈的2.9倍,在dbench中是2.8倍,在si bench中是2.9倍。
我们还测量了纪元固定的开销。同时运行40个并发的Docker容器(排除工作负载)。结果表明,纪元固定对性能影响最小,展示了其在管理排序方面的效率。
最后,OP-FTL的开销如下:我们使用Comos Open SSD板并生成4KB随机写后接fsync。OP-FTL中确保存储顺序的开销可以忽略不计,因为它可以以任何顺序通过缓存刷新写操作,同时保持映射更新按顺序进行。通过这种设计,O-FTL可以在充分利用SSD内部硬件并行性的同时,确保纪元之间的存储顺序。
总结 🎓


本节课中我们一起学习了OPIMQ如何通过协调主机和存储端,在多队列块设备中强制执行存储顺序。它使用纪元固定和双流写来保持流内和流间的顺序。OPIMQ为缓存屏障命令在通用存储产品中的应用开辟了新途径。OPIMQ是开源的,欢迎大家测试和探索。
028:重新发现AWUPF - 利用SSD中的原子写入释放关键容错能力


在本教程中,我们将学习一篇来自FAST25存储大会的研究论文。该研究探讨了如何利用固态硬盘(SSD)中名为“原子写入单元”(AWUPF)的固有特性,来优化文件系统和数据库的日志机制,从而显著提升性能。我们将从SSD的基本工作原理开始,逐步深入到如何利用AWUPF来减少日志开销,并通过一个名为Poseidon的日志结构存储系统进行案例分析。
概述:SSD与原子写入
首先,我们来简要回顾一下SSD如何处理写入操作。你可能知道,SSD中的闪存不支持直接覆盖写入。相反,SSD内部有一个闪存转换层(FTL),它负责管理数据从逻辑地址到物理地址的映射。
当一个写入操作从主机端发起时,数据会被写入闪存中的一个新物理位置。如果是对现有数据的覆写操作,新数据也会被存储在其他地方。为了确保数据可访问,FTL的映射表必须更新为新的映射信息。
由于映射表对于访问新数据至关重要,其更新必须在映射单元级别保持原子性,以维护数据一致性。正是由于这种机制,SSD天生就提供了一定程度的原子性保证。为了规范这种行为,NVMe标准引入了一个名为“原子写入保证单位”(AWUP)的特性,它定义了SSD保证原子性的最小写入单元。通过分析,我们发现大多数SSD提供4KB的AWUP。
这引出了一个重要问题:既然SSD本身已经通过AWUP提供了原子性保证,我们是否还需要为日志机制进行优化?
传统日志机制的挑战
上一节我们介绍了SSD的原子性,本节我们来看看传统系统如何确保数据一致性。如你所知,文件系统和数据库依赖日志或预写日志来防止部分更新。它们不是将更改直接写入存储位置,而是先将更新记录在一个日志区域,稍后通过检查点操作应用到主存储。
然而,这种方法由于涉及双重写入过程(先写日志,再写实际数据)而引入了显著的性能开销。那么,如果我们能利用AWUP而不是传统的日志记录呢?既然AWUP已经保证了原子性,我们就有可能消除双重写入的需求,从而显著减少开销并提升性能。
现有研究与我们的方法
现在,让我们看看以往的研究是如何处理这个问题的。一些方法修改了SSD固件以启用事务性写入,而另一些则探索了在主机端重载SSD命令。尽管这些研究利用了SSD的原子性特征,但它们都需要在SSD层面进行修改。
但正如前面提到的,SSD已经通过AWUPF提供了原生的原子性支持。如果我们将事务限制在单个页面内,就可以利用SSD的原子性能力来保证原子操作。因此,我们的方法不是修改SSD本身,而是旨在通过利用SSD现有的AWUPF特性来减少日志开销。
但这又引出了另一个关键问题:哪种系统能够通过单个原子写入命令高效地更新元数据?
案例研究:日志结构存储系统
基于上述标准,我们选择了日志结构存储系统作为案例研究。在传统的日志结构系统中,数据采用追加写入策略。由于其异地更新方式,记录数据位置的元数据必须随每次写入操作而更新。
例如,假设数据A、B、C和D已经存储,它们的位置被记录在映射表中。如果B被更新为B‘,日志系统会将B’写入SSD上下一个可用位置,而不是覆盖现有的B。由于B的位置发生了变化,映射表也必须更新。
但如果在此更新过程中发生崩溃,就可能导致映射表损坏,进而引发数据不一致。如果元数据更新只完成了一部分,系统可能会引用过时或错误的位置。为了防止这种情况,元数据更新必须是原子的。
为了确保原子性,日志系统通常为元数据使用日志记录,以便在崩溃后能正确恢复。然而,日志记录带来了显著的性能开销。当禁用日志时,元数据更新在内存中处理,并仅定期刷新到存储。在这种情况下,由于追加写入方案,顺序写入和随机写入之间的性能差异很小。
但是,当启用日志时,写入性能会急剧下降,降幅高达25倍。这种影响对于小型随机写入尤为严重。原因在于,在检查点期间,随机写入可能会更新多个元数据页面,从而显著增加元数据页面的写入次数,导致更高的性能成本,使日志记录成为日志系统的主要瓶颈。
Poseidon系统与AWUPF的应用
在我们的研究中,我们选择Poseidon作为目标日志系统。这是一个由三星开发的开源存储系统。当在启动器上设置一个或多个块设备时,它们会连接到存储池中的多个SSD。在此设置中,用户数据作为日志段(Log Segment)管理,而元数据则作为RAID 10管理。
让我简要介绍一下Poseidon的元数据结构。在Poseidon中,数据以追加写入的方式顺序写入。
以下是一个理解其工作原理的例子:
- 当写入数据时,它首先存储在内存中分配的条带中。
- 然后,它被写入逻辑条带,逻辑条带代表实际使用的存储空间。
- 此时,为了将内存条带映射到逻辑条带,系统使用一个名为“条带映射表”的元数据来管理映射关系。
- 接下来,条带映射表的更新被提交到日志区域。
- 之后,内存中的映射表相应更新。
这里,映射表指的是基于主机地址对用户数据和元数据的映射。映射表中的每个条目包含一个条带ID、虚拟设备ID、逻辑条带ID和偏移量。由于每个虚拟条带直接映射到逻辑条带,知道条带ID和条带内的偏移量,就能确定数据在SSD上的确切物理位置。
数据B和C也遵循相同的过程。最后,当日志区域变满时,会触发检查点。此时,由于我们更新了三个元数据页面(称为M页面),这些M页面被检查点化并写回到元数据区域,以确保一致性和持久性。
在Poseidon中,一个M页面的大小等于AWUP(例如4KB),这允许我们利用AWUP来确保原子性,同时减少双重写入。
让我们看一个例子:
- 如果写入操作A只更新一个M页面,它可以通过原子写入直接写入存储。
- 相比之下,如果写入操作B更新两个M页面,它必须先提交到日志,稍后再通过检查点写入,以保持一致性。
- 然而,如果写入操作C也只更新一个M页面,它也可以直接写入。
因此,以前由于日志记录,所有三个M页面都需要覆写,但采用我们的方法后,只有一个M页面需要覆写。这显著减少了日志开销,并通过最小化不必要的覆写提高了写入性能。
实现挑战与解决方案
虽然AWUPF支持直接写入元数据,但对同一M页面同时应用直接写入和日志记录可能导致一致性问题。
下图展示了写入操作期间内存和存储中的映射表是如何更新的。
当写入A修改内存中的M页面0和M页面1时,为了保持一致性,它必须在任何进一步操作发生之前完全提交。然而,如果写入B在写入A提交之前修改了内存中的映射表,并直接写入了M页面1(包含A1),那么写入A的部分元数据就会过早地存储在SSD中。此时如果发生崩溃,M页面1是完整的,但M页面0是旧的,因为写入A从未完全提交。这导致不一致状态,因为写入A的数据只被部分更新。
为确保一致性,内存中的映射表应在提交完成后再更新。
- 首先,写入A成功提交。
- 只有在此之后,内存中的映射表才被更新。
- 现在,当写入B到达时,它可以安全地将M页面1写入存储,因为A1已经被提交。
如果在此场景下发生崩溃,恢复过程很简单。M页面1已经包含A1和B,确保了它们的持久性。A0可以从提交日志中恢复,保持了原子性并防止了数据损坏。
第二个问题出现在以下场景中:在写入A和写入B完成后,写入C将写入与写入A相同的位置。结果,内存中的映射表用写入C的最新信息更新,然后直接写回存储。然而,如果此时发生崩溃,一个关键问题就会出现,因为系统会从提交日志中恢复写入A的数据,有效地覆盖了写入C的最新信息,导致最新写入操作在恢复过程中丢失。
这导致了数据不一致,因为最新的写入操作被过时的提交日志数据覆盖。
为了解决这个问题,我们为每个M页面引入了一个版本号,并在每次直接写入或提交时递增它。这确保了更高的版本号总是代表最新的数据页面,防止在恢复过程中恢复过时的数据。
当需要提交的写入操作到达时,M页面版本号递增1,并且提交日志会记录更新后的版本信息。在写入A提交且映射表更新后,写入B到达,版本号再次递增,并执行直接写入。之后,当写入C修改内存中的映射表时,版本号进一步递增,接着执行另一次直接写入。
如果发生崩溃,恢复过程如下:系统比较提交日志中的版本号与每个对应M页面的当前版本号。如果提交日志版本号不大于当前版本号,则不需要恢复,因为M页面已经包含更新的版本。在这种情况下,由于A0的提交日志版本号不大于M页面0的当前版本号,A1的提交日志版本号也不大于M页面1的当前版本号,因此不需要任何恢复,确保了数据一致性并防止了数据丢失。
性能评估
我们在Poseidon中实现了我们的方法,并配置了如下实验环境。我们测试了三种配置:
- Journal:使用日志选项的原始Poseidon。
- Our Approach:根据更新的M页面数量选择性应用日志记录或直接写入。
- Direct:始终将修改的M页面直接写入,不确保数据一致性(仅作对比)。
首先,我们使用相同数量的工作线程比较了我们的方法与传统日志技术的性能。Poseidon利用存储性能开发工具包(SPDK),这是一个用户态存储I/O框架,用于高效管理对SSD的I/O操作。工作线程分为处理元数据日志和检查点的元数据工作线程,以及处理用户数据I/O的用户数据工作线程。
在随机写入工作负载下,即使增加元数据工作线程的数量,日志方法的性能也保持恒定。这是因为检查点开销成为主导瓶颈,限制了额外工作线程带来的性能提升。相比之下,我们的方法将性能提高了3.3倍。增加元数据工作线程数量会带来更好的性能扩展,这与日志方法不同。
由于只有1个M页面被4KB写入更新,直接写入方法的结果与我们的方法相同。然而,在顺序写入中,当工作线程数量超过一定阈值时,直接写入和我们的方法都经历了性能下降。这种下降是由对M页面访问的争用引起的,多个工作线程竞争相同的元数据页面,导致延迟增加和效率降低。
基于1MB请求大小的测试显示,由于标准基准测试(如fio)的顺序写入,其在随机工作负载下的性能与顺序写入性能相似。然而,我们的方法仍然平均快了20%。此外,所有三种方法从6个元数据工作线程开始性能达到饱和,此时增加元数据工作线程数量不再提高性能,因为系统受到元数据访问争用的瓶颈限制。
为了评估写入大小的影响,我们使用fio测试了不同大小下的性能。顺序写入性能在三种配置下相似。在随机写入工作负载中,我们的方法实现了高达3.6倍的性能提升。随着写入变得更顺序,检查点开销减少,使得我们的方法在2MB写入大小时性能接近直接写入方法。
我们评估了各种基准测试,我们的方法在以下场景中具有优势:
- 由小型随机写入主导的工作负载(如
fileserver)显示高达14% 的改进。 - 涉及小型异步随机写入的工作负载(如
varmail)显示高达73% 的改进。 - 在随机数据库工作负载(如
TPC-DS)中观察到显著的性能改进。
总结
本节课我们一起学习了如何重新发现并利用SSD中的原子写入保证单位(AWUPF)。AWUPF传统上被视为一个有趣的SSD特性,但本研究展示了其在辅助主机端数据一致性管理方面的潜力。
通过利用AWUPF进行日志系统元数据更新,我们通过减少日志开销和最小化冗余写入,实现了显著的性能提升。我们相信,如果其他系统架构重新设计以与AWUP大小对齐,这种方法可以进一步扩展到其他系统。

感谢阅读。



029:AtomicDisk - 防御驱逐攻击的安全虚拟磁盘 🔒



在本教程中,我们将学习一项名为 AtomicDisk 的研究工作。这是一个为可信执行环境设计的、能够防御“驱逐攻击”的安全虚拟磁盘。我们将从背景知识开始,逐步理解攻击原理、AtomicDisk的核心设计,并最终了解其性能表现。
背景与威胁模型
如今,可信执行环境 在云环境中被广泛使用。它们可以保护其内部的敏感代码和数据,免受外部特权攻击者(如主机操作系统、虚拟机监控程序和其他应用程序)的侵害。
首先,我们信任TEE硬件和软件(如Intel SGX)能够保护内存和CPU的机密性与完整性。因此,我们信任所有在TEE边界内执行的软件。
然而,我们也必须考虑云环境中的强大攻击者。他们是特权的、在线的且主动的。这意味着他们拥有多项关键能力:完全控制TEE之外的所有主机资源;可以在TEE执行的任何时刻发起攻击;可以操纵TEE与主机块设备之间的任何I/O请求和响应。
这引出了一个关键点:TEE缺乏I/O保护。这就是为什么需要一个安全虚拟磁盘。它通过提供特定的安全属性来填补关键的I/O保护空白,保护TEE的磁盘I/O,使文件系统无需担忧TEE的安全风险,并且由于只提供简单的读写同步接口,也简化了安全分析和推理。
安全属性与现有方案
为了对抗如此强大的对手,安全虚拟磁盘应提供机密性、完整性、新鲜性和一致性这四项基本安全保证。
- 机密性:保护写入的数据不被未授权访问。
- 完整性:保证数据不被篡改或损坏。
- 新鲜性:防止重放攻击,确保总是提供最新的数据。
- 一致性:即使在系统崩溃后也能维持数据一致性。
在Linux上,最先进的解决方案是Device Mapper,它结合了DM-crypt和DM-integrity。然而,这种设置只解决了机密性和完整性。
在Intel SGX上,SGX Protected File System 提供了全部四项属性(简称CIFC)。SGX PFS围绕三个核心组件构建:修改的默克尔树、FS缓存和日志,以实现机密性和完整性。Intel通过采用认证加密增强了经典的MHT。具体来说,每个内部MHT节点存储其子节点的加密密钥和消息认证码。新鲜性通过版本号验证来保证。位于安全内存中的缓存存储元数据节点和MHT节点,以提升I/O性能。最后,日志在驱逐前维护MHT节点的先前版本,从而确保一致性。这三个组件协同工作,使SGX PFS实现了所有四项CIFC安全属性。
驱逐攻击详解
在深入探讨驱逐攻击之前,我们先明确快照的概念。我们将快照定义为特定时间点的持久化系统状态,例如Docker容器快照。同样,飞地也有快照,即保护文件状态的受保护文件。非预期快照是指应用程序逻辑未预期的快照。
以SGX PFS为例。想象一个在飞地内的应用程序向SGX PFS保护的文件写入一系列数据块。从应用程序的视角看,这些块应该作为一个整体被写入并持久化。然而,问题在于当SGX PFS内的缓存达到其容量时,会启动一个驱逐过程。驱逐导致了部分持久化状态的创建,例如块B1到B4。这就是我们定义的非预期快照。随后,当执行单个sync命令时,应用程序预期的完整且一致的快照(块B1到B8)才被创建。
在正常环境中,这些驱逐生成的快照是无害的。但在TEE中,这些快照是脆弱的。以下是它们在TEE中易受攻击的三个主要原因:
- TEE可以生成它们:因为后扩展器对顺序和原子性保证施加的约束很少,允许TEE的缓存层在任何时间执行驱逐。
- 攻击者可以捕获它们:因为攻击者控制飞地外的一切,包括监控I/O。
- 攻击者可以重放它们:由SGX PFS生成的快照共享相同的根密钥,因此飞地无法区分它们。
为了说明这些漏洞的实际影响,我们来看一个使用Redis的具体攻击示例。部署Redis服务通常需要两个基本步骤:首先,创建并初始化Redis配置文件(如果不存在);其次,启动Redis服务器以处理用户请求。
驱逐攻击在第一步期间分四个关键阶段展开:
- SGX PFS由于MHT缓存驱逐,生成了一个非预期快照。关键的是,此快照可能缺少必要的密码要求。
- 控制飞地外部环境的攻击者捕获了这些脆弱的快照。
- 攻击者可以使用这些快照重启或初始化一个新的飞地。
- 因此,攻击者绕过了任何身份验证机制,获得了对Redis的完全未授权访问。
实际上,可以利用深度快照来攻击更多应用程序。像MongoDB和Cassandra这样的流行数据库使用类似基于配置的身份验证。更有趣的是,非预期快照可能涉及多个文件,我们将此留作未来工作。
AtomicDisk 的设计与实现
为了防止驱逐攻击,我们提出了一个新的安全属性,称为同步原子性。该属性保证在sync之前所有被驱逐的写入都以“全有或全无”的方式提交。如下图所示,AtomicDisk是唯一实现所有安全属性的安全解决方案。
为了达成同步原子性,我们通过以下关键修改扩展了SGX PFS,开发了AtomicDisk:
- 我们为数据块引入了双状态系统:
committed和uncommitted。这允许我们跟踪磁盘镜像的提交状态。 - 我们引入了一个称为
commit的操作,用于将块从uncommitted状态转换到committed状态。 - 我们修改了重启时的恢复过程。AtomicDisk现在可以识别并拒绝任何未提交的写入。
commit操作将磁盘镜像的数据块转换为committed状态。为了支持这一点,我们使用一个committed标志扩展了PFS元数据节点。默认情况下,该标志设置为false。commit操作由用户发起的sync触发,并按以下步骤进行:
- 它执行标准的PFS刷新过程,包括将脏节点写入日志并将其刷新到持久存储。
- 它将
committed标志更新为true,并将修改后的元数据节点刷新到磁盘。这标志着整个磁盘镜像现在已被提交。 - 由于日志不再需要用于恢复,可以安全地删除它。
相反,内部刷新操作可能由写命令触发,会将committed标志设置回false。这表明磁盘镜像代表了一个由于缓存驱逐而产生的非预期快照。
如果标志为false,表示处于未提交状态,磁盘镜像不能直接打开。相反,我们依赖日志将磁盘镜像恢复到最新的同步状态。在日志中,每个块被分配两种状态之一:committed或uncommitted。块在日志中的首次出现被标记为committed,因为日志存储的是块的先前版本,这表示要么是对空块的新写入,要么是对已提交块的首次覆盖写入。日志中同一块的后续覆盖写入则被标记为uncommitted。
在恢复过程中,AtomicDisk从头到尾读取日志。对于每个首次遇到的逻辑块(如前所述,标记为committed),AtomicDisk将这些块恢复到磁盘镜像。使用内存位图来记录已恢复的块,并识别后续的uncommitted块。在日志重放完成后,AtomicDisk将committed块标志更新为true,此时,磁盘镜像内的所有数据块都已成功提交。
性能评估与总结
我们比较了AtomicDisk与两个基线方案:PFS-disk和Crypto-disk。PFS-disk是一个将I/O重定向到SGX保护文件的安全块设备。Crypto-disk是Linux DM-crypt在SGX中的镜像版本。
首先展示安全结果。我们使用跟踪驱动的基准测试,其中每个跟踪以一个单独的sync请求结束。如图所示,AtomicDisk只生成一个由sync引起的预期快照,而PFS-disk每sync生成数十万个快照,因此容易受到驱逐攻击。
接下来是性能评估。FIO配置如下所列。在FIO基准测试中,AtomicDisk和PFS-disk具有相似的写入性能,因为AtomicDisk将已提交和未提交的块都保存到日志中,因此具有与PFS-disk相同的写入放大。AtomicDisk和PFS-disk也具有相似的读取性能,因为它们处理读取请求的方式完全相同。Crypto-disk表现更优,因为它只执行数据加密,没有MHT结构,避免了写入放大,但缺乏新鲜性和一致性。
在Stream基准测试中,结果与FIO相似,但由于这些工作负载由小型I/O操作主导,性能差距缩小。在YCSB基准测试中,我们选择了两个应用:Redis和BadgerDB(一个基于LSM的键值存储)。对于Redis,由于Redis的I/O模式,所有三种磁盘都实现了相似的性能。对于BadgerDB,AtomicDisk和PFS仍然实现了相似的性能。AtomicDisk和Crypto-disk之间的差距由于均匀的写入分布而缩小,这导致频繁的小型随机写入,成为Crypto-disk的性能瓶颈。
总结:
- 我们识别了驱逐攻击,这是一种针对Intel SGX的新型攻击。
- 我们引入了同步原子性,作为防御驱逐攻击的基本安全属性。
- 我们开发了AtomicDisk,一个在保持与SGX PFS相当性能的同时,实现了同步原子性的安全虚拟磁盘。


AtomicDisk已在GitHub上开源,您可以通过此链接查看更多细节。
030:MedFS - 通过元数据启用的增量压缩追求低更新开销

概述
在本节课中,我们将学习一篇关于移动文件系统增量压缩的研究工作。这项名为MedFS的技术旨在通过利用文件系统元数据区的空闲空间来存储微小的数据更新(增量),从而显著减少对闪存存储的写入压力,延长设备寿命,并提升I/O性能。我们将从问题背景、现有方法、MedFS的设计原理、实现细节以及实验结果等方面进行系统性的讲解。
移动存储面临的挑战 🚨
随着移动应用变得越来越密集,它们向移动存储生成了大量的写入I/O请求。移动存储基于闪存,而闪存具有有限的寿命,只能承受有限次数的编程/擦除周期。这引发了对其使用寿命的严重担忧。
问题正在恶化。制造商正试图迁移到高密度闪存,例如QLC(四层单元)。QLC的P/E周期限制仅为1000次。结合应用密集写入和高密度闪存寿命短这两个因素,闪存寿命成为一个严峻的问题。
因此,本工作的动机是实现透明的写入压力减少。如果能做到这一点,将是一个双赢的局面:移动应用可以继续保持高密集度,而制造商则可以更有信心地转向更高密度、更廉价、容量更大的闪存,如QLC。
F2FS文件系统与现有方法回顾 📁
基于上述动机,让我们审视移动文件系统F2FS。它基本上是一个日志结构文件系统。我们特别关注其索引节点结构。如图所示,索引节点内有一个内联区域,这个区域占据了索引节点空间的很大一部分。但根据我们的分析,这个区域经常未被充分利用,存在大量空闲空间。
我们的方法是进行增量压缩。基本思路是利用这个未使用的区域来存储微小的增量,以减少写入压力。
在介绍我们的方法之前,先回顾一下F2FS现有的写入压力减少方法。
- 内联文件:如果文件足够小,可以直接将数据存储到索引节点的内联区域。这样可以将索引节点和数据块合并到单个索引节点写入,减少写入次数。但问题是,只有少量文件符合此优化条件。
- 常规数据压缩:如图所示,将多个数据块压缩后打包到单个块中写入,可以减少写入I/O次数。但问题在于,常规数据压缩的压缩率取决于数据模式,通常压缩效果不佳,这限制了压缩的实用性。此外,压缩也增加了系统设计复杂性,并增加了读/写操作的I/O开销。
移动I/O负载分析与关键观察 🔍
上一节我们回顾了现有方法,本节我们来看看对移动I/O负载的分析结果,这为我们提供了关键洞察。
我们分析了移动I/O工作负载,得到以下几点观察:
- 内联区域空闲:约90%的文件没有使用其索引节点内联区域中的所有指针。这意味着这些文件的内联区域有一些空闲空间。
- 写入以更新为主:约80%的写入操作是更新操作。
- 更新变化微小:一个非常重要的观察是,这些更新只对现有内容进行很小的更改。增量非常微小。
- 特定文件类型是主要来源:进一步分析I/O写入的文件类型,我们发现SQLite和临时文件贡献了大部分I/O写入,并且它们是微小更新的主要来源。
- 内联区域利用率极低:更令人惊讶的是,超过80%到90%的内联区域是未使用的。这意味着文件通常只有一两个指向数据块的指针。
- 索引节点缓存良好:我们还发现索引节点在页面缓存中缓存效果很好。这意味着我们可以在索引节点实际刷新到存储之前,将多个小增量追加到其内联区域中。
基于这些观察,我们认为增量压缩可能是一个很好的方向。
MedFS系统架构 🏗️
基于上述分析,我们提出了MedFS。下图展示了我们提案的架构。它基于F2FS,但包含了两个主要组件:DCI 和 DCM。
- DCI:增量内联。当产生一个块写入时,DCI首先检查它是否为更新操作。如果是更新,DCI再检查增量的大小。如果增量很小,DCI就将增量插入到索引节点的内联区域中。
- DCM:增量压缩管理器。因为索引节点的大小仍然有限,内联区域可能会溢出。在这种情况下,选定的增量将从内联区域中被逐出,而被逐出的增量将由DCM处理。DCM将使用常规的数据块来存储这些被逐出的增量。
这就是我们方法的整体架构。
DCI:增量内联的工作原理 ⚙️
上一节我们介绍了整体架构,本节我们来详细看看DCI是如何工作的。
在更新时,DCI首先将原始基础数据与传入的新数据进行比较,以产生增量。比较基于异或操作。
如示例所示,因为变化很小,异或结果包含大量0。我们可以使用轻量级编码(如游程编码或LZO)将增量转换为一个非常小的单元,然后将其嵌入到索引节点的内联区域中。这就是DCI的基本工作原理。
需要强调的是,因为这是增量压缩,对于常规的增量压缩,可能会产生“增量的增量”,层层叠加,这会增加读取开销。但在这项工作中,我们将一个基础块所能拥有的最大增量数量限制为1。这意味着每次更新时,我们都会创建一个新的增量来替换旧的增量。
处理内联区域溢出 🔄
如前所述,内联区域的大小是有限的。那么当内联区域溢出时该怎么办?基本上,我们会执行增量替换。
这里需要考虑几种情况:
- 如果文件正在增长(即添加新指针),但内联区域已满,我们以先进先出的方式替换现有的增量。
- 其他情况是在执行更新时。实际上这里有一些细节,您可以查阅我们的论文。但核心概念是:如果我们替换一个现有的增量,并且这种替换能带来更好的长期I/O效率,那么我们就会这样做。这是基本的原则。
在替换时,被选中的增量将从内联区域中逐出,而被逐出的增量将由DCM处理。
DCM:增量压缩管理器 ❄️
现在,让我简要解释一下DCM是如何工作的。
DCM接收被逐出的增量。基本上,DCM像存储数据块一样,在常规块中存储增量。它使用元页面、映射块和压缩块来存储被逐出的增量。
但有一点需要强调:DCM只处理冷的、写入密集的文件。为什么?因为增量压缩在读取数据时,必须读取基础块和增量块,这意味着读取开销增加了。所以DCM只接受来自那些确实是冷的且写入密集的文件的被逐出增量。这是基于一个聚类算法来识别冷且写入密集的文件。
再次说明,您可以查阅论文以获取更多细节。
一致性与恢复机制 🛡️
我们的方法引入了基础块和增量块,因此必须强制执行新的写入顺序。这里有两条规则:
- 基础块必须在增量块之前写入(“写入”意味着持久化到存储)。然后增量块必须在索引节点之前写入。这是为了避免出现悬空增量。即,在恢复时,如果我们发现一个增量,却找不到对应的基础块,就会出错。通过强制执行此写入顺序,我们可以避免这种错误。
- 还有另一条排序规则。
总之,基于这些规则以及F2FS原有的恢复逻辑,我们可以保证数据的一致性。
实验结果与性能评估 📊
我们在智能手机上进行了实验,将原始F2FS、支持常规数据压缩的F2FS以及我们的MedFS进行了比较。
以下是主要结果:
- 写入流量减少:平均而言,我们将写入流量减少了超过一半(>50%)。这非常可观。这可以转化为寿命延长超过100%。
- 性能优异的原因:增量压缩效果如此之好,是因为我们专注于变化,而移动I/O的变化是微小的。相比之下,常规压缩的压缩率取决于数据模式,通常效果不佳。
- 用户感知延迟:在写入密集型场景下,它略微改善了执行时间。
- I/O延迟:我们将写入I/O延迟降低了约30%。有趣的是,我们也改善了读取延迟。这是因为我们显著减少了写入I/O次数,从而进一步缓解了调度I/O队列的争用,这就是读取I/O延迟也得到改善的原因。
总结
本节课我们一起学习了MedFS,一个针对移动文件系统的增量压缩方案。
- 问题:移动应用产生大量微小更改,高密度闪存寿命有限,构成矛盾。
- 洞察:移动文件系统(如F2FS)的索引节点内联区域存在大量未利用空间,且更新产生的增量通常很小。
- 方案:提出MedFS,包含两个核心组件:
- DCI:将微小增量直接嵌入索引节点的内联区域。
- DCM:当内联区域溢出时,将增量存储到常规数据块中(仅针对冷且写入密集的文件)。
- 优势:与常规压缩相比,增量压缩专注于变化本身,对移动I/O中常见的微小更新压缩效率极高。
- 效果:实验表明,MedFS能显著减少写入流量(>50%),延长闪存寿命(>100%),并降低I/O延迟。


总之,通过巧妙地利用文件系统元数据中的空闲空间来压缩存储微小的数据更新,MedFS为移动设备存储的寿命和性能优化提供了一种有效的透明解决方案。
031:无需维护两次——去重中的元数据合并管理


在本教程中,我们将学习一篇来自FAST‘25存储大会的研究。该研究揭示了当前去重文件系统性能开销高的原因,并提出了一种创新的元数据合并管理方案,旨在实现近乎零开销的高性能去重。
概述:去重文件系统的性能瓶颈
去重技术已被广泛应用于归档存储、存储控制器和云存储等场景,以降低存储成本。随着持久内存等加速存储设备的出现以及去重算法的改进,去重过程变得更快,这使得去重文件系统日益流行。
然而,一个关键问题随之产生:这些去重文件系统的性能表现如何?为了研究这个问题,研究团队使用FIO工具,在5%去重率的场景下,对多个去重文件系统进行了压力测试,块大小从4KB到2MB不等。
实验结果显示,当前先进的去重文件系统可能遭受超过13%的性能损失。这表明去重文件系统仍然存在较高的开销。
深入分析:开销从何而来?
为了探究性能损失背后的原因,研究团队进一步分析了在2MB块大小下的去重I/O路径。
他们发现,去重元数据的维护在I/O路径中导致了19%到33%的开销。这个开销甚至可能高于重复数据识别过程本身。这个结果表明,看似无害的去重元数据维护,可能带来相当大的性能损失。
那么,为什么元数据维护会产生如此高的开销呢?要回答这个问题,我们首先需要理解去重文件系统的元数据是什么。
核心概念:去重文件系统的关键数据结构
去重文件系统利用两个关键数据结构来移除重复数据:
- 逻辑到物理映射表:这个表维护了从逻辑块到物理块的映射。因此,多个逻辑块可以共享同一个物理块以实现去重。此表由文件系统维护。
- 指纹到物理映射表:这个表维护了从数据块的指纹到其对应物理块的映射,同时包含一个引用计数,用于指示有多少个逻辑块共享这个物理块。此表由去重模块维护。
接下来,让我们仔细看看去重文件系统如何利用这些数据结构工作。
写入唯一数据块
假设我们在逻辑块1的位置写入一个唯一的数据块。
- 去重文件系统首先通过计算输入数据的指纹,并在F2P表中搜索该指纹来识别重复。
- 此时没有找到匹配的指纹,表明输入数据块是唯一的。
- 因此,去重文件系统直接分配并写入这个数据块。
- 之后,去重文件系统需要持久化一个新的F2P条目。该条目记录了其指纹到实际块位置的映射,并将引用计数设为1,表明此块是唯一的。
- 最后,去重文件系统持久化一个新的L2P条目,将逻辑块1映射到已写入的物理块1。
此后,对该写入块的访问都可以根据这个L2P条目中的映射进行。
写入重复数据块
现在,假设我们在逻辑块2的位置写入一个重复的数据块。
- 同样地,计算指纹并搜索F2P表。
- 此时恰好找到一个匹配的记录,表明输入数据块是重复的。
- 在这种情况下,去重文件系统增加这个已找到的F2P条目的引用计数。
- 最后,去重文件系统持久化一个新的L2P条目,将逻辑块2映射到同一个物理块。
此后,对逻辑块1或逻辑块2的访问都会被重定向到同一个物理块,从而实现了去重。
问题总结:去重文件系统效率低下的根源
通过前面的分析我们可以总结,无论写入的是唯一数据块还是重复数据块,我们都必须维护F2P表。维护这个表引入了额外的I/O操作和顺序保证点。
- 额外的I/O导致了写入放大。
- 顺序保证点降低了I/O并发性。
这两点共同严重损害了性能。
核心洞察:合并元数据管理
然而,研究团队发现,独立维护F2P表甚至是不必要的。
他们的核心洞察是:既然文件系统的L2P条目和去重的F2P条目都映射到同一个物理块号,那么它们自然可以合并到一个I/O操作中。
请看右侧的示意图。我们简单地将指纹信息整合到现有的文件系统L2P条目中。我们将这种合并后的元数据称为逻辑-指纹-物理映射。
这种合并的元数据带来了三个好处:
- 去重元数据可以嵌入到文件系统元数据中,因此不需要额外的元数据I/O。
- 合并后元数据的崩溃一致性由文件系统天然保证,因此不需要额外的顺序保证点。
- 合并后的元数据可以复用文件系统现有的I/O路径,因此降低了开发复杂度。
基于LFP映射设计,研究团队提出了 GotaFS 文件系统,旨在实现高性能去重。
GotaFS的设计目标
GotaFS有四个主要设计目标:
- 通用性:兼容传统文件系统的I/O路径。
- 高效性:提供细粒度的块级重复识别和快速的重复查找。
- 内存高效:能适应不同内存需求的多种场景。
- 可配置性:去重功能可以启用或禁用,而不影响原始文件系统的功能或效率。
在本教程中,我们重点探讨GotaFS如何实现通用性和高效性。
实现通用性:溢出指纹表
实现通用性面临的挑战源于LFP映射带来的数据管理不兼容性。具体来说,文件系统的L2P条目可以将一段连续的逻辑块范围映射到物理块。而F2P条目为了实现细粒度的块级去重,只将单个指纹映射到单个物理块号。这导致了L2P条目的大小可变,从而影响了文件系统布局,使文件系统分配路径复杂化,既不通用也不实用。
为了解决这个挑战,GotaFS采用了一个简单的解决方案:将这类连续块的指纹存储到一个额外的空间中,这个空间称为溢出指纹表。对于这些块,在其对应的LFP条目中存储一个空指纹。该表驻留在存储设备上,近乎直接地将物理块号映射到指纹。因此,如果发现一个LFP条目的指纹为空,GotaFS可以根据其物理块号快速访问这个表来定位其真实的指纹。
实现高效性:全局LFP表
实现高效性面临的挑战同样源于LFP映射带来的方法管理不兼容性。具体来说,文件系统按逻辑块号组织其L2P条目以实现快速文件索引。然而,F2P条目应该按指纹组织以实现快速查找,否则去重模块必须执行昂贵的线性扫描来查找一个指纹。
为了解决这个挑战,GotaFS引入了一个内存中的表来按指纹组织LFP条目,称之为全局LFP表。该表首先将LFP条目转换为F2P条目,然后将它们插入一个动态哈希表中。GotaFS使用RCU锁和每桶锁来确保安全的并发访问。借助这个表,GotaFS可以快速识别指纹是否存在,从而实现快速的重复数据识别。
综上所述,通过溢出指纹表和全局LFP表,GotaFS可以在LFP映射设计的基础上实现通用且高效的去重。
性能评估
研究团队在英特尔持久内存上评估了GotaFS,并将其与Mdedup、LightDedup和NOVA(基础文件系统)进行了比较。
基础性能评估
首先使用4KB I/O大小进行性能评估。在0%、15%和75%去重率下,GotaFS比LightDedup快9%到32%,并且大幅优于MDedup。值得注意的是,在0%去重率下,GotaFS甚至与基础文件系统NOVA性能相近,这得益于其降低的元数据开销。
接着评估在连续2MB I/O下的性能。在此设置下,GotaFS同样表现出色,比LightDedup快8%到35%,并大幅优于MDedup。一个有趣的发现是,在0%去重率下,GotaFS甚至能略微超过NOVA的性能。研究发现,这主要是因为指纹计算可以与内部持久内存I/O重叠,并且这种轻微的计算延迟通过降低I/O发出频率,减少了持久内存缓冲区的争用。
I/O路径分解
进一步分解2MB块I/O的路径。分解结果显示,性能提升确实来自于减少的元数据开销。具体来说,得益于LFP映射,GotaFS能将去重元数据维护的开销降低75%到92%。当然,GotaFS仍然有一些元数据开销,这主要是由维护其提出的表(如溢出指纹表)引起的。但幸运的是,这部分开销很小,可以忽略不计。
可扩展性评估
研究团队还将GotaFS移植到F2FS文件系统上,并在NVMe SSD平台上进行了FIO实验。比较GotaFS与F2FS以及其他先进的SSD去重文件系统的结果显示,GotaFS同样能显著优于其他去重文件系统,并与F2FS性能相近。这主要得益于降低的去重元数据开销,表明GotaFS在不同的文件系统和存储设备上具有良好的可扩展性。
总结
本节课我们一起学习了去重文件系统优化的重要研究。该研究揭示了去重元数据I/O开销可能很高,原因在于其额外的I/O操作和顺序保证点。
研究提出的LFP映射创新性地将去重元数据与文件系统元数据合并,从而消除了这些实际开销。LFP条目复用了文件系统成熟的I/O路径和崩溃一致性机制,无需引入额外的I/O和顺序保证点。
最后,基于LFP的去重文件系统GotaFS被证明能够超越当前先进的去重文件系统,实现近乎零的去重元数据开销,从而获得最佳性能。




032:Archer - 面向移动设备高速响应的、具备页面关联规则感知的自适应内存压缩


在本教程中,我们将学习一篇来自FAST‘25存储大会的研究论文。该论文提出了一种名为“Archer”的新技术,旨在通过感知页面间的关联规则,实现自适应内存压缩,从而显著提升移动设备的响应速度。我们将从背景动机、核心问题、设计原理到评估结果,系统地了解这项工作的全貌。
背景与动机 🧐
如今,人们花费在移动设备上的时间越来越多。这些设备包括智能手机、自动驾驶汽车、无人机、机器人等多种形态。在这些设备上,运行着大量新兴应用,例如基于Transformer的AI模型、AR/VR服务、车载摄像头服务、手机游戏和短视频等。
这些应用通常是内存密集型的,但不幸的是,移动设备上的内存资源却非常稀缺。为了解决这个问题,内存压缩技术被广泛应用于移动设备中。
然而,我们发现现有的页面压缩效率低下。这个问题对用户体验产生了负面影响。具体来说,我们测量了Google应用商店中热门应用的启动速度、连续拍照的响应时间以及短视频的帧率。
即使内存空间成功被节省,从图中我们可以看到,启用内存压缩后,应用启动延迟增加了约68.7%。同时,从子图B可以看到,在内存压力下,连续拍照的响应时间延长了1.6倍。此外,从子图C可以看到,短视频播放和滑动的平均帧率从约50 FPS下降到了约27 FPS。
如右图所示,内存压缩的软件栈位于Linux内核中,它会阻塞主线程的执行。这也是导致用户体验下降的根本原因。
问题根源分析 🔍
我们进一步的分析表明,导致这种性能下降的关键原因之一是一个名为“直接回收”的内存回收机制。直接回收是Linux内核中的一项基本内存回收功能,它会损害性能,因为任何页面分配都必须等待压缩过程完成。结果,前台应用的主任务会被挂起,直到其内存需求得到满足。
即使内核线程kswapd可以压缩单个页面,但系统在释放足够空间之前仍可能陷入循环。我们需要知道,单独压缩页面是合理的,但这样做会消除读取放大。例如,如果我们一起压缩10个页面,但未来只访问其中的一个,操作系统也需要解压全部10个页面。这显然是不合理的。
核心研究问题与洞察 💡
因此,本文提出了一个根本性问题:我们能否进行大粒度压缩(例如批量压缩10个页面),同时不增加读取放大? 答案是肯定的。
这基于一个有趣的观察:26.3%的匿名页面与其他页面存在强关联性。页面间的这种内部关联性在早期的研究中未被察觉。就像“啤酒和尿布”的关系一样,许多页面总是被一起访问。这种关联是隐式的,但可以被挖掘出来。如果系统能感知到这些高度关联的页面并对它们进行大粒度压缩,性能就有可能得到显著提升,从而解决上述的用户体验问题。
Archer系统设计 🏗️
在本文中,我们提出了Archer系统。通过对匿名页面进行关联规则挖掘,内存压缩效率可以得到显著提高。然而,在系统层挖掘页面关联规则并不容易,存在许多基本挑战。为了应对这些挑战,Archer设计了三个核心组件:足迹流生成器、频繁模式列表和自适应压缩区域。
组件一:足迹流生成器
我们知道,传统的关联规则挖掘算法(如Apriori和FP-Growth)适用于挖掘静态事务数据集。然而,在移动系统中,页面访问在运行时持续发生。Archer需要能够在页面被访问时更新页面关联信息。
为此,Archer在运行时收集页面访问模式,并生成一个“足迹流”。具体来说,足迹流生成器维护一个滑动窗口来动态生成足迹流并挖掘关联规则。
组件二:频繁模式列表
我们知道,Linux内核中传统的LRU列表不适合支持页面关联规则挖掘。因此,在这项工作中,我们将LRU与FP-Tree协同设计,提出了一种名为FP-List的新结构。通过协同设计,FP-List可以在线高效地管理和挖掘内存匿名页面。
组件三:自适应压缩区域
我们也应该知道,页面级压缩不应被完全抛弃。这是因为也有很多页面没有关联性,因此不应该进行批量压缩。即使是有关联的页面,其关联规模也不是一个固定值。例如,页面A可能与3个页面相关,而页面B可能与10个页面相关。
因此,Archer在实践中需要自适应地压缩页面。为了支持这一特性,本文进一步提出了自适应压缩区域。ACR基于Linux内核中原有的ZRAM区域实现。在ACR中,被一起压缩的页面用一个虚拟句柄进行索引。
系统评估与结果 📊
我们在真实的移动设备上进行了评估。具体来说,我们在三个平台上进行了评估:Google Pixel 6、Google Pixel 3和华为P20。在这些平台上,评估了三种典型场景:应用启动速度、拍照性能和帧率。
从图中可以看出,冷启动和热启动速度都得到了提升。这里,热启动意味着启动一个已在后台驻留的应用,而冷启动意味着启动一个未在后台缓存的应用。这两种应用启动方式的启动过程是不同的。
我们可以看到,与Android中原有的压缩机制(即ZRAM)相比,在Pixel 3上,冷启动的平均速度提升了约37.2%,在P20上提升了30.6%,在Pixel 6上提升了32.9%。此外,三个平台上的平均热启动速度分别提升了55.3%、47%和29%。平均拍照速度和帧率分别提升了1.2倍和1.3倍。
除了平均性能提升,更重要的是,我们还评估了尾部延迟。从图中可以看出,各种场景的尾部延迟显著降低,这意味着最坏情况可以被有效避免。
除了上述场景,Archer还评估了Transformer模型。这是一种新兴的内存密集型应用,由于自注意力的二次缩放和高维嵌入,其在推理时需要大量内存。正如我们所评估的,Transformer的平均延迟比CNN高7.3倍。然而,当启用Archer时,在Pixel 6上,其延迟降低了39.2%。
总结 📝
本节课中,我们一起学习了Archer系统。本文表明,页面压缩并不适合移动场景下的内存密集型应用。我们工作的核心见解是,通过挖掘页面关联规则,我们证明了在移动系统中进行大粒度压缩是可行的。这个问题在早期的工作中未被重视。



受我们观察的启发,并为了应对技术挑战,本文提出了Archer。Archer是一个面向移动设备高速响应的、具备页面关联规则感知的自适应内存压缩系统。评估结果表明,它能显著提升应用启动、拍照、视频播放及AI模型推理等多种场景的性能和响应速度。
033:VectorCDC - 利用向量指令加速数据去重



概述
在本教程中,我们将学习一篇来自FAST‘25存储大会(USENIX文件与存储技术会议)的论文《VectorCDC: Accelerating Data Deduplication with Vector Instructions》。我们将探讨数据去重的基本概念,了解传统算法的瓶颈,并重点学习如何利用现代CPU的向量指令(如AVX、SSE)来高效加速一种名为“无哈希CDC”的算法家族,从而实现高达21倍的性能提升。
背景:数据去重与CDC算法
上一节我们提到了数据去重的重要性。本节中,我们来看看实现去重的一个关键步骤:内容定义分块。
数据去重是一种用于识别和消除重复数据以节省网络和存储成本的机制。它包含多个阶段,其中一个关键阶段是将文件分割成块。
以下是分块算法的两种主要类型:
- 基于哈希的算法:例如Rabin分块。它通过滑动窗口计算哈希值来确定分块边界。这类算法存在数据依赖性,难以并行化。
- 无哈希算法:例如RAM(快速最大值分块)。它通过寻找数据区域中的极值(如最大值)来确定边界,逻辑更简单,并行潜力更大。
传统的加速尝试(如SSCDC)专注于加速基于哈希的算法,但由于需要处理数据依赖和使用昂贵的scatter/gather指令,加速效果有限(通常只有1.2-1.8倍)。
VectorCDC的核心设计思想
既然加速基于哈希的算法遇到瓶颈,VectorCDC选择了一条不同的道路:加速无哈希CDC算法。
VectorCDC的设计基于一个关键发现:大多数无哈希CDC算法都可以归结为两个通用阶段:
- 极值字节搜索阶段:在一个数据区域内寻找最大值或最小值。
- 范围扫描阶段:将数据区域的字节与一个目标值进行顺序比较。
通过利用SIMD向量指令优化这两个阶段,就能高效加速整个算法家族。
关键技术:向量指令与两阶段优化
上一节我们介绍了VectorCDC的两阶段模型。本节中,我们来看看如何利用向量指令具体实现这两个阶段。
阶段一:极值字节搜索加速
目标是找到一块数据中的最大值。传统方法是顺序比较。使用向量指令可以并行比较。
操作步骤如下:
- 数据打包:将字节数据加载到向量寄存器中。例如,一个128位寄存器可以打包16个字节。
- 并行比较:使用一条向量最大值指令(如
_mm_max_epu8),对两个寄存器中的字节进行并行比较,结果存入新寄存器。 - 归约树:重复步骤2,将结果寄存器再次两两比较,形成一棵归约树,直到得到一个包含全局最大值的寄存器。
代码概念描述:
// 假设 v1, v2 是包含打包字节的向量寄存器
__m128i result = _mm_max_epu8(v1, v2); // 并行计算16对字节的最大值
// 重复此过程,形成归约树,最终找到全局最大值
阶段二:范围扫描加速
目标是将数据区域的每个字节与一个目标值进行比较,直到找到满足条件的字节(例如,大于等于目标值)。
操作步骤如下:
- 加载目标值:将目标字节重复填充到一个向量寄存器中。
- 打包加载数据:将待扫描的数据字节打包加载到另一个向量寄存器。
- 并行比较:使用一条向量比较指令(如
_mm_cmpge_epu8),一次性判断所有加载的字节是否满足条件。 - 滑动窗口:如果没有找到匹配,则滑动窗口,加载下一组字节,重复步骤3。
代码概念描述:
// target_v 是重复填充了目标值的向量寄存器
// data_v 是包含打包数据字节的向量寄存器
__m128i cmp_result = _mm_cmpge_epu8(data_v, target_v); // 并行比较
// 检查cmp_result中是否有非零位(表示找到匹配)
// 若无,则移动指针,加载下一组数据
算法重构与性能优势
我们已经掌握了两个核心的向量化阶段。现在,让我们看看如何用它们重构具体的算法,并了解其带来的性能优势。
以RAM算法为例:
- 第一步:在固定大小窗口内寻找最大值。这正好对应 极值字节搜索阶段。
- 第二步:从窗口后开始,顺序扫描字节,与找到的最大值比较。这正好对应 范围扫描阶段。
因此,在VectorCDC框架下,RAM算法被重构为一个极值字节搜索加上一个范围扫描。其他无哈希算法(如FastCDC、AE)也可以类似地用这两个阶段重新设计。
这种重构的优势在于:
- 高效并行:充分利用CPU的SIMD单元,单条指令处理多个数据。
- 避免依赖:无哈希算法本身数据依赖性低,易于向量化。
- 通用性强:一套优化方法适用于多种算法。
性能评估与总结
最后,我们来审视VectorCDC的实际效果。论文使用了多种数据集进行评估。
速度对比结果:
- 在VM备份数据集上,使用8KB块大小时,向量化加速后的VRAM算法吞吐量达到约2.5 GB/s。
- VRAM比所有其他对比算法快12到15倍。
- 与同样使用向量加速的SSCDC相比,VRAM的加速比高达21倍(SSCDC仅获得1.2-1.7倍加速比)。
关键结论:
- 高性能:VectorCDC通过加速无哈希CDC算法,实现了数量级的吞吐量提升。
- 无损压缩率:加速过程不会对数据去重的最终空间节省率产生负面影响。
- 向后兼容:该方法与现有的去重系统兼容。
总结
本节课中我们一起学习了VectorCDC的核心思想。我们首先回顾了数据去重及CDC分块算法的背景,指出了传统哈希算法加速的瓶颈。接着,我们深入探讨了VectorCDC如何另辟蹊径,通过识别并向量化“极值字节搜索”和“范围扫描”这两个通用阶段,来高效加速整个无哈希CDC算法家族。最后,我们看到了该方法带来的显著性能提升,在保持去重效率的同时,获得了高达21倍的吞吐量增长。



VectorCDC的代码已在GitHub上开源,为存储系统优化提供了有力的新工具。
034:Oasis - 一种基于全距离草图的核外近似图系统




在本教程中,我们将学习一篇名为《Oasis: An Out-of-core Approximate Graph System via All-Distances Sketches》的论文。该论文由香港中文大学和德克萨斯大学达拉斯分校合作完成。我们将探讨图数据结构的重要性、传统图处理方法的局限性,并详细介绍一种名为“全距离草图”的近似处理方法,以及旨在将其投入实际应用的Oasis系统。
概述
图是一种强大的数据结构,能够表达广泛的现实世界关系。它将实体存储为顶点,将实体间的连接存储为边。图区别于其他数据结构的关键在于“邻域”概念,即一个顶点如何与图中其他顶点相连,这能提供关键信息。
许多重要应用依赖于邻域信息,例如社交网络分析、推荐系统和导航规划。
处理图应用的一种直接方法是内存图处理,即将整个图存储在内存中进行处理。这种方法高效,但对于大规模图来说成本高昂。
另一种方法是核外图处理,它将图数据保留在存储设备中,仅在需要时将一小部分加载到内存。这种方法成本较低,但通常受限于缓慢的I/O速度。
由于内存处理和核外处理之间存在权衡,我们开始探索另一个方向:近似图处理。其核心见解是,在许多实际应用中,精确答案并非总是必需的。这为我们利用近似方法来打破上述权衡提供了空间。
背景:全距离草图
在众多近似方法中,全距离草图(All-Distances Sketch,简称ADS)最近成为一种有前景的方案,能够捕获邻域信息。具体来说,ADS是一种为每个顶点定义的概率数据结构,它是一个草图,用于总结顶点U如何连接到图中的其他顶点。
更精确地说,顶点U的ADS包含U连接到其他“地标顶点”的距离。
根据现有研究,ADS是唯一结合了以下三个特性的草图方案:
- 多功能性:可用于各种应用。
- 可控且可保证的精度:用户可以通过调整ADS参数来控制估计时的误差范围。
- 可扩展性:使用ADS的空间和时间复杂度几乎随图规模线性增长。
然而,尽管ADS在理论上发展成熟,但其在实际应用中的使用仍存在巨大鸿沟,主要原因是它需要极高的内存消耗。
挑战与Oasis系统
我们面临的关键挑战如下:
- 当前ADS研究主要集中于理论视角,并提出了全内存环境下的算法。然而,根据我们的研究,能够高效构造顶点ADS的最新方案需要巨大的内存量,可能高达图本身大小的5倍,内存开销非常高。
- 由于管理ADS更为复杂,现有核外图系统中的大多数技术对ADS场景实际上是无效的。如果直接在传统核外系统上运行ADS构造,通常会导致性能极差。
鉴于这些挑战,我们提出了Oasis,一个核外近似图系统,旨在通过有效利用策略将ADS技术带入实际应用。
Oasis系统架构与工作流程

上图展示了Oasis的系统架构和工作流程。系统包含两个主要模块:ADS构造模块和估计模块。
工作流程如下:
- 用户提供两个输入:K值(控制草图大小与精度的权衡)和Oasis可用的内存量。
- 这两个输入进入ADS构造模块。Oasis处理原始图,生成ADS并将其存储在存储设备中。
- 一旦ADS准备就绪,用户向我们的估计模块提供他们的ADS估计器。
- 最后,估计模块接管工作,优化加载必要ADS和运行估计器以生成近似答案的过程。
ADS构造模块的优化技术
ADS构造面临的首要且最重要的挑战是减少内存消耗。为此,我们采用了一种基于分区的方法来帮助管理有限的内存,同时确保局部访问。分区是传统核外图系统中一项成熟的技术,其核心思想是将数据划分为多个存储分区,这样我们一次只处理一两个分区,从而有效控制内存使用量。
鉴于ADS体积庞大,分区自然适合Oasis。由于使用了分区,用户可以根据其机器内存容量调整分区数量,使得Oasis能够处理不同规模的图。
为了进一步提高效率,我们进一步将聚合图分解为多个大小相等的块,每个块包含一组目标顶点。通过这种方式,不同的线程可以处理不同的块。由于每个块包含一组目标顶点,这意味着我们可以防止它们同时更新相同的ADS,从而无需使用锁来处理冲突,通过这种块设计保持流程的平稳高效。
由于ADS是构造过程中最大的数据结构,如何最小化加载ADS的数量至关重要。我们提出了活动数据分离技术来最小化对活动ADS的加载。“活动ADS”指的是当前迭代中需要处理的ADS集合。
传统方法必须加载整个ADS并搜索活动ADS,这非常低效,因为我们只需要其中一小部分。活动数据分离通过复制活动ADS并将其存储在单独的文件(仅包含活动ADS)中来应对此问题。这样,在下一次迭代中,我们无需直接从原始ADS文件读取数据,而可以直接读取包含活动ADS的独立文件,从而节省I/O。处理完的活动ADS可以在文件中批量删除,因为构造过程会在下一次迭代中产生新的活动ADS。
接下来是选择性ADS访问技术,旨在通过仅加载实际接收更新的ADS来减少不必要的ADS读取。
假设整个图中唯一的新更新是顶点B处的X。与盲目加载所有ADS不同,此方法首先对边数据进行一些额外的I/O读取以获取邻居信息。通过这样做,我们可以识别哪些顶点是真正需要的,并仅加载那些顶点的ADS。在示例中,我们只需要加载顶点C和D的ADS。
重要的是,由于边数据通常比ADS小得多,因此花费一些额外的I/O读取边数据是值得的,因为与减少ADS读取所节省的I/O相比,这部分开销较小。
ADS估计模块的框架
上一节我们介绍了ADS构造的优化设计。本节中,我们来看看Oasis的ADS估计框架。
该框架包含两个组件:
- ADS估计器:由用户提供。
- 查询队列:由用户填充,用于指示哪些ADS将被加载到内存中。
队列中的每个查询包含目标顶点ID(即需要加载和估计的ADS对应的顶点ID)以及指向正确ADS估计器的指针。如果用户填充了ADS估计器和查询队列,那么我们的Oasis将自动处理这些查询,并在进行估计时尝试优化性能。
该框架中的一个主要优化点是如何将查询分配给不同的线程。这种任务分配会极大地影响整体性能。例如,如果我们给一个线程分配了多个需要相同ADS的查询,那么该线程只需要加载一次ADS就可以服务多个查询,这将显著提高I/O利用率。
为此,我们提出了两种不同的方法来提高利用率。由于时间限制,此处不深入细节,有兴趣的读者请参阅论文。
实验评估与结论
我们将Oasis与两种内存方案进行比较,分别称为Basic和SOTA。
- Basic是ADS公式的直接实现,它从每个顶点执行图遍历,遍历次数很高,时间复杂度为O(V * E)。
- SOTA方案旨在实现显著更低的时间复杂度。它很特殊,因为它尝试在转置图上运行。与需要遍历整个图的原始图遍历不同,有界图遍历可以在需要时停止。遍历次数仅为O(E log V),远少于Basic方案。
我们的Oasis ADS构造试图将SOTA方案与分区技术相结合,以减少内存消耗,并且我们还提出了其他优化来提高整体ADS构造效率。
在默认将图划分为16个分区的情况下,实验结果显示:
- 在运行时间方面,我们的Oasis方案仍比SOTA方案差(因为SOTA是全内存处理),大约慢两倍。
- 但在内存消耗方面,Oasis所需的内存开销减少了约10倍。我们认为这是一个值得的交易,因为节省的大量内存使得该技术能够在合理的成本下应用。
在Oasis ADS构造的各种优化技术中,最有效的是活动数据分离,其次是分块,然后是选择性ADS访问。
总结
本节课中,我们一起学习并介绍了一个名为Oasis的系统。该系统旨在以低内存开销和高效率管理全距离草图,从而将这一强大的近似图处理技术推向实际应用。




035:PolyStore - 利用异构存储设备的组合能力

概述
在本节课中,我们将学习PolyStore,这是一个旨在为带宽密集型应用充分利用异构存储设备组合能力的新型系统。我们将探讨传统分层存储设计的局限性,并详细解析PolyStore如何通过其创新的元数据层、动态数据放置机制和灵活的缓冲区缓存策略来解决这些问题。
存储需求与硬件演进
过去几十年,需要持久化存储的数据规模迅速增长,即使在单机上,众多应用的需求也是如此。更重要的是,这些应用对存储带宽的要求越来越高,从每秒约100兆字节增长到如今的每秒数十吉字节。
随着应用对存储带宽需求的增长,存储硬件在过去两年也快速发展,从NVMe SSD到字节可寻址的低延迟持久内存,再到具有更高带宽的超快NVMe SSD。
为了充分利用这些硬件的特性,开发者们致力于开发新的文件系统,以适配其硬件特性,同时也通过增加新功能来改造现有的文件系统。
传统分层存储设计及其局限
由于存储设备之间存在性能、成本和容量差距,很自然地会以分层方式组织它们,将较慢的设备置于较高延迟的层级。这种设计理念催生了两种经典方式:缓存和分层。
缓存:新的写入请求被导向低延迟设备(我们称之为更快设备),存储系统异步地将数据从更快设备驱逐到更慢设备(我们称之为更慢设备)。对于读取操作,如果数据命中更快存储,则直接读取;否则,系统会及时将数据从更慢存储迁移到更快设备。
分层:与缓存类似,分层也在低延迟(更快)设备中缓冲数据。然而,它以成本感知的方式在快慢设备间移动数据。
在这两种设计中,都使用更快的存储设备来吸收新数据的写入请求。然而,现有的异构存储系统存在几个缺陷,未能充分利用硬件和软件的潜力。
以下是现有系统面临的主要问题:
-
无法利用异构存储的组合带宽:因为它们将设备按分层布局排列。当多个线程同时向快速设备写入时,底层设备的带宽完全未被利用。以使用持久内存和NVMe SSD为例,在多线程基准测试中,无论是缓存还是分层方法,在随机写入和读取工作负载下,都无法接近带宽极限。这种分层设计规避了累积存储带宽的利用。
-
缺乏针对存储异构性的灵活DRAM缓冲区缓存机制:存储系统使用DRAM缓冲区来暂存写入请求,并将频繁访问的数据保留在DRAM中以应对高延迟。然而,现有系统严重依赖Linux页面缓存,它无法有效处理设备异构性。首先,操作系统页面缓存默认会绕过某些设备(如持久内存),并且无法区分读写操作的不对称性能。此外,它无法在考虑设备异构性的情况下,在设备之间提供灵活的准入和驱逐控制,因为它们缺乏在异构存储设备之上进行全局视图管理的机制。
-
系统设计未考虑重用成熟的设备优化文件系统:例如,一些方法为特定类型设备提出了分层文件系统,将数据放置决策编码在文件系统中。而另一些方法则在块层管理设备,但只使用一个文件系统。这两种设计都导致成熟的、针对硬件优化的文件系统未被充分利用。
其系统设计中的关键洞察是,它们将数据放置决策与软件栈耦合在一起。
PolyStore的设计理念
有人可能会认为,使用RAID 0(在设备间条带化数据)来利用设备带宽,同时考虑它们之间的性能差异,不是一个更直接的解决方案吗?然而,如前所述,这种方法仍然将数据放置决策编码在块层,并向上层隐藏了设备异构性。结果就是,它无法利用针对设备优化的文件系统,也无法为准入和驱逐控制提供设备特定的DRAM缓冲区策略。
因此,我们认为,用于管理存储异构性的理想系统应首先利用多个存储设备的组合带宽。最重要的是,它应该将数据放置决策解耦,并将其置于存储软件栈之上。这样,它可以自然地重用过去几年为每个独立存储设备量身定制的成熟文件系统,并提供灵活的DRAM缓冲区缓存驱逐和准入策略。
PolyStore架构与核心组件
为了避免上述缺陷,我们提出并设计了PolyStore,这是一个位于设备优化内核级文件系统之上的元层。PolyStore包含两个组件:一个具有数据放置决策的用户级运行时,用于利用设备带宽;以及一个处理进程间共享、资源公平性和安全性的操作系统组件。
现在,让我分解PolyStore的组件和设计理念,从数据索引结构开始。
数据索引结构
为了获得异构存储设备的组合带宽,第一步是在这些设备间分布数据。同时,我们知道超过吉字节的大型文件(如数据流文件和数据库文件)对带宽敏感。因此,PolyStore尝试将一个PolyStore逻辑文件的数据块分布到底层每个独立文件系统上的物理文件中。
这是通过一个范围树数据结构实现的,该结构中的每个节点代表放置在对应存储设备上的一系列数据块。更重要的是,这个数据结构还编码了跨异构存储设备的数据放置和访问模式信息,这是带宽利用和灵活DRAM缓冲区缓存策略的关键结构。
动态数据放置机制
最大化异构存储设备的组合带宽并非易事,因为将来自应用程序线程的I/O请求正确映射到存储设备至关重要。挑战在于以下两个方面:首先,如前所述,存储介质在为应用提供吞吐量方面具有不同的特性。其次,以静态方式确定从多个线程到存储设备的最佳I/O请求映射,无法适应工作负载的变化。这表明我们需要一个动态数据放置机制来最大化存储带宽利用率。
为了克服上述挑战,我们提出了一种基于周期的吞吐量监控和动态数据放置机制。让我用一个例子来说明。最初,PolyStore将应用程序I/O线程分为两组,分别向持久内存和NVMe写入数据。我们可以根据从供应商处获得的设备规格启发式信息进行划分,或者平均分配。
PolyStore开始将一个线程从NVMe组移动到持久内存组,因为我们希望首先饱和具有更高带宽的设备。如果这种重新分配提高了组合带宽,我们就将另一个线程从NVMe组移动到持久内存组,看看是否继续提高带宽利用率。然而,如果不是这种情况,则意味着持久内存带宽过饱和,然后我们恢复到先前的配置。最终,PolyStore收敛到一个能够有效利用组合存储带宽的局部最优配置。
DRAM缓冲区缓存设计
如前所述,存储系统使用DRAM缓冲区来隐藏异构存储设备的延迟。考虑到它们之间的性能不对称性,更有效地使用DRAM缓冲区缓存在这些设备之间进行协调至关重要。因为如前所述,PolyStore已经提供了一个索引结构,该结构编码了哪些数据范围放置在哪个设备上的数据放置信息。
在此索引结构之上,PolyStore可以自适应地分配DRAM缓冲区缓存,并采用设备特定的策略进行准入和驱逐控制。以使用持久内存和NVMe SSD为例。如前所述,持久内存的读取速度非常快,虽然低于DRAM,但仍处于同一数量级。因此,PolyStore允许直接访问持久内存上的只读数据,而无需分配额外的DRAM缓冲区。
然而,持久内存的写入性能比读取慢得多。因此,当应用程序修改数据块时,PolyStore仅在数据被修改时才为持久内存分配DRAM缓冲区。除了编码的数据放置信息外,PolyStore中的索引结构还可以促进跨设备的数据迁移。因为这种索引结构还可以跟踪数据的“热度”和“冷度”,因为它位于每个数据平面操作(如读写操作)的关键路径上。
举例来说,假设由于内存压力,需要从DRAM缓冲区驱逐一系列频繁访问的数据。与传统缓冲区缓存不同,在传统设计中,数据需要被驱逐到其在较慢设备中的原始位置。而在PolyStore中,由于我们在不同存储设备之上拥有全局视图,因此,如果空间允许,PolyStore可以将较慢设备上的热数据刷新到更快的设备上。这样,下次访问这些数据块时,速度会更快。最后,PolyStore会对较慢设备上的原始数据块进行垃圾回收。
评估与性能分析
作为设计在操作系统内核中设备优化文件系统之上的元层,PolyStore在提供跨不同文件系统的崩溃一致性方面带来了一些挑战,因为它们都提供不同且多样的原子性和持久性保证,同时在进程间提供共享、安全性和公平性方面也存在挑战。更多技术细节请参阅我们的论文。
我们现在通过回答以下两个问题来评估PolyStore的设计:首先,PolyStore能否利用组合带宽?其次,PolyStore能否为实际应用带来好处?
在我们的实验设置中,我们使用持久内存加NVMe SSD作为异构存储配置,并使用Nova和Ext4分别作为PolyStore中持久内存和NVMe的文件系统。我们还将PolyStore与采用分层设计理念的最先进系统进行比较,这些系统包括采用缓存的Atlas和采用分层的SPFS。我们还评估了更多异构存储系统配置、更多不同文件系统的组合以及更多最先进系统的比较。详情请参阅我们的论文。
组合带宽利用能力
我们首先通过多个多线程基准测试访问私有文件来评估利用组合设备带宽的能力,并试图观察它是否能饱和存储带宽。我们从顺序追加工作负载开始,研究数据放置对带宽利用率的影响。
如图所示,由于其分层设计,缓存和分层方法都无法接近带宽极限。然而,PolyStore可以实现高达这些方法6.1倍的性能,并有效饱和可用组合带宽的90%。对于读取工作负载,PolyStore也可以实现高达这些系统3.3倍的带宽,因为PolyStore提供了可扩展的索引机制和动态数据放置机制,可以同时将数据分布在两个设备上。
现在,让我进一步分解和分析在32个基准测试线程上的顺序追加工作负载。首先,仅使用静态的、成本感知的数据放置,而不使用我们基于周期的动态方法。最初,基准测试线程被分为两组,分别向持久内存和NVMe写入数据。然而,一旦使用持久内存的线程组完成工作,由于缺乏动态数据机制,持久内存带宽变得空闲,PolyStore无法动态地重新映射I/O请求来使用持久内存,导致其带宽未被利用。如图所示,虽然静态方法在一定程度上可以实现组合带宽,但它无法适应工作负载的变化。
现在,让我们看看当启用动态数据放置的PolyStore时的情况。在初始阶段,没有太大差异。后来,当持久内存带宽变得空闲,并且使用持久内存的线程组已经完成时,PolyStore开始将线程从NVMe组移动到持久内存组,旨在优化组合带宽利用率。结果,通过这种动态数据放置机制,PolyStore可以以动态方式将数据从逻辑文件分布到物理文件,从而饱和更多的存储带宽。
实际应用收益
我们最终研究了PolyStore是否能惠及实际应用,我们使用了RocksDB和YCSB工作负载。为了评估交付给应用的端到端性能,我们为所有系统启用了DRAM缓冲区缓存。如前所述,操作系统页面缓存无法处理设备特性及其异构性。
以具有50%读取-修改-写入工作负载的YCSB工作负载F为例,让我们看看PolyStore是否能带来好处。首先,如果我们只为PolyStore使用操作系统页面缓存,它只显示出边际效益,因为RocksDB并非一直执行I/O请求,并且操作系统页面缓存无法有效地为DRAM缓冲区处理设备异构性以进行数据准入和驱逐控制。然而,如果启用PolyStore的DRAM缓冲区缓存机制进行这种灵活控制,它可以有效缓解持久内存的写入性能,并如前所述自适应地将数据从NVMe迁移到持久内存。
因此,与最先进的分层设计系统相比,PolyStore可以实现高达1.7倍的加速。
总结


本节课中,我们一起学习了PolyStore系统。随着存储带宽持续增长,分层设计理念不再适合带宽密集型应用。现有的异构存储系统受限于分层理念的设计缺陷,以及其固有的无法利用设备优化文件系统和异构性感知的DRAM缓冲区策略的问题。为此,我们提出了PolyStore,这是一个位于成熟的设备专用文件系统之上的元层,具有动态数据放置功能,可在多个设备间水平扩展数据,以及处理设备异构性的差异化缓冲区机制。
036:液态硬盘——面向海量数据的DNA块设备设计

在本教程中,我们将学习DNA作为存储介质的基础知识,探讨如何将其集成到现有存储系统中,并重点介绍一种名为“液态硬盘”的创新DNA块设备设计,以解决海量数据存储的挑战。
DNA存储基础 🧬
上一节我们介绍了教程的主题,本节中我们来看看DNA存储的基本原理。DNA由四种不同的核苷酸(A、T、C、G)组成,是众所周知的遗传信息载体。如今,随着生物技术的进步,DNA也可以承载通用信息。具体来说,每条DNA链都有其对应的核苷酸序列,我们可以用这个序列来表示信息。
此外,DNA合成技术使我们能够合成具有指定序列的DNA,而DNA测序技术使我们能够读取DNA链的核苷酸序列。借助这两种技术,我们可以在DNA中存储和检索信息。
作为存储介质,DNA有两个显著优势:高密度和长寿命。
以下是DNA存储的具体优势:
- 高密度:DNA存储密度可达每立方毫米10^18个碱基,这比硬盘高出约8个数量级,比闪存高出约5个数量级。
- 长寿命:DNA可以保存数个世纪,而固态硬盘和硬盘的寿命大约只有五年。
- 发展速度:DNA存储技术的发展速度相对较慢。
DNA存储硬件由多个芯片组成,每个芯片是一个“点阵”。一个“点”相当于一个试管,其中存储着大量DNA链。
DNA链分为四个部分:起始引物、末端引物、索引区和有效载荷区。我们使用有效载荷区存储数据,并使用引物和索引来定位DNA链。
具体来说:
- 我们使用引物来索引DNA链集合。一个“测序集合”是具有相同引物的DNA链集合,位于同一个“点”内,集合中的所有链会被同时测序。
- 为了区分在同一测序集合内同时测序的DNA链,我们在索引区为每条链编码一个唯一的标识符。
DNA存储硬件支持三种操作:写入、读取和擦除。
- 写入操作:首先将二进制数据编码为核苷酸序列,然后合成具有相应序列的DNA链,最后将链存储到“点”中。
- 读取操作:首先对一个测序集合中的所有DNA链进行测序以获得核苷酸序列,然后将核苷酸序列解码为二进制数据。
- 擦除操作:由于DNA修改技术尚不成熟,我们需要在写入前进行擦除,这与闪存固态硬盘类似。擦除操作会清除“点”中的所有普通DNA链。
构建DNA块设备 💾
了解了DNA存储基础后,一个问题随之产生:如何将DNA存储集成到当前的存储系统中?我们的工作重点是构建一个DNA块设备。
块接口是存储系统中最关键的接口之一。它简单,允许我们以固定大小的块粒度随机访问数据。此外,块接口通用性强,广泛应用于各种场景,包括人工智能、云存储和归档存储。因此,我们提议构建DNA块设备,以将DNA存储集成到现有存储系统中。
我们首先审视一个简单的DNA块设备设计及其存在的问题。
对于一个简单的设计,我们首先将存储空间划分为块。我们将连续的DNA链映射到一个块中,然后为每个块分配物理块地址。
支持块操作看似直接:
- 读取:DNA存储硬件的读取粒度是一个包含约100万条链的测序集合。要读取一个块,我们必须读取该测序集合内的所有块。
- 写入:硬件的写入粒度是一条链。要写入一个块,我们只需写入该块的链。
- 更新:由于需要“先擦后写”,要更新一个块,我们需要执行擦除操作。擦除粒度是一个包含约2000个测序集合的“点”。因此,要更新一个块,我们首先需要读取该“点”内的所有测序集合,然后擦除整个“点”,最后再写入新的块以及其他块。更新一个块需要读取和写入1200万个块,成本极高。
为了降低更新成本,一个直接的方法是采用异地更新,就像固态硬盘所做的那样。
我们向用户提供逻辑块地址,并使用一个转换表将LBA转换为PBA,从而实现异地更新。
异地更新带来了垃圾,需要定期进行垃圾回收。为了便于垃圾回收,我们通常记录一些垃圾回收元数据,包括将PBA转换回LBA的逆向转换表,以及记录物理块有效性的有效位图。
然而,由于DNA块设备存储的是EB级别的数据,包括转换表、逆向转换表和有效位图在内的元数据也达到了PB级别。因此,元数据难以存储在传统存储设备中,需要存储在DNA里。
但是,将元数据存储在DNA中带来了挑战:维护这些元数据会重新引入昂贵的DNA更新操作。具体来说,对于每次写入,我们需要更新转换表条目、更新逆向转换表条目并使旧块失效。这三个过程使得昂贵的DNA更新操作卷土重来。
液态硬盘设计 🚀
为了解决这些挑战,我们提出了液态硬盘。
液态硬盘采用了三项关键技术:
- 双层转换表:避免更新转换表。
- 共生元数据:避免更新垃圾回收元数据。
- 延迟失效:降低维护有效位图的成本。
接下来,我们进一步审视液态硬盘的设计细节。
双层转换表
我们提出双层转换表来降低更新转换表条目的成本。具体来说,我们对转换表本身也进行异地更新。因此,我们引入了另一层来支持转换表层的异地更新。现在我们有了两层:L0和L1 DNA转换层。
- L0条目用于索引L1条目在DNA中的地址。
- L1条目用于将LBA转换为PBA。
此外,L0条目在GB级别,这意味着它可以存储在快速的固态硬盘中。因此,更新L0条目的成本可以忽略不计。
共生元数据
我们提出共生元数据来降低维护垃圾回收元数据的成本。
首先,我们观察到垃圾回收元数据总是与块数据一起被访问。其次,我们观察到物理块是异地更新的,因此物理块本身没有DNA更新操作。
基于这些观察,我们将垃圾回收元数据与物理块共生存储,以消除DNA更新。具体来说:
- 对于一个物理块,我们利用最后一条DNA链的未使用区域来存储逆向转换表的条目,即将该物理块的LBA存储在其自身的未使用区域。
- 此外,我们添加一条额外的链来表示该块的有效性,称之为“失效链”。如果失效链已被写入,则该物理块失效;如果失效链未被写入,则该块有效。
通过共生元数据,垃圾回收元数据随着物理块数据以异地更新的方式一同更新。因此,我们在维护垃圾回收元数据时消除了DNA更新操作。
延迟失效
我们引入延迟失效来进一步减少更新块的开销。
在块更新操作中,我们需要使旧块失效。因此,我们需要先读取转换表以获取旧的PBA,然后向旧的PBA写入失效链以使旧物理块失效。这个读取操作位于更新块的关键路径上。如前所述,读取粒度远大于写入粒度,因此读取操作大大增加了更新成本。
为了解决这个问题,我们将失效操作延迟到读取块时进行,从而将这次DNA读取操作从更新块的关键路径上移除。
更多细节请参阅我们的论文。
性能评估 📊
接下来,让我们进入性能评估部分。在评估中,我们基于模拟器实现了系统。主要有三个对比系统:
- LiqSD:我们的系统。
- CrossDetail:一个块大小为24MB的块设备,其元数据小到可以存储在固态硬盘中。
- NoDetail:前面提到的简单设计。
我们使用三个指标来衡量系统性能:
- 读取放大:请求的读取数据量与实际的读取数据量之比。
- 写入放大:请求的写入数据量与实际的写入数据量之比。
- 额外读取比率:由写入操作引起的请求读取数据量与实际写入数据量之比。
在微基准测试中:
- 与NoDetail设计相比,LiqSD将读取放大幅度降低了高达7个数量级。
- LiqSD的额外读取比率几乎为0。
- 与NoDetail设计相比,LiqSD将写入放大幅度降低了高达6000倍。
在真实世界工作负载跟踪测试中:
- 与CrossDetail设计相比,LiqSD将写入放大幅度降低了高达3000倍。
- 与CrossDetail设计相比,LiqSD将读取放大幅度降低了高达7倍。
我们还评估了不同块大小(从4KB到24MB)的性能。总体而言,在真实世界工作负载中,4KB的块大小实现了最佳的读写性能。
总结 ✨
本节课中我们一起学习了DNA存储的基础和集成挑战。
我们提出了一种全面的DNA块设备设计。其核心思想是异地更新。面临的挑战是维护元数据的成本高昂。而关键技术是双层转换表、共生元数据和延迟失效。


通过本教程,我们了解了如何利用DNA的特性构建高效的海量数据存储设备,并掌握了解决相关设计挑战的创新方法。
037:基于基序的DNA存储生成工具

在本节课中,我们将要学习一种创新的DNA数据存储方法——基于基序的存储,并了解一个能够自动生成符合生物约束条件的DNA基序序列的生成工具。这种方法旨在以更低的成本和更高的可靠性存储海量数据。
概述:为什么需要DNA数据存储?
近年来,对数字数据存储的需求呈指数级增长,存储介质也在不断演进以满足这一需求。然而,当前如存储卡和芯片等存储设备寿命有限,需要每隔几年更换。此外,全球数据中心每年的能耗已超过英国的总能耗。因此,迫切需要一种新的数据存储方法。
DNA已被证明是解决这些问题的潜在方案。它允许在微观体积内存储海量数据,密度高达每立方毫米10^18字节。此外,DNA的半衰期可达500年,在适当条件下,信息可保存长达2000年。尽管DNA数据存储是一项相对较新的技术,但当前DNA技术的进步已使得将整个英文维基百科存储在单个试管中成为可能。
如何将数据存储在DNA中?
DNA是一种由四种化学碱基(也称为碱基)序列组成的分子。这些碱基可表示为A、T、C和G。DNA数据存储的工作原理类似于DNA存储我们身体的遗传信息,它可以通过使用这四种碱基(A、T、C、G)构建序列来存储合成信息,就像计算机排列字节一样。
上一节我们介绍了数据编码的原理,本节中我们来看看将数据存入DNA的实际操作流程。
数据存储到DNA的过程主要分为以下步骤:
- 编码:将数字信息(如二进制数据)转换为由A、T、C、G组成的DNA序列。
- 合成:实际构建DNA链。
- 存储:保存合成好的DNA。
- 测序:读取DNA链中的信息。
- 解码:将读取的DNA序列转换回原始信息。
然而,在这些步骤中存在一些限制。特别是在合成步骤,由于当前技术限制,我们无法稳定合成长度超过200个碱基的DNA链。但最重要的是,合成成本非常高,目前约为每TB 4亿美元。因此,存储新数据非常昂贵,商业上不可行。
解决方案:基于基序的DNA数据存储
为了解决成本问题,人们提出了一种称为“基于基序的DNA数据存储”的方法。
在这种方法中,合成成本不再与要存储的数据量成正比,而是固定的。其工作原理是:在基于基序的存储中,你拥有一组固定数量的短DNA序列,称为“基序”。这些基序只需要在开始时合成一次。
随后,这些基序可以以低成本的方式复制,并可以组装形成新的序列。组装它们的方法是使用一种称为“Gibson组装”的技术。
为了说明这一点,这里简化展示两个基序(基序1和基序2)的组装过程。它们会有一个重叠区域,这个重叠区域允许它们彼此结合。然后,所有缺失的碱基会被填补,最终形成一个由两个原始基序组成的新序列。这个重叠区域被称为“键”。
基序的结构与生物约束
这引出了基序的结构。我们刚刚看到需要“键”来组装基序,因此我们希望基序结构在链的两端都有键,中间则是“有效载荷”区域,用于存放实际信息。
由于键用于组装,这意味着它们不能出现在序列的其他任何地方。并且由于它们是唯一的,它们也可以用于实际访问数据。
我们刚刚看到了基序的理想结构,但并非每个基序都是“好”基序。有些基序集合可以比其他集合更可靠地存储数据。这是因为DNA容易出错。
以下是DNA存储中常见的三种错误类型及其相关的生物约束:
- 同聚物:指单个碱基的连续重复。我们尽量避免同聚物,因为它们会增加序列出错的概率。
- 发夹结构:在某些条件下,由于DNA链具有柔性,它可以自身折叠并与自身结合,这使得数据访问更加困难。因此我们也尽量避免发夹结构。
- GC含量:指序列中G和C碱基的百分比。我们通常将其保持在20%到60%之间,以确保DNA链的稳定性。
基序生成工具的引入
那么,我们实际想要的是什么?我们试图以经济高效的方式存储数据(使用基于基序的DNA数据存储),但同时也要确保数据存储的可靠性,这意味着我们需要符合上述生物约束条件。
这正是我们引入工具的地方。这个工具使我们能够做到这一点,我们称之为“基序生成工具”。
这个工具将以下信息作为输入:
- 关于基序的信息,例如有效载荷长度和您希望拥有的基序数量。
- 关于同聚物、发夹结构和GC含量的约束条件。
允许用户输入这些约束条件非常重要,因为随着DNA技术的进步,这些约束条件会随时间变化。用户将能够重新使用该工具,更新约束条件,然后获得一组新的基序。同样,允许用户指定基序数量等信息也很重要,以便他们可以根据自己的用例定制基序。
我们的工具将输出一组符合这些约束条件的基序。换句话说,您可以用经济高效且可靠的方式存储数据。
工具的工作原理:马尔可夫链
但是,我们如何实际生成这样一组基序呢?我们想到,生成序列在某种程度上类似于生成文本,但这次我们只有四个字母:A、T、C、G。
在文本生成的背景下,马尔可夫链已被证明非常有效。因此,我们也在我们的上下文中使用马尔可夫链,逐个碱基地构建我们的基序。
在这个例子中,我们从一个碱基(例如碱基A)开始,然后我们想添加下一个碱基(A、T、C或G)。那么,如何从当前只有一个碱基的状态转移到下一个有两个碱基的状态呢?这需要使用概率。
这些概率被称为“转移概率”。这些概率直接与“将某个碱基添加到我们的序列中是否违反约束条件”相关。例如,如果添加碱基A到我们的序列中违反了约束条件,我们就会尽量避免添加该碱基,转移到该状态的概率将远低于转移到任何其他状态的概率。
这一部分稍微技术性一些。但你可以理解的是,底部的转移概率公式考虑了一个“分数”,而你在顶部看到的这个“分数”综合考虑了我们之前提到的所有约束:同聚物、发夹结构、GC含量,以及我们不希望在有效载荷中出现用于组装的“键”这一事实。
此外,我们使用对数分数而不是原始分数,这是因为用计算机处理相乘的小数可能很困难,所以我们改用对数分数。
为了让你更好地理解这些分数的样子,我将以同聚物的对数分数为例进行说明。
对于同聚物对数分数,我们已经有了当前的序列(此处用蓝色表示),现在我们试图为添加一个碱基(本例中为碱基A)给出一个对数分数。分数应该是多少?我们试图避免出现同聚物,因为同聚物越长,序列中出现错误的几率就越高。
因此,思路是:如果添加该碱基增加了你的同聚物长度,那么对数分数应该降低。这是我们希望在这条曲线中看到的形状:在X轴上,是同聚物长度,你可以看到当长度为0时,同聚物对数分数远高于长度为5时的情况。
我们找到的、具有我们想要形状的方程如下:
同聚物对数分数 = - (当前同聚物长度 / 用户允许的最大同聚物长度)^超参数
这个方程考虑了当前同聚物长度(在我们的例子中是长度5),以及用户给出的约束条件(即确保较少错误的最大允许同聚物长度),还有一个控制曲线形状的超参数。因为我们并不完全确定我们的对数分数应该是什么样子,所以这是一个可以根据每个用例优化输出的可调参数。
工具性能评估
现在我们有了工具,如何评估它是否是一个好工具呢?为此,我们决定将我们的工具与其他现有的序列生成工具进行比较。
以下是用于对比的工具:
- DNA Fountain (由Erlich等人提出)
- 有限状态机编码 (由Cao等人提出)
- 短聚体组合编码方案 (由Press等人提出)
- 随机生成的DNA序列
前三种工具已有自己的方法来克服之前提到的生物约束,而随机生成的DNA序列只是随机生成,并未实际考虑这些约束。
对于实际评估,我们决定在两种不同场景下进行评估:首先针对每个约束单独评估,然后在一个结合了所有约束的案例研究中,比较每个工具实际返回一组基序所需的时间。
针对每个约束单独评估,我们得到了三个图表。左上角是GC含量,右边是同聚物,底部是发夹结构。我们在Y轴上比较了计算不同长度(X轴显示)的基序集所需的时间。
你可能会注意到,并非我之前提到的所有工具都出现在这些图表上。Cao等人和Press等人的工具没有出现在任何约束图表上,因为它们生成序列所需的时间远高于其他工具,长达数分钟,而这些工具只需毫秒级。
从这些图表中已经可以看出,以蓝色显示的基序生成工具在性能上超过了以黄色显示的DNA Fountain工具,以及Cao等人和Press等人的工具。在所有这三种不同的约束条件下,随着序列长度增加和问题复杂性增加,它在同聚物和发夹结构约束方面也优于随机生成的序列。
你可能会注意到,对于GC含量,随机生成序列的时间是恒定且接近零的,这是因为随机生成序列的预期GC含量已经是50%,这已经在我们约束条件范围内了。
案例研究:综合约束下的表现
既然我们看到了这些工具在每个单独约束下的表现,我们可以继续看看当我们将所有这些约束结合起来时它们的表现如何,这实际上更有趣,因为当前从事DNA数据存储的公司都有针对所有这些约束的规范,以确保DNA链的稳定和可靠。
这正是我们在案例研究中所做的。在案例研究中,我们参考了当前DNA合成公司(如IDT)对同聚物、发夹结构和GC含量使用的约束条件。例如,他们不允许长度大于5的同聚物,GC含量需要在25%到65%之间。
针对这些真实世界的数值,我们得到了评估结果:基序生成工具能够在不到三秒的时间内返回一组符合所有这些约束条件的基序序列。而对于所有其他工具,在五分钟的评估阈值内,我们无法获得任何符合这些约束条件的基序集。
因此,在这个真实案例研究中,我们再次看到基序生成工具优于所有其他工具。
总结
本节课中我们一起学习了基于基序的DNA数据存储及其生成工具。
我们决定专注于基于基序的DNA数据存储,因为它允许以经济高效的方式存储数据。我们创建了一个基序生成工具,它实现了基序生成的自动化,并确保您可以以可靠的方式存储数据。
我们的工具基于当前适用的生物约束条件,性能优于其他工具。


借助这个工具,我们希望为未来DNA数据存储更易于访问和负担得起做出贡献。
038:纪念Erik Riedel与计算存储演进 🕯️💾

概述
在本节中,我们将回顾FAST 25存储大会上对已故社区重要成员Erik Riedel的纪念致辞。内容将涵盖他的学术贡献、行业服务,以及其早期关于计算存储的开创性工作如何持续影响当今技术发展。我们还将了解以其名字命名的最佳论文奖。
我是Garth Gibson,在此悼念我们社区失去的一位重要成员。
Erik Riedel于今年大约七个月前离开了我们。
明天晚上6:30,欢迎所有希望花些时间追忆、分享故事并与他的家人共处的人们,前往Ellameda六号房间。
您既可以看到这张15年前的家庭合影,也可以看看如今他们在台下的样子。这将是一个让在不同环境中认识Erik的人们相聚的活动。
对于不了解Erik的人,我想指出他的服务记录,并鼓励大家效仿:他在学术界服务30年,在工业界服务25年,曾担任项目委员会联合主席并参与众多活动,例如2008年的本次活动。他在SNIA(存储网络工业协会)的技术委员会和各种标准委员会任职15年,其中一些工作最终形成了标准。他在所有这些事务中都付出了大量努力。
对我而言,记忆要回溯到他还是研究生的时候。我曾与他合作完成他的博士论文。
其标题“Active Disks”(主动磁盘)今天听起来可能无关紧要,但其核心趋势您一定会认同:电子技术持续以更低的成本提供更多的晶体管和计算性能,因此可以用处理器和运行其上的程序来替代硬件逻辑。那么,如何在设备内部实现这一点?又可以用它来做什么?
大约30年前,他研究了这个问题,并且采用了一种我非常欣赏的方法:他选择了一个重要的应用——数据库,一个重要的真实生产级开源数据库Postgres,然后找到了足够的信息来决定将哪些任务卸载到设备中执行,并评估了能获得何种收益。
由于当时的成本限制和有限的计算能力,无法在其上消耗大量计算周期,不能占用大量状态存储空间,并且必须在计算后大幅减少所需带宽。如今情况已大为改善。
所以,今天在闪存设备上,可以放置一个FPGA,同时查看多个闪存存储体并进行大量数据缩减。这个由少数人在网络和其他大学领域也进行过探索的想法,确立了一种我们至今仍在追寻的架构模型。
例如,三星的设备正在这样做,并且有开源接口和NVMe CSD(计算存储设备)标准。
近年来,Erik一直在关注碳足迹问题。他在绿色存储工作组任职了相当长的时间,我认为他协助创办了该小组。我看了他在存储开发者大会上的演讲(您可以在YouTube或SNIA网站上找到),他谈到了当专注于改善特性时,可以采取的一系列工程化思考方式。
据我理解,他讨论的关键一点是降低IT设备的碳足迹影响。尤其是在当今使用GPU的背景下,这方面的压力很大。但其核心思想是:我们所使用的设备,其大部分碳足迹是在设备制造过程中产生的,远早于您的使用阶段。
如果您是设备所有者并希望对其进行操作,那么在购买之后能做的改进有限。对于前置成本该怎么办?是的,您可以协助改善采矿或冶炼过程。但您也可以说,这些设备的使用寿命通常比我们实际使用的时间长得多。
该系统的大多数组件至少能使用10年,并且其中许多组件的合同保证可更换期长达10年。因此,他们开始研究回收利用设备,并将其与可用的替换部件结合,以提供成本更低的完整系统(因为他们重复使用了仍可工作的设备),从而分摊原始制造过程中的环境代价。这是非常出色的工作,鼓励您去观看那个视频,内容也很有趣。
在Erik去世后,FAST指导委员会意识到社区的巨大损失,决定将最佳论文奖更名为Erik Riedel最佳论文奖,我们即将颁发此奖。我认为这非常棒。
我期待明天晚上与任何想来和我们聊聊Erik Riedel及其成就的人们交谈。
谢谢。


总结
本节课中,我们一起学习了Erik Riedel对存储社区的贡献。从他的博士论文“Active Disks”开始,我们看到了计算存储这一核心概念的早期探索:即利用设备内部不断增强的计算能力来优化特定应用(如数据库查询)。其核心思想是通过FPGA等处理器在存储设备端进行数据预处理和缩减,以减少带宽需求。这项开创性工作为今天的NVMe CSD等标准奠定了基础。此外,我们还了解了他晚年对IT设备全生命周期碳足迹的关注,以及社区通过命名最佳论文奖来纪念他的方式。他的工作体现了将前沿学术研究与实际工程问题相结合的持久影响力。
039:Test of Time Award 获奖演讲与缓存算法演进 🏆

在本节课中,我们将学习FAST 2025存储大会上颁发的“Test of Time Award”(经得起时间考验奖)的详情,并深入了解获奖论文背后的核心思想——一种高效构建缓存未命中率曲线(MRC)的算法。我们将从奖项背景开始,逐步解析获奖研究的技术突破、历史渊源及其深远影响。
奖项介绍与评选过程 🏅
“Test of Time Award”设立至今已有13年。该奖项旨在表彰那些对存储社区产生重大影响的论文。
参评论文必须至少发表10年。其理念在于,获奖成果不应仅是发表时引人注目或获得最佳论文奖的工作,而应是真正产生了深远影响的研究。因此,需要给予足够的时间来验证其影响力。
评选工作由FAST指导委员会和往届程序委员会主席负责,具体流程由Raju和我管理。
以下是历届获奖者名单,我们在此不逐一回顾。你可以回顾这些论文,或许会想起其中一些真正出色的研究。

今年的评选过程如下:我们共有四篇提名论文,17位评审提交了总计63份评审意见。随后,Raji和我审阅了这些意见,并于一月份开会做出了最终选择。我们选出的论文,相信大家会一致认为是真正杰出的工作。
评审委员会对这篇论文给出了一些评价。一位评审提到:“我见过一些商业系统,包括存储系统和数据库系统,都从这项工作中汲取灵感,为客户和现场服务工程师提供了关于为其缓存分配更多或更少内存的预期效果的洞察。”


另一位评审说:“这篇论文真正革新了在线环境中的MRC使用。据我所知,它在已知的MRC曲线算法中性能最佳,或者至少是难以被超越的,并且非常精确。”
还有评审表示:“这篇论文使MRC变得实用并推广了其使用。事实上,我们公司经常使用所提出的算法或其变体,研究缓存策略的人员将这篇论文视为一个里程碑。”
基于这些评价,有些人可能已经猜到了获奖论文。
获奖论文是:《Efficient MRC Construction with SHARDS》,作者是Carl Waldspurger、Noham Park、Alexander Garthwaite和Irfan Ahmad。我们很幸运,他们今天都在现场领奖。现在,我将把讲台交给他们。

恭喜你们所有人。
获奖感言与研究起源 🎤
感谢Jeff,也感谢奖项委员会,我们很荣幸获得这个奖项。
我想花点时间回顾一下这项工作的起源和背景。当时,我们四人都在一家名为Cloud Physics的初创公司工作,该公司专注于虚拟化系统中的资源管理。
这项研究源于一个真实的客户挑战,一个现实世界的问题:为闪存缓存确定合适的大小。当时,闪存缓存与传统存储一起变得越来越流行。这提醒我们,尽管初创公司节奏紧迫,但优秀的研究仍然可以诞生,而且需求往往是激发最佳创意的动力。
我们首先从客户站点收集IO轨迹并测试缓存大小启发式方法,但结果不如预期。
我们意识到的一个更有原则的方法是生成未命中率曲线。这个概念可以追溯到1970年的Mattson。然而,即使是最好的现有方法也过于耗费资源,尤其是对于大型轨迹。
由于在之前的项目中使用过采样方法和技术,我们探索了采样是否能使MRC构建更高效。但这具有挑战性,因为在一个大型稀疏地址空间中进行采样,同时跟踪重用情况,是前所未有的事情。
关键洞察在于,使用确定性空间哈希可以驱动随机采样,这构成了SHARDS算法的基础。
我们的一些同事最初相当怀疑,但早期实验超出了预期。有一段时间,结果好得令人难以置信,我甚至担心可能有一个错误人为地夸大了我们的准确性。
但一切顺利。随后,我们继续开发了SHARDS的恒定空间变体,该变体能够用不到1MB的内存为任意长度的轨迹构建精确的MRC,这使得即使在资源最受限的系统中也变得可行。
在我们向FAST提交初稿和最终定稿之间,我们增加了将空间哈希应用于非堆栈替换算法(如ARC或LRS)的初步结果,为我们后续的微型模拟研究铺平了道路。
多年来,看到人们在这些想法基础上进行构建,真的非常令人兴奋。我们对此认可深表感激,谢谢。
缓存管理的历史脉络 📜
我是Irfan Ahmad。感谢Carl,他是这篇论文的领导者、第一作者,也是许多后续工作的主要贡献者。
正如Carl所说,回顾内存资源管理领域的工作,追溯到计算技术的黎明时期,那是一段非常有趣的时光。
Carl向大家介绍了导致这项特定研究的背景,而我想从更早的时候开始分享。让我们回到起点,分享一些我们学到的东西,其中许多你们可能知道,但有些人可能不了解。
当我们回顾并试图理解内存资源管理、置换算法(通常我们只称之为“缓存”)时,我们追溯并寻找它的起源。
这张图片是Atlas系统的控制台。描述其虚拟内存的论文发表于1962年,这意味着这项工作可能大约在1961年完成。据我所知,该系统在这篇论文于1962年4月发表时就已经在交付使用了。
实际上,这是一篇非常出色的论文。当我阅读它时,感觉就像在读一篇两天前发表的论文。它写得非常好,我们只需要将“磁鼓”替换为“SSD”,将“磁芯”替换为“DRAM”,它就能讲得通,甚至可能今天还能发表在FAST上。
这台机器使用了位,并试图以此来管理内存。但这是我所知的第一个虚拟内存系统。我们早些时候讨论过,我们都不记得有比这更早的了。
有趣的是,为什么这具有相关性?因为这台机器尽管具有创新性,但在生产部署中却遇到了灾难性的系统颠簸。系统运行良好,但突然会发生颠簸,整个系统就会卡住,控制台会变得无响应,你无法提交新任务。情况非常糟糕。
不仅如此,这在20世纪60年代实际上是一个相当普遍的问题。问题如此严重,以至于IBM在认为自己解决这个问题之前,不愿意交付基于虚拟内存的系统。当时市场上已经有多款其他系统了。
因此,在20世纪60年代中期,IBM启动了一个大型项目,研究其虚拟内存系统的置换算法。这是因为现场的程序员提出了需求,他们除了虚拟内存外不想用任何其他方式编程——既然有了虚拟内存,一切都变得简单得多,因为你可以获得零基址和连续的内存分配范围。
因此,出于无奈或必要性,他们启动了这个项目,这实际上引发了一系列事件,最终促成了我们的工作。
1966年,作为该项目的一部分,Belady在尝试研究LRU时发表了一篇论文,这篇论文被引用了极高的次数,因为我们几乎所有人在某个时间点都在处理内存管理或缓存替换策略,并且不得不与那个令人敬畏的MIN算法进行比较。
这个离线最优算法可以追溯到1966年。现在你可以看到,从1962年到1966年,我们已经经历了这个理解周期:嘿,你必须做更好的内存管理,你必须从根本和形式上研究这个问题。
这篇半经典的论文发表了。有趣的是,这同样可能像是今年FAST的论文:“希望通过基于先前引用来预测未来引用,以改进替换决策”。我们确实是站在巨人的肩膀上。
一些有趣的事情:左侧是Belady论文中的图表,X轴是缓存大小,Y轴是访问频率。这真的很有趣,类似的图表在今天发表的论文中仍然出现。这是我发现的第一个实际绘制了未命中率曲线的图表。可能之前就存在,我只是不知道。
他们构建这个曲线的方法是通过运行多个实验并在它们之间画线。这样你就可以看出工作负载在缓存效率方面的形状。这是非常重要的事情。
从1966年到1970年,人们已经厌倦了运行大量实验来绘制这些曲线。
因此,在1970年,IBM的Dick Mattson再次发表了他的开创性工作:一个单遍算法,可以为特定类型(即堆栈距离算法)的算法绘制整个曲线。正如Carl提到的,这在当时是革命性的,因为现在你只需要运行一遍就能得到完整的曲线,只要算法是堆栈距离型的(当时许多算法都是,只是最近其他算法才变得越来越流行)。
现在到了1971年。这篇论文发表了,但在工业环境中的采用率很低,因为对于长轨迹来说,运行起来非常困难,因为它是一个昂贵的算法。在随后的几十年里,数据结构得到了改进,但算法的复杂性没有改变。
这就是Carl、Alex、Noham和我在2011年、2012年所处的位置,我们试图解决这个问题,从那里开始,并意识到这个算法对于你想做的事情来说太昂贵了,这导致了Carl之前提到的近似算法。
研究轶事与算法精进 💡
一些有趣的故事:当我们写这篇论文时,你应该引用你想引用的东西的最早实例。在这个案例中,这是一个时钟算法。我们几个人讨论说:“嘿,你上次读原始的时钟算法论文是什么时候?”我没有读过,我想去找找看。
我们在Google和你能想到的每个搜索引擎上搜索,试图找到这篇论文的PDF。我们找不到。这怎么可能?在2014年(也许是2015年),互联网上怎么会没有这篇论文?
于是我们联系了一些朋友,去麻省理工学院图书馆的书架上找,看看能否找到Corbató(Fernando Corbato,图灵奖得主,领导了Multics项目,Unix由此而来,进而衍生出我们今天使用的所有东西,包括这台笔记本电脑上的操作系统)的论文。
Sam(我猜是他)去了,在麻省理工学院图书馆也找不到这篇论文,随后一阵恐慌。这篇被引用了成千上万次的论文怎么会不在互联网上,也不在图书馆里?那我们一直在引用什么?有人读过这篇论文吗?它在哪里?

幸运的是,麻省理工学院的Simon教授联系上了Corbató,他当时已经退休很久,80多岁了,正在度假。Corbató在度假时回复说:“嘿,我很惊讶没有副本,我想我阁楼的一个盒子里有一份。”
恐慌继续,我们希望他有一份,因为其他人都没有。总之,他度假回来,找到了论文。我们得到了PDF。现在,它终于可以在multicians.org上找到了,你可以直接搜索到它。
这只是有趣的故事,我们都经历过一些有趣的冒险和趣事,我们想分享其中的几个。
算法核心:调整采样与误差分布 🔧
我也想感谢委员会和合著者。这是一个有趣的项目,我仍然记得很清楚,尽管那是大约10年前的事了。
我第一次想到这个想法是在我面试Cloud Physics时,我还没有入职。在一次乘车途中(我不确定是不是出租车),Carl提到了这个想法。我当时想:“这行不通。”因为过程是非平稳的,如果你采样,你不会得到平均行为。我实际上这么说了。
那是我第一次见到Irfan。然后Carl说:“哦,好吧,但我还是想试试。”我回到学校。几周后,Carl给我发来了数据:“嘿,这是初步数据。我们做了这些轨迹。”
我花了整整一天试图证明它为什么行不通。在几个月里,我相当怀疑。直到我深入其中后,我感到困惑,并试图弄清楚它为什么实际上有效。这对我来说是一段有趣的旅程。
我也想感谢大家。这是一次非常美妙的经历。我想接着Carl提到的一点来说,那就是算法的惊人准确性。
特别值得一提的是,我们提出了调整后的SHARDS算法的概念。基本观察是,你希望从样本和模拟中得到的是对完整事件集的保真度,就像你根本没有采样一样。
这里的关键观察是:我们如何获取关于整个轨迹运行的、易于测量的某些信息,并将其与我们的特定样本的表现进行比较,然后调整样本以更接近原始情况。
对于MRC来说,基本观察是,你在MRC中测量的基本上是事件。尽管我们将其打印为比率,但实际上你得到的是每个距离上的计数。观察发现,距离越远(在某种意义上距离越长),这类事件的数量就越少,因为它们依赖于中间发生了许多其他事件。
一个有趣的性质是,在采样的情况下,因为距离越大,导致该距离发生的事件就越多,所以你的采样捕捉到该信号的可能性就越大,你会在那里得到一些结果。另一方面,如果你过度采样那些具有最大距离的事件,这很难做到,因为这类事件并不多,因为它们依赖于中间发生了太多其他事情。
观察结果是,如果存在误差,误差实际上会出现在小距离处。并非全部都在最小距离处,但大部分会集中在那里。这些是你可能少计或多计的地方。当你这样做时,会产生相当显著的影响。
因此,调整后的SHARDS算法本质上是:计算完整运行发生的事件总数,将其与我们样本中实际得到的事件数量进行比较。你期望的是,如果样本大小是整个集合的某个百分比,你测量到的事件数量也应该有类似的比例关系。如果不是,那么这实际上是对你误差程度的一个很好的估计。
因此,调整后的SHARDS算法基本上利用了这个差值来进行调整,主要调整最短距离,因为那是大部分误差所在。这实际上对提高准确性产生了深远的影响。
我喜欢这个故事的地方在于,它试图在样本显示的内容与你易于测量的整体真实情况之间建立联系,然后结合识别误差分布中的某些偏差,使你的模拟实际上变得更好。我真心认为这是一个更通用的技术,我一直在思考如何将其用于其他地方。
研究后续与未来展望 🚀
我想给大家快速更新一下研究进展。记得和我的合著者们坐下来讨论时,我们对这篇论文感到非常兴奋,那种兴奋感是巨大的,因为你感觉你真的发现了一些有趣的东西。
我们当时想,好吧,其他研究人员会跟进这项工作吗?这实际上是一个有趣的问题。其他人会尝试改进它或以我们未曾想到的方式应用它吗?我非常高兴在论文发表后很长一段时间里,我们之间不断来回发送基于这项工作并改进它或修复某些特定边界情况的论文。
我们中的许多人也在该领域发表了额外的著作,正如Carl所提到的。
我们现在已经有了一个证明,证明采样算法有效,并且有其准确性的界限,这真的令人兴奋。
此外,作为这项工作一部分形成的想法和模式,现在已经衍生出一家新公司,名为Magnian。它是一家商业公司,帮助设计存储设备、云块存储或内容分发网络的公司使用这些技术来优化其内存层次结构。
最后,我相信我们可以代表这里的每个人说,这项工作尚未完成。如果你来和我们交谈,我们会告诉你其他一些尚未探索的有趣领域。所以,如果你是对高影响力工作感兴趣的学生,在缓存和内存层次结构的预测性能建模领域存在着巨大的机会,并且在减少全球闲置内存所浪费的总能耗(千兆瓦级别)方面,有着实际的世界性影响机会。真正高影响力的工作等待着大家。


总结 📝


在本节课中,我们一起学习了FAST 2025“Test of Time Award”的获奖研究。我们回顾了该奖项的意义与评选标准,深入探讨了获奖论文《Efficient MRC Construction with SHARDS》的核心贡献:一种利用确定性空间哈希驱动随机采样,从而高效、精确构建缓存未命中率曲线(MRC)的算法。我们追溯了缓存管理算法从20世纪60年代虚拟内存系统到现代研究的历史脉络,理解了像MIN(Belady算法) 和 Mattson算法 这样的奠基性工作。最后,我们看到了这项研究如何从解决实际工业问题出发,通过创新的调整采样技术精进算法,并最终衍生出新的商业应用和未来的研究方向。这项工作是连接理论算法与工业实践、并持续产生影响的典范。

浙公网安备 33010602011771号