vinum 卷管理器
最后更新于
最后更新于
原文:
无论硬盘类型如何,总是存在潜在的问题。硬盘可能太小、太慢,或者可靠性不足,无法满足系统的要求。尽管硬盘的容量越来越大,但数据存储需求也在不断增加。常常需要一个比硬盘容量更大的文件系统。为了解决这些问题,已经提出并实现了各种解决方案。
其中一种方法是使用多个有时是冗余的硬盘。除了支持各种卡和控制器用于硬件独立磁盘冗余阵列(RAID)系统外,FreeBSD 基本系统还包括 vinum 卷管理器,它是一个块设备驱动程序,实现了虚拟磁盘驱动器,并解决了这三个问题。vinum 提供比传统磁盘存储更高的灵活性、性能和可靠性,并实现了 RAID
-0、RAID
-1 和 RAID
-5 模型,既可以单独使用,也可以组合使用。
本章概述了传统磁盘存储的潜在问题,并介绍了 vinum 卷管理器。
警告
已弃用 vinum,并且在 FreeBSD 15.0 及以后的版本中不再存在。建议用户迁移到 、、、 或 。
注意
从 FreeBSD 5 开始,vinum 被重写为适应 ,同时保留了原始的思想、术语和磁盘上的元数据。这个重写版本被称为 gvinum(即 GEOM vinum)。虽然本章使用 vinum 这个术语,但所有命令的调用应使用
gvinum
。内核模块的名称已从原来的 vinum.ko 更改为 geom_vinum.ko,所有设备节点位于 /dev/gvinum 下,而不是 /dev/vinum。从 FreeBSD 6 开始,原始的 vinum 实现不再出现在代码库中。
现代系统经常需要高并发地访问数据。例如,大型 FTP 或 HTTP 服务器可以维护数千个并发会话,并通过多个 100 Mbit/s 的连接与外部世界通信,这远超大多数硬盘的持续传输速率。
当前的硬盘可以以最高 70 MB/s 的速度顺序传输数据,但在许多独立进程访问同一硬盘的环境下,这个速度并不重要,而且它们可能只能达到这些数值的一小部分。在这种情况下,更有意义的是从磁盘子系统的角度来看问题。关键的参数是传输给子系统带来的负载,或者传输占用参与传输的硬盘的时间。
在任何磁盘传输中,硬盘必须先定位磁头,等待第一个扇区通过读写磁头,然后执行传输。这些动作可以视为原子操作,因为中途打断它们是没有意义的。
假设一次典型的传输大约是 10 kB:当前一代高性能硬盘平均定位磁头的时间是 3.5 毫秒。最快的硬盘转速为每分钟 15,000 转,因此平均旋转延迟(半圈)是 2 毫秒。在 70 MB/s 的传输速率下,传输本身大约需要 150 微秒,这几乎可以忽略不计,相比之下,定位时间占据了绝大部分时间。在这种情况下,实际的传输速率降到了每秒 1 MB 以上,并且显然高度依赖于传输的大小。
传统且明显的解决方法是“更多的盘片”:与其使用一块大硬盘,不如使用多个较小的硬盘,总存储空间相同。每个硬盘都能独立进行定位和传输,因此有效吞吐量将接近所用硬盘的数量。
然而,实际的吞吐量提高幅度小于所用硬盘的数量。尽管每个硬盘都能够并行传输,但无法确保请求能均匀分布在各个硬盘上。不可避免地,某些硬盘的负载将高于其他硬盘。
图 1. 连接组织
另一种映射方法是将地址空间划分为更小的、大小相等的单元,并将它们顺序存储在不同的设备上。例如,前 256 个扇区存储在第一块硬盘上,接下来的 256 个扇区存储在第二块硬盘上,以此类推。直到最后一块硬盘填满,过程重复进行。这个映射方法称为 条带化(striping) 或 RAID-0。
图 2. 条带化组织
硬盘的最终问题是它们不可靠。尽管近年来硬盘的可靠性大大提高,但硬盘仍然是服务器中最容易发生故障的核心组件。待发生故障,后果可能是灾难性的,更换故障的硬盘并恢复数据可能导致服务器停机。
解决这个问题的一种方法是 镜像(mirroring),即 RAID-1
,它将数据保存在不同的物理硬件上各有一份副本。对卷的任何写入操作都会同时写入两个硬盘;读取操作可以从任何一个硬盘上获取,因此如果一个硬盘发生故障,数据仍然可以从另一个硬盘上获得。
镜像有两个问题:
它需要比非冗余解决方案多一倍的磁盘存储空间。
写入操作必须同时写入两个硬盘,因此它们占用了比非镜像卷多一倍的带宽。读取操作不受性能影响,甚至可能更快。
另一种解决方案是 奇偶校验(parity),它在 RAID
2、3、4 和 5 等级中得到了实现。其中,RAID-5
是最有趣的。在 vinum 中,它是条带化组织的一种变体,其中每个条带中的一个块用于存储其他块的奇偶校验块。vinum 中实现的 RAID-5
结构类似于条带化结构,只不过它通过在每个条带中包含一个奇偶校验块来实现 RAID-5
。根据 RAID-5
的要求,这个奇偶校验块的位置从一个条带到下一个条带不断变化。数据块中的数字表示相对块号。
图 3. RAID
-5 组织结构
与镜像相比,RAID-5
的优势在于它显著减少了存储空间的需求。读取访问与条带化组织类似,但写入访问明显更慢,约为读取性能的 25%。如果一个硬盘发生故障,阵列仍然可以在降级模式下运行,剩余可访问硬盘中的读取操作仍然正常进行,而故障硬盘的读取则会通过从其他剩余硬盘的相应块重新计算得到。
为了解决这些问题,vinum 实现了一个四级层次结构的对象体系:
最显著的对象是虚拟磁盘,称为 卷(volume)。卷本质上与 UNIX® 磁盘驱动器具有相同的属性,尽管有一些小的区别。例如,卷没有大小限制。
卷由 plex 组成,每个 plex 表示一个卷的整个地址空间。这个层次结构提供了冗余。可以把 plex 看作是镜像阵列中的单个硬盘,每个硬盘都包含相同的数据。
由于 vinum 存在于 UNIX® 磁盘存储框架中,因此可以使用 UNIX® 分区作为构建多磁盘 plex 的基础块。事实上,这样做太不灵活,因为 UNIX® 磁盘只能有有限数量的分区。因此,vinum 将单个 UNIX® 分区(称为 驱动器)划分为连续区域,这些区域称为 子磁盘(subdisk),并将它们作为构建 plex 的基础块。
子磁盘位于 vinum 驱动器 上,当前是 UNIX® 分区。vinum 驱动器可以包含任意数量的子磁盘。除了驱动器开头的一个小区域(用于存储配置和状态信息)外,整个驱动器都可以用于数据存储。
以下部分介绍了这些对象如何提供 vinum 所需的功能。
Plex 可以包含多个子磁盘,这些子磁盘分布在 vinum 配置中的所有驱动器上。因此,单个驱动器的大小不会限制 plex 或卷的大小。
vinum 通过将多个 plex 附加到一个卷来实现镜像。每个 plex 表示卷中的数据。一个卷可以包含从一个到八个 plex。
虽然一个 plex 代表一个卷的完整数据,但某些表示部分可能会物理缺失,可能是有意的(通过不为 plex 的某些部分定义子磁盘)或偶然的(由于驱动器故障)。只要至少一个 plex 能提供卷完整地址范围的数据,卷就能正常工作。
vinum 在 plex 层次上实现了连接和条带化:
连接 plex 使用每个子磁盘的地址空间。连接的 plex 是最灵活的,因为它们可以包含任意数量的子磁盘,且子磁盘的长度可以不同。通过添加额外的子磁盘,可以扩展 plex。与条带化 plex 相比,它们所需的 CPU 时间较少,尽管 CPU 开销的差异不可测量。另一方面,连接的 plex 更容易出现热点,其中一个硬盘非常活跃,而其他硬盘处于空闲状态。
条带化 plex 将数据条带化存储在每个子磁盘上。所有子磁盘必须具有相同的大小,且必须至少有两个子磁盘,以将其与连接 plex 区分开来。条带化 plex 的最大优势是它们减少了热点问题。通过选择最佳大小的条带(大约 256 kB),可以平衡各个组件驱动器的负载。通过添加新的子磁盘扩展 plex 是非常复杂的,vinum 没有实现这种扩展。
表 1. vinum Plex 组织方式
连接式
1
是
否
大型数据存储,具有最大灵活性和适度性能
条带化
2
否
是
高性能,适用于高度并发访问
配置文件介绍了单个 vinum 对象。一个简单卷的定义可能如下所示:
这个文件介绍了四个 vinum 对象:
drive 行介绍了一个磁盘分区(驱动器)及其相对于底层硬件的位置。它被赋予符号名称 a。这种将符号名称与设备名称分开的方法允许磁盘在不同位置之间移动而不会产生混淆。
volume 行介绍了一个卷。唯一需要的属性是名称,这里是 myvol。
plex 行定义了一个 plex。唯一必需的参数是组织方式,这里是 concat。不需要名称,因为系统会通过添加后缀 .px 自动生成名称,其中 x 是该卷中 plex 的编号。因此,这个 plex 将被命名为 myvol.p0。
sd 行介绍了一个子磁盘。最小规格是存储子磁盘的驱动器名称和子磁盘的长度。无需名称,因为系统会通过添加后缀 .sx 自动分配名称,其中 x 是该 plex 中子磁盘的编号。因此,vinum 将此子磁盘命名为 myvol.p0.s0。
图 4. 简单的 vinum 卷
该图以及随后出现的图形表示了一个卷,其中包含 plex,而 plex 又包含子磁盘。在这个例子中,卷包含一个 plex,而该 plex 包含一个子磁盘。
这个特定的卷与常规磁盘分区没有特别的优势。它包含一个单一的 plex,因此没有冗余。该 plex 包含一个子磁盘,因此与常规磁盘分区在存储分配上没有区别。接下来的部分将展示更有趣的配置方法。
可以通过镜像来增加卷的弹性。在布置一个镜像卷时,重要的是要确保每个 plex 的子磁盘位于不同的驱动器上,这样才能避免驱动器故障同时影响两个 plex。以下配置展示了一个镜像卷:
在这个例子中,无需再次定义驱动器 a,因为 vinum 会在其配置数据库中跟踪所有对象。处理完这个定义后,配置变成了:
图 5. 一个镜像的 vinum 卷
在这个例子中,每个 plex 包含完整的 512 MB 地址空间。如同之前的例子,每个 plex 只包含一个子磁盘。
前面例子中的镜像卷比未镜像的卷更能抵抗故障,但其性能较差,因为每次对卷的写入都需要同时写入到两个驱动器,占用了更大比例的磁盘带宽。性能考量需要另一种方法:不使用镜像,而是将数据条带化分布到尽可能多的磁盘驱动器上。以下配置展示了一个条带化卷,plex 被条带化到四个磁盘驱动器上:
如之前一样,无需定义已经为 vinum 所知的驱动器。处理完这个定义后,配置变成了:
图 6. 条带化的 vinum 卷
借助足够的硬件,可以构建出相较于标准 UNIX® 分区具有更高韧性和性能的卷。一个典型的配置文件可能如下所示:
第二个 plex 的子磁盘与第一个 plex 的子磁盘错开了两个驱动器。这有助于确保写入操作不会写入到相同的子磁盘,即使传输跨越了两个驱动器。
图 7. 一个镜像条带化的 vinum 卷
vinum 为 plex 和子磁盘分配默认名称,尽管可以覆盖这些名称。我们不建议覆盖默认名称,因为这样做不会带来显著的优势,且可能导致混淆。
名称可以包含任何非空白字符,但建议仅限字母、数字和下划线字符。卷、plex 和子磁盘的名称最长可达 64 个字符,驱动器的名称最长可达 32 个字符。
vinum 对象在 /dev/gvinum 层次结构中分配设备节点。上面的配置将导致 vinum 创建以下设备节点:
每个卷的设备条目。这些是 vinum 使用的主要设备。上述配置会包括设备 /dev/gvinum/myvol、/dev/gvinum/mirror、/dev/gvinum/striped、/dev/gvinum/raid5 和 /dev/gvinum/raid10。
所有卷都会在 /dev/gvinum/ 下获得直接条目。
目录 /dev/gvinum/plex 和 /dev/gvinum/sd,分别包含每个 plex 和每个子磁盘的设备节点。
例如,考虑以下配置文件:
尽管建议不要为 plex 和子磁盘分配特定名称,但 vinum 驱动器必须命名。这使得在将驱动器移动到不同位置时,仍然可以自动识别它。驱动器名称最长可达 32 个字符。
vinum 将配置信息存储在磁盘切片中,形式与配置文件基本相同。当从配置数据库读取时,vinum 会识别一些在配置文件中不允许使用的关键字。例如,磁盘配置可能包含以下文本:
显而易见的区别是位置和命名信息的存在,这些信息是允许但不推荐的,以及状态信息。vinum 不存储关于驱动器的任何信息,它通过扫描配置的磁盘驱动器来查找具有 vinum 标签的分区。这使得 vinum 能够正确识别驱动器,即使它们被分配了不同的 UNIX® 驱动器 ID。
当 vinum 使用 gvinum start
启动时,vinum 会从某个 vinum 驱动器读取配置数据库。在正常情况下,每个驱动器包含配置数据库的副本,因此读取哪个驱动器并不重要。然而,在崩溃之后,vinum 必须确定哪个驱动器是最新更新的,并从该驱动器读取配置。然后,它会根据需要从逐渐较旧的驱动器更新配置。
对于具有完全镜像文件系统的机器,使用 vinum 作为根文件系统也是理想的。设置这种配置比镜像一个任意文件系统要复杂一些,因为:
根文件系统必须在引导过程中很早就可用,因此 vinum 基础设施必须在此时已经可用。
包含根文件系统的卷还包含系统引导程序和内核。这些必须使用主机系统的本地工具(如 BIOS)读取,而这些工具通常无法了解 vinum 的细节。
在以下章节中,术语“根卷”通常用来描述包含根文件系统的 vinum 卷。
当前的 FreeBSD 引导程序只有 7.5 KB 的代码,并且不能理解 vinum 的内部结构。这意味着它无法解析 vinum 配置数据,也无法识别启动卷的元素。因此,需要一些解决方法来为引导程序提供一个标准 a
分区的假象,该分区包含根文件系统。
为了使这种情况成为可能,根卷必须满足以下要求:
根卷不能是条带化(stripe)或 RAID
-5 类型。
根卷的每个 plex 中不能包含超过一个连接子磁盘(concatenated subdisk)。
需要注意的是,使用多个 plex 每个包含一个根文件系统的副本是可行的,并且是期望的。引导程序过程将只使用一个副本来查找引导程序和所有启动文件,直到内核挂载根文件系统为止。这些 plex 中的每个单独的子磁盘需要其自己的 a
分区假象,以便相应的设备可以引导系统。严格来说,这些伪造的 a
分区不必位于每个设备中的相同偏移量,但为了避免混淆,最好按这种方式创建 vinum 卷,使结果镜像设备是对称的。
为了为每个包含根卷部分的设备设置这些 a
分区,需要执行以下步骤:
使用以下命令检查该设备的根卷子磁盘的位置、偏移量以及大小:
vinum 的偏移量和大小是以字节为单位的。必须将这些值除以 512,以获得要用于 bsdlabel
的块号。
对参与根卷的每个设备运行以下命令:
devname 必须是磁盘的名称,例如没有切片表的磁盘的名称 da0,或切片名称,例如 ad0s1。
如果设备上已经存在一个 a
分区(来自预 vinum 根文件系统),则应将其重命名为其他名称,以便它仍然可访问(以防万一),但默认情况下不再用于引导系统。当前挂载的根文件系统不能被重命名,因此必须在从 "Fixit" 媒体引导时执行此操作,或者在镜像中使用两步过程,首先操作当前未引导的磁盘。
必须将该设备上的 vinum 分区的偏移量(如果有)与根卷子磁盘的偏移量相加。所得的值将成为新 a
分区的 offset
值。这个分区的 size
值可以直接从上面的计算中获得。fstype
应该是 4.2BSD
。fsize
、bsize
和 cpg
的值应选择与实际文件系统相匹配,尽管在这个上下文中,它们并不是特别重要。
这样,就会建立一个新的 a
分区,它将与该设备上的 vinum 分区重叠。只有当 vinum 分区已正确标记为 vinum
文件系统类型时,bsdlabel
才会允许这种重叠。
现在,所有包含根卷副本的设备上都有一个伪造的 a
分区。强烈建议使用以下命令验证结果:
请记住,所有包含控制信息的文件都必须相对于根文件系统,这个根文件系统在 vinum 卷中,在设置新的 vinum 根卷时,可能与当前活动的根文件系统不匹配。因此,必须特别小心处理 /etc/fstab 和 /boot/loader.conf 文件。
在下次重启时,引导程序应该能够从新的基于 vinum 的根文件系统获取相应的控制信息,并相应地进行操作。内核初始化过程结束时,在所有设备都已声明之后,显示的成功消息应该类似于:
在设置好 vinum 根卷之后,运行 gvinum l -rv root
的输出可能如下所示:
需要注意的值是 135680
,即相对于分区 /dev/da0h 的偏移量。这在 bsdlabel
中等同于 265 个 512 字节的磁盘块。类似地,根卷的大小是 245760 个 512 字节的块。包含第二个副本的 /dev/da1h 设备有对称的设置。
这些设备的 bsdlabel
可能如下所示:
在上面的示例中,整个设备专门用于 vinum,并且没有剩余的预 vinum 根分区。
以下是一些已知的常见问题和解决方法。
如果由于某种原因系统无法继续启动,可以通过在 10 秒警告时按下空格键来中断引导程序。可以通过输入 show
来检查引导程序变量 vinum.autostart
,并使用 set
或 unset
进行修改。
如果 vinum 内核模块尚未列在自动加载的模块列表中,可以输入 load geom_vinum
来加载它。
加载完成后,可以输入 boot -as
继续启动过程,-as
参数请求内核询问要挂载的根文件系统(-a
),并使启动过程在单用户模式下停止(-s
),此时根文件系统以只读方式挂载。这样,即使只有一个 plex 的多 plex 卷被挂载,也不会导致 plex 之间的数据不一致。
在提示输入根文件系统时,可以输入任何包含有效根文件系统的设备。如果 /etc/fstab 配置正确,默认值应该类似于 ufs:/dev/gvinum/root
。一个典型的替代选择可能是 ufs:da0d
,它可能是包含预 vinum 根文件系统的假设分区。如果此处输入的是某个别名 a
分区,必须确保它实际引用的是 vinum 根设备的子磁盘,因为在镜像设置中,这只会挂载一个镜像根设备的一部分。如果此文件系统以后要以读写方式挂载,则需要移除其他 plex,因为这些 plex 会携带不一致的数据。
如果引导程序在 vinum 安装后被破坏,就会发生这种情况。不幸的是,vinum 会在其分区的开始处意外地留下 4 KB 的空闲空间,然后才开始写入 vinum 头信息。然而,第一阶段和第二阶段的引导程序以及 bsdlabel
需要 8 KB。因此,如果 vinum 分区从 0 偏移量开始,且该分区位于一个本应可启动的切片或磁盘中,vinum 设置将破坏引导程序。
同样,如果通过从 "Fixit" 媒体启动并使用 bsdlabel -B
重新安装引导程序来恢复上述情况,重新安装的引导程序会破坏 vinum 头,导致 vinum 无法找到其磁盘。尽管实际的 vinum 配置数据和 vinum 卷中的数据不会被破坏,并且可以通过重新输入相同的 vinum 配置数据来恢复所有数据,但这种情况很难修复。需要将整个 vinum 分区移动至少 4 KB,以避免 vinum 头和系统引导程序的冲突。
硬盘负载的均匀性与数据在硬盘上的分布方式密切相关。以下讨论中,便于理解的方式是将磁盘存储看作一个包含大量数据扇区的空间,可以通过编号访问,就像一本书的页面一样。最明显的方法是将虚拟磁盘划分为多个连续的扇区组,每组大小与单个硬盘的大小相同,并按此方式存储,类似于将一本大书撕成若干小部分。该方法称为 连接(concatenation),其优点是硬盘不需要有任何特定的大小关系。当访问虚拟磁盘时,访问均匀分布在其地址空间时,这种方法效果良好。如果访问集中在较小的区域,效果则较差。 说明了在连接组织中,存储单元分配的顺序。
RAID
提供了各种形式的容错机制,但 RAID-0 有些误导,因为它没有提供冗余。条带化需要稍多的工作来定位数据,并且当传输跨多个硬盘时,它可能会导致额外的 I/O 负载,但它也可以提供更均匀的硬盘负载。 说明了在条带化组织中,存储单元分配的顺序。
总结了每种 plex 组织方式的优缺点。
vinum 维护一个 配置数据库,介绍了系统中已知的对象。用户最初通过一个或多个配置文件使用 创建配置数据库。vinum 在每个受其控制的硬盘 设备 上存储其配置数据库的副本。该数据库在每次状态变化时更新,以便在重启时准确恢复每个 vinum 对象的状态。
在处理完该文件后, 会产生以下输出:
此输出显示了 的简要列出格式。图形化表示见于 。
以图形化方式展示了结构。
此卷在 中进行了表示。条带的深浅表示在 plex 地址空间中的位置,最浅的条带在前,最深的条带在后。
表示该卷的结构。
处理此文件后, 会在 /dev/gvinum 中创建以下结构:
卷对系统的表现与磁盘相同,唯一的例外是,vinum 不对卷进行分区,因此卷中没有分区表。这要求对一些磁盘工具进行修改,特别是 ,以防它尝试将 vinum 卷名的最后一个字母解释为分区标识符。例如,磁盘驱动器的名称可能是 /dev/ad0a 或 /dev/da2h。这些名称分别表示第一个(0)IDE 磁盘(ad)上的第一个分区(a)和第三个(2)SCSI 磁盘(da)上的第八个分区(h)。相比之下,vinum 卷可能被称为 /dev/gvinum/concat,它与分区名称没有任何关系。
要在此卷上创建文件系统,请使用 :
GENERIC 内核不包含 vinum。可以构建一个包含 vinum 的自定义内核,但不推荐这样做。启动 vinum 的标准方法是将其作为内核模块加载。因为 启动时会检查模块是否已加载,如果没有,它会自动加载模块,所以不需要使用 。
Gvinum 总是能够在内核模块加载后通过 实现自动启动。要在启动时加载 Gvinum 模块,请将 geom_vinum_load="YES"
添加到 /boot/loader.conf。
为了确保 vinum 在系统启动时能被加载,必须让 能够在启动内核之前加载 vinum 内核模块。可以通过在 /boot/loader.conf 文件中加入以下行来实现:
可以观察到,伪造的 a
分区的 size
参数与上面计算出的值相匹配,而 offset
参数是 vinum 分区 h
内的偏移量与该分区在设备或切片中的偏移量之和。这是避免 中描述的问题的典型设置。整个 a
分区完全位于包含所有 vinum 数据的 h
分区内。
如果 /boot/loader 无法加载,但主引导程序仍然加载(在启动过程开始后屏幕的左列可见一个破折号),可以尝试通过按空格键中断主引导程序。这样可以使引导程序停在 进行操作。在此阶段,可以尝试从备用分区启动,例如包含先前根文件系统的分区。