ZoneFS - 面向分区块设备的分区文件系统¶
简介¶
zonefs 是一个非常简单的文件系统,它将分区块设备的每个分区都公开为一个文件。与具有原生分区块设备支持的常规 POSIX 兼容文件系统(例如 f2fs)不同,zonefs 不会对用户隐藏分区块设备的顺序写入约束。表示设备的顺序写入分区的文件的写入必须从文件末尾开始按顺序写入(仅追加写入)。
因此,zonefs 本质上更接近原始块设备访问接口,而不是功能齐全的 POSIX 文件系统。zonefs 的目标是通过用更丰富的文件 API 替换原始块设备文件访问,从而简化应用程序中分区块设备支持的实现,避免依赖对开发者来说可能更晦涩的直接块设备文件 ioctl。这种方法的一个例子是在分区块设备上实现 LSM(日志结构合并)树结构(如 RocksDB 和 LevelDB 中使用的),允许 SSTable 存储在分区文件中,类似于常规文件系统,而不是作为整个磁盘的扇区范围。引入更高级别的构造“一个文件是一个分区”可以帮助减少应用程序中所需的更改,并引入对不同应用程序编程语言的支持。
分区块设备¶
分区存储设备属于一类存储设备,其地址空间划分为多个分区。一个分区是一组连续的 LBA,所有分区都是连续的(没有 LBA 间隔)。分区可能具有不同的类型。
传统分区:属于传统分区的 LBA 没有访问约束。可以执行任何读取或写入访问,类似于常规块设备。
顺序分区:这些分区接受随机读取,但必须按顺序写入。每个顺序分区都有一个由设备维护的写指针,该指针跟踪设备下一次写入的强制起始 LBA 位置。由于这种写入约束,顺序分区中的 LBA 不能被覆盖。顺序分区必须先使用特殊命令(分区重置)擦除,然后才能重写。
分区存储设备可以使用各种记录和介质技术来实现。当今最常见的分区存储形式是在叠瓦式磁记录 (SMR) HDD 上使用 SCSI 分区块命令 (ZBC) 和分区 ATA 命令 (ZAC) 接口。
固态磁盘 (SSD) 存储设备也可以实现分区接口,例如,减少由于垃圾回收造成的内部写入放大。NVMe 分区命名空间 (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):虽然区域中已写入的数据仍然可读,但该区域将不能再被写入。对该区域的任何用户操作(区域管理命令或读/写访问)都无法将该区域状态改回正常的读/写状态。虽然标准未定义设备将区域转换为只读状态的原因,但一个典型的原因为硬盘驱动器上的写入磁头出现缺陷(此磁头下的所有区域都会更改为只读状态)。
一个区域可能会转换为离线状态 (BLK_ZONE_COND_OFFLINE):离线区域既不能读取也不能写入。任何用户操作都无法将离线区域改回可操作的良好状态。与区域转换为只读状态类似,驱动器将区域转换为离线状态的原因也未定义。一个典型的原因为硬盘驱动器上的读写磁头出现缺陷,导致磁头下的盘片上的所有区域都无法访问。
未对齐的写入错误:当设备执行写入请求时,如果主机发出的写入请求的起始扇区与区域写入指针位置不对应,则会发生这些错误。即使 zonefs 对顺序区域强制执行顺序文件写入,但在一个非常大的直接 I/O 操作被拆分为多个 BIO/请求或异步 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 大小,以反映永久存储在文件区域中的数据量。
访问权限更改:区域状态更改为只读状态会通过更改文件访问权限来指示,以使文件变为只读状态。这会禁用对文件属性和数据修改的更改。对于离线区域,文件的所有权限(读取和写入)都被禁用。
zonefs I/O 错误恢复采取的进一步操作可以通过用户使用 “errors=xxx” 挂载选项进行控制。下表总结了 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 标准将只读区域的写入指针定义为无效,从而无法发现已写入该区域的数据量。如上一节所述,在运行时发现只读区域的情况下。区域文件的大小与其上次更新的值保持不变。
“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,则无论是否使用 explicit-open 挂载选项,nr_active_seq_files 的值都永远不能超过 nr_active_seq_files 的值。
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
zone 文件子目录的大小指示每种类型的分区存在的文件数。在此示例中,只有一个传统的区文件(所有传统的区都聚合在一个文件中)。
# 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: -
文件块数(“Blocks”)以 512B 块为单位,表示最大文件大小为 524288 * 512 B = 256 MB,这对应于本示例中的设备分区容量。需要注意的是,“IO block”字段始终指示写入的最小 I/O 大小,并且对应于设备的物理扇区大小。