Files
home-network-k3s-impl/docs/plans/ssd-etcd-isolation.md
T
jakman85andClaude Opus 4.8 cbc2301b30 docs: reconcile docs/comments with post-upgrade cluster state
Text-only cleanup after the upgrade campaign. Updates stale version
strings, prose, and comments to the verified current facts; no
Kubernetes behavior or version pins changed except the kubectl
toolchain bump (below).

Verified facts applied: k3s v1.36.4+k3s1, Debian 13 trixie,
Cilium 1.20.1 / Gateway API v1.6.1, Longhorn 1.12.1, node RAM 24 GiB,
dedicated virtio2 SSD etcd disk per node, Velero chart 12.x / v1.18,
Grafana 13.2.0.

- Version strings across infra/ and platform/ READMEs + comments
- kubectl toolchain 1.31.4 -> 1.36.4 (devcontainer/ci/conformance
  versions.env; 1.31 vs a 1.36 cluster exceeded the +-1 skew)
- Retired-single-apps-Kustomization refs -> apps-wave-0/1/2 + apps-vms
  with per-wave SOPS decryption (flux/clusters docs + bootstrap.sh)
- COMPLETED/historical banners on the three executed plan docs
- Resolved TODO(WS-SEC) markers -> Renovate-maintained ranges
- Appended dated DECISIONS-LOG entry (2026-08-30) superseding the
  2026-08-26 LIVE STATE line

Left as revalidation items (not blind-edited): external-snapshotter
v8.2.0 vs 1.36, Cilium ext_authz/forward-auth relevance under 1.20.1,
conformance ImageVolume feature-gate assertions under 1.36.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-30 15:13:12 -05:00

14 KiB
Raw Blame History

SSD-backed etcd isolation for the k3s dev cluster

✅ COMPLETED / historical — executed 2026-08-30, see git log

This proposal was approved and carried out. Each node now has a dedicated virtio2 SSD etcd disk (/dev/vdc, local-zfs) hosting the etcd data dir. The Status: PROPOSAL line below and the sizing/RAM figures are the original investigation record — retained for the reasoning, not as a live to-do. Current state is tracked in infra/PHASE1-STATUS.md and the 2026-08-30 DECISIONS-LOG entry.

Status: PROPOSAL — awaiting user approval. No changes have been made. Author: investigation agent (read-only), 2026-08-29 Problem: Longhorn replica-rebuild I/O storms on the HDD-backed VM disks starve etcd's WAL fsync, crash-looping k3s and flapping the kube-vip VIP (192.168.2.50:6443). Even concurrent-replica-rebuild-per-node-limit=1 tips etcd. Question: Can the Proxmox host's ~1 TB system SSD (rpool) host dedicated SSD-backed storage for etcd (and/or Longhorn) to end these incidents?

Verdict: YES for etcd (strongly recommended). NO for Longhorn.


1. Measured facts (all collected read-only on 2026-08-29)

1.1 rpool — the system/boot SSD pool

Property Value
Pool rpool, mirror (mirror-0) of 2 devices
Devices ata-MTFDDAK960TDS_... × 2 — Micron 5100/52x0 960 GB, 2.5" SATA
Class Enterprise SATA SSD with power-loss protection (PLP) — safe for sync/fsync
Health Percent_Lifetime_Remain = 97% (3% used), 13 904 power-on hours, 0 reallocated sectors, ~96 TB written. Excellent.
Pool size / alloc / free 888 G / 538 G / 350 G free (zpool list, 60% cap)
ZFS AVAIL ~323 GiB available to any new dataset/zvol
System root rpool/ROOT/pve-1 = 430 G used (Proxmox host /)
Proxmox storage on it local-zfs → pool rpool/data, content images,rootdir, sparse 1 (thin), ~338 GiB available per pvesm status
Pool settings compression on, sync standard (inherited), dedup off, recordsize 128K

local-zfs already advertises images,rootdir content, so Proxmox can place VM disks on the SSD today with no storage-config change.

1.2 Current VM disk backing — confirms the HDD assumption

VMs (bpg/OpenTofu-provisioned): dev-1 = vmid 105, dev-2 = 102, dev-3 = 101. Each has two disks, both on the Storage datastore:

  • Storage = ZFS pool on 2× WDC WUH721818ALE6L4 18 TB HDD (sdc/sdd, ROTA=1), mirror, 16.4 T.
  • Per node: virtio0 = vm-<id>-disk-0 (64 G, OS/root) and virtio1 = vm-<id>-disk-1 (128 G, Longhorn data). volblocksize 16K.

So etcd, the OS, and Longhorn all currently sit on spinning rust. That is the root cause: etcd WAL fsync competes with Longhorn rebuild I/O on the same HDD spindles.

(Storage2 = 2× Seagate ST8000VN004 8 TB HDD is a separate HDD pool, not involved.)

1.3 Size of the hot data

  • etcd DB: ~82.6 MB. Measured via apiserver metric apiserver_storage_size_bytes{storage_cluster_id="etcd-0"} = 8.264e7. A 4-day-old cluster running ~18 services. Even allowing generous WAL + growth headroom, etcd fits in single-digit GB with room to spare — trivially ×3.
  • Longhorn: ~22 GB actual data per node right now, but the data disk is 128 GiB and Longhorn reports storageScheduled ≈ 152 GiB (thin-overcommitted) vs storageMaximum ≈ 125 GiB per node. 18 PVCs, numberOfReplicas=3. Provisioned total ≈ 152 GiB. Longhorn is the churn source we want to isolate etcd from, and it can grow toward 128 GiB/node.

1.4 Sizing conclusion

Candidate Per-node footprint ×3 on rpool Fits in 323 GiB avail?
etcd ~83 MB now; size a 16 GiB disk ~48 GiB reserved (thin) Yes, easily
Longhorn ~22 GB now, up to 128 GiB provisioned up to ~384 GiB No — would fill/threaten the boot disk

2. Viability verdict

etcd on SSD: viable and high-leverage. etcd is ~83 MB; three 16 GiB SSD-backed disks cost ~48 GiB of thin allocation (actual ~250 MB) against 323 GiB free on an enterprise, PLP-equipped, 97%-healthy SSD mirror. This directly removes the WAL fsync path from the HDD contention domain — the smallest-footprint fix for the exact failure mode. This is the recommendation.

Longhorn on SSD: not viable here. Its provisioned footprint (~384 GiB across 3 replicas at full size) does not fit alongside the 430 GiB Proxmox root on a 888 GiB pool, and co-locating heavy Longhorn rebuild I/O on the boot SSD reintroduces contention with the host itself. Keep Longhorn on HDD (Storage). Rejected.


3. Options evaluated

  • Option A — dedicated small SSD disk per node, etcd-only (RECOMMENDED). Add a ~16 GiB SSD-backed virtual disk (zvol on rpool/data via local-zfs) to each of the 3 VMs, mount it at the k3s etcd directory (/var/lib/rancher/k3s/server/db), migrate etcd data onto it one node at a time. Isolates WAL fsync from HDD Longhorn churn. Minimal space, minimal blast radius.
  • Option B — Longhorn replicas/disk on SSD. Rejected (§2): does not fit and re-couples host I/O.
  • Option C — do nothing; wait for the SSD mini-PCs. Zero risk, zero work, but leaves the cluster fragile to every node bounce until the mini-PC migration lands (deferred, no committed date). Option A is a cheap, reversible bridge that Option C eventually supersedes (§7).

Recommendation: Option A now, as an interim fix, carried by tofu.


4.1 Model it in tofu (preferred over manual)

The VMs are bpg/OpenTofu-managed (infra/tofu/). Disks are declared as disk blocks in main.tf; both current disks (virtio0, virtio1) point at var.datastore (Storage). Add a third disk on the SSD datastore:

  1. New variables in infra/tofu/variables.tf:
    variable "etcd_ssd_datastore" {
      description = "SSD-backed datastore for the dedicated etcd disk (VERIFIED: local-zfs = rpool/data, ZFS mirror on Micron 5100 SATA SSDs w/ PLP)."
      type        = string
      default     = "local-zfs"
    }
    variable "etcd_disk_gib" {
      description = "Dedicated SSD etcd disk per node, GiB (etcd DB is ~83 MB; 16 GiB is generous headroom)."
      type        = number
      default     = 16
    }
    
  2. New disk block in the VM resource in main.tf, appended AFTER the existing virtio1 block (keep interface order ascending so bpg does not reorder):
    # Dedicated SSD etcd disk (virtio2 -> /dev/vdc). Isolates etcd WAL fsync from
    # the HDD Storage pool where the OS + Longhorn disks live.
    disk {
      datastore_id = var.etcd_ssd_datastore
      interface    = "virtio2"
      size         = var.etcd_disk_gib
      discard      = "on"
      cache        = "none"   # honor guest fsync through to ZFS ZIL (PLP SSD = safe+fast)
      iothread     = false
    }
    
  3. Plan and inspect before applying. tofu plan MUST show an in-place update (disk add) for the targeted VM — NOT a replacement. If it shows replace, stop and reassess (do not destroy a running etcd node).
  4. Apply one node at a time with -target (see §4.3 for quorum ordering), e.g. -target='proxmox_virtual_environment_vm.node["k3s-dev-3"]' (adjust to the actual resource address in this repo). A disk add is applied online by bpg, but treat the node as going down for the etcd migration regardless.

Coupling caveat — dev-3 memory drift. dev-1/dev-2 are at memory_mib = 24576 but dev-3 is live at 16384 (drift from tofu default 24576). A tofu apply that touches dev-3 will also try to reset its memory to 24576. Per the Proxmox rule, memory changes need a full stop/start, not a reboot. Decide deliberately: either (a) let the same maintenance window fix the memory drift on dev-3 (stop → set → start), or (b) -target narrowly and accept that dev-3's plan will include the memory change. Do dev-3 in its own window with eyes on it.

Manual alternative (if not using tofu): on the Proxmox host, qm set <vmid> -virtio2 local-zfs:16,discard=on,cache=none then stop/start the VM. This drifts from tofu state and is discouraged — if done, import/reconcile into tofu afterward. Prefer the tofu path.

4.2 Optional space guarantee

local-zfs is sparse (thin), so the etcd zvols consume only real usage. To guarantee etcd can never be squeezed if the system disk fills, set a modest refreservation on each etcd zvol (e.g. zfs set refreservation=16G rpool/data/vm-<id>-disk-2). 3×16 GiB = 48 GiB reserved out of 323 GiB avail — safe. This is a host-side ZFS tweak, not expressible in bpg; apply manually if desired.

4.3 Migrate etcd onto the new disk — quorum-safe, ONE node at a time

k3s has no flag to relocate only the etcd directory (--data-dir moves everything), so the clean approach is to mount the SSD disk at /var/lib/rancher/k3s/server/db (this directory holds etcd/ — member data, WAL, snapshots — and nothing else that matters).

Pre-flight: confirm all 3 etcd members healthy and the cluster is quiet (no active Longhorn rebuild). kubectl get nodes all Ready; check etcd member health.

Per node, repeat for dev-3, then dev-2, then dev-1 (leader last):

  1. Verify 3/3 etcd members healthy BEFORE touching this node (losing one of three keeps quorum at 2/3; never take a second node until the previous is fully back).
  2. Stop k3s on the node: systemctl stop k3s. (Node leaves quorum; 2/3 remain.)
  3. Identify the new disk (/dev/vdc — the third virtio disk) and format it: mkfs.ext4 -L k3s-etcd /dev/vdc (ext4 or xfs; ext4 is fine and simple).
  4. Preserve and move existing data:
    mv /var/lib/rancher/k3s/server/db /var/lib/rancher/k3s/server/db.bak
    mkdir /var/lib/rancher/k3s/server/db
    mount /dev/vdc /var/lib/rancher/k3s/server/db
    rsync -aHAX --numeric-ids /var/lib/rancher/k3s/server/db.bak/ /var/lib/rancher/k3s/server/db/
    
  5. Persist the mount in /etc/fstab by UUID (blkid /dev/vdc), e.g. UUID=<uuid> /var/lib/rancher/k3s/server/db ext4 defaults,noatime 0 2, then systemctl daemon-reload.
  6. Start k3s: systemctl start k3s. Wait until the node is Ready AND its etcd member is healthy and has re-joined (member list shows 3 started, no learner stuck). Confirm the VIP 192.168.2.50:6443 is stable.
  7. Only after full recovery, remove the backup: rm -rf /var/lib/rancher/k3s/server/db.bak. Then proceed to the next node.

Reproducibility for rebuilds: the SSD disk + mount should also be baked into the node cloud-init first-boot baseline (the same template that already provisions the Longhorn virtio1 disk and swap-off), so a tofu-rebuilt node comes up with /dev/vdc formatted and mounted at the etcd path automatically. Update that template in the same change set as the tofu disk block.

4.4 Validation

  • kubectl get nodes — all 3 Ready; no flapping over a 30–60 min soak.
  • etcd: 3 members, all started/healthy, one leader; DB size sane (~tens–hundreds MB).
  • On each node, df -h /var/lib/rancher/k3s/server/db shows the new /dev/vdc mount.
  • Regression test the original failure: with the cluster healthy, trigger a controlled Longhorn replica rebuild (e.g. bounce ONE node after approval) and confirm etcd stays healthy and the VIP does not flap — the whole point of the change.
  • Host-side: zpool status rpool clean; zfs list rpool/data shows the 3 new vm-*-disk-2 zvols with small real usage.

4.5 Rollback

Per node, fully reversible while db.bak still exists (before step 7): systemctl stop k3s → umount .../server/db → rmdir the empty mountpoint → mv db.bak db → remove the fstab line → systemctl start k3s. To fully revert the disk, detach virtio2 (qm set <vmid> -delete virtio2 after stop) or tofu revert the disk block, then stop/start. Because etcd is a replicated 3-member cluster, a botched single node can also simply be reset and re-joined from the other two.


5. Risks

  1. System-disk contention / exhaustion (top risk). rpool is also the Proxmox boot/root disk (430 GiB used of 888 GiB). Mitigations: keep the etcd disks tiny (16 GiB, thin), optional refreservation (§4.2), and never put Longhorn here (§2). etcd's steady-state real usage is ~hundreds of MB; the marginal I/O it adds to the SSD is negligible next to what it removes from the HDD.
  2. Consumer-SSD fsync/PLP caveat — N/A here, but assert it. etcd correctness under power loss depends on honest fsync. These are Micron 5100/52x0 enterprise SATA SSDs with power-loss protection, so sync=standard + cache=none is both fast and durable. (If the disks were consumer SSDs without PLP this concern would be real — it is not, for this hardware.)
  3. Quorum during migration. Mitigated by strict one-node-at-a-time with full recovery gates (§4.3) and leader-last ordering. Never take a second node down before the previous rejoins.
  4. tofu coupling with dev-3 memory drift + apply semantics (§4.1). An apply touching dev-3 also resets memory 16384→24576 (needs stop/start). Verify tofu plan shows disk add, not VM replace, before applying. Use -target.
  5. Backup/restore assumptions. k3s etcd snapshots (/var/lib/rancher/k3s/server/db/snapshots) now live on the SSD too — fine, but confirm any snapshot/backup job still points at the (unchanged) path.

6. Recommendation summary

Implement Option A: a 16 GiB SSD-backed virtio2 disk per k3s VM (zvol on rpool/data via local-zfs), etcd-only, migrated one node at a time with the quorum-safe procedure in §4.3, modeled in tofu (+ cloud-init baseline) for reproducibility. Do not move Longhorn to SSD. This is a small, reversible, high-leverage fix targeting the exact WAL-starvation failure.


7. Relationship to the deferred SSD-mini-PC migration

This is explicitly an interim bridge on the current Proxmox dev host (192.168.2.102), whose bulk storage is HDD. Once the k3s cluster migrates to the planned all-SSD mini-PCs, every I/O path (OS, etcd, Longhorn) is already on SSD and the HDD-contention failure mode disappears — the dedicated etcd disk becomes unnecessary. At that point the isolation can be either dropped or carried forward harmlessly (a dedicated etcd disk is still good hygiene). Because Option A is captured in tofu + cloud-init, it travels with the cluster definition and can be removed in one reviewed change when the mini-PCs land. Until then it stops the recurring incidents at minimal cost.