Proxmox VEVI · ZFSZFS architecture
Special vdevs, metadata devices and native encryption
What you'll learn
- Explain why a special vdev is part of the pool and not a cache, and what its loss costs
- Size and add a special vdev, and choose a special_small_blocks value deliberately
- Configure ZFS native encryption on a Proxmox dataset and manage its key
- State what encryption protects and what it leaves readable
Prerequisites
Verified against Proxmox VE 9.2.4 · Proxmox Backup Server 4.2.5 · Ceph Squid / Tentacle · Debian 13 (Trixie) · Linux kernel 7.0 (PVE 9.2 default) · 2026-08-12
The previous lesson covered three devices you can add to a pool that all share a comforting property: losing them costs you performance, not data. L2ARC is a read cache and its contents are a copy. A SLOG holds sync writes that have not yet reached the pool, and its loss costs you at most the last few seconds of acknowledged writes.
A special vdev is not in that family, and treating it as if it were is the most expensive mistake in this lesson. It is a top-level vdev holding the only copy of data the pool cannot function without. Lose it and you lose the pool — every dataset, every zvol, every snapshot.
The payoff for that risk is large enough that it is still worth doing. A pool of spinning disks with a mirrored NVMe special vdev behaves like flash for directory traversal, snapshot listing and small-file I/O, because all of that is metadata and metadata now lives on NVMe.
What actually goes on it
The upstream description is precise about the contents: “metadata, the
indirect blocks of user data, intent log (in absence of separate log
device), and deduplication tables.” Optionally, small data blocks join
them via the special_small_blocks property.
That list explains the performance effect. On a RAIDZ2 pool of spinning
disks, a ls -R across a large dataset is thousands of small random
reads of metadata blocks scattered across seven-millisecond seeks.
Moving those to NVMe does not make the disks faster; it removes the
disks from that path entirely.
The comparison that matters:
| Device class | Holds | If it fails |
|---|---|---|
cache (L2ARC) | A second copy of cached reads | Reads fall back to the pool. No data loss |
log (SLOG) | Sync writes not yet committed | At most the last few seconds of sync writes |
special | The only copy of metadata and small blocks | The pool is gone |
dedup | The only copy of the dedup table | The pool is gone |
There is one structural constraint to know before planning: “A pool must maintain at least one normal (non-dedup/-special) vdev before other devices can be assigned to the special class.” You cannot build a pool entirely out of special vdevs, which is occasionally what someone tries when they discover how much the class helps.
Adding one, and sizing it
POOL=tank
# always dry-run first: -n prints what would be done and does nothing
zpool add -n "$POOL" special mirror \
/dev/disk/by-id/nvme-SPECIAL_A \
/dev/disk/by-id/nvme-SPECIAL_B \
/dev/disk/by-id/nvme-SPECIAL_C
zpool add "$POOL" special mirror \
/dev/disk/by-id/nvme-SPECIAL_A \
/dev/disk/by-id/nvme-SPECIAL_B \
/dev/disk/by-id/nvme-SPECIAL_C
zpool status "$POOL"Sizing has two components, and only one of them is predictable.
Metadata alone is typically a fraction of a percent of the pool for VM-disk workloads, where the average block is large. A rule of thumb around 0.3% of pool capacity is the usual starting figure, and it is generous for zvols and stingy for a pool full of small files.
Small data blocks, if you enable them, are entirely workload-dependent and can be arbitrarily large. This is where sizing goes wrong.
POOL=tank
zdb -Lbbbs "$POOL" 2>/dev/null | tail -40An added special vdev only receives new allocations. Existing
metadata stays where it was written, on the spinning disks, and it
migrates only as those blocks are rewritten. On a pool holding VM disks
that happens gradually; on a pool holding cold archive data it may never
happen. The way to force it is a full rewrite — zfs send | zfs receive
into a new dataset, or a restore from PBS — which is worth knowing
before you conclude that the new NVMe made no difference.
special_small_blocks, and the way it goes wrong
The property is documented as “the threshold block size for including small file or zvol blocks into the special allocation class”, with a default of 0 — meaning no data blocks at all, metadata only. Valid values run “from 0 to maximum block size (16 MiB)”, and “before setting this property, a special class vdev must be added to the pool.”
DATASET=tank/smallfiles
zfs set special_small_blocks=32K "$DATASET"
zfs get -r special_small_blocks tankPOOL=tank
zpool list -v "$POOL"
zpool status "$POOL"$ zpool list -v tankNAME SIZE ALLOC FREE FRAG CAP HEALTH
tank 43.7T 9.12T 34.6T 4% 20% ONLINE
raidz2-0 21.8T 4.51T 17.3T 4% 20% ONLINE
...
raidz2-1 21.8T 4.52T 17.3T 4% 20% ONLINE
...
special - - - - - -
mirror-2 1.86T 1.79T 73.4G 61% 96% ONLINE
nvme-SPECIAL_A - - - - - ONLINE
nvme-SPECIAL_B - - - - - ONLINEIllustrative output
Removing one: mostly, you cannot
This is the constraint that turns a special vdev from a tuning decision into an architecture decision.
The zpool remove documentation lists what can be removed: “hot spare,
cache, log, and both mirrored and non-redundant primary top-level vdevs,
including dedup and special vdevs”. Then it gives the condition:
“Top-level vdevs can only be removed if the primary pool storage does
not contain a top-level raidz or draid vdev.”
Two further requirements apply: all top-level vdevs must have matching
ashift, and all encrypted dataset keys must be loaded.
So:
| Pool data vdevs | Can the special vdev be removed? |
|---|---|
| Mirrors | Yes, if ashift matches across all top-level vdevs |
| RAIDZ1/2/3 | No |
| dRAID | No |
The combination people most want — a large RAIDZ2 pool of spinning disks accelerated by a special vdev — is precisely the one where the decision is permanent. Adding the vdev is a one-way door on that pool, and the only exit is to destroy the pool and rebuild it.
Which makes the mirror width and the device quality decisions you make once, carefully, rather than something to improve later.
ZFS native encryption
Encryption is a per-dataset property, and like volblocksize it is
fixed at creation: “encryption must be specified at dataset creation
time and it cannot be changed afterwards.” It also “requires a
keyformat to be set at dataset creation time.”
POOL=tank
DATASET="$POOL/encrypted"
zfs create \
-o encryption=aes-256-gcm \
-o keyformat=passphrase \
-o keylocation=prompt \
"$DATASET"
zfs get encryption,keyformat,keylocation,keystatus "$DATASET"keyformat accepts passphrase, raw or hex, with raw and hex keys
required to be 32 bytes. keylocation accepts prompt (the default),
a file:// path, or an HTTP/HTTPS URL.
Descendants inherit the key from their encryption root by default,
and operations on the root apply implicitly to everything inheriting
from it. A child can become its own encryption root by specifying a
keyformat at creation time.
zfs get -r keystatus,encryptionroot,mounted tankCommon mistakes
- Treating a special vdev like L2ARC. L2ARC loss is free. Special vdev loss is the pool.
- A single-device special vdev on a redundant pool. The pool’s failure tolerance becomes one device.
- Adding it to a RAIDZ pool without realising it cannot be removed.
zpool removerefuses on any pool containing a RAIDZ or dRAID top-level vdev. special_small_blocksat or aboverecordsize. Everything goes to the special vdev, it fills, allocations silently revert, and performance decays with no warning.- Expecting existing metadata to migrate. Only new allocations go to the special class; a rewrite is what moves the rest.
- Sizing from a rule of thumb after enabling small blocks. Measure
with
zdb -Lbbbs— the distribution is workload-specific. keylocation=file://on the same disk as the data, described in a compliance document as encryption at rest. It protects against a removed disk and nothing more.- Encrypting a dataset “later”. The property is create-time only; the migration is a copy.
Key takeaways
- A special vdev holds metadata, indirect blocks and optionally small data blocks. It is part of the pool, not a cache: its loss loses everything.
- Its redundancy must match the pool’s. Three-way NVMe mirror for a RAIDZ2 pool.
- It cannot be removed from a pool containing RAIDZ or dRAID vdevs. On those pools, adding one is permanent.
special_small_blocksdefaults to 0. Setting it at or aboverecordsizesends everything to the special class, which then fills silently and reverts.- Only new allocations use the special class; existing metadata migrates when rewritten.
- Native encryption is create-time only, per dataset, inherited from an encryption root, and does not hide dataset names, sizes or snapshot structure.
- An unloaded key means unmounted datasets and guests that will not start. Choose the key-availability model before the first encrypted dataset, not during the first unplanned reboot.
Knowledge check
Knowledge check · 4 questions
Q1. A pool has two RAIDZ2 vdevs and a single NVMe drive added as a special vdev. The NVMe fails outright. What is the state of the pool?
Q2. A dataset with recordsize=128K is given special_small_blocks=128K. Performance is excellent for a week and then declines steadily. zpool status reports no errors. What happened?
Q3. Which statements about ZFS native encryption on a Proxmox host are accurate? Select all that apply.
Q4. A special vdev added to a pool of RAIDZ2 vdevs can be removed later with zpool remove if it turns out to have been a mistake.
Passing score: 75%. Answers are checked in this browser.