ZoneFS - 分区块设备的文件系统

简介

zonefs 是一个非常简单的文件系统,它将分区块设备的每个分区公开为一个文件。与具有原生分区块设备支持的常规 POSIX 兼容文件系统(例如 f2fs)不同,zonefs 不会对用户隐藏分区块设备的顺序写入约束。代表设备顺序写入分区的文件必须从文件末尾开始按顺序写入(仅追加写入)。

因此,zonefs 本质上更接近于原始块设备访问接口,而不是功能齐全的 POSIX 文件系统。zonefs 的目标是通过用更丰富的文件 API 替换原始块设备文件访问来简化应用程序中对分区块设备的支持,从而避免依赖可能对开发人员来说更晦涩难懂的直接块设备文件 ioctl。这种方法的一个例子是在分区块设备上实现 LSM(日志结构合并)树结构(例如 RocksDB 和 LevelDB 中使用的),方法是允许 SSTable 存储在分区文件中,类似于常规文件系统,而不是作为整个磁盘的扇区范围。引入更高级的构造“一个文件是一个分区”可以帮助减少应用程序中所需的更改量,并引入对不同应用程序编程语言的支持。

分区块设备

分区存储设备属于一类存储设备,其地址空间被划分为多个分区。一个分区是连续 LBA 的一个组,并且所有分区都是连续的(没有 LBA 间隙)。分区可能具有不同的类型。

  • 常规分区:属于常规分区的 LBA 没有访问约束。可以执行任何读取或写入访问,类似于常规块设备。

  • 顺序分区:这些分区接受随机读取,但必须按顺序写入。每个顺序分区都有一个由设备维护的写指针,用于跟踪设备下一次写入的强制起始 LBA 位置。由于这种写入约束,顺序分区中的 LBA 不能被覆盖。在重写之前,必须首先使用特殊命令(分区重置)擦除顺序分区。

可以使用各种记录和媒体技术来实现分区存储设备。当今最常见的形式是使用 Shingled Magnetic Recording (SMR) HDD 上的 SCSI Zoned Block Commands (ZBC) 和 Zoned ATA Commands (ZAC) 接口。

固态硬盘 (SSD) 存储设备也可以实现分区接口,例如,减少由于垃圾回收导致的内部写入放大。NVMe Zoned NameSpace (ZNS) 是 NVMe 标准委员会的技术提案,旨在向 NVMe 协议添加分区存储接口。

Zonefs 概述

Zonefs 将分区块设备的分区公开为文件。代表分区的文件的按分区类型分组,分区类型本身由子目录表示。此文件结构完全使用设备提供的分区信息构建,因此不需要任何复杂的磁盘元数据结构。

磁盘元数据

zonefs 磁盘元数据减少为一个不可变的超级块,该超级块持久存储一个幻数和可选的特征标志和值。挂载时,zonefs 使用 blkdev_report_zones() 获取设备分区配置,并仅根据此信息使用静态文件树填充挂载点。文件大小来自设备分区类型和设备本身管理的写指针位置。

超级块始终写入磁盘上的扇区 0。存储超级块的设备的第一个分区永远不会被 zonefs 公开为分区文件。如果包含超级块的分区是一个顺序分区,则 mkzonefs 格式化工具总是“完成”该分区,即将其转换为完整状态以使其只读,从而防止任何数据写入。

分区类型子目录

代表相同类型分区的文件的都集中在挂载时自动创建的同一个子目录下。

对于常规分区,使用子目录“cnv”。但是,只有当设备具有可用的常规分区时才会创建此目录。如果设备在扇区 0 只有一个常规分区,则该分区不会公开为文件,因为它将用于存储 zonefs 超级块。对于此类设备,将不会创建“cnv”子目录。

对于顺序写入分区,使用子目录“seq”。

这两个目录是 zonefs 中存在的唯一目录。用户无法创建其他目录,也无法重命名或删除“cnv”和“seq”子目录。

由 stat() 或 fstat() 系统调用获得的 struct stat 的 st_size 字段指示的目录大小指示目录下的文件数量。

分区文件

分区文件使用它们在特定类型分区集中代表的分区编号命名。也就是说,“cnv”和“seq”目录都包含名为“0”、“1”、“2”的文件。文件编号还表示设备上递增的分区起始扇区。

不允许对超出文件最大大小(即超出分区容量)的分区文件执行所有读取和写入操作。任何超过分区容量的访问都会失败,并显示 -EFBIG 错误。

不允许创建、删除、重命名或修改文件和子目录的任何属性。

stat() 和 fstat() 报告的文件块数表示分区文件的容量,换句话说,就是最大文件大小。

常规分区文件

常规分区文件的大小固定为它们代表的分区的大小。常规分区文件不能被截断。

这些文件可以使用任何类型的 I/O 操作随机读取和写入:缓冲 I/O、直接 I/O、内存映射 I/O (mmap) 等。除了上面提到的文件大小限制之外,这些文件没有 I/O 约束。

顺序分区文件

“seq”子目录中分组的顺序分区文件的大小表示文件相对于分区起始扇区的分区写指针位置。

顺序分区文件只能从文件末尾开始按顺序写入,也就是说,写入操作只能是追加写入。Zonefs 不会尝试接受随机写入,并且会使任何具有不对应于文件末尾或未完成的最后一个已发出的写入(对于异步 I/O 操作)的起始偏移量的写入请求失败。

由于页面缓存的脏页面写回不能保证顺序写入模式,因此 zonefs 会阻止顺序文件上的缓冲写入和可写共享映射。只有直接 I/O 写入才能被这些文件接受。 zonefs 依赖于块层电梯实现的将写入 I/O 请求顺序传递给设备。必须使用实现分区块设备的顺序写入功能的电梯(ELEVATOR_F_ZBD_SEQ_WRITE 电梯功能)。默认情况下,在设备初始化时为分区块设备设置此类型的电梯(例如 mq-deadline)。

对于顺序分区文件中的读取操作使用的 I/O 类型没有限制。接受缓冲 I/O、直接 I/O 和共享读取映射。

仅允许将顺序分区文件截断为 0,在这种情况下,分区将重置以将文件分区写指针位置倒回到分区开始处,或者截断为分区容量,在这种情况下,文件分区将转换为 FULL 状态(完成分区操作)。

格式化选项

可以在格式化时启用 zonefs 的几个可选功能。

  • 常规分区聚合:可以将连续常规分区的范围聚合为单个更大的文件,而不是默认的每个分区一个文件。

  • 文件所有权:分区文件的所有者 UID 和 GID 默认值为 0(root),但可以更改为任何有效的 UID/GID。

  • 文件访问权限:可以更改默认的 640 访问权限。

IO 错误处理

分区块设备可能会因与常规块设备类似的原因(例如,由于坏扇区)而导致 I/O 请求失败。但是,除了这种已知的 I/O 故障模式外,管理分区块设备行为的标准还定义了导致 I/O 错误的附加条件。

  • 一个分区可能会转换为只读状态 (BLK_ZONE_COND_READONLY):虽然分区中已写入的数据仍然可读,但该分区不能再写入。用户对分区(分区管理命令或读/写访问)的任何操作都无法将分区状态改回正常的读/写状态。虽然设备将分区转换为只读状态的原因未由标准定义,但这种转换的典型原因将是 HDD 上的有缺陷的写磁头(此磁头下的所有分区都更改为只读)。

  • 一个分区可能会转换为离线状态 (BLK_ZONE_COND_OFFLINE):无法读取或写入离线分区。没有任何用户操作可以将离线分区转换回可操作的良好状态。与分区只读转换类似,驱动器将分区转换为离线状态的原因未定义。典型的原因是 HDD 上有缺陷的读写磁头导致断裂的磁头下的盘片上的所有分区都无法访问。

  • 未对齐的写入错误:这些错误是由于主机发出写入请求时,起始扇区不对应于设备执行写入请求时的分区写指针位置而导致的。即使 zonefs 强制顺序分区进行顺序文件写入,在拆分为多个 BIO/请求或异步 I/O 操作的非常大的直接 I/O 操作部分失败的情况下,仍然可能发生未对齐的写入错误。如果发往设备的一组顺序写入请求中的一个写入请求失败,则在其之后排队的所有写入请求都将变为未对齐状态并失败。

  • 延迟写入错误:与常规块设备类似,如果启用了设备端写入缓存,则当设备写入缓存刷新时(例如在 fsync() 上),可能会在先前完成的写入范围内发生写入错误。与之前的立即未对齐写入错误情况类似,延迟写入错误可能会通过分区的缓存顺序数据流传播,导致在导致错误的扇区之后丢弃所有数据。

zonefs 检测到的所有 I/O 错误都会通过触发或检测到错误的系统调用的错误代码返回来通知用户。zonefs 响应 I/O 错误而采取的恢复操作取决于 I/O 类型(读取与写入)和错误原因(坏扇区、未对齐的写入或分区状态更改)。

  • 对于读取 I/O 错误,zonefs 不会执行任何特定的恢复操作,但前提是文件分区仍处于良好状态,并且文件 inode 大小与其分区写指针位置之间没有不一致。如果检测到问题,则执行 I/O 错误恢复(请参阅下表)。

  • 对于写入 I/O 错误,始终执行 zonefs I/O 错误恢复。

  • 分区状态更改为只读或离线也始终会触发 zonefs I/O 错误恢复。

Zonefs 最小 I/O 错误恢复可能会更改文件大小和文件访问权限。

  • 文件大小更改:顺序分区文件中的立即或延迟写入错误可能会导致文件 inode 大小与文件中成功写入的数据量不一致。例如,多 BIO 大型写入操作的部分失败将导致分区写指针部分前进,即使整个写入操作将被报告为对用户失败。在这种情况下,必须提前文件 inode 大小以反映分区写指针更改,并最终允许用户在文件末尾重新开始写入。还可以减小文件大小以反映在 fsync() 上检测到的延迟写入错误:在这种情况下,分区中有效写入的数据量可能少于最初由文件 inode 大小指示的数据量。在此类 I/O 错误之后,zonefs 始终修复文件 inode 大小,以反映持久存储在文件分区中的数据量。

  • 访问权限更改:分区状态更改为只读状态,这通过文件访问权限的更改来指示,以使文件变为只读状态。这将禁用对文件属性和数据修改的更改。对于离线分区,将禁用对文件的所有权限(读取和写入)。

用户可以使用“errors=xxx”挂载选项控制 zonefs I/O 错误恢复采取的进一步操作。下表总结了 zonefs I/O 错误处理的结果,具体取决于挂载选项和分区状态

+--------------+-----------+-----------------------------------------+
|              |           |            Post error state             |
| "errors=xxx" |  device   |                 access permissions      |
|    mount     |   zone    | file         file          device zone  |
|    option    | condition | size     read    write    read    write |
+--------------+-----------+-----------------------------------------+
|              | good      | fixed    yes     no       yes     yes   |
| remount-ro   | read-only | as is    yes     no       yes     no    |
| (default)    | offline   |   0      no      no       no      no    |
+--------------+-----------+-----------------------------------------+
|              | good      | fixed    yes     no       yes     yes   |
| zone-ro      | read-only | as is    yes     no       yes     no    |
|              | offline   |   0      no      no       no      no    |
+--------------+-----------+-----------------------------------------+
|              | good      |   0      no      no       yes     yes   |
| zone-offline | read-only |   0      no      no       yes     no    |
|              | offline   |   0      no      no       no      no    |
+--------------+-----------+-----------------------------------------+
|              | good      | fixed    yes     yes      yes     yes   |
| repair       | read-only | as is    yes     no       yes     no    |
|              | offline   |   0      no      no       no      no    |
+--------------+-----------+-----------------------------------------+

更多说明

  • 如果未指定任何 errors 挂载选项,“errors=remount-ro”挂载选项是 zonefs I/O 错误处理的默认行为。

  • 使用“errors=remount-ro”挂载选项,将文件访问权限更改为只读状态适用于所有文件。文件系统以只读方式重新挂载。

  • 由于设备将分区转换为离线状态而导致的访问权限和文件大小更改是永久性的。使用 mkfs.zonefs (mkzonefs) 重新挂载或重新格式化设备不会将离线分区文件更改回良好状态。

  • 由于设备将分区转换为只读状态而导致的文件访问权限更改是永久性的。重新挂载或重新格式化设备不会重新启用文件写入访问。

  • remount-ro、zone-ro 和 zone-offline 挂载选项隐含的文件访问权限更改对于处于良好状态的分区是临时的。卸载并重新挂载文件系统将恢复受影响文件的先前默认(格式化时间值)访问权限。

  • repair 挂载选项仅触发最小的 I/O 错误恢复操作集,即良好状态的分区的文件大小修复。设备指示为只读或离线状态的分区仍然意味着如上表所述更改分区文件访问权限。

挂载选项

zonefs 定义了几个挂载选项: * errors=<behavior> * explicit-open

“errors=<behavior>”选项

“errors=<behavior>”选项挂载选项允许用户指定 zonefs 响应 I/O 错误、inode 大小不一致或分区状态更改的行为。定义的行为如下

  • remount-ro(默认)

  • zone-ro

  • zone-offline

  • repair

先前部分详细介绍了为每个行为定义的运行时 I/O 错误操作。挂载时 I/O 错误将导致挂载操作失败。只读分区的处理在挂载时和运行时也有所不同。如果在挂载时发现只读分区,则始终以与离线分区相同的方式处理该分区,即,禁用所有访问并将分区文件大小设置为 0。这是必要的,因为 ZBC 和 ZAC 标准将只读分区的写指针定义为 invalib,因此无法发现已写入分区的数据量。如上一节所述,如果在运行时发现只读分区。分区文件的大小与其上次更新的值保持不变。

“explicit-open”选项

分区块设备(例如 NVMe 分区命名空间设备)可能对可以处于活动状态的分区数量有限制,即,处于隐式打开、显式打开或关闭状态的分区。如果用户发出写入请求时,文件的分区尚未处于活动状态,则这种潜在的限制会转化为应用程序因超过此限制而看到写入 IO 错误的风险。

为了避免这些潜在的错误,“explicit-open”挂载选项强制使用打开分区命令激活分区,即在首次打开文件进行写入时。如果分区打开命令成功,则可以保证应用程序可以处理写入请求。相反,如果分区未满或为空,则“explicit-open”挂载选项将在最后一次 close() 分区文件时导致向设备发出分区关闭命令。

运行时 sysfs 属性

zonefs 为已挂载的设备定义了几个 sysfs 属性。所有属性都是用户可读的,可以在目录 /sys/fs/zonefs/<dev>/ 中找到,其中 <dev> 是已挂载的分区块设备的名称。

定义的属性如下。

  • max_wro_seq_files:此属性报告可以打开进行写入的顺序分区文件的最大数量。此数字对应于设备支持的最大显式或隐式打开分区数。值为 0 表示设备没有限制,并且任何分区(任何文件)都可以随时打开进行写入和写入,而不管其他分区的状态如何。当使用 *explicit-open* 挂载选项时,当已打开进行写入的顺序分区文件的数量达到 *max_wro_seq_files* 限制时,zonefs 将使任何请求打开顺序分区文件进行写入的 open() 系统调用失败。

  • nr_wro_seq_files:此属性报告当前打开进行写入的顺序分区文件的数量。当使用“explicit-open”挂载选项时,此数字永远不能超过 *max_wro_seq_files*。如果未使用 *explicit-open* 挂载选项,则报告的数字可以大于 *max_wro_seq_files*。在这种情况下,应用程序有责任不要同时写入超过 *max_wro_seq_files* 个顺序分区文件。否则可能导致写入错误。

  • max_active_seq_files:此属性报告处于活动状态的顺序分区文件的最大数量,即部分写入(不为空或已满)或具有显式打开的分区(仅当使用 *explicit-open* 挂载选项时才会发生)的顺序分区文件。此数字始终等于设备支持的最大活动分区数。值为 0 表示已挂载的设备对可以活动的顺序分区文件的数量没有限制。

  • nr_active_seq_files:此属性报告当前活动的顺序分区文件的数量。如果 *max_active_seq_files* 不为 0,则 *nr_active_seq_files* 的值永远不能超过 *nr_active_seq_files* 的值,而不管是否使用 *explicit-open* 挂载选项。

Zonefs 用户空间工具

mkzonefs 工具用于格式化分区块设备以与 zonefs 一起使用。此工具可在 Github 上获得,网址为

https://github.com/damien-lemoal/zonefs-tools

zonefs-tools 还包括一个测试套件,该套件可以针对任何分区块设备运行,包括使用分区模式创建的 null_blk 块设备。

示例

以下格式化了一个启用了常规分区聚合功能的具有 256 MB 分区的 15TB 主机管理型 SMR HDD

# mkzonefs -o aggr_cnv /dev/sdX
# mount -t zonefs /dev/sdX /mnt
# ls -l /mnt/
total 0
dr-xr-xr-x 2 root root     1 Nov 25 13:23 cnv
dr-xr-xr-x 2 root root 55356 Nov 25 13:23 seq

分区文件子目录的大小指示每种类型分区存在的文件数量。在此示例中,只有一个常规分区文件(所有常规分区都聚合在单个文件下)

# ls -l /mnt/cnv
total 137101312
-rw-r----- 1 root root 140391743488 Nov 25 13:23 0

此聚合的常规分区文件可以用作常规文件

# mkfs.ext4 /mnt/cnv/0
# mount -o loop /mnt/cnv/0 /data

在此示例中,将文件分组到顺序写入分区的“seq”子目录具有 55356 个分区

# ls -lv /mnt/seq
total 14511243264
-rw-r----- 1 root root 0 Nov 25 13:23 0
-rw-r----- 1 root root 0 Nov 25 13:23 1
-rw-r----- 1 root root 0 Nov 25 13:23 2
...
-rw-r----- 1 root root 0 Nov 25 13:23 55354
-rw-r----- 1 root root 0 Nov 25 13:23 55355

对于顺序写入分区文件,文件大小随着数据附加到文件末尾而变化,类似于任何常规文件系统

# dd if=/dev/zero of=/mnt/seq/0 bs=4096 count=1 conv=notrunc oflag=direct
1+0 records in
1+0 records out
4096 bytes (4.1 kB, 4.0 KiB) copied, 0.00044121 s, 9.3 MB/s

# ls -l /mnt/seq/0
-rw-r----- 1 root root 4096 Nov 25 13:23 /mnt/seq/0

可以将写入的文件截断为分区大小,从而防止任何进一步的写入操作

# truncate -s 268435456 /mnt/seq/0
# ls -l /mnt/seq/0
-rw-r----- 1 root root 268435456 Nov 25 13:49 /mnt/seq/0

截断为 0 大小允许释放文件分区存储空间并重新开始对文件进行附加写入

# truncate -s 0 /mnt/seq/0
# ls -l /mnt/seq/0
-rw-r----- 1 root root 0 Nov 25 13:49 /mnt/seq/0

由于文件在磁盘上静态映射到分区,因此 stat() 和 fstat() 报告的文件块数表示文件分区的容量

# stat /mnt/seq/0
File: /mnt/seq/0
Size: 0             Blocks: 524288     IO Block: 4096   regular empty file
Device: 870h/2160d  Inode: 50431       Links: 1
Access: (0640/-rw-r-----)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2019-11-25 13:23:57.048971997 +0900
Modify: 2019-11-25 13:52:25.553805765 +0900
Change: 2019-11-25 13:52:25.553805765 +0900
Birth: -

以 512B 块为单位的文件块数(“Blocks”)给出了最大文件大小 524288 * 512 B = 256 MB,这对应于此示例中的设备分区容量。值得注意的是,“IO 块”字段始终指示写入的最小 I/O 大小,并且对应于设备的物理扇区大小。