Incus使用ZFS作为存储
在 Incus快速起步 实践中,可以看到 incus admin init 交互过程会让你选择存储引擎,实际上这个存储引擎和 Docker存储 是相似的,能够支持 ZFS Linux LVM逻辑卷管理 Btrfs 等不同的卷管理系统,以实现快照,clone的等高级功能。当然,最简单的形式还是 dir 即目录存储。
由于我最初规划在 纳斯NASSE C246 ITX主板 组装机上采用它多块 NVMe存储 存储,所以操作系统安装 Ubuntu Linux 的时候,将操作系统Installer的默认卷管理中 / 目录的卷 ubuntu-lv 扩展到占用了整块磁盘。后来的规划有变,我采用了 3块 NVIDIA Tesla A2 GPU运算卡 ( PCIe bifurcation ) + 1块 Nvidia Tesla P10 GPU运算卡 ( OCuLink )来构建 思考机器 ,这就导致主机上只剩下一块操作系统使用的 铠侠KIOXIA EXCERIA G2 NVMe SSD存储 。
那么问题来了,我该怎样将已经完全被 Linux LVM逻辑卷管理 占据磁盘的空间划分出一部分来提供给 ZFS ,以实现最完美的容器本地化存储?
检查当前存储(pv,vg和lv使用情况)
root@xcloud:~# pvs
PV VG Fmt Attr PSize PFree
/dev/nvme0n1p3 ubuntu-vg lvm2 a-- <1.82t 0
root@xcloud:~# vgs
VG #PV #LV #SN Attr VSize VFree
ubuntu-vg 1 1 0 wz--n- <1.82t 0
root@xcloud:~# lvs
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
ubuntu-lv ubuntu-vg -wi-ao---- <1.82t
可以看到,当前PV使用了 /dev/nvme0n1 的分区3,并且VG已经将所有的PV空间都使用完了( VFree=0 ),而且LV也已经将分配的VG空间分配完。
备注
Incus支持使用 Incus使用LVM
这说明,当前 Linux LVM逻辑卷管理 实际上是完全使用了磁盘空间(分区3),那么该如何腾出部分空构建给ZFS:
思路一: 收缩 PV
警告
虽然操作分区表(如使用 fdisk 或 parted)收缩 LVM 所在的主分区在 Linux 下可以通过计算扇区(Sector)来实现,但是实践上存在极大风险:
物理卷(PV)收缩的“碎片化”陷阱: 释放出来的 PE 并不一定整齐地排列在物理磁盘的末尾
长时间运行的系统,PE的分配可能是离散的(比如磁盘头部有数据,中部是空闲,尾部又有数据)。如果直接去截断物理分区,只要磁盘末尾还残留有一个被占用的 PE,就会导致分区表损坏、数据毁灭性丢失。
解决的方法可以尝试: pvmove --alloc anywhere 把散落在磁盘尾部的 PE 一个个手动搬运到磁盘头部。(需要使用 pvdisplay -m 查看PE的物理分布)这个过程极其耗时且容错率极低。只有这步成功之后才能执行 pvresize
不推荐ZFS和LVM共享一块物理磁盘的不同分区: ZFS拥有自己的虚拟设备(vdev)调度层、极其激进的写入缓存以及对底层硬件拓扑的预判。
步骤概述:
[原始状态: 100% 空间归 LVM] -> [/dev/sda1 (全部为 LVM PV)]
[步骤 1: 压缩 LV] -> [LV 缩小] + [VG 内出现空闲 PE]
[步骤 2: 转移 PE] -> 将所有在尾部的 PE 用 pvmove 赶到磁盘前半部分
[步骤 3: 压缩 PV] -> 运行 pvresize 强行让 LVM 释放物理盘尾部空间
[步骤 4: 修改分区表] -> 使用 parted 缩小 /dev/sda1,让磁盘末尾出现 Unallocated Space
[步骤 5: 建立新分区] -> 将末尾空间建为 /dev/sda2 -> 交付给 ZFS zpool
我的实际实践: ZFS运行在独立逻辑卷(LV)上
LVM是一个非常高效、极其轻量化的设备映射器(Device Mapper)抽象层。在Linux内核中,LVM的逻辑卷(LV)到物理卷(PV)的映射主要是通过扇区偏移量(Sector Offset)的查表映射来完成的。这种内核级别的地址换算,其CPU周期消耗和I/O延迟在现代硬件(尤其是NVMe SSD)上几乎完全可以忽略不计(通常小于1%)。
传统的文件系统上创建Loopback文件,然后把文件挂载给ZFS系统。这种模式虽然简单易用,但是会导致双重日志在(Double Journaling),双重缓存(Double Caching)以及严重的数据块对齐(Alignment)消耗。所以注重性能场合不适用。
但是直接使用裸LV上构建ZFS则能够保持高性能:
零文件系统开销(Zero Filesystem Overhead) : LV 没有经过 EXT4 或 XFS 的格式化,它对于宿主机内核来说,就是一个纯净的、连续的物理块设备(Block Device),地位和
/dev/sda或/dev/nvme0n1p2完全对等。ZFS 可以直接接管这块设备的底层 I/O 队列。单一缓存机制(Single Cache Strategy) : ZFS 拥有自己极其强大的内存缓存机制——ARC(Adaptive Replacement Cache,自适应替换缓存)。由于底层是裸 LV,数据会直接从 ZFS 写入底层硬件,宿主机的 Page Cache(系统缓存)不会对这部分数据进行二次缓存。这避免了内存的白白浪费和双重缓存同步带来的延迟。
完美继承 ZFS 的高级特性 : 在 LV 之上建立 ZFS 后,Incus 在创建、克隆和快照 Debian 容器时,完全是在 ZFS 内部通过修改指针实现的写时复制(CoW)。LVM 只充当空间提供者,不会干扰 ZFS 的快照性能。
备注
虽然 LVM 层几乎没有消耗,但是如果没有做到 扇区对齐(Sector Alignment) 会导致性能急剧下降,所以务必确认:
确保 LVM 的 PE(Physical Extent)对齐 : LVM 在创建 VG 时,默认的 PE 大小是 4MB,并且会自动对齐到 1MB 边界。这天然就是 4KB 的倍数,所以 LVM 是完美对齐的。
创建 ZFS 存储池时强行指定 ``ashift=12`` (最关键) : ashift=12 代表 $2^{12} = 4096$ 字节(即 4KB)。这会强制 ZFS 内部所有的数据块(Block)按照 4KB 边界进行严格对齐写入。
首先将当前LVM中的
LV收缩:
--resizefs 参数能够同时收缩LV上的文件系统(ext4,xfs)sudo lvreduce --resizefs -L 400G /dev/ubuntu-vg/ubuntu-lv
传统Linux运维中,缩减一个 ext4 分区或逻辑卷,标准流程必须是 "先使用 resize2fs 缩小文件系统,然后再用 lvreduce 缩小逻辑卷"。但是,现代Linux(例如Ubuntu 24.04)所带的 lvm2 工具链已经非常只能。当执行 lvreduce 时加上 --resizefs 参数, LVM会在后台自动、原子化地按顺序执行完整的先收缩文件系统再收缩LV的流程 :
自动检测: LVM会自动检测 /dev/ubuntu-vg/ubuntu-lv 上承载的是什么文件系统(例如识别出 ext4 )
第一步(隐藏调用 resize2fs ): LVM会在后台先自动调用 resize2fs (如果是挂载状态且内核支持,则会在线缩减;如果需要卸载,则会提示):
Do you want to unmount "/" ? [Y|n] y
umount: /: target is busy.
fsadm: Cannot proceed with mounted filesystem "/".
/usr/sbin/fsadm failed: 1
Filesystem resize failed.
二步(执行 lvreduce ): 只有当后台的 resize2fs 成功完成、文件系统已经安全收缩之后,LVM 才会真正去调整逻辑卷(LV)的边界,切掉尾部的空间。
备注
由于我这里是调整 / 根目录的LVM,所以需要使用Ubuntu Live CD/USB 启动盘 引导系统,或者进入 Recovery Mode(恢复模式)的 root shell。在根目录没有被挂载(Unmounted)的状态下,完成 在Recovery模式下收缩系统根目录的LVM逻辑卷(LV)
现在执行
vgs可以看到ubuntu-vg上有空闲的空间大约1.4T可以用于创建新的LV:
VG #PV #LV #SN Attr VSize VFree
ubuntu-vg 1 1 0 wz--n- <1.82t <1.43t
创建LV命名为
incus-lv:
incus-lv 的LVlvcreate -l 100%FREE -n incus-lv ubuntu-vg
注意,这里使用了参数 -l (小写),该参数后面跟的是逻辑块(Extents)的数量或百分比表达式。当使用 -l 100%FREE 时,LVM会自动检查当前 ubuntu-vg 还剩多少可用的物理扩展单元(PE),然后全部分配给这个新增的逻辑卷。
如果使用 -L (大写)参数,则后面更具体的容量单位数字(如 -L 60G , -L 500M )
现在检查 lvs 可以看到新创建的LV:
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
incus-lv ubuntu-vg -wi-a----- <1.43t
ubuntu-lv ubuntu-vg -wi-ao---- 400.00g
现在正式开始 在LVM逻辑卷
/dev/ubuntu-vg/incus-lv上创建ZFS的zpool:
incus-zpool-sudo zpool create -f -o ashift=12 \
-O compression=lz4 \
-O atime=off \
incus-zpool /dev/ubuntu-vg/incus-lv
注意参数要点:
-o ashift=12: 强行将 ZFS 的物理块对齐到 4KB,消除因LVM虚拟块设备引起的扇区错位-O compression=lz4: 开启内建的 LZ4 压缩。LZ4 的速度极快,几乎不占用 CPU,但能节约20%~30% 的实际磁盘空间,且因为写入的数据变小了,反而能提升 SSD 的寿命和读写吞吐-O atime=off: 关闭文件访问时间记录。如果不关,容器里每次读取一个配置文件,ZFS 都要往盘里写入一次“访问时间”,这在大量微服务高频读取时会产生严重的 I/O 拖累执行初始化操作:
$ sudo incus admin init
Would you like to use clustering? (yes/no) [default=no]:
Do you want to configure a new storage pool? (yes/no) [default=yes]:
Name of the new storage pool [default=default]:
Name of the storage backend to use (lvm, lvmcluster, zfs, btrfs, dir) [default=zfs]:
Create a new ZFS pool? (yes/no) [default=yes]: no
Name of the existing ZFS pool or dataset: incus-zpool
Would you like to create a new local network bridge? (yes/no) [default=yes]:
What should the new bridge be called? [default=incusbr0]:
What IPv4 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]:
What IPv6 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]:
Would you like the server to be available over the network? (yes/no) [default=no]:
Would you like stale cached images to be updated automatically? (yes/no) [default=yes]:
Would you like a YAML "init" preseed to be printed? (yes/no) [default=no]:
注意,因为我已经创建了一个 incus-zpool 所以这里不要用默认的创建zpool,而是输入已经创建的zpool名
思路三: ZFS 支持的 Loopback 文件
另一个更为简单的方法是使用 dd 在文件系统中创建一个巨大的空文件,然后基于该文件创建ZFS池,这样这个zpool就能被Incus管理作为存储驱动:
# 创建磁盘文件
dd if=/dev/zero of=/var/zfs-backend.img bs=1G count=50 status=progress
# 基于文件创建ZFS池zpool
sudo zpool create incus-zfs /var/zfs-backend.img
备注
这个方法是Incus/LXD 官方文档在单机测试无多余盘时,最推荐的 ZFS 尝鲜方法
在ZFS上存储的容器数据
在采用上述方法二实现了ZFS存储池来提供 incus 存储后端后,并且完成了初始化,就可以创建运行容器:
incus launch images:debian/12 debian12-dev
当运行容器后,在Host主机上检查 df -h 就会看到,这个容器的数据是存储在 icus-zpool 的一个数据集中,具体就是 incus-zpool/containers/debian12-dev :
Filesystem Size Used Avail Use% Mounted on
...
incus-zpool/containers/debian12-dev 1.4T 296M 1.4T 1% /var/lib/incus/storage-pools/default/containers/debian12-dev
实际上incus对ZFS的存储做了精细的划分,使用 zfs list 可以看到:
NAME USED AVAIL REFER MOUNTPOINT
incus-zpool 298M 1.38T 96K legacy
incus-zpool/buckets 96K 1.38T 96K legacy
incus-zpool/containers 46.8M 1.38T 96K legacy
incus-zpool/containers/debian12-dev 46.7M 1.38T 295M legacy
incus-zpool/custom 96K 1.38T 96K legacy
incus-zpool/deleted 576K 1.38T 96K legacy
incus-zpool/deleted/buckets 96K 1.38T 96K legacy
incus-zpool/deleted/containers 96K 1.38T 96K legacy
incus-zpool/deleted/custom 96K 1.38T 96K legacy
incus-zpool/deleted/images 96K 1.38T 96K legacy
incus-zpool/deleted/virtual-machines 96K 1.38T 96K legacy
incus-zpool/images 249M 1.38T 96K legacy
incus-zpool/images/3b3bd7f47fcaf505ddedf82c2a61ca27207ea945d3f595fa61076330f471e95d 249M 1.38T 249M legacy
incus-zpool/virtual-machines 96K 1.38T 96K legacy