Enrolling a cache node on an md array
Enrolling a cache node on an md array
Audience: whoever is standing up a regional cache node on a box whose disks all carry the OS.
Scope: device-mode enrollment where the target is a Linux RAID array the operator assembled, rather than a spare whole disk.
Durability rule, unchanged: cache bytes are disposable and Object Storage remains authoritative. Nothing in this runbook risks data that matters. What it risks is a reinstall window and a one-time enrollment token.
Read section 1 before you schedule anything.
It is the only part of this document with a deadline attached.
1. The ordering trap
Real bare metal has no spare disk. Every disk carries a partition of the OS, so whole-disk device mode has nothing to offer and directory mode has nowhere with enough free space. The way out is to reinstall with the OS on a small mirror and give the remainder to an array the cache owns outright:
nvme0n1p1 + nvme1n1p1 64 GiB each -> md0 RAID1 ext4 /
nvme0n1p2 + nvme1n1p2 remainder -> md1 RAID0 ext4 /var/platinum/cacheDo not reinstall the box until array support has shipped to the control plane this box enrolls against. After the reinstall both disks carry OS partitions, so whole-disk device mode is dead, and the 64 GiB mirror is below the directory-mode floor. The box would have no enrollable target at all and would sit with zero cache until the code lands.
While you wait, the existing capacity-raise route can lift a directory-mode node
to 0.7 × (available + used) on its current filesystem. One click, no code, and
it costs nothing to undo.
2. Build the array
Partition at install time: p1 = 64 GiB on each disk, RAID1, ext4, /. p2 =
the remainder on each disk, no filesystem and no RAID unless the installer
offers RAID0 on p2 directly.
If you are building it by hand:
mdadm --create /dev/md1 --homehost=any --metadata=1.2 --level=0 --chunk=512 \
--raid-devices=2 /dev/nvme0n1p2 /dev/nvme1n1p2--homehost=any is mandatory, not a preference. mdadm records a homehost in
the array metadata; an array pinned to the name of the box that created it does
not auto-assemble on a box that answers to a different name, which is what a
rename or a reimage produces. udev then names it /dev/md/<host>:<name> instead
of /dev/md/<name>, the enrollment refuses it, and the refusal names the fix:
mdadm --assemble --update=homehost --homehost=any /dev/md1RAID0 is deliberate. One member failure takes the whole cache, and that is the correct trade: the bytes are disposable, the node drops out of region topology within the freshness window, and hosts fall through to Object Storage at a reduced hit rate. RAID1 would halve the capacity to buy redundancy the cache does not need.
3. Check the array before minting anything
sudo infra/scripts/install-cache-node.sh --check-array /dev/md1Read-only. It takes the device from the command line and from nowhere else: no token, no control plane, no persisted state, and it writes nothing, mounts nothing and reports nothing. It prints every fact the inventory will report — the array's uuid link, its size, its members with the disks, models and serials under them — and every reason a plan for it would be refused, not just the first. It exits non-zero when it would be refused.
Fix everything it names before minting a token. The refusals it can print:
| reason | what it means | fix |
|---|---|---|
is_os_disk | the array shares a block device with /, /boot, /boot/efi or swap | pick a different array |
mounted | it, or something on it, is mounted | unmount it |
has_holders | something is stacked on top of it | dismantle that first |
foreign_homehost | assembled under another host's homehost | mdadm --assemble --update=homehost --homehost=any /dev/md1 |
has_signature | it already carries a filesystem or partition table | wipefs -a /dev/md1 |
no array uuid | no /dev/disk/by-id/md-uuid-* link | the array is not assembled, or udev has not settled |
no member ids | the array reports no members | re-check mdadm --detail /dev/md1 |
wipefs -a /dev/md1 is the one unguarded destructive command in this whole
flow. It is on a non-boot device, and --check-array tells you in advance
whether you need it and names the exact device. Re-run --check-array after it.
4. Enrol
- Mint a discovery token in the admin panel.
- Paste the printed install command on the box. Phase 1 probes disks and NICs read-only, reports an inventory, installs a systemd oneshot to continue in the background, and exits. It is safe to close the terminal.
- In the panel the array appears in "format a whole device", typed
md · nvme, with its members listed. - The confirm dialog enumerates every member — partition, size, parent disk,
model, serial — and states what the array currently carries. Read it against
the hardware. The type-to-confirm phrase is the device and its size, e.g.
md1 3.96TB. - Accept the prefilled capacity. It is the whole device, and that is the number to take: the installer formats this array for the cache alone, so the budget is the filesystem. The 1–2% that mkfs spends on the md data offset and ext4's own metadata is not knowable from the panel, and the box measures the real filesystem after mounting it and clamps the budget to what is actually there, logging the clamp and posting it as a stage message.
- Approve. Phase 2 formats, mounts by UUID with
nofail, writes the marker, and registers.
What phase 2 does that is worth knowing
mkfs.ext4 -m 1. ext4 reserves 5% for root by default, which on a 3.96 TB array is ~197 GB the cache can never reach.-m 1recovers ~158 GB of it and still leaves ~39 GB the kernel enforces against the unprivilegedplatinum-cacheaccount, underneath the store's own sampled free-space floor. Two independent layers, one of them not ours to get wrong. No-i, no-N, no-E stride: default geometry already clears the inode invariant by ~3.7x.- The inode invariant is asserted, not assumed. After the mount, the installer checks the filesystem holds at least 1.5× the store's index-entry cap in inodes. A filesystem this run created that fails it is a die naming the shortfall — reformat with a larger inode count and re-run. A filesystem it was handed is not its to reformat and runs conservatively instead.
- Watermarks are measured, not chosen.
0.92/0.89is written only when the freshly mounted filesystem has ≥98% of its blocks available and the inode assertion passed. Anything else gets0.80/0.70with the reason logged. This is never keyed on the storage mode, because a device-mode apply also runs over a filesystem an earlier crashed attempt left behind. - A capacity the filesystem cannot hold is clamped, not fatal — unless the clamp would cost more than 10% of the plan, which means the wrong device, not an overhead estimate. Before this, the mismatch was a die after mkfs had already run, and re-planning a used token answers 409, so the oneshot retried the identical plan forever against a destroyed disk.
5. Accept the node
Do all of this before it carries traffic.
- Reboot the box twice. After each:
systemctl is-active platinum-cache-agent, norefusing to servein the journal, and no fault badge in the panel. - Confirm the panel shows
filesystem_sizeas the raise basis and a capacity within 2% of the array size. - Confirm on the box:
stat -f -c '%a %b %c' /var/platinum/cache # bavail blocks inodes grep WATERMARK /etc/platinum/cache-agent.env # expect 0.92 / 0.89 systemctl is-enabled platinum-cache-mount-missing.service - Record the cold-scan time from
/startupzat a representative fill. That number is the day-2 acceptance gate for the deploy timeout.
The mount-missing alert
The agent unit carries ConditionPathIsMountPoint on the data root. That is
what stops it from writing a whole array's worth of cache onto the OS disk if
the array does not come back from a reboot — but systemd counts a unit skipped
by a failed condition as a successful no-op, so OnFailure= never fires and
nothing tells anyone.
platinum-cache-mount-missing.service inverts the same condition, so exactly
one of the two runs on every boot and the other one shouts. It is installed and
enabled only where the data root really is a mount point, and it uses the same
alert script as the agent's own failure unit. If that unit is missing or its
alert path is broken, a missing mount is silent again — which is exactly why
step 5.3 checks it is enabled.
6. When a member disk dies
The array is gone and so is the cache. Nothing durable is lost.
mdadm --stop /dev/md1
# replace the disk, then recreate the array exactly as in section 2
mdadm --create /dev/md1 --homehost=any --metadata=1.2 --level=0 --chunk=512 \
--raid-devices=2 /dev/nvme0n1p2 /dev/nvme1n1p2
mkfs.ext4 -q -F -m 1 -L ptcache /dev/md1
mount /dev/md1 /var/platinum/cache
sudo infra/scripts/install-cache-node.sh --rearm-storage-guardThe recreated array is a new filesystem: the marker naming this node went
with the old one and the fstab entry addresses a UUID that no longer exists, so
the storage guard correctly refuses to serve. --rearm-storage-guard reads the
node id out of the installed env file, refuses a filesystem whose marker names a
different node, rewrites the marker, refreshes the fstab entry, points the
guard at the mount and restarts the agent.
No token, no control-plane call, no re-enrollment. The node keeps its id, its credentials and its place in region topology — re-enrolling would churn the one identifier all three are keyed on, for a disk swap.
7. Testing this without hardware
infra/test/loop-md-rig.sh builds a real md array out of two non-sparse loop
images and runs the real managed enrollment against it: real mkfs, real
fstab, real systemd units, real service account, real marker, real registration.
It runs on any Linux box.
sudo infra/test/loop-md-rig.sh \
--control-plane-url http://100.x.x.x:3002 --admin-token pt_live_... \
--size-gib 4 --level 0 --scenario happyScenarios: happy, m1, m5, inodes, clamp, clamp10x, signature,
homehost. Then --keep, reboot, and --reboot-check; or --break-array to
prove what happens when the array does not come back. --teardown-only is
complete and idempotent.
PT_CACHE_INSTALL_ALLOW_LOOP_MEMBERS=1 is the one documented door through the
loop-member refusal. It must be set in the environment of the install itself; no
plan and no control plane can set it, and every other guard still applies in
full.
What a loop rig cannot prove: loop devices do not survive a reboot, so
--reboot-check re-attaches them. Boot-time reassembly of a real NVMe array by
that box's initramfs, the array's exact data offset and therefore the final df
size to within 1–2%, real NVMe throughput at multi-terabyte fill, and days-long
fill-and-evict behaviour are all only observable on the hardware.
8. Residual risk you are accepting
- RAID0 has no redundancy. One NVMe failure takes the entire cache. The mitigation is the stale-node alert and fall-through to Object Storage, not redundancy.
- One reinstall window with zero cache on that node.
wipefs -a /dev/mdXis unguarded. It is the one place a typo is destructive, and it is on a non-boot device that--check-arraynames for you in advance.filesystem_total_bytes,min_free_bytesand the storage guard are self-reported by a root-run agent. The raised ceiling is a guardrail against an admin typo, not a security boundary. Root already editsPT_CACHE_CAPACITY_BYTESon the box directly. This is consistent with the threat model and is stated so nobody later mistakes it for a boundary.- The final budget may land 1–2% under the array size. It will be a logged clamp visible in the panel as a stage message, not a failure.
- A disk replacement is handled on the box, not in the control plane.
--rearm-storage-guardcovers the box; the control-plane-side story for a replaced node is unchanged.