Per-file access to cold, tiny-file trees on HDD is ~275 IOPS whatever the importer; only zfs send reads sequentially. Options recorded, direction: per-file for the 16 small instances, zfs send for nextcloud/seafile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8.4 KiB
plakar incus integration — design notes
Status: importer scaffolded and compiling 2026-08-23 in
perso/plakar/integration-incus (walker modeled on the official sftp
integration; enumerate project → per-instance sftp walk → per-file
records + synthetic .incus.yaml; allow/deny lists; VM sftp failures
reported as record errors, not fatal). See its README for
build/install/TODO. Untested against a live incus yet.
Goal: plakar source connector for Incus so instance backups land in a plakar kloset on S3 (leg 4 of the ks4 3-2-1 plan — see plan.md).
Decision 1 — per-file importer, not tarball streaming
The proxmox integration streams one full vzdump archive per VM
(FLAG_STREAM). For ks4 that model reads ~1.7 TB nightly (seafile
933 G, nextcloud 725 G): the incus backup/export API produces full
tarballs only — shared/api/instance_backup.go has just
instance_only and optimized_storage, no incremental option.
Instead the importer emits one record per file with real stat info
and a lazy reader. kloset's engine then does the incremental work for
us: snapshot/backup.go (checkVFSCache) reuses the cached object
when a path's stat matches the previous run and never opens the
file. Nightly cost after the initial seed = stat-walk + changed
files only; S3 receives only new chunks. Bonus: per-file browse and
restore of any snapshot.
File access is native incus REST: GET /1.0/instances/{name}/sftp
(a real SFTP session, also available per storage volume). Works over
the local unix socket or remote :8443, on stopped containers (the
daemon mounts the volume on demand), no ZFS assumptions.
Decision 2 — do not reuse incus copy --refresh / migration API
Considered and rejected. copy --refresh is POST /1.0/instances
with source.refresh=true (shared/api/instance.go:373) and is
not zfs-only — transports are negotiated per storage driver
(MigrationTypes(): zfs/btrfs/ceph native streams, universal RSYNC
fallback, driver_common.go:230). But:
- the receiver must be another incus daemon speaking the migration
websocket protocol (
internal/migration/migrate.proto); - the optimized payload is an opaque
zfs sendstream — storing those in plakar means chain-of-increments restore onto a real zpool, no per-file browse, no pruning, one corrupt link breaks the chain; - the RSYNC transport needs a materialized previous copy on the receiver — a full-size local mirror, i.e. leg 1 rebuilt inside a plugin;
- gotcha:
driver_zfs.go:801— refresh without snapshot copying forces RSYNC even zfs↔zfs (keepsnapshots.scheduleon sources).
copy --refresh stays what legs 1–2 use (host↔host replication);
plakar is content-addressed archiving to dumb storage. Complementary,
not competing.
Decision 3 — read the backup project replicas, not live instances
Reading a live rootfs is fuzzy. Leg 1 (incus-copy.sh -p backup -s backup, 01:00) already produces quiescent, crash-consistent stopped
replicas — the importer targets those (?project=backup), scheduled
after the copy completes. DB consistency remains the mariadb-dump
job's responsibility (plakar mysql importer / dump step).
Decision 4 — enumerate everything by default
incus-backup.db manifest drift (stale spot, missing livetrail,
outline, login…) is the failure mode to kill: the importer backs
up all instances in the project by default, opt-out via config,
never opt-in.
Other design points
- Location scheme
incus://<remote>/<project>/<instance>/...— stable pathnames keep the kloset VFS cache effective. - Synthetic records per instance: config + profiles
(
GET /1.0/instances/{name}, expanded) so restore can recreate the instance before pushing files back. - Optional later
mode=image: full tarball via the backups API for occasional exact-image restores (incus import); plakar dedups unchanged chunks between runs. Not the nightly path. - Restore path (exporter): create instance from stored config → push
file tree back via sftp. Weaker than
incus importbut granular.
Upstreaming (decided 2026-08-23: Route B — community integration)
Repo: github.com/jlutran/plakar-integration-incus (remote set,
initial commit done). Path to publication, in order:
- unit tests + first real run against ks4's
backupproject - open a PlakarKorp/plakar issue proposing the integration
(disclose the Apache-2.0
lxc/incusclient dependency) - push the repo, then PR a recipe against
PlakarKorp/hub→ installable asplakar pkg add incus - (optional, later) adoption into the
integrationsmonorepo: module rename togithub.com/PlakarKorp/integrations/incus, orphanintegration/incusbranch,incus/incuslayout
Packaging & deployment
Plugins are standalone executables (gRPC over stdio) — no rebuild of plakar, fully compatible with the APT-installed binary on ks4:
cd plakar/integration-incus && make # build importer/exporter
plakar pkg create manifest.yaml v0.1.0 # → incus_v0.1.0_linux_amd64.ptar
# on ks4, as root (plugins are per-user, cron runs as root):
plakar pkg add ./incus_v0.1.0_linux_amd64.ptar
plakar source add ks4-incus incus://... # then list it in /root/scripts/plakar-sources
Installing a local .ptar file needs no plakar login (the plugin
registry is only for fetching by name). Model the manifest.yaml on
integration-proxmox (tier: third-party, api_version: v1.1.0).
Compatibility contract = the go-kloset-sdk wire protocol: pin the SDK
to the installed plakar line (v1.1.x), rebuild the ptar on a plakar
major upgrade; a mismatch surfaces at backup time, so run a manual
plakar backup @ks4-incus after plakar upgrades.
The giant-instance problem (2026-08-24, seed attempt #1)
The per-file S3 seed of the whole backup project stalled at
~1 MiB/s once it reached the big replicas: sdb at 96 % util doing
~275 × 4 KiB reads/s — cold random tiny-file reads on an HDD are
seek-bound, regardless of importer (the fs importer hit the same
wall on the live pool; sftp changes nothing). At ~275 IOPS,
nextcloud (725 G, ~500 k files) + seafile (933 G, millions of block
files) need weeks to seed. The 16 small instances are unaffected
(solar smoke test: 5 GiB in 2m38s).
Options considered for nextcloud/seafile:
- backups-API tarball (
mode=image) — sequential-ish but incus materializes the tarball server-side first (no room on/; would needstorage.backups_volumeon the backup pool) and the tar walk itself seeks like any tree walk. Double IO nightly. Weak. zfs sendof the replica dataset — the only truly sequential read (disk block order, ~100+ MiB/s, no temp space): seed in hours. Full send nightly is ~4 h read for both giants; CDC chunking dedups unchanged stream regions. Restore =zfs receive+incus admin recover(document!). Backend-specific — acceptable as an opt-in mode for exactly these two.- Per-file + warm metadata — after a seed, nightly walks are
stat-only; keeping dnodes resident (
zfs_arc_min≈ 8 G) could make them fast. Doesn't solve the seed. - Exclude giants from this leg — leaves seafile without any fresh off-site copy (data leg already excludes it). Not acceptable long-term.
Direction (pending decision): seed the 16 small instances per-file
(exclude_instances=nextcloud,seafile), then implement option 2 as a
zfs+send:// mode or side-channel for the two giants, and revisit 3
for nightly stat-walk speed.
Open questions
- uid/gid view through the instance sftp endpoint (idmap handling for unprivileged containers) — verify what stat returns and what restore must remap.
- xattrs / ACLs / device nodes over sftp — coverage and fidelity.
- VMs: file access needs a running incus-agent — out of scope for ks4 (containers only); document the limitation.
Scheduling/lockingdecided 2026-08-23: the S3 instance leg is only as fresh as the last completed local incus-copy, so both run chained in one cron entry —incus-copy.sh -p backup -s backup ; <plakar incus leg>— never at a fixed offset (a long refresh, e.g. a post-gap full re-send, would race it).;not&&: a partially failed copy leaves stale but internally consistent replicas, still worth uploading. Same rule for manual test runs: check/var/log/incus-copy.logcompleted first.- S3 hardening: scoped credentials (no delete), versioning/object lock, plakar client-side encryption passphrase storage.