ZFS 技巧与知识 

Last Update: 2023-12-23

目录

创建 zpool 时使用合适的 ashift

ashift 值确定了 ZFS 分配 block 的最小值与增量的梯度。

TL;DR: 通常 ashift=12 足以应对绝大多数的情况。但如果 zpool 中只存放数据库文件,ashift 的值应该与数据库的 page size 相同。 即,postgresql 数据库对应 ashift=13,MySQL/MariaDB 对应 ashift=14,这样可以让 ZFS 的每次 I/O 都至少是一个 page 的大小 (I/O 的最大值还受 recordsize 的影响)。

机械硬盘

以西数 HC320 8T SATA HDD 为例,如下是 HDD SMART 信息中的 sector (扇区) 部分,它用于指导用户给硬盘分区、建立文件系统:

# smartctl -a /dev/ada0
...
Sector Sizes:     512 bytes logical, 4096 bytes physical
...

sector (扇区) 是机械硬盘读写的最小单位。可以看到,HC320 的物理扇区大小是 4096b,在逻辑上兼容 512B 大小的扇区。

实际上,如果文件系统真的一次操作 512B 的数据,那么硬盘必须先把这 512B 所在的 4K 扇区读到硬盘的缓存中,再从缓存里的这 4K 数据中找要文件系统要的这 512B 数据返回给文件系统。

如果这 512B 数据需要修改,文件系统把要保存的 512B 数据发给硬盘后,如果硬盘在读取这 512B 数据时产生的缓存没有失效,就直接把这 512B 数据覆盖回缓存中的这 4K 数据对应的位置,再把这 4K 数据写回到硬盘。如果缓存失效,就必须先把这 512B 数据所在的 4K 扇区中的数据读到缓存里才能继续下一步覆盖、回写的操作。

(这种物理扇区大小为 4k 的硬盘被称为 Advanced Format Disk 高级格式盘 ,也被称为 4K 盘。)

所以,理想的情况是,文件系统操作的每一个 block 正好可以通过一次或两次对于 HDD 的读写来完成。这意味着要做到:

  1. 文件系统最好将 block 的大小设置为 physical sector 大小的整数倍
    • 通常取 1 倍,即 block 大小等于 physical sector 大小 (4K)
    • 此时文件系统读写的每个 block 的大小是一个 physical sector 的大小
  2. 让每次读写操作的第一个字节存储在每个扇区的第一个字节 (4K 对齐)
    • 这让一个 block 的数据 (4K) 正好存储在一个 physical sector (4K) 中
    • 如果一个 block 的数据 (4K) 存储在第一个 physical sector 的后 2K 和第二个 physical sector 的前 2K 中,那么文件系统要操作这个 block 时就会出现读写放大的现象
      • 在读数据时,硬盘会先读取第一个 physical sector 的 4K 数据,取出后 2K 部分;然后读取第二个 physical sector 的 4K 数据,再取出前 2K 部分;最后把这两部分数据按先后顺序拼接返回给文件系统
      • 在写数据冲,硬盘会先读取第一个 physical sector 的 4K 数据,用 block 的前 2K 部分覆盖这个 physical sector 的后 2K 部分,然后把这个 physical sector 的数据写回硬盘;然后读取第二个 physical sector 的 4K 数据,用 block 的后 2K 部分覆盖这个 physical sector 的前 2K 部分,然后再把这个 physical sector 的数据写回硬盘

ashift 作为 block 的最小值与增量梯度,当 ashift=12 时 block 的最小值是 2^12=4k,其增量也是 4k,正好是一个硬盘扇区的大小。当 ashift=13 时 block 的最小值是 2^13=8k,其增量也是 8k,是 2 个扇区的大小,即每次分配 2 个扇区,增量也是 2 个扇区。

所以 ashift 的值一般在 12 和 13 之间选。通常 ashift=12。

固态硬盘

所有的 SSD 都会报扇区大小是 512B,但这个值几乎没有什么用。

比如,这是普通消费级 nvme SSD 使用的 LBA 大小与支持的大小:

# smartctl /dev/nvme0
...
Namespace 1 Formatted LBA Size:     512
...
Supported LBA Sizes (NSID 0x1)
Id Fmt  Data  Metadt  Rel_Perf
0 +     512       0         2
1 -    4096       0         1
...

比如,这是 16G intel 傲腾 SSD 使用的 LBA 大小与支持的大小:

# smartctl -a /dev/nvme2
...
Namespace 1 Formatted LBA Size:     512
...
Supported LBA Sizes (NSID 0x1)
Id Fmt  Data  Metadt  Rel_Perf
0 +     512       0         2
...

固态硬盘的读写行为与机械硬盘不同,固态硬盘上有多个闪存颗粒,每个颗粒内有多个 block,一个 block 又由多个 page 组成。固态硬盘读写的最小单位是一个 page。

注意: SSD 颗粒上的 block 不是文件系统上的 block,不要搞混。

而这个 page 的大小,各厂商没有统一,有的是 4k 有的是 8k。Intel 的文档说,他们的 SSD 可以在文件系统使用 4k block 的情况下很好地工作 (在 SSD 主控层面实现了相关功能)。但是,根据经验,几乎所有的 ssd 都可以在文件系统使用 4k block 的情况下提供正常的性能表现。

所以,ashift 的值应该按照文件系统使用 4k block 的条件来选择。注意到,HDD 每次读写的数据也是 4k 大小。那么在 SSD 的 ashift 选值上可以参考 HDD:

所以,理想的情况是,文件系统操作的每一个 block 正好可以通过一次或两次对于 HDD 的读写来完成。这意味着,文件系统最好将 block 的大小设置为物理扇区空间的整数倍。

ashift 作为 block 的最小值与增量梯度,当 ashift=12 时 block 的最小值是 2^12=4k,其增量也是 4k,正好是一个硬盘扇区的大小。当 ashift=13 时 block 的最小值是 2^13=8k,其增量也是 8k,是 2 个扇区的大小,即每次分配 2 个扇区,增量也是 2 个扇区。

所以 ashift 的值一般在 12 和 13 之间选。通常 ashift=12。

针对使用条件调整 recordsize 的值

recordsize 的值确定了 ZFS 分配 block 的最大值。并且,ZFS 默认的 128K 足以应付绝大多数的情况。

sector 是硬盘存取的最小单位;block 是文件系统存取的最小单位,可以随意设定,但必须是 sector 的整数倍。

这个值表示的是 block 的最大值,它的增量梯度是一个 ashift 的大小,ashift 也是 block 的最小值。在 ashift=12 的情况下 (2^12=4k),如果用户创建了一个 4k 大小的文件,那么 ZFS 会给这个文件分一个 4k 的 block 并把数据存进去;9k 的文件对应 12k 的 block;20k 的文件对应 20k 的 block。

上面的 block 分配方式的前提是 Zpool 组合磁盘的方法是 strip 或者 mirror。raid 模式中,由于需要额外的块来存储校验和,所以块分配的计算更复杂。

在一个 4 盘 raidz2 的情况下,数据会被分成 2 份到 2 个数据盘上,再生成 2 个校验码到 2 个校验盘上。而 ashift 规定了每个 block 的最小值,这就意味着,两个数据盘每次能分配给数据的最小空间是 8k: 每个数据盘分出一个最小的 4k block。此时这个 4 盘 raidz2 阵列中,block 大小只能以 8k 为梯度递增。

当一个文件大小是 250k 时,两个数据盘上各有一个 128k 的 block 即可存储这个文件 (数据部分占用 256k)。

但如果一个文件的大小是 257k,此时各个盘上会有一个 128k 的 block 和一个 4k 的 block 来存储这个文件 (数据部分占用 264k)。

如果用来存储 postgresql 数据文件,应该将值调整为 postgresql 的 page size,即 8k。

如果用来存储 MySQL 数据文件,应该将值调整为 MySQL (InnoDB) 的 page size,即 16k。

对于存储数据库文件的 dataset 来说,使用过大的 recordsize 会导致读放大。

比如 MySQL 的 InnodDB 引擎使用的 page size 是 16k,数据库每次读写数据都要先取到这个 page 才能继续下面的操作。

当 MySQL 需要读一个 page 中前 1k 的数据并修改最后 2k 的数据,如果 recordsize 使用 ZFS 默认 128k 的话,MySQL 会去读这个 16k page 所在的 block (大小是 128k),然后再从这 128k 的数据中找到要用的那 16k。在改好这 16k 中最后 2k 的数据后,还要把这 16k 数据和多余的那 112k 按照对应位置再拼回 128k,并将这 128k 数据写到硬盘上另一个 block 中。

在这个过程中,有 112k 的数据本不必被读写。

这个读写放大的行为虽然对于延迟的影响不是特别大,但是它严重浪费了存储通道上的带宽。比如 100MB/s 的存储带宽每秒可以传输不到 800 个 128k 的 block,或者 6250 个 16k 的 block。这意味着,使用 128k recordsize 的情况下,数据库的 IOPS 在 800 以下,而使用 16k recordsize 的情况下,数据库的 IOPS 可以达到 6250,差距还是很大的。

关闭 atime

文件的 atime 在读取文件的时候被更新,这意味着,读文件时,文件的 metadata 被更新。这让文件系统产生了不必要的写操作。因为,通常,用户并不关心这个文件什么时候被读取过。

FreeBSD 在安装时创建的 zroot 这个 zpool 的 atime 除了 zroot/var/mail 是开着的以外,默认是关闭的。但是用户自己创建的 zpool 的 atime 属性是默认开着的。

推荐总是关闭 atime。

文件系统压缩

ZFS 在压缩算法上有一些特殊优化,可以让 ZFS 不压缩那些无法被压缩的数据。

ZFS 先在内存里使用指定的压缩算法压缩一个 recordsize 大小的块,然后检查压缩结果是否比压缩前至少小八分之一。如果没有实现至少 12.5% 的压缩,那么 ZFS 就会丢弃已经被压缩过的数据,写入未压缩过的数据;如果压缩后的 recordsize 不大于原始大小的 87.5%,那么被压缩后的 recordsize 会被写入文件系统。

被压缩的 recordsize 占用的空间不大于 112k (128k*87.5%)。并不是整个文件压完之后按字节写入,而是以 recordsize 的大小为单位压缩并写入。

文档中还提到,如果 block 的大小小于 8 * <sector_size> 想要将被压缩后的数据存入硬盘,就必须要有更高的压缩率。

sector 大小为 4k 时,这个压缩率标准改变的临界点是 block 是否小于 8 * <sector_size> = 8 * 4k = 32k。

比如,要写入 8k 的数据,那么必须实现至少 50% 的压缩,这 8k 的数据才会以被压缩后的格式写入硬盘,占用 4k 硬盘空间;如果没有实现至少 50% 的压缩,那么就会把未压缩的数据直接写入硬盘,占用 8k 硬盘空间。

注: block size < 32k 时,不同梯度范围对应不同的压缩率要求,这里只举压缩率必须为 50% 的一个例子。

要被写入的数据不论如何都会被完整地压缩一遍,所以这个策略不会优化写数据的性能,它优化的目标是在读数据时节省 CPU 算力。

目前值得推荐的有两种算法,一种是 ZFS 默认的 lz4,另一种是 zstd。lz4 比 zstd 速度快,zstd 比 lz4 压缩效果好。

L2ARC 用于存储自身索引需要的内存空间

L2ARC 内存储的最小 unit 是一个 recordsize 的数据,而每在 L2ARC 中存储一个 unit 就需要 70 Byte 的内存空间。

第一个例子,zpool 中的所有 dataset 都使用 128KiB 的 recordsize;使用 160GiB 的硬盘分区作为 L2ARC。

注: FreeNAS 一开始在文档中说明 L2ARC 的大小应小于 5 倍内存空间的大小,后来的 TrueNAS 文档里推荐不超过 10 倍内存空间的大小。

160 Gib / 128 KiB = 160 * 1024 * 1024 KiB / 128 KiB = 167772160 KiB / 128 KiB = 1310720 unit

可以计算出 160 GiB 的 L2ARC 设备可以存储 1310720 个 128KiB 的数据块。

1310720 unit * 70 Byte/unit = 91750400 Byte = 91750400 / 1024 / 1024 MiB = 87.5 MiB

所以,如果一个 160GiB 的 L2ARC 设备被用于 recordsize 为 128KiB 的 zpool 则消耗 87.5MiB 的内存。

第二个例子,zpool 中的所有 dataset 都使用 8KiB 的 recordsize;使用 160GiB 的硬盘分区作为 L2ARC。

160 Gib / 8 KiB = 160 * 1024 * 1024 KiB / 8 KiB = 167772160 KiB / 8 KiB = 20971520 unit

20971520 unit * 70 Byte/unit = 1468006400 Byte = 1468006400 / 1024 / 1024 MiB = 1400 MiB

所以,如果一个 160GiB 的 L2ARC 设备被用于 recordsize 为 8KiB 的 zpool 则消耗 1400MiB 的内存。

L2ARC 持久化

L2ARC persistent 在 OpenZFS 2.0 引入。其开关参数为 l2arc_rebuild_enabled 其官方描述为 Rebuild the persistent L2ARC when importing a pool. 默认值为 1

l2arc_rebuild_enabled=1 表示打开 L2ARC 持久化,即重启之后 L2ARC Deivce 中的数据还能用。这个参数中 rebuild 的意思是从 L2ARC Device 里读取数据,在内存里重建 L2ARC 的索引。而不是重建 L2ARC 中的数据。

在 OpenZFS 2.0 之前,L2ARC 的索引数据保存在内存中,遇到重启内存中的索引就丢了,而 L2ARC Device 中的数据在没有索引之后就意味着失效了。OpenZFS 2.0 引入的新功能会在重启 (导致 L2ARC 索引丢失) 之后,重新读取 L2ARC 中的数据建立索引,也就避免了整个 L2ARC Device 中的数据失效的问题。

查看被压缩的文件的真实大小

使用 du 命令的 -A 选项可以显示文件声称的大小,而 du 的默认行为是计算文件真正使用的空间。

ZFS sync I/O 的写入性能差

客户端通过 NFSv3 向 NFS 服务端的 ZFS 文件系统以 sync I/O 的方式写入数据时速度只有 18 MB/s,但内网速度是千兆,这速度显然没跑满网络带宽。

关闭 ZFS 的 sync I/O

在 NFS 服务器上执行 zfs get sync 可以看到所有 zfs 文件系统使用了 async 还是 sync:

NAME                PROPERTY  VALUE     SOURCE
mymir2              sync      standard  default
mymir2/data         sync      standard  default
mymir2/videos       sync      standard  default

sync 属性的取值可以为:

数据被以同步写入的方式写入时 只有要写入的数据数据被真正地写入硬盘之后,才会返回写入成功信号 ,这就是问题所在。

执行 zfs set sync=disabled <zpool_name>/<zpool_zfs> 可以关闭同步写入,使用异步写入,让 I/O 异步执行,速度就变快了。

再执行 zfs get sync 可以看到:

NAME                PROPERTY  VALUE     SOURCE
mymir2              sync      standard  default
mymir2/data         sync      disabled  local
mymir2/videos       sync      standard  default

此时数据传输速度可以到相对正常的 70 MB/s 左右,在 50 - 77 MB/s 波动。

数据传输完毕建议把 I/O 方式改回 standard ,由写入数据的程序决定如何写入。 (毕竟异步读写的方式不是那么安全)

执行 sudo zfs set sync=standard <zpool_name>/<zpool_zfs> 可以让 sync 属性的值恢复到默认。

比如,执行 sudo zfs set sync=standard mymir2/data 后再执行 zfs get sync 可以看到:

NAME                PROPERTY  VALUE     SOURCE
mymir2              sync      standard  default
mymir2/data         sync      standard  local
mymir2/videos       sync      standard  default

如果想把 SOURCE 字段也恢复原样,需要执行 sudo zfs inherit -rS sync <zpool_name>/<zpool_zfs> ,比如 sudo zfs inherit -rS sync mymir2/datamymir2/datasync 属性从父路径继承。再执行 zfs get sync 可以看到:

NAME                PROPERTY  VALUE     SOURCE
mymir2              sync      standard  default
mymir2/data         sync      standard  default
mymir2/videos       sync      standard  default

给 zpool 加独立 ZIL (SLOG 组件)

ZIL 和 SLOG 的描述可以读本站的 ZFS ZIL(SLOG) 组件 一文。

用于 ZIL 的设备应该是可以高速读写的 SSD,使用 4K 读写速度低下的 HDD 作为 ZIL 可能会降低整个 zpool 的性能。后文用 SLOG 设备 指代 具有 ZIL 功能的独立设备或分区

给没有 SLOG 设备的池添加一个 SLOG 设备

执行 zpool add <zpool_name> log <device> 可以为 <zpool_name> 添加 SLOG 设备,比如 zpool add mymir2 log /dev/da6p2

向已有一个 SLOG 设备的池添加另一个 SLOG 设备使之互为镜像

执行 zpool attach <zpool_name> <log_device> <device> 可以为 <zpool_name> 这个 pool 的 SLOG 设备创建镜像,比如 zpool attach mymir2 da6p2 /dev/da6p1:

直接添加两个互为镜像的 SLOG 设备

执行 zpool add <zpool_name> log mirror <device_a> <device_b> 可以为 <zpool_name> 这个 pool 添加 SLOG 设备,并让两个设备互为镜像,比如 zpool add mymir2 log mirror /dev/da6p1 /dev/da6p2