S3 Storage High Availability
A Patroni cluster survives a node loss; the S3 repository its WAL and backups stream into must survive one too, or “high availability” quietly ends at the backup path. The store is a MinIO or silo addon (the two are interchangeable — silo is Pigsty’s MinIO fork with the same feature surface). Two fault domains matter, and they are orthogonal:
| Fault | Who absorbs it |
|---|---|
| A disk dies inside a node | the drive-level EC under SNMD/MNMD, or the storage layer under MinIO’s data directory (ZFS) |
| A node dies entirely | MinIO’s own erasure coding (EC) across hosts |
This page records the shapes pgcli supports for combining the two, and how to pick among them.
The deployment modes
MinIO classifies its layouts by node count and drive count per node (SNSD / SNMD / MNSD / MNMD). pgcli exposes all four:
| Mode | Shape | Use it for |
|---|---|---|
| SNSD (single-node, single-drive) | one node, one data directory — the default | dev, test, demos — and, paired with ZFS below, any single-host deployment that needs disk redundancy |
| SNMD (single-node, multi-drive) | one node, several drives — one --drive per drive |
a single host with N ≥ 4 data disks that must survive a disk loss without a filesystem layer |
| MNSD (multi-node, single-drive) | ≥ 4 nodes, one data directory per node | compact high-availability deployments |
| MNMD (multi-node, multi-drive) | ≥ 2 nodes, several drives each — --drive plus the full --endpoint matrix |
surviving a disk loss and a node loss without a filesystem layer |
SNMD is native: pg addon install minio --drive /mnt/disk1 --drive ...
(one flag per drive) starts one MinIO process that erasure-codes across the
drives. What it buys, measured on a live 4-drive set: 2 parity shards by
default, so it tolerates 2 drive failures; with 1 drive down reads and
writes continue, with 2 down reads still succeed but writes are refused (the
quorum boundary); usable capacity is about half the raw total; a drive that
returns is healed by MinIO itself. The same --drive works on
silo.
MNMD is native too: keep --drive for this node’s drives and add the whole
cluster’s host×drive endpoint matrix with --endpoint — one URL per drive on
every node, each naming that node’s /data1../dataN slot. Measured on a live
4-node × 4-drive set (both minio and silo): 16 drives online report EC:4
in a single erasure set of stripe size 16; losing one whole node (12/16)
keeps reads and writes working; losing a second node (8/16) refuses writes
and fails reads, and the set self-heals to 16/16 on restart. The full
walkthrough is Addons → MinIO → Multi-Node Multi-Drive
(MNMD).
SNMD vs ZFS: two ways to survive a disk
Both protect a single host’s data against disk loss; they differ in where the redundancy lives and what that costs.
native SNMD/MNMD (--drive) |
ZFS pool under --data-dir |
|
|---|---|---|
| quorum unit | the drive — MinIO counts drives as failure members | invisible to MinIO — one big drive, the pool absorbs disk loss |
| layout changeable later | fixed at install (the drive set is the EC set) | freely — swap disks, grow, migrate raidz1→raidz2; --data-dir never changes |
| usable capacity, 4 disks | ~half (2 data + 2 parity) | depends on vdev: raidz2 = 2×disk, raidz1 = 3×disk (more usable) |
| rebuild | MinIO heals a returned drive | ZFS resilvers locally |
| serves which modes | SNMD (single host) and MNMD (across hosts) | SNSD and MNSD (same recipe under each node) |
| heterogeneous nodes | every node must contribute the same drive count | a 2-disk node and a 6-disk node look identical (one endpoint each) |
The honest tradeoff: native multi-drive is simpler — one command, no
filesystem to provision, and it gets you disk redundancy with zero ZFS setup.
ZFS is more flexible — the layout stays changeable underneath, it saves
more capacity on the same disks (raidz1 keeps 3 of 4 usable where SNMD’s
EC:2 keeps 2 of 4), and it tolerates nodes that are not identical. So:
- single host, want disk redundancy without touching ZFS → SNMD
- single host, want the most usable capacity / a layout you can change later → SNSD + ZFS
- distributed cluster, disk and node failure → both paths are first-class now: native MNMD, or MNSD + per-node ZFS — see “HA with 4 hosts, each with several disks” below
Neither is wrong on a single box — SNMD’s 50% capacity is the price of not managing a pool, and ZFS’s flexibility is the price of provisioning one. The rest of this page documents the ZFS path and the MNMD alternative for multi-host sets.
ZFS: the flexible disk layer
The recipe is identical under SNSD and MNSD: build a zpool from the host’s
data disks, create one dataset for the store, and point --data-dir at its
mount point. MinIO sees “one big reliable drive”; it never learns how many
physical disks are under it.
The pool must sit on separate devices from the root filesystem — which is also
what MinIO’s drive check and
pgcli’s install-time advisory want (drive is part of root drive, will not be used). A ZFS mount on its own disks satisfies that trivially.
Layout by disk count
| Data disks | SNSD (ZFS is the only defense) | MNSD node (EC above absorbs node loss) |
|---|---|---|
| 1 | no redundancy — fine for dev/test, a dead disk means re-seeding the repo | one big disk per node is the plain MNSD shape; no ZFS needed |
| 2 | mirror |
mirror |
| 3 | raidz1 (2D usable) |
raidz1 |
| 4 | raidz2 on spinning disks; raidz1 when SSD rebuilds are quick and the capacity matters; 2 × mirror for write-heavy stores |
raidz1 — one parity is enough to keep disk loss invisible to MinIO |
| 5–8 | raidz2 ((N−2)D usable) |
raidz1, or raidz2 with large HDDs |
| > 8 | prefer two smaller raidz2/raidz1 vdevs striped over the pool — a resilver across 12+ disks is a long exposure window |
same: keep any single vdev ≤ ~8 disks |
The two columns differ in one line of reasoning: under SNSD ZFS must survive the disk and whatever happens during its rebuild, so parity depth buys safety outright; under MNSD, ZFS only has to keep a disk failure from escalating into a node loss, so one parity plus quick local resilver is the job — and beyond that, prefer more nodes over deeper local RAID.
For a 4-disk host under SNSD — the most common single-box question — the usual choices are:
| Layout | Usable | Survives | Character |
|---|---|---|---|
raidz2 (like RAID6) |
2 × disk | 2 disks | the safe default on spinning disks: resilvers on large drives are long, and raidz1’s single parity does not survive one more loss during a rebuild |
raidz1 (like RAID5) |
3 × disk | 1 disk | the capacity pick for SSDs / small disks, where a resilver takes minutes, not hours |
2 × mirror (striped) |
2 × disk | 1 disk per mirror (2 if in different mirrors) | best small-write performance — wide raidz is the worst shape for it |
Under SNSD ZFS is the store’s only line of defense, so the default leans
conservative: raidz2 unless the disks are fast enough to rebuild quickly and
the capacity is worth the thinner margin.
MinIO’s own guidance to avoid RAID underneath its EC mode targets the double-redundancy of RAID + cross-node EC. Under SNSD there is no EC — ZFS is the only protection the data has — so the guidance does not apply, and following it literally (“single drive, no ZFS”) on a 4-disk host would mean losing the whole store to one dead disk.
Co-locating with PostgreSQL: ZFS caches aggressively (ARC, by default up to half of RAM). On a host that also runs the database, cap it — e.g.
options zfs:zfs_arc_max=8589934592in/etc/modprobe.d/zfs.conf— so the two workloads do not fight over memory.
HA with 4 hosts, each with several disks
This is the hybrid the layout table above points at, and there are two first-class ways to build it: native MNMD — MinIO erasure-codes across every disk of every node — or MNSD + a ZFS pool under each node’s data directory — MinIO sees one drive per node and the pools absorb disk loss. They fail differently, so the choice is real either way.
Native MNMD
One matrix, no filesystem layer: each node passes its own drives with
--drive and every node carries the identical host×drive endpoint list. For
TLS, generate one shared leaf and install the same pair on every node (see the
TLS note after the measurements below).
Measured on a live 4-node × 4-drive set (the same on minio and silo): the 16-drive set reports EC:4 in one erasure set of stripe size 16 — losing 4 drives of 16 stays healthy.
| Event | Online | Effect (measured) |
|---|---|---|
| 1–4 drives lost | ≥ 12/16 | reads and writes continue; returned drives are healed by MinIO |
| 1 node down (its 4 drives) | 12/16 | reads and writes continue — a 64 MiB round-trip stayed byte-identical |
| 2 nodes down | 8/16 | writes refused (Resource requested is unwritable), reads fail too |
| nodes restarted | 16/16 | self-heals |
Usable capacity follows the parity ratio: EC:4 over 16 drives keeps 12/16
of raw bytes. The costs: the layout is fixed at install (the matrix is the EC
set), every node must contribute the same number of drives, and pgcli cannot
detect a matrix typo on another node — keeping the N pg.yaml files
identical is the operator’s job.
For TLS across the grid, serve one shared certificate: mint a single self-
signed leaf with pg cert --host <every node address, comma-separated> and
install it on every node with --tls-cert/--tls-key (the same two files
byte-identical everywhere, the endpoint matrix all https://). The grid then
forms with no per-node CA to reconcile — verified on a live 4-node set: the ring
came up Network: 4/4 OK with an empty CAs/ dir, because that shared leaf is
its own trust anchor and every node already holds it. The fallback is the
generated mode: if you let each node run a bare --tls, every host mints its
own CA and the first cross-node handshake dies with x509: certificate signed by unknown authority until you seed one shared CA into every node’s cert dir by
hand — which the shared-pg cert path avoids entirely.
MNSD + per-node ZFS
MNSD across the hosts, ZFS under each node’s data directory. Each node
exposes exactly one endpoint (its ZFS-backed /data); disk failures are
healed by the local pool and never reach MinIO; node failures are absorbed by
EC quorum.
What each layer then tolerates on a 4-node cluster:
| Event | Handled by | Effect |
|---|---|---|
| 1 disk in a node’s raidz pool | ZFS (resilver) | invisible to MinIO, no quorum math |
| 1 node down | EC (writes need ⌈4/2⌉+1 = 3 of 4 online) | reads and writes continue |
| 2 nodes down | EC reads only (⌈4/2⌉ = 2 of 4) | readable, writes refused until a node returns |
| a disk and its node failing together | EC (3 of 4) | still safe — the surviving pools heal after the node returns |
Two properties make the scheme flexible:
- Endpoint count = node count, not disk count. A node with 2 disks and a
node with 6 disks look identical to MinIO. The four hosts may run
different layouts —
raidz1×4 disks here, 2-waymirrorthere,raidz2×6 over there — heterogeneity costs nothing. - Pool capacities should be roughly aligned. EC sets the usable capacity
of the whole cluster to the weakest member’s free space (3 × 12T pools +
1 × 4T pool → you get 4T × striping, not 40T). If the disks genuinely
differ, thin provisioning (
zvol-based sparse datasets, or simplyzfs set refquota) hides the mismatch from MinIO — the quota caps the big pools at the small one’s size, and nothing wastes a rebuild.
Why not stripe the per-node pool to reclaim capacity. MinIO’s “no RAID
under EC” advice is about capacity, and the arithmetic is real: a plain
4-node MNSD EC set already halves the raw total, so an unmirrored raidz_none
pool on each node (all disks striped, zero local redundancy) does squeeze out
roughly a third more cluster capacity than raidz1 would. But EC counts a
failure in nodes, and a striped pool turns a single dead disk into a whole
offline node — one ordinary disk failure burns a slot of the budget EC set
aside for losing an entire machine, forcing a network-wide rebuild of that
node’s whole pool and leaving zero margin until it finishes. The capacity is
only “free” because you quietly downgraded disk fault-tolerance to node
fault-tolerance. raidz1 per node is the point where the two layers stop
stealing from each other: the pool absorbs disk loss invisibly, MinIO’s EC
budget stays reserved for node loss. (If the reclaim-everything answer truly
fits, the shape that maximizes it is plain MNSD on one big disk per node —
no ZFS at all — not a striped pool that hides the same single-point risk one
layer down.)
Which of the two
| native MNMD | MNSD + per-node ZFS | |
|---|---|---|
| setup | one matrix command per node, no filesystem to provision | zpool + dataset on every node first, then one endpoint per node |
| what EC counts | drives — one node down is just 4 of 16 members lost | nodes — a disk failure never reaches MinIO at all |
| measured fault margin | 4-node × 4-drive set: OK to 12/16 drives (one full node), fails at 8/16 | 4 nodes: reads at 2/4, writes at 3/4 nodes |
| usable capacity | 12/16 of raw on that set (EC:4) — the parity is MinIO’s choice for the stripe | local raidz1 keeps 3/4 per pool, then EC halves across nodes |
| layout later | fixed at install — the matrix is the EC set | changeable — disks, vdevs, layouts move without touching MinIO |
| heterogeneous nodes | not possible: every node must contribute the same drive count | natural — each node is just one endpoint, any local layout |
| TLS | one shared cert serves the whole grid: pg cert a single leaf covering every node address, --tls-cert/--tls-key it identically everywhere (no CA to reconcile) |
the same — it’s still a TLS grid between nodes, so the same shared-leaf setup applies |
Pick MNMD when the nodes are identical, the disk plan is settled, and you want disk and node redundancy from MinIO alone with nothing to provision underneath. Pick MNSD + ZFS when nodes differ in disk count or size, the layout may change later, or you want to reason about failures in whole nodes instead of in drives.
Choosing a shape
| Situation | Shape |
|---|---|
| laptop / demo / CI | SNSD, default data dir — nothing to decide |
| one host with N ≥ 4 data disks, backups must survive a disk | SNMD (--drive × N) — zero filesystem setup; or SNSD + ZFS (raidz2 on spinning disks, raidz1 on fast SSDs) for more usable capacity and a changeable layout |
| a few hosts, one data disk each, must survive a host | MNSD plain |
| identical hosts with several data disks each, must survive a disk and a host | MNMD (--drive + full --endpoint matrix) — no filesystem layer; or MNSD + per-node ZFS, see “Which of the two” above |
| hosts whose disk counts/sizes differ | MNSD + per-node ZFS (layouts may differ per node; keep capacities aligned) — MNMD needs equal drive counts |
The store’s TLS story is independent of the topology: whichever shape you
pick, --tls (or pg cert-minted BYO certificates) works the same — see
Generating a Certificate.
Related
- Addons → MinIO — install,
--tls, distributed mode constraints (the hard rules this page builds on), and the MNMD walkthrough - Addons → Silo — the drop-in fork; identical modes
- Cluster Backup — how the repo is wired into Patroni
(
pg backup setup) - Example: HA Cluster with a Self-CA MinIO — an end-to-end verified walkthrough of the single-node variant