ZFS
zfspool storage type. Getting the pool layout right is the one decision you cannot easily undo.Pool builder · capacity, redundancy, IOPS
All vdevs healthy. The pool stripes data across every top-level vdev.
Why mirrors are the default advice for VM storage
# striped mirrors (RAID10) from four NVMe drives, 4K sectors
zpool create -o ashift=12 tank \
mirror /dev/disk/by-id/nvme-A /dev/disk/by-id/nvme-B \
mirror /dev/disk/by-id/nvme-C /dev/disk/by-id/nvme-D
# make it a PVE storage (thin zvols, VMs + CTs)
pvesm add zfspool tank-vm --pool tank --content images,rootdir --sparse 1
# or let the GUI/API do both: Node → Disks → ZFS → Create
pvesh create /nodes/pve1/disks/zfs --name tank --raidlevel raid10 \
--devices /dev/nvme0n1,/dev/nvme1n1,/dev/nvme2n1,/dev/nvme3n1 \
--ashift 12 --compression lz4 --add_storage 1$ zpool status tank
pool: tank
state: DEGRADED
status: One or more devices could not be used because the label is missing or
invalid. Sufficient replicas exist for the pool to continue
functioning in a degraded state.
action: Replace the device using 'zpool replace'.
config:
NAME STATE READ WRITE CKSUM
tank DEGRADED 0 0 0
mirror-0 DEGRADED 0 0 0
nvme-A ONLINE 0 0 0
nvme-B UNAVAIL 0 0 0
mirror-1 ONLINE 0 0 0
nvme-C ONLINE 0 0 0
nvme-D ONLINE 0 0 0
$ zpool replace tank nvme-B /dev/disk/by-id/nvme-E # resilver startsCopy-on-write: why snapshots are instant and free (at first)
Take a snapshot, then overwrite blocks
Writes never overwrite in place: the new block goes to free space, then every parent up to the uberblock is rewritten to point at it. Click a data block to overwrite it.
None yet. A snapshot just keeps the current uberblock's tree alive.
zvols for VMs, datasets for containers
| zvol (VM disk) | dataset / subvol (container) | |
|---|---|---|
| name | rpool/data/vm-100-disk-0 | rpool/data/subvol-101-disk-0 |
| what it is | a virtual block device at /dev/zvol/…, the guest puts its own filesystem on it | a ZFS filesystem mounted on the host, the CT sees its files directly |
| block size | volblocksize, fixed at creation: 16K default (OpenZFS 2.2+). Larger = better compression and less RAIDZ padding, worse for small random writes | recordsize, a maximum (128K default): small files use small blocks |
| size limit | volsize (the virtual disk size) | refquota (the CT disk size) |
| thin? | only with sparse 1 on the storage (no refreservation) | always: quota, not a reservation |
| compression | lz4 inherited from the pool: cheap, usually a net speedup | same, and it compresses per file record |
ARC, and the extra vdev classes
Size the ARC
The ARC is ZFS's read cache in RAM. It does shrink under memory pressure, but not instantly, and RAM promised to VMs is not something you want to compete for. New installs since PVE 8.1 cap it at 10% of RAM, at most 16 GiB. That already covers the documented rule of thumb for this pool.
# /etc/modprobe.d/zfs.conf options zfs zfs_arc_max=13743895347 # apply now without reboot: echo 13743895347 > /sys/module/zfs/parameters/zfs_arc_max # rpool as root: also run update-initramfs -u -k all
Storage replication: ZFS's answer to shared storage
pvesr periodically sends incremental snapshots of a guest's disks to another node, so migration only transfers the latest delta and HA can restart the guest there. The price is the interval: anything written since the last successful run is lost on a crash. Let it run, then pull the plug.Replicate VM 100 from pve1 to pve2
Every run, pvesr takes a new snapshot of each of the guest's disks, sends only the blocks changed since the last common snapshot (zfs send -i) and then deletes the older snapshot on both sides. The first run is a full send. Migration and HA failover can then use the replica instead of copying the whole disk.
$ zfs list -o name,used,refer,volsize,volblocksize,compressratio -r rpool/data
NAME USED REFER VOLSIZE VOLBLOCK RATIO
rpool/data 61.2G 96K - - 1.52x
rpool/data/subvol-101-disk-0 1.31G 1.31G - - 2.03x
rpool/data/vm-100-disk-0 38.4G 31.0G 64G 16K 1.41x
# snapshots: PVE wraps them (qm snapshot also saves the VM config / RAM state)
qm snapshot 100 before-upgrade
zfs list -t snapshot -o name,used,refer rpool/data/vm-100-disk-0
# rpool/data/vm-100-disk-0@before-upgrade 7.4G 30.9G ← USED = blocks only it still holds
# the raw ZFS equivalent
zfs snapshot rpool/data/vm-100-disk-0@manual
zfs rollback rpool/data/vm-100-disk-0@manual# replicate VM 100 to pve2 every 15 minutes, max 50 MB/s
pvesr create-local-job 100-0 pve2 --schedule "*/15" --rate 50
pvesr list # jobs
pvesr status # last sync, duration, next run, fail count
pvesr schedule-now 100-0 # run it as soon as possible
# under the hood, per disk, per run:
# zfs snapshot rpool/data/vm-100-disk-0@__replicate_100-0_<epoch>__
# zfs send -i @<previous> @<new> | ssh pve2 zfs recv ...
# then the previous replication snapshot is removed on both sides- why it mattersPool layout decides random IOPS: each vdev is roughly one disk's worth. For VM storage, use striped mirrors; use RAIDZ for capacity workloads like backups. Losing any top-level vdev loses the pool.
- why it mattersCopy-on-write makes snapshots instant, but a snapshot pins every block overwritten after it. Old snapshots quietly eat space; watch the USED column and prune them.
- why it mattersZFS replication gives local disks a failover story, but it is asynchronous: RPO equals the schedule. Zero-loss failover needs shared storage such as Ceph.