87 lines
4.8 KiB
Markdown
87 lines
4.8 KiB
Markdown
# SPDK NVMe/TCP target — long-lived, multi-version
|
|
|
|
This is the fleet's **root disk**: a userspace SPDK `nvmf_tgt` exporting each
|
|
rootfs release as **two read-only NVMe/TCP namespaces** (erofs DATA + dm-verity
|
|
HASH — the distro's systemd-veritysetup can't do the in-file hash-offset layout,
|
|
so they're separate namespaces with distinct UUIDs).
|
|
|
|
## Why the target is long-lived
|
|
|
|
`nvmf_tgt` sits in the **live data path of every running host's root fs**.
|
|
Restarting the process tears down **every** NVMe/TCP association at once
|
|
(fleet-wide, not version-scoped) — a host whose root link drops past
|
|
`ctrl_loss_tmo` gets root I/O errors and must reboot. So **the process must never
|
|
restart for a routine change.**
|
|
|
|
A host changes root version **only by rebooting** (no live swap). Therefore:
|
|
**version transitions are reboots; SPDK changes are additive/online; the target
|
|
process stays up.** Routine rollouts never restart SPDK.
|
|
|
|
## How it works
|
|
|
|
| Piece | Role |
|
|
|---|---|
|
|
| [`spdk-target.sh`](spdk-target.sh) | **Persistent entrypoint.** Starts `nvmf_tgt` once, creates the TCP transport + discovery listener, then reconciles every staged release. Never restarted to change versions. On (re)start it re-adds everything under `./releases/`, so a serving-host reboot restores all served versions. |
|
|
| [`spdk-release.sh`](spdk-release.sh) | **In-container control** (JSON-RPC). `add`/`remove`/`list`/`drain-status`/`reconcile` a version **live**. One **subsystem per version** (distinct NQN) so adding/removing one never disturbs the subsystem other hosts are attached to. Removal is **drain-gated**. |
|
|
| [`spdk-ctl.sh`](spdk-ctl.sh) | **Host-side ops.** Stages a release's artifacts from its OCI image into `./releases/<ver>/` and drives the live target via `docker compose exec`. |
|
|
| `./releases/<version>/` | Per-version `rootfs.erofs` + `rootfs.hash` + `verity.json`. `verity.json` is the **single source of truth** for that version's namespace UUIDs + subsystem NQN (written by `scripts/extract-artifacts.sh`), so the host's by-id verity refs and the target's namespace UUIDs always agree. |
|
|
|
|
Per-version namespace UUIDs are required: two versions served at once must have
|
|
globally-distinct namespace UUIDs or they collide on the same
|
|
`/dev/disk/by-id/nvme-uuid.<uuid>` symlink on the host. `extract-artifacts.sh`
|
|
derives them deterministically (UUIDv5 of the version) and bakes them into both
|
|
the immutable UKI cmdline and `verity.json`.
|
|
|
|
## Rolling update (add new, drain old, clean up)
|
|
|
|
```bash
|
|
cd serving/spdk
|
|
|
|
./spdk-ctl.sh load 1.3.0 # serve 1.3.0 ALONGSIDE the running 1.2.0 (live, no restart)
|
|
# ... bump the deployment manifest digest; reboot hosts → they come up on 1.3.0 ...
|
|
./spdk-ctl.sh list # watch 1.2.0 CONNS drop toward 0
|
|
# VERSION CONNS NQN
|
|
# rootfs-1.3.0 7 nqn.2026-06.org.example.fleet:rootfs-1.3.0
|
|
# rootfs-1.2.0 0 nqn.2026-06.org.example.fleet:rootfs-1.2.0
|
|
./spdk-ctl.sh unload 1.2.0 # drain-gated: refuses while CONNS > 0
|
|
```
|
|
|
|
`unload` **refuses to remove a version that still has connected controllers**
|
|
(an assigned-but-offline host could still return to it). Drain those hosts
|
|
first, or `./spdk-ctl.sh unload 1.2.0 --force` to **evict** them (they lose root
|
|
I/O — only for a known-dead version).
|
|
|
|
In-container equivalents (what `spdk-ctl.sh` calls):
|
|
|
|
```bash
|
|
docker compose exec nvmeof spdk-release list
|
|
docker compose exec nvmeof spdk-release add 1.3.0
|
|
docker compose exec nvmeof spdk-release drain-status 1.2.0
|
|
docker compose exec nvmeof spdk-release remove 1.2.0 [--force]
|
|
```
|
|
|
|
## Capacity & availability
|
|
|
|
- Each concurrently-served version costs target memory/hugepages + a file-backed
|
|
bdev. Plan for N live versions (current + previous + canary) during long
|
|
rollouts; raise `HUGEPAGES` accordingly.
|
|
- The target is still a **single-node SPOF**. Even with online add/remove, a node
|
|
failure or a planned `nvmf_tgt` restart drops every connection. The structural
|
|
fix is **HA + NVMe-oF multipath (ANA)**: ≥2 targets serving byte-identical,
|
|
same-UUID, read-only namespaces (replication is coherent precisely because the
|
|
root is read-only + content-addressed). Cheap host-side mitigation: generous
|
|
`ctrl_loss_tmo`/`reconnect_delay`. See `docs/TODO.md` #6/#7.
|
|
- There is **no zero-impact single-node restart** — only drain/failover.
|
|
|
|
## Bootstrap (simple single-version)
|
|
|
|
`docker compose up -d` still works with one version: the OCI image volume at
|
|
`/artifacts` is **seeded** into `./releases/${IMAGE_VERSION}/` and served
|
|
automatically. Use `spdk-ctl.sh load` to add more versions on top.
|
|
|
|
## Legacy
|
|
|
|
[`../spdk-setup.sh`](../spdk-setup.sh) is the original **single-version,
|
|
configure-at-start** entrypoint (changing versions = container restart = all
|
|
connections dropped). Superseded by the long-lived target; kept for reference.
|