fix(truenas): give passthrough drives iothreads and size the VM for ZFS+NFS#48
Merged
Conversation
…FS+NFS
The truenas template shipped 4G / 1 vCPU, and every passthrough data drive was
serviced on QEMU's main loop. On a storage VM that combination serializes
everything: nfsd's thread pool, ZFS checksumming/compression/write aggregation,
and vCPU execution all contend for a single core, while disk I/O shares the
main loop with the vCPU it is blocking.
Measured on a 60T two-drive pool serving a VM boot disk over NFSv4:
| op | ops | avg RTT | avg exec |
|-------|--------|---------|----------|
| READ | 74.3M | 38.3ms | 63.2ms |
| WRITE | 19.0M | 89.1ms | 10129.2ms |
WRITE averaged 89ms of round-trip but 10.1s of total execution. That gap is not
the network (0.39ms ping, 1 retransmit in 104.9M RPC calls) and not the pool
(zpool iostat showed 1-3ms write latency, FRAG 3%, no scrub or resilver) — it
is queueing in front of a single-vCPU NFS server.
Three changes:
- Each passthrough disk now declares its own iothread object and binds it on
the virtio-blk-pci device, moving drive I/O off the main QEMU loop. IDs are
derived from the deterministic drive index so multiple drives cannot
collide (a duplicate object id makes QEMU refuse to start).
- CPUs go from 1 to 2, exposed as one hyperthreaded core (1 socket, 1 core,
2 threads), which is enough to keep nfsd off the critical path without
taking a second physical core from the host.
- Memory goes from 4G to 6G. Sized from measurement rather than convention:
ARC held 2.1G against a 2.9G cap at a 95% hit rate with arc_no_grow and
memory_throttle_count both zero, so ARC was working well and merely short
of headroom.
Passthrough drives also gain discard=unmap, matching the OS disk.
Tests cover the iothread object/device pairing, declaration ordering, per-drive
uniqueness across a built machine, that non-passthrough disks are unaffected,
and the hyperthreaded -smp topology. The missing-binding, colliding-id and
collapsed-threads mutations all fail them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The
truenastemplate shipped 4G / 1 vCPU, and every passthrough data drive was serviced on QEMU's main loop. On a storage VM that combination serializes everything:nfsd's thread pool, ZFS checksumming/compression/write aggregation, and vCPU execution all contend for a single core — while disk I/O shares the main loop with the vCPU it is blocking.This surfaced while investigating a self-hosted CI runner whose boot disk lives on NFS backed by this VM. The guest sat at 80-94% iowait with idle
us/sy, and CI jobs crawled.Measurement
Per-op NFSv4 client stats (
/proc/self/mountstats) against a 60T two-drive pool:WRITE averaged 89ms of round-trip but 10.1s of total execution — a 113x gap. That gap is client-side queueing, and it is not explained by either end of the wire:
zpool iostat -lshowed 1-3ms write latency, FRAG 3%, CAP 20%, both poolsONLINE, no scrub or resilver running.What is left is the NFS server itself: a single vCPU in front of a pool that retires writes in milliseconds.
Changes
Per-drive iothreads. Each passthrough disk now declares its own iothread object and binds it on the
virtio-blk-pcidevice, moving drive I/O off the main QEMU loop:IDs derive from the deterministic drive index, so multiple drives cannot collide — a duplicate object id makes QEMU refuse to start outright.
CPUs 1 → 2, exposed as one hyperthreaded core (1 socket, 1 core, 2 threads). Enough to keep
nfsdoff the critical path without taking a second physical core from the host.Memory 4G → 6G, sized from measurement rather than convention. On the 4G VM, ARC held 2.1G against a 2.9G cap at a 95% hit rate, with
arc_no_growandmemory_throttle_countboth zero — ARC was working well and merely short of headroom, not starved. 6G lifts the default cap to ~3G and leaves ~1G free fornfsd.Passthrough drives also gain
discard=unmap, matching the OS disk.Note the published template docs already specified different defaults than the code shipped — the code had drifted from its own documentation. This brings them back in line.
Expected impact
The vCPU change is what targets the 10.1s writes. Memory is secondary: ARC was already at a 95% hit rate, so more RAM buys little on its own. If 2 vCPUs prove insufficient under load, that is the knob to revisit before RAM.
Testing
go build ./...,make lint(0 issues), and the fullgo test ./...suite pass.New tests cover the iothread object/device pairing, declaration ordering (the object must precede the device referencing it), per-drive ID uniqueness across a built machine, that non-passthrough disks are unaffected, and the hyperthreaded
-smptopology.Each assertion was mutation-checked — removing the iothread binding, colliding the IDs, and collapsing threads to 1 all fail the suite.
Generated args verified end-to-end for a two-drive truenas-like config:
Note for existing VMs
Templates apply at create time, so this does not change an already-created VM. An existing truenas VM needs its persisted config updated and a restart (which takes the NAS down) to pick these up.