Skip to main content
RunBook Academy

Proxmox VEVI · ZFSZFS architecture

Special vdevs, metadata devices and native encryption

Advanced⏱ ~26 minzpoolzfs

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

Not yet marked complete on this device.

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 classHoldsIf it fails
cache (L2ARC)A second copy of cached readsReads fall back to the pool. No data loss
log (SLOG)Sync writes not yet committedAt most the last few seconds of sync writes
specialThe only copy of metadata and small blocksThe pool is gone
dedupThe only copy of the dedup tableThe 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

Data-loss riskadd a mirrored special vdev to an existing pool
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.

Service impact possiblemeasure the actual block-size distribution before deciding
POOL=tank
zdb -Lbbbs "$POOL" 2>/dev/null | tail -40

An 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.”

Configuration changesend small blocks to the special vdev, on one dataset
DATASET=tank/smallfiles
zfs set special_small_blocks=32K "$DATASET"
zfs get -r special_small_blocks tank
Read-only / Safethe check that catches it before the users do
POOL=tank
zpool list -v "$POOL"
zpool status "$POOL"
Read-only / Safe
$ zpool list -v tank
NAME                     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          -      -      -     -      -  ONLINE

Illustrative 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 vdevsCan the special vdev be removed?
MirrorsYes, if ashift matches across all top-level vdevs
RAIDZ1/2/3No
dRAIDNo

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.”

Configuration changecreate an encrypted dataset
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.

Read-only / Safekey state across the pool
zfs get -r keystatus,encryptionroot,mounted tank

Common 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 remove refuses on any pool containing a RAIDZ or dRAID top-level vdev.
  • special_small_blocks at or above recordsize. 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_blocks defaults to 0. Setting it at or above recordsize sends 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

  1. 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?

  2. 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?

  3. Q3. Which statements about ZFS native encryption on a Proxmox host are accurate? Select all that apply.

  4. 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.