为何我重新实现了LVM(尽管保证更差)

Lobsters Hottest 工具

摘要

作者解释了为什么他们重新实现了LVM,尽管保证更差,以优化microVM在hypervisors上的启动性能,专注于使用Linux device-mapper的特定工作负载需求。

<p><a href="https://lobste.rs/s/khm6np/why_i_reimplemented_lvm_with_worse">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/08/18 16:21

# 为何我重新实现了LVM(牺牲了部分保证) 来源:https://depot.dev/blog/why-i-reimplemented-lvm > 等等,你重写了LVM,还让它变得更差了?! 这是我开始重新实现LVM时脑海里听到的问题——毕竟LVM已经稳定运行了二十年。但我无法为我们的microVM裸机管理程序使用原生的LVM2:它功能过于庞杂,对我们的工作负载假设过于简单。 ## 运行microVM很奇特 这一切始于我们转向在裸机管理程序上运行microVM来承载托管客户工作负载的沙箱。这意味着我们运行着昂贵的裸机,绝不应在客户工作负载之间浪费任何一毫秒——因此我们实现了亚秒级的microVM启动。 在本地SSD上实现亚秒级启动已是个不小的挑战,但我们还要使用高性能的网络存储做到这一点,因此我们不得不为自身(重新)发明一些工具。 **注意**:这一转变也意味着我们正在自动扩缩昂贵的裸机,因此启动管理程序并为microVM启动做准备所花费的每一秒都至关重要。 ## LVM究竟在做什么? 我们欣赏LVM的原因在于它极其简洁,且因为数据平面基于Linux内核的设备映射器基础设施,它在使用最少“黑魔法”的同时实现了良好性能。 然而LVM的设计初衷是相当安全并提供保障,例如确保在创建和删除卷时崩溃不会导致数据消失。更进一步,如果你误删了数据,你有很大机会能恢复它! 这是以全局锁(准确说是卷组范围锁)为代价的,如果每周只执行一次操作,这完全可以接受。但若一次操作需持有卷组锁约100毫秒,而你想每秒并行执行多达200次这样的操作,这就几乎不可能了。这让人不禁怀疑是否选错了工具。 ### LVM的保障 vs 我们实际需要(且愿意承担成本)的保障 锁定和数据安全确实是很棒的特性,但如果因此导致工作负载无法达到性能目标,你就无法使用该方案。我们面对LVM正是这种情况。 我们特别需要快速解决方案的场景之一是组装供管理程序启动microVM使用的块设备。本质上,它需要从远程主机获取网络块设备,在其上添加动态大小的缓存,最终生成单个块设备。 深入探讨本文最重要的部分:我们需要一种非常快速的方式从大型内存池中分配可变容量的RAM,然后通过设备映射器创建带缓存的块设备。 在此特定场景下,崩溃安全性并不能为我们带来太多价值。管理程序崩溃会导致临时microVM工作负载失败(因此被丢弃)以及易失性内存丢失(无论如何都会导致数据丢失),所以崩溃保护要么没有价值,要么没有需要保护的数据。 我们需要相当好的性能和高可靠性,因此我们使用了与LVM相同的设备映射器基础设施。只是我们的组装方式不同。 ## 解决方案架构 作为第一层,我们使用提供多种块设备的网络存储。这部分内容超出了本文范围,但我们拥有自己的存储代理和存储守护进程,它们负责呈现可变大小的块设备。 第二层是Linux内核提供的内存块设备。实现这一点有多种机制和模式,但最终结果基本相同:一个由我们存储代理管理的内存池。 一旦拥有此内存池,存储代理中的进程内分配器将对其进行管理。分配器在内存中维护映射、切片及所有其他数据,能够处理自定义大小和形状的分配。 由于此关键路径在进程内完成,我们无需打开磁盘或进行任何“真实”的IO操作,因此分配速度极快。这种分配是我们唯一需要某种锁定机制的地方,且仅用于内存映射。 由于映射确保了内存块设备使用不存在竞争,我们可以享受内核的异步设备映射器创建,相比相同用例下使用LVM实现了约100倍的性能提升。 尽管第一个原型仍然使用`dmsetup`,它已经足够满足我们的启动时间目标。只是它并不那么优雅。 ### 如果存储代理崩溃了怎么办? 最妙的是:我们实现了控制平面的智能部分(分配等),但我们不直接“编程数据平面”。内核保存着我们映射关系的关键部分。我们的代理会定期将映射保存到磁盘,但我们可以通过读取内核中的设备映射器映射来重建并验证它。这意味着我们的代理可以随时崩溃而不影响运行中的工作负载。即使恢复失败,现有的设备映射器设备仍会继续工作。 ### 为何不使用(其他存储技术)? 在此之前,我们评估了多种存储解决方案(它们各有千秋!),但没有一个完全适合。这篇博文聚焦于我们存储栈的一部分,但我想稍作扩展。 我们期望的架构和性能目标使得ZFS不适合我们,即使经过调优。Btrfs和Stratis也是如此,且它们都无法提供块设备。虽然存在环回设备,但我们不想走文件系统的方向。 更好的替代方案是让我们拥有更多自己的存储栈,但这是另一篇文章的主题。 ## 代价是什么? 开发过程比我预期的要快得多,但代价是调试一些与锁定、异步工作(指我们而非内核)、网络块设备协调(它们在本地呈现时可能随机耗时更长)以及各种奇怪行为相关的棘手场景。不过它很快就变得相当稳定了。 ## 值得吗? 值得!我认为有时重新发明基础技术是值得的,尤其是当它开辟了新途径时。 例如,否则我们需要花费数倍于这100毫秒的锁时间,才能在不到一秒的时间内启动一个microVM。有趣的是,我们并未重新发明整个栈,我们只是替换了处理极端稀缺资源的那个非常缓慢的控制平面。 我们并没有止步于此。*少做事*更快,但*不做事*最快。我们将这个理念应用到了启动路径的其他几个部分,但这些内容值得单独撰文说明。 下一步是拥有更多自己的存储栈。 ## 常见问题 **为何LVM对启动microVM来说太慢?** 我们启动microVM的启动时间目标非常紧张,必须权衡每一毫秒。LVM对卷操作需要卷组范围锁,在我们的环境中该锁大约持有100毫秒。每周一次操作时这微不足道。但当你想每秒并行执行多达200次操作来启动microVM时,单锁的串行化就会破坏你的延迟目标,而非底层数据路径。 **如果存储代理崩溃,运行中的microVM会怎样?** 它们会继续运行。内核保存着关键部分——实际的设备映射器映射,因此已组装的设备会继续处理IO,无论我们的代理状态如何。代理宕机期间你失去的是控制平面,所以在它恢复前不会有新分配。代理会定期将映射写入磁盘,也可以通过读取内核中的设备映射器状态来重建和验证映射。即使恢复失败,已组装的设备仍会继续工作。 **可以不使用LVM而直接使用设备映射器吗?** 可以。设备映射器是内核设施,你可以通过其ioctl接口直接编程,使用`dmsetup`这样的工具或从自己的进程操作。LVM是建立在它之上的控制平面,增加了元数据、锁定和恢复功能。这种分离对我们成为可能:我们保留了LVM使用的相同内核数据平面,只替换了其上的控制平面。我们的第一个原型使用`dmsetup`驱动设备映射器,已足够满足启动时间目标,只是不够优雅。 **在什么情况下放弃LVM的崩溃安全是错误的选择?** 当数据本应持久存在于崩溃时。我们的权衡之所以有效,是因为管理程序崩溃会同时导致临时microVM工作负载和支撑其设备的易失性内存失效,因此没有值得保护的剩余物。如果你的卷保存着持久数据,或者你想恢复意外删除的卷,LVM的元数据和锁定确实在为你做实际工作,这100毫秒是值得的。 Héja Péter (Vau)Héja Péter \(Vau\) Depot 高级基础设施工程师

相似文章

或许我们该重新审视微内核

Lobsters Hottest

本文认为,现代硬件(特别是IOMMU)通过消除上下文切换的开销,使微内核再次变得可行,并建议重新审视微内核设计以提高安全性和可靠性。